Repository: ninenines/cowboy Branch: master Commit: 9f580ea964c4 Files: 395 Total size: 2.3 MB Directory structure: gitextract_a0ff0mga/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── ci.yaml ├── .gitignore ├── CONTRIBUTING.asciidoc ├── LICENSE ├── Makefile ├── README.asciidoc ├── doc/ │ └── src/ │ ├── guide/ │ │ ├── book.asciidoc │ │ ├── constraints.asciidoc │ │ ├── cookies.asciidoc │ │ ├── cowboy.sty │ │ ├── erlang_web.asciidoc │ │ ├── flow_diagram.asciidoc │ │ ├── getting_started.asciidoc │ │ ├── handlers.asciidoc │ │ ├── introduction.asciidoc │ │ ├── listeners.asciidoc │ │ ├── loop_handlers.asciidoc │ │ ├── middlewares.asciidoc │ │ ├── migrating_from_1.0.asciidoc │ │ ├── migrating_from_2.0.asciidoc │ │ ├── migrating_from_2.1.asciidoc │ │ ├── migrating_from_2.10.asciidoc │ │ ├── migrating_from_2.11.asciidoc │ │ ├── migrating_from_2.12.asciidoc │ │ ├── migrating_from_2.13.asciidoc │ │ ├── migrating_from_2.14.asciidoc │ │ ├── migrating_from_2.2.asciidoc │ │ ├── migrating_from_2.3.asciidoc │ │ ├── migrating_from_2.4.asciidoc │ │ ├── migrating_from_2.5.asciidoc │ │ ├── migrating_from_2.6.asciidoc │ │ ├── migrating_from_2.7.asciidoc │ │ ├── migrating_from_2.8.asciidoc │ │ ├── migrating_from_2.9.asciidoc │ │ ├── modern_web.asciidoc │ │ ├── multipart.asciidoc │ │ ├── performance.asciidoc │ │ ├── req.asciidoc │ │ ├── req_body.asciidoc │ │ ├── resource_design.asciidoc │ │ ├── resp.asciidoc │ │ ├── rest_flowcharts.asciidoc │ │ ├── rest_handlers.asciidoc │ │ ├── rest_principles.asciidoc │ │ ├── routing.asciidoc │ │ ├── specs.asciidoc │ │ ├── static_files.asciidoc │ │ ├── streams.asciidoc │ │ ├── ws_handlers.asciidoc │ │ └── ws_protocol.asciidoc │ ├── manual/ │ │ ├── cowboy.asciidoc │ │ ├── cowboy.get_env.asciidoc │ │ ├── cowboy.set_env.asciidoc │ │ ├── cowboy.start_clear.asciidoc │ │ ├── cowboy.start_tls.asciidoc │ │ ├── cowboy.stop_listener.asciidoc │ │ ├── cowboy_app.asciidoc │ │ ├── cowboy_compress_h.asciidoc │ │ ├── cowboy_constraints.asciidoc │ │ ├── cowboy_constraints.int.asciidoc │ │ ├── cowboy_constraints.nonempty.asciidoc │ │ ├── cowboy_decompress_h.asciidoc │ │ ├── cowboy_handler.asciidoc │ │ ├── cowboy_handler.terminate.asciidoc │ │ ├── cowboy_http.asciidoc │ │ ├── cowboy_http2.asciidoc │ │ ├── cowboy_loop.asciidoc │ │ ├── cowboy_metrics_h.asciidoc │ │ ├── cowboy_middleware.asciidoc │ │ ├── cowboy_req.asciidoc │ │ ├── cowboy_req.binding.asciidoc │ │ ├── cowboy_req.bindings.asciidoc │ │ ├── cowboy_req.body_length.asciidoc │ │ ├── cowboy_req.cast.asciidoc │ │ ├── cowboy_req.cert.asciidoc │ │ ├── cowboy_req.delete_resp_header.asciidoc │ │ ├── cowboy_req.filter_cookies.asciidoc │ │ ├── cowboy_req.has_body.asciidoc │ │ ├── cowboy_req.has_resp_body.asciidoc │ │ ├── cowboy_req.has_resp_header.asciidoc │ │ ├── cowboy_req.header.asciidoc │ │ ├── cowboy_req.headers.asciidoc │ │ ├── cowboy_req.host.asciidoc │ │ ├── cowboy_req.host_info.asciidoc │ │ ├── cowboy_req.inform.asciidoc │ │ ├── cowboy_req.match_cookies.asciidoc │ │ ├── cowboy_req.match_qs.asciidoc │ │ ├── cowboy_req.method.asciidoc │ │ ├── cowboy_req.parse_cookies.asciidoc │ │ ├── cowboy_req.parse_header.asciidoc │ │ ├── cowboy_req.parse_qs.asciidoc │ │ ├── cowboy_req.path.asciidoc │ │ ├── cowboy_req.path_info.asciidoc │ │ ├── cowboy_req.peer.asciidoc │ │ ├── cowboy_req.port.asciidoc │ │ ├── cowboy_req.push.asciidoc │ │ ├── cowboy_req.qs.asciidoc │ │ ├── cowboy_req.read_and_match_urlencoded_body.asciidoc │ │ ├── cowboy_req.read_body.asciidoc │ │ ├── cowboy_req.read_part.asciidoc │ │ ├── cowboy_req.read_part_body.asciidoc │ │ ├── cowboy_req.read_urlencoded_body.asciidoc │ │ ├── cowboy_req.reply.asciidoc │ │ ├── cowboy_req.resp_header.asciidoc │ │ ├── cowboy_req.resp_headers.asciidoc │ │ ├── cowboy_req.scheme.asciidoc │ │ ├── cowboy_req.set_resp_body.asciidoc │ │ ├── cowboy_req.set_resp_cookie.asciidoc │ │ ├── cowboy_req.set_resp_header.asciidoc │ │ ├── cowboy_req.set_resp_headers.asciidoc │ │ ├── cowboy_req.sock.asciidoc │ │ ├── cowboy_req.stream_body.asciidoc │ │ ├── cowboy_req.stream_events.asciidoc │ │ ├── cowboy_req.stream_reply.asciidoc │ │ ├── cowboy_req.stream_trailers.asciidoc │ │ ├── cowboy_req.uri.asciidoc │ │ ├── cowboy_req.version.asciidoc │ │ ├── cowboy_rest.asciidoc │ │ ├── cowboy_router.asciidoc │ │ ├── cowboy_router.compile.asciidoc │ │ ├── cowboy_static.asciidoc │ │ ├── cowboy_stream.asciidoc │ │ ├── cowboy_stream.data.asciidoc │ │ ├── cowboy_stream.early_error.asciidoc │ │ ├── cowboy_stream.info.asciidoc │ │ ├── cowboy_stream.init.asciidoc │ │ ├── cowboy_stream.terminate.asciidoc │ │ ├── cowboy_stream_h.asciidoc │ │ ├── cowboy_tracer_h.asciidoc │ │ ├── cowboy_websocket.asciidoc │ │ └── http_status_codes.asciidoc │ └── specs/ │ ├── index.ezdoc │ ├── rfc6585.ezdoc │ └── rfc7230_server.ezdoc ├── ebin/ │ └── cowboy.app ├── erlang.mk ├── examples/ │ ├── README.asciidoc │ ├── chunked_hello_world/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── chunked_hello_world_app.erl │ │ ├── chunked_hello_world_sup.erl │ │ └── toppage_h.erl │ ├── compress_response/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── compress_response_app.erl │ │ ├── compress_response_sup.erl │ │ └── toppage_h.erl │ ├── cookie/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ ├── src/ │ │ │ ├── cookie_app.erl │ │ │ ├── cookie_sup.erl │ │ │ └── toppage_h.erl │ │ └── templates/ │ │ └── toppage.dtl │ ├── echo_get/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── echo_get_app.erl │ │ ├── echo_get_sup.erl │ │ └── toppage_h.erl │ ├── echo_post/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── echo_post_app.erl │ │ ├── echo_post_sup.erl │ │ └── toppage_h.erl │ ├── eventsource/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── priv/ │ │ │ └── index.html │ │ ├── relx.config │ │ └── src/ │ │ ├── eventsource_app.erl │ │ ├── eventsource_h.erl │ │ └── eventsource_sup.erl │ ├── file_server/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── priv/ │ │ │ ├── small.ogv │ │ │ ├── test.txt │ │ │ ├── video.html │ │ │ └── 中文/ │ │ │ └── 中文.html │ │ ├── relx.config │ │ └── src/ │ │ ├── directory_h.erl │ │ ├── directory_lister.erl │ │ ├── file_server_app.erl │ │ └── file_server_sup.erl │ ├── hello_world/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── hello_world_app.erl │ │ ├── hello_world_sup.erl │ │ └── toppage_h.erl │ ├── markdown_middleware/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── priv/ │ │ │ ├── small.ogv │ │ │ └── video.md │ │ ├── relx.config │ │ └── src/ │ │ ├── markdown_converter.erl │ │ ├── markdown_middleware_app.erl │ │ └── markdown_middleware_sup.erl │ ├── rest_basic_auth/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── rest_basic_auth_app.erl │ │ ├── rest_basic_auth_sup.erl │ │ └── toppage_h.erl │ ├── rest_hello_world/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── relx.config │ │ └── src/ │ │ ├── rest_hello_world_app.erl │ │ ├── rest_hello_world_sup.erl │ │ └── toppage_h.erl │ ├── rest_pastebin/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── priv/ │ │ │ ├── index.html │ │ │ └── index.txt │ │ ├── relx.config │ │ └── src/ │ │ ├── rest_pastebin_app.erl │ │ ├── rest_pastebin_sup.erl │ │ └── toppage_h.erl │ ├── ssl_hello_world/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── priv/ │ │ │ └── ssl/ │ │ │ ├── cert.pem │ │ │ └── key.pem │ │ ├── relx.config │ │ └── src/ │ │ ├── ssl_hello_world_app.erl │ │ ├── ssl_hello_world_sup.erl │ │ └── toppage_h.erl │ ├── upload/ │ │ ├── Makefile │ │ ├── README.asciidoc │ │ ├── priv/ │ │ │ └── index.html │ │ ├── relx.config │ │ └── src/ │ │ ├── upload_app.erl │ │ ├── upload_h.erl │ │ └── upload_sup.erl │ └── websocket/ │ ├── Makefile │ ├── README.asciidoc │ ├── priv/ │ │ └── index.html │ ├── relx.config │ └── src/ │ ├── websocket_app.erl │ ├── websocket_sup.erl │ └── ws_h.erl ├── plugins.mk ├── rebar.config ├── src/ │ ├── cowboy.erl │ ├── cowboy_app.erl │ ├── cowboy_bstr.erl │ ├── cowboy_children.erl │ ├── cowboy_clear.erl │ ├── cowboy_clock.erl │ ├── cowboy_compress_h.erl │ ├── cowboy_constraints.erl │ ├── cowboy_decompress_h.erl │ ├── cowboy_dynamic_buffer.hrl │ ├── cowboy_handler.erl │ ├── cowboy_http.erl │ ├── cowboy_http2.erl │ ├── cowboy_http3.erl │ ├── cowboy_loop.erl │ ├── cowboy_metrics_h.erl │ ├── cowboy_middleware.erl │ ├── cowboy_quicer.erl │ ├── cowboy_req.erl │ ├── cowboy_rest.erl │ ├── cowboy_router.erl │ ├── cowboy_static.erl │ ├── cowboy_stream.erl │ ├── cowboy_stream_h.erl │ ├── cowboy_sub_protocol.erl │ ├── cowboy_sup.erl │ ├── cowboy_tls.erl │ ├── cowboy_tracer_h.erl │ ├── cowboy_websocket.erl │ └── cowboy_webtransport.erl └── test/ ├── compress_SUITE.erl ├── cover.spec ├── cowboy_ct_hook.erl ├── cowboy_test.erl ├── decompress_SUITE.erl ├── draft_h3_webtransport_SUITE.erl ├── examples_SUITE.erl ├── h2spec_SUITE.erl ├── handlers/ │ ├── accept_callback_h.erl │ ├── accept_callback_missing_h.erl │ ├── asterisk_h.erl │ ├── charset_in_content_types_provided_h.erl │ ├── charset_in_content_types_provided_implicit_h.erl │ ├── charset_in_content_types_provided_implicit_no_callback_h.erl │ ├── charsets_provided_empty_h.erl │ ├── charsets_provided_h.erl │ ├── compress_h.erl │ ├── content_types_accepted_h.erl │ ├── content_types_provided_h.erl │ ├── crash_h.erl │ ├── create_resource_h.erl │ ├── custom_req_fields_h.erl │ ├── decompress_h.erl │ ├── default_h.erl │ ├── delay_hello_h.erl │ ├── delete_resource_h.erl │ ├── echo_h.erl │ ├── expires_h.erl │ ├── generate_etag_h.erl │ ├── hello_h.erl │ ├── if_range_h.erl │ ├── last_modified_h.erl │ ├── long_polling_h.erl │ ├── long_polling_sys_h.erl │ ├── loop_handler_abort_h.erl │ ├── loop_handler_body_h.erl │ ├── loop_handler_endless_h.erl │ ├── loop_handler_timeout_h.erl │ ├── loop_handler_timeout_hibernate_h.erl │ ├── loop_handler_timeout_info_h.erl │ ├── loop_handler_timeout_init_h.erl │ ├── multipart_h.erl │ ├── provide_callback_missing_h.erl │ ├── provide_range_callback_h.erl │ ├── range_satisfiable_h.erl │ ├── ranges_provided_auto_h.erl │ ├── ranges_provided_h.erl │ ├── rate_limited_h.erl │ ├── read_body_h.erl │ ├── resp_h.erl │ ├── resp_iolist_body_h.erl │ ├── rest_hello_h.erl │ ├── send_message_h.erl │ ├── set_options_h.erl │ ├── stop_handler_h.erl │ ├── stream_handler_h.erl │ ├── stream_hello_h.erl │ ├── streamed_result_h.erl │ ├── switch_handler_h.erl │ ├── switch_protocol_flush_h.erl │ ├── ws_active_commands_h.erl │ ├── ws_deflate_commands_h.erl │ ├── ws_deflate_opts_h.erl │ ├── ws_dont_validate_utf8_h.erl │ ├── ws_handle_commands_h.erl │ ├── ws_ignore.erl │ ├── ws_info_commands_h.erl │ ├── ws_init_commands_h.erl │ ├── ws_init_h.erl │ ├── ws_ping_h.erl │ ├── ws_set_options_commands_h.erl │ ├── ws_shutdown_reason_commands_h.erl │ ├── ws_terminate_h.erl │ └── wt_echo_h.erl ├── http2_SUITE.erl ├── http_SUITE.erl ├── http_perf_SUITE.erl ├── loop_handler_SUITE.erl ├── metrics_SUITE.erl ├── misc_SUITE.erl ├── plain_handler_SUITE.erl ├── proxy_header_SUITE.erl ├── req_SUITE.erl ├── rest_handler_SUITE.erl ├── rfc6585_SUITE.erl ├── rfc7230_SUITE.erl ├── rfc7231_SUITE.erl ├── rfc7538_SUITE.erl ├── rfc7540_SUITE.erl ├── rfc8297_SUITE.erl ├── rfc8441_SUITE.erl ├── rfc9114_SUITE.erl ├── rfc9114_SUITE_data/ │ ├── client.key │ ├── client.pem │ ├── server.key │ └── server.pem ├── rfc9204_SUITE.erl ├── rfc9220_SUITE.erl ├── security_SUITE.erl ├── static_handler_SUITE.erl ├── static_handler_SUITE_data/ │ └── static_files_app.ez ├── stream_handler_SUITE.erl ├── sys_SUITE.erl ├── tracer_SUITE.erl ├── ws_SUITE.erl ├── ws_SUITE_data/ │ ├── ws_echo.erl │ ├── ws_echo_timer.erl │ ├── ws_init_shutdown.erl │ ├── ws_max_frame_size.erl │ ├── ws_send_many.erl │ ├── ws_subprotocol.erl │ ├── ws_timeout_cancel.erl │ └── ws_timeout_hibernate.erl ├── ws_autobahn_SUITE.erl ├── ws_autobahn_SUITE_data/ │ └── client.json ├── ws_handler_SUITE.erl ├── ws_perf_SUITE.erl └── ws_perf_SUITE_data/ ├── ascii.txt ├── grok_segond.txt └── japanese.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Don't include Erlang.mk in diffs. erlang.mk -diff # Don't change line endings in our test data on Windows. test/ws_perf_SUITE_data/*.txt -text ================================================ FILE: .github/workflows/ci.yaml ================================================ ## Use workflows from ninenines/ci.erlang.mk to test Cowboy. name: Check Cowboy on: push: branches: - master pull_request: schedule: ## Every Monday at 2am. - cron: 0 2 * * 1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: check: name: Check if: ${{ !cancelled() }} uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master dialyzer-no-quicer: name: Check / Dialyzer (without COWBOY_QUICER) runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install latest Erlang/OTP uses: erlef/setup-beam@v1 with: otp-version: '> 0' - name: Run Dialyzer (without COWBOY_QUICER) run: make dialyze COWBOY_QUICER=0 examples: name: Check examples runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install latest Erlang/OTP uses: erlef/setup-beam@v1 with: otp-version: '> 0' - name: Run ct-examples run: make ct-examples - name: Upload logs uses: actions/upload-artifact@v4 if: always() with: name: Common Test logs (examples) path: | logs/ !logs/**/log_private ================================================ FILE: .gitignore ================================================ .cowboy.plt .erlang.mk _rel cowboy.d deps doc/guide.pdf doc/html doc/man3 doc/man7 ebin/*.beam ebin/test examples/*/ebin examples/*/*.d logs relx test/*.beam ================================================ FILE: CONTRIBUTING.asciidoc ================================================ = Contributing This document is a guide on how to best contribute to this project. == Definitions *SHOULD* describes optional steps. *MUST* describes mandatory steps. *SHOULD NOT* and *MUST NOT* describes pitfalls to avoid. _Your local copy_ refers to the copy of the repository that you have on your computer. _origin_ refers to your fork of the project. _upstream_ refers to the official repository for this project. == Discussions For general discussion about this project, please open a ticket. Feedback is always welcome and may transform in tasks to improve the project, so having the discussion start there is a plus. Alternatively you may try the https://discord.gg/x468ZsxG[Discord server] or, if you need the discussion to stay private, you can send an email at contact@ninenines.eu. == Support Free support is generally not available. The rule is that free support is only given if doing so benefits most users. In practice this means that free support will only be given if the issues are due to a fault in the project itself or its documentation. Paid support is available for all price ranges. Please send an email to contact@ninenines.eu for more information. == Bug reports You *SHOULD* open a ticket for every bug you encounter, regardless of the version you use. A ticket not only helps the project ensure that bugs are squashed, it also helps other users who later run into this issue. You *SHOULD* give as much information as possible including what commit/branch, what OS/version and so on. You *SHOULD NOT* open a ticket if another already exists for the same issue. You *SHOULD* instead either add more information by commenting on it, or simply comment to inform the maintainer that you are also affected. The maintainer *SHOULD* reply to every new ticket when they are opened. If the maintainer didn't say anything after a few days, you *SHOULD* write a new comment asking for more information. You *SHOULD* provide a reproducible test case, either in the ticket or by sending a pull request and updating the test suite. When you have a fix ready, you *SHOULD* open a pull request, even if the code does not fit the requirements discussed below. Providing a fix, even a dirty one, can help other users and/or at least get the maintainer on the right tracks. You *SHOULD* try to relax and be patient. Some tickets are merged or fixed quickly, others aren't. There's no real rules around that. You can become a paying customer if you need something fast. == Security reports You *SHOULD* open a ticket when you identify a DoS vulnerability in this project. You *SHOULD* include the resources needed to DoS the project; every project can be brought down if you have the necessary resources. You *SHOULD* send an email to contact@ninenines.eu when you identify a security vulnerability. If the vulnerability originates from code inside Erlang/OTP itself, you *SHOULD* also consult with OTP Team directly to get the problem fixed upstream. == Feature requests Feature requests are always welcome. To be accepted, however, they must be well defined, make sense in the context of the project and benefit most users. Feature requests not benefiting most users may only be accepted when accompanied with a proper pull request. You *MUST* open a ticket to explain what the new feature is, even if you are going to submit a pull request for it. All these conditions are meant to ensure that the project stays lightweight and maintainable. == Documentation submissions You *SHOULD* follow the code submission guidelines to submit documentation. The documentation is available in the 'doc/src/' directory. There are three kinds of documentation: manual, guide and tutorials. The format for the documentation is Asciidoc. You *SHOULD* follow the same style as the surrounding documentation when editing existing files. You *MUST* include the source when providing media. == Examples submissions You *SHOULD* follow the code submission guidelines to submit examples. The examples are available in the 'examples/' directory. You *SHOULD* focus on exactly one thing per example. == Code submissions You *SHOULD* open a pull request to submit code. You *SHOULD* open a ticket to discuss backward incompatible changes before you submit code. This step ensures that you do not work on a large change that will then be rejected. You *SHOULD* send your code submission using a pull request on GitHub. If you can't, please send an email to contact@ninenines.eu with your patch. The following sections explain the normal GitHub workflow. === Cloning You *MUST* fork the project's repository on GitHub by clicking on the _Fork_ button. On the right page of your fork's page is a field named _SSH clone URL_. Its contents will be identified as `$ORIGIN_URL` in the following snippet. On the right side of the project's repository page is a similar field. Its contents will be identified as `$UPSTREAM_URL`. Finally, `$PROJECT` is the name of this project. To setup your clone and be able to rebase when requested, run the following commands: [source,bash] $ git clone $ORIGIN_URL $ cd $PROJECT $ git remote add upstream $UPSTREAM_URL === Branching You *SHOULD* base your branch on _master_, unless your patch applies to a stable release, in which case you need to base your branch on the stable branch, for example _1.0.x_. The first step is therefore to checkout the branch in question: [source,bash] $ git checkout 1.0.x The next step is to update the branch to the current version from _upstream_. In the following snippet, replace _1.0.x_ by _master_ if you are patching _master_. [source,bash] $ git fetch upstream $ git rebase upstream/1.0.x This last command may fail and ask you to stash your changes. When that happens, run the following sequence of commands: [source,bash] $ git stash $ git rebase upstream/1.0.x $ git stash pop The final step is to create a new branch you can work in. The name of the new branch is up to you, there is no particular requirement. Replace `$BRANCH` with the branch name you came up with: [source,bash] $ git checkout -b $BRANCH _Your local copy_ is now ready. === Source editing There are very few rules with regard to source code editing. You *MUST* use horizontal tabs for indentation. Use one tab per indentation level. You *MUST NOT* align code. You can only add or remove one indentation level compared to the previous line. You *SHOULD NOT* write lines more than about a hundred characters. There is no hard limit, just try to keep it as readable as possible. You *SHOULD* write small functions when possible. You *SHOULD* avoid a too big hierarchy of case clauses inside a single function. You *SHOULD* add tests to make sure your code works. === Committing You *SHOULD* run Dialyzer and the test suite while working on your patch, and you *SHOULD* ensure that no additional tests fail when you finish. You can use the following command to run Dialyzer: [source,bash] $ make dialyze You have two options to run tests. You can either run tests across all supported Erlang versions, or just on the version you are currently using. To test across all supported Erlang versions: [source,bash] $ make -k ci To test using the current version: [source,bash] $ make tests You can then open Common Test logs in 'logs/all_runs.html'. By default Cowboy excludes a few test suites that take too long to complete. For example all the examples are built and tested, and one Websocket test suite is very extensive. In order to run everything, do: [source,bash] $ make tests FULL=1 Once all tests pass (or at least, no new tests are failing), you can commit your changes. First you need to add your changes: [source,bash] $ git add src/file_you_edited.erl If you want an interactive session, allowing you to filter out changes that have nothing to do with this commit: [source,bash] $ git add -p You *MUST* put all related changes inside a single commit. The general rule is that all commits must pass tests. Fix one bug per commit. Add one feature per commit. Separate features in multiple commits only if smaller parts of the feature make sense on their own. Finally once all changes are added you can commit. This command will open the editor of your choice where you can put a proper commit title and message. [source,bash] $ git commit Do not use the `-m` option as it makes it easy to break the following rules: You *MUST* write a proper commit title and message. The commit title is the first line and *MUST* be at most 72 characters. The second line *MUST* be left blank. Everything after that is the commit message. You *SHOULD* write a detailed commit message. The lines of the message *MUST* be at most 80 characters. You *SHOULD* explain what the commit does, what references you used and any other information that helps understanding why this commit exists. You *MUST NOT* include commands to close GitHub tickets automatically. === Cleaning the commit history If you create a new commit every time you make a change, however insignificant, you *MUST* consolidate those commits before sending the pull request. This is done through _rebasing_. The easiest way to do so is to use interactive rebasing, which allows you to choose which commits to keep, squash, edit and so on. To rebase, you need to give the original commit before you made your changes. If you only did two changes, you can use the shortcut form `HEAD^^`: [source,bash] $ git rebase -i HEAD^^ === Submitting the pull request You *MUST* push your branch to your fork on GitHub. Replace `$BRANCH` with your branch name: [source,bash] $ git push origin $BRANCH You can then submit the pull request using the GitHub interface. You *SHOULD* provide an explanatory message and refer to any previous ticket related to this patch. You *MUST NOT* include commands to close other tickets automatically. === Updating the pull request Sometimes the maintainer will ask you to change a few things. Other times you will notice problems with your submission and want to fix them on your own. In either case you do not need to close the pull request. You can just push your changes again and, if needed, force them. This will update the pull request automatically. [source,bash] $ git push -f origin $BRANCH === Merging This is an open source project maintained by independent developers. Please be patient when your changes aren't merged immediately. All pull requests run through a Continuous Integration service to ensure nothing gets broken by the changes submitted. Bug fixes will be merged immediately when all tests pass. The maintainer may do style changes in the merge commit if the submitter is not available. The maintainer *MUST* open a new ticket if the solution could still be improved. New features and backward incompatible changes will be merged when all tests pass and all other requirements are fulfilled. ================================================ FILE: LICENSE ================================================ Copyright (c) 2011-2025, Loïc Hoguin Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: Makefile ================================================ # See LICENSE for licensing information. PROJECT = cowboy PROJECT_DESCRIPTION = Small, fast, modern HTTP server. PROJECT_VERSION = 2.14.2 PROJECT_REGISTERED = cowboy_clock # Options. PLT_APPS = public_key ssl # ct_helper gun common_test inets CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl #CT_OPTS += +JPperf true +S 1 # Dependencies. LOCAL_DEPS = crypto DEPS = cowlib ranch dep_cowlib = git https://github.com/ninenines/cowlib 2.16.0 dep_ranch = git https://github.com/ninenines/ranch 1.8.1 ifeq ($(COWBOY_QUICER),1) DEPS += quicer dep_quicer = git https://github.com/emqx/quic main endif DOC_DEPS = asciideck TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper gun dep_ct_helper = git https://github.com/extend/ct_helper master dep_gun = git https://github.com/ninenines/gun master # CI configuration. dep_ci.erlang.mk = git https://github.com/ninenines/ci.erlang.mk master DEP_EARLY_PLUGINS = ci.erlang.mk AUTO_CI_OTP ?= OTP-LATEST-24+ AUTO_CI_WINDOWS ?= OTP-LATEST-24+ # Hex configuration. define HEX_TARBALL_EXTRA_METADATA #{ licenses => [<<"ISC">>], links => #{ <<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.14/guide/">>, <<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.14/manual/">>, <<"GitHub">> => <<"https://github.com/ninenines/cowboy">>, <<"Sponsor">> => <<"https://github.com/sponsors/essen">> } } endef hex_req_ranch = >= 1.8.0 and < 3.0.0 hex_req_cowlib = >= 2.16.0 and < 3.0.0 # Standard targets. include erlang.mk # Don't run the examples/autobahn test suites by default. ifndef FULL CT_SUITES := $(filter-out examples http_perf ws_autobahn ws_perf,$(CT_SUITES)) endif # Don't run HTTP/3 test suites on Windows. ifeq ($(PLATFORM),msys2) CT_SUITES := $(filter-out rfc9114 rfc9204 rfc9220,$(CT_SUITES)) endif # Compile options. ERLC_OPTS += +warn_missing_spec +warn_untyped_record # +bin_opt_info TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}' ifeq ($(COWBOY_QUICER),1) ERLC_OPTS += -D COWBOY_QUICER=1 TEST_ERLC_OPTS += -D COWBOY_QUICER=1 endif # Generate rebar.config on build. app:: rebar.config # Fix quicer compilation for HTTP/3. autopatch-quicer:: $(verbose) printf "%s\n" "all: ;" > $(DEPS_DIR)/quicer/c_src/Makefile.erlang.mk # Dialyze the tests. #DIALYZER_OPTS += --src -r test # h2spec setup. GOPATH := $(ERLANG_MK_TMP)/gopath export GOPATH H2SPEC := $(GOPATH)/src/github.com/summerwind/h2spec/h2spec export H2SPEC # @todo It would be better to allow these dependencies to be specified # on a per-target basis instead of for all targets. test-build:: $(H2SPEC) $(H2SPEC): $(gen_verbose) mkdir -p $(GOPATH)/src/github.com/summerwind $(verbose) git clone --depth 1 https://github.com/summerwind/h2spec $(dir $(H2SPEC)) || true $(verbose) $(MAKE) -C $(dir $(H2SPEC)) build MAKEFLAGS= || true # Prepare for the release. prepare_tag: $(verbose) $(warning Hex metadata: $(HEX_TARBALL_EXTRA_METADATA)) $(verbose) echo $(verbose) echo -n "Most recent tag: " $(verbose) git tag --sort taggerdate | tail -n1 $(verbose) git verify-tag `git tag --sort taggerdate | tail -n1` $(verbose) echo -n "MAKEFILE: " $(verbose) grep -m1 PROJECT_VERSION Makefile $(verbose) echo -n "APP: " $(verbose) grep -m1 vsn ebin/$(PROJECT).app | sed 's/ //g' $(verbose) echo -n "GUIDE: " $(verbose) grep -h dep_$(PROJECT)_commit doc/src/guide/*.asciidoc || true $(verbose) echo $(verbose) echo "Links in the README:" $(verbose) grep http.*:// README.asciidoc $(verbose) echo $(verbose) echo "Titles in most recent CHANGELOG:" $(verbose) for f in `ls -rv doc/src/guide/migrating_from_*.asciidoc | head -n1`; do \ echo $$f:; \ grep == $$f; \ done $(verbose) echo $(verbose) echo "Dependencies:" $(verbose) grep ^DEPS Makefile || echo "DEPS =" $(verbose) grep ^dep_ Makefile || true $(verbose) grep ^hex_req_ Makefile || true $(verbose) echo $(verbose) echo "rebar.config:" $(verbose) cat rebar.config || true ================================================ FILE: README.asciidoc ================================================ = Cowboy Cowboy is a small, fast and modern HTTP server for Erlang/OTP. == Goals Cowboy aims to provide a *complete* HTTP stack in a *small* code base. It is optimized for *low latency* and *low memory usage*, in part because it uses *binary strings*. Cowboy provides *routing* capabilities, selectively dispatching requests to handlers written in Erlang. Because it uses Ranch for managing connections, Cowboy can easily be *embedded* in any other application. Cowboy is *clean* and *well tested* Erlang code. == Online documentation * https://ninenines.eu/docs/en/cowboy/2.14/guide[User guide] * https://ninenines.eu/docs/en/cowboy/2.14/manual[Function reference] == Offline documentation * While still online, run `make docs` * User guide available in `doc/` in PDF and HTML formats * Function reference man pages available in `doc/man3/` and `doc/man7/` * Run `make install-docs` to install man pages on your system * Full documentation in Asciidoc available in `doc/src/` * Examples available in `examples/` == Getting help * https://discord.gg/x25nNq2fFE[Discord server] * https://github.com/ninenines/cowboy/issues[Issues tracker] * https://ninenines.eu/services[Commercial Support] * https://github.com/sponsors/essen[Sponsor me!] ================================================ FILE: doc/src/guide/book.asciidoc ================================================ // a2x: --dblatex-opts "-P latex.output.revhistory=0 -P doc.publisher.show=0 -P index.numbered=0" // a2x: --dblatex-opts "-s cowboy" // a2x: -d book --attribute tabsize=4 = Cowboy User Guide // REST: where should i handle bindings? init, probably. qs? in media type functions // REST: explain how a module per media type is good; module may be shared between client/server = Rationale include::modern_web.asciidoc[The modern Web] include::erlang_web.asciidoc[Erlang and the Web] = Introduction include::introduction.asciidoc[Introduction] include::getting_started.asciidoc[Getting started] include::flow_diagram.asciidoc[Flow diagram] = Configuration include::listeners.asciidoc[Listeners] include::routing.asciidoc[Routing] include::constraints.asciidoc[Constraints] = Handlers include::handlers.asciidoc[Handlers] include::loop_handlers.asciidoc[Loop handlers] include::static_files.asciidoc[Static files] = Request and response include::req.asciidoc[Request details] include::req_body.asciidoc[Reading the request body] include::resp.asciidoc[Sending a response] include::cookies.asciidoc[Using cookies] include::multipart.asciidoc[Multipart] = REST include::rest_principles.asciidoc[REST principles] include::rest_handlers.asciidoc[Handling REST requests] include::rest_flowcharts.asciidoc[REST flowcharts] include::resource_design.asciidoc[Designing a resource handler] = Websocket include::ws_protocol.asciidoc[The Websocket protocol] include::ws_handlers.asciidoc[Websocket handlers] = Advanced include::streams.asciidoc[Streams] include::middlewares.asciidoc[Middlewares] include::performance.asciidoc[Performance] = Additional information include::migrating_from_2.14.asciidoc[Changes since Cowboy 2.14] include::migrating_from_2.13.asciidoc[Migrating from Cowboy 2.13 to 2.14] include::migrating_from_2.12.asciidoc[Migrating from Cowboy 2.12 to 2.13] include::migrating_from_2.11.asciidoc[Migrating from Cowboy 2.11 to 2.12] include::migrating_from_2.10.asciidoc[Migrating from Cowboy 2.10 to 2.11] include::migrating_from_2.9.asciidoc[Migrating from Cowboy 2.9 to 2.10] include::migrating_from_2.8.asciidoc[Migrating from Cowboy 2.8 to 2.9] include::migrating_from_2.7.asciidoc[Migrating from Cowboy 2.7 to 2.8] include::migrating_from_2.6.asciidoc[Migrating from Cowboy 2.6 to 2.7] include::migrating_from_2.5.asciidoc[Migrating from Cowboy 2.5 to 2.6] include::migrating_from_2.4.asciidoc[Migrating from Cowboy 2.4 to 2.5] include::migrating_from_2.3.asciidoc[Migrating from Cowboy 2.3 to 2.4] include::migrating_from_2.2.asciidoc[Migrating from Cowboy 2.2 to 2.3] include::migrating_from_2.1.asciidoc[Migrating from Cowboy 2.1 to 2.2] include::migrating_from_2.0.asciidoc[Migrating from Cowboy 2.0 to 2.1] include::migrating_from_1.0.asciidoc[Migrating from Cowboy 1.0 to 2.0] include::specs.asciidoc[HTTP and other specifications] ================================================ FILE: doc/src/guide/constraints.asciidoc ================================================ [[constraints]] == Constraints Constraints are validation and conversion functions applied to user input. They are used in various places in Cowboy, including the router and the `cowboy_req` match functions. === Syntax Constraints are provided as a list of fields. For each field in the list, specific constraints can be applied, as well as a default value if the field is missing. A field can take the form of an atom `field`, a tuple with constraints `{field, Constraints}` or a tuple with constraints and a default value `{field, Constraints, Default}`. The `field` form indicates the field is mandatory. Note that when used with the router, only the second form makes sense, as it does not use the default and the field is always defined. Constraints for each field are provided as an ordered list of atoms or funs to apply. Built-in constraints are provided as atoms, while custom constraints are provided as funs. When multiple constraints are provided, they are applied in the order given. If the value has been modified by a constraint then the next one receives the new value. For example, the following constraints will first validate and convert the field `my_value` to an integer, and then check that the integer is positive: [source,erlang] ---- PositiveFun = fun (_, V) when V > 0 -> {ok, V}; (_, _) -> {error, not_positive} end, {my_value, [int, PositiveFun]}. ---- We ignore the first fun argument in this snippet. We shouldn't. We will simply learn what it is later in this chapter. When there's only one constraint, it can be provided directly without wrapping it into a list: [source,erlang] ---- {my_value, int} ---- === Built-in constraints Built-in constraints are specified as an atom: [cols="<,<",options="header"] |=== | Constraint | Description | int | Converts binary value to integer. | nonempty | Ensures the binary value is non-empty. |=== === Custom constraints Custom constraints are specified as a fun. This fun takes two arguments. The first argument indicates the operation to be performed, and the second is the value. What the value is and what must be returned depends on the operation. Cowboy currently defines three operations. The operation used for validating and converting user input is the `forward` operation. [source,erlang] ---- int(forward, Value) -> try {ok, binary_to_integer(Value)} catch _:_ -> {error, not_an_integer} end; ---- The value must be returned even if it is not converted by the constraint. The two other operations are currently experimental. They are meant to help implement HATEOAS type services, but proper support for HATEOAS is not expected to be available before Cowboy 3.0 because of Cowboy's current router's limitations. The `reverse` operation does the opposite: it takes a converted value and changes it back to what the user input would have been. [source,erlang] ---- int(reverse, Value) -> try {ok, integer_to_binary(Value)} catch _:_ -> {error, not_an_integer} end; ---- Finally, the `format_error` operation takes an error returned by any other operation and returns a formatted human-readable error message. [source,erlang] ---- int(format_error, {not_an_integer, Value}) -> io_lib:format("The value ~p is not an integer.", [Value]). ---- Notice that for this case you get both the error and the value that was given to the constraint that produced this error. Cowboy will not catch exceptions coming from constraint functions. They should be written to not emit any exceptions. ================================================ FILE: doc/src/guide/cookies.asciidoc ================================================ [[cookies]] == Using cookies Cookies are a mechanism allowing applications to maintain state on top of the stateless HTTP protocol. Cookies are a name/value store where the names and values are stored in plain text. They expire either after a delay or when the browser closes. They can be configured on a specific domain name or path, and restricted to secure resources (sent or downloaded over HTTPS), or restricted to the server (disallowing access from client-side scripts). Cookie names are de facto case sensitive. Cookies are stored client-side and sent with every subsequent request that matches the domain and path for which they were stored, until they expire. This can create a non-negligible cost. Cookies should not be considered secure. They are stored on the user's computer in plain text, and can be read by any program. They can also be read by proxies when using clear connections. Always validate the value before using it, and never store any sensitive information inside it. Cookies set by the server are only available in requests following the client reception of the response containing them. Cookies may be sent repeatedly. This is often useful to update the expiration time and avoid losing a cookie. === Setting cookies By default cookies are defined for the duration of the session: [source,erlang] ---- SessionID = generate_session_id(), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0). ---- They can also be set for a duration in seconds: [source,erlang] ---- SessionID = generate_session_id(), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0, #{max_age => 3600}). ---- To delete cookies, set `max_age` to 0: [source,erlang] ---- SessionID = generate_session_id(), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0, #{max_age => 0}). ---- To restrict cookies to a specific domain and path, the options of the same name can be used: [source,erlang] ---- Req = cowboy_req:set_resp_cookie(<<"inaccount">>, <<"1">>, Req0, #{domain => "my.example.org", path => "/account"}). ---- Cookies will be sent with requests to this domain and all its subdomains, and to resources on this path or deeper in the path hierarchy. To restrict cookies to secure channels (typically resources available over HTTPS): [source,erlang] ---- SessionID = generate_session_id(), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0, #{secure => true}). ---- To prevent client-side scripts from accessing a cookie: [source,erlang] ---- SessionID = generate_session_id(), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0, #{http_only => true}). ---- Cookies may also be set client-side, for example using Javascript. === Reading cookies The client only ever sends back the cookie name and value. All other options that can be set are never sent back. Cowboy provides two functions for reading cookies. Both involve parsing the cookie header(s) and so should not be called repeatedly. You can get all cookies as a key/value list: [source,erlang] Cookies = cowboy_req:parse_cookies(Req), {_, Lang} = lists:keyfind(<<"lang">>, 1, Cookies). Or you can perform a match against cookies and retrieve only the ones you need, while at the same time doing any required post processing using xref:constraints[constraints]. This function returns a map: [source,erlang] #{id := ID, lang := Lang} = cowboy_req:match_cookies([id, lang], Req). You can use constraints to validate the values while matching them. The following snippet will crash if the `id` cookie is not an integer number or if the `lang` cookie is empty. Additionally the `id` cookie value will be converted to an integer term: [source,erlang] CookiesMap = cowboy_req:match_cookies([{id, int}, {lang, nonempty}], Req). Note that if two cookies share the same name, then the map value will be a list of the two cookie values. A default value can be provided. The default will be used if the `lang` cookie is not found. It will not be used if the cookie is found but has an empty value: [source,erlang] #{lang := Lang} = cowboy_req:match_cookies([{lang, [], <<"en-US">>}], Req). If no default is provided and the value is missing, an exception is thrown. ================================================ FILE: doc/src/guide/cowboy.sty ================================================ \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{asciidoc-dblatex}[2012/10/24 AsciiDoc DocBook Style] %% Just use the original package and pass the options. \RequirePackageWithOptions{docbook} %% Define an alias for make snippets to be compatible with source-highlighter. \lstalias{makefile}{make} ================================================ FILE: doc/src/guide/erlang_web.asciidoc ================================================ [[erlang_web]] == Erlang and the Web Erlang is the ideal platform for writing Web applications. Its features are a perfect match for the requirements of modern Web applications. === The Web is concurrent When you access a website there is little concurrency involved. A few connections are opened and requests are sent through these connections. Then the web page is displayed on your screen. Your browser will only open up to 4 or 8 connections to the server, depending on your settings. This isn't much. But think about it. You are not the only one accessing the server at the same time. There can be hundreds, if not thousands, if not millions of connections to the same server at the same time. Even today a lot of systems used in production haven't solved the C10K problem (ten thousand concurrent connections). And the ones who did are trying hard to get to the next step, C100K, and are pretty far from it. Erlang meanwhile has no problem handling millions of connections. At the time of writing there are application servers written in Erlang that can handle more than two million connections on a single server in a real production application, with spare memory and CPU! The Web is concurrent, and Erlang is a language designed for concurrency, so it is a perfect match. Of course, various platforms need to scale beyond a few million connections. This is where Erlang's built-in distribution mechanisms come in. If one server isn't enough, add more! Erlang allows you to use the same code for talking to local processes or to processes in other parts of your cluster, which means you can scale very quickly if the need arises. The Web has large userbases, and the Erlang platform was designed to work in a distributed setting, so it is a perfect match. Or is it? Surely you can find solutions to handle that many concurrent connections with your favorite language... But all these solutions will break down in the next few years. Why? Firstly because servers don't get any more powerful, they instead get a lot more cores and memory. This is only useful if your application can use them properly, and Erlang is light-years ahead of anything else in this respect. Secondly, today your computer and your phone are online, tomorrow your watch, goggles, bike, car, fridge and tons of other devices will also connect to various applications on the Internet. Only Erlang is prepared to deal with what's coming. === The Web is soft real time What does soft real time mean, you ask? It means we want the operations done as quickly as possible, and in the case of web applications, it means we want the data propagated fast. In comparison, hard real time has a similar meaning, but also has a hard time constraint, for example an operation needs to be done in under N milliseconds otherwise the system fails entirely. Users aren't that needy yet, they just want to get access to their content in a reasonable delay, and they want the actions they make to register at most a few seconds after they submitted them, otherwise they'll start worrying about whether it successfully went through. The Web is soft real time because taking longer to perform an operation would be seen as bad quality of service. Erlang is a soft real time system. It will always run processes fairly, a little at a time, switching to another process after a while and preventing a single process to steal resources from all others. This means that Erlang can guarantee stable low latency of operations. Erlang provides the guarantees that the soft real time Web requires. === The Web is asynchronous Long ago, the Web was synchronous because HTTP was synchronous. You fired a request, and then waited for a response. Not anymore. It all began when XmlHttpRequest started being used. It allowed the client to perform asynchronous calls to the server. Then Websocket appeared and allowed both the server and the client to send data to the other endpoint completely asynchronously. The data is contained within frames and no response is necessary. Erlang processes work the same. They send each other data contained within messages and then continue running without needing a response. They tend to spend most of their time inactive, waiting for a new message, and the Erlang VM happily activate them when one is received. It is therefore quite easy to imagine Erlang being good at receiving Websocket frames, which may come in at unpredictable times, pass the data to the responsible processes which are always ready waiting for new messages, and perform the operations required by only activating the required parts of the system. The more recent Web technologies, like Websocket of course, but also HTTP/2.0, are all fully asynchronous protocols. The concept of requests and responses is retained of course, but anything could be sent in between, by both the client or the browser, and the responses could also be received in a completely different order. Erlang is by nature asynchronous and really good at it thanks to the great engineering that has been done in the VM over the years. It's only natural that it's so good at dealing with the asynchronous Web. === The Web is omnipresent The Web has taken a very important part of our lives. We're connected at all times, when we're on our phone, using our computer, passing time using a tablet while in the bathroom... And this isn't going to slow down, every single device at home or on us will be connected. All these devices are always connected. And with the number of alternatives to give you access to the content you seek, users tend to not stick around when problems arise. Users today want their applications to be always available and if it's having too many issues they just move on. Despite this, when developers choose a product to use for building web applications, their only concern seems to be "Is it fast?", and they look around for synthetic benchmarks showing which one is the fastest at sending "Hello world" with only a handful concurrent connections. Web benchmarks haven't been representative of reality in a long time, and are drifting further away as time goes on. What developers should really ask themselves is "Can I service all my users with no interruption?" and they'd find that they have two choices. They can either hope for the best, or they can use Erlang. Erlang is built for fault tolerance. When writing code in any other language, you have to check all the return values and act accordingly to avoid any unforeseen issues. If you're lucky, you won't miss anything important. When writing Erlang code, you can just check the success condition and ignore all errors. If an error happens, the Erlang process crashes and is then restarted by a special process called a supervisor. Erlang developers thus have no need to fear unhandled errors, and can focus on handling only the errors that should give some feedback to the user and let the system take care of the rest. This also has the advantage of allowing them to write a lot less code, and let them sleep at night. Erlang's fault tolerance oriented design is the first piece of what makes it the best choice for the omnipresent, always available Web. The second piece is Erlang's built-in distribution. Distribution is a key part of building a fault tolerant system, because it allows you to handle bigger failures, like a whole server going down, or even a data center entirely. Fault tolerance and distribution are important today, and will be vital in the future of the Web. Erlang is ready. === Learn Erlang If you are new to Erlang, you may want to grab a book or two to get started. Those are my recommendations as the author of Cowboy. ==== The Erlanger Playbook The Erlanger Playbook is an ebook I am currently writing, which covers a number of different topics from code to documentation to testing Erlang applications. It also has an Erlang section where it covers directly the building blocks and patterns, rather than details like the syntax. You can most likely read it as a complete beginner, but you will need a companion book to make the most of it. Buy it from the https://ninenines.eu[Nine Nines website]. ==== Programming Erlang This book is from one of the creator of Erlang, Joe Armstrong. It provides a very good explanation of what Erlang is and why it is so. It serves as a very good introduction to the language and platform. The book is http://pragprog.com/book/jaerlang2/programming-erlang[Programming Erlang], and it also features a chapter on Cowboy. ==== Learn You Some Erlang for Great Good! http://learnyousomeerlang.com[LYSE] is a much more complete book covering many aspects of Erlang, while also providing stories and humor. Be warned: it's pretty verbose. It comes with a free online version and a more refined paper and ebook version. ================================================ FILE: doc/src/guide/flow_diagram.asciidoc ================================================ [[flow_diagram]] == Flow diagram Cowboy is a lightweight HTTP server with support for HTTP/1.1, HTTP/2 and Websocket. It is built on top of Ranch. Please see the Ranch guide for more information about how the network connections are handled. === Overview image::http_req_resp.png[HTTP request/response flowchart] As you can see on the diagram, the client begins by connecting to the server. This step is handled by a Ranch acceptor, which is a process dedicated to accepting new connections. After Ranch accepts a new connection, whether it is an HTTP/1.1 or HTTP/2 connection, Cowboy starts receiving requests and handling them. In HTTP/1.1 all requests come sequentially. In HTTP/2 the requests may arrive and be processed concurrently. When a request comes in, Cowboy creates a stream, which is a set of request/response and all the events associated with them. The protocol code in Cowboy defers the handling of these streams to stream handler modules. When you configure Cowboy you may define one or more module that will receive all events associated with a stream, including the request, response, bodies, Erlang messages and more. By default, Cowboy comes configured with a stream handler called `cowboy_stream_h`. This stream handler will create a new process for every request coming in, and then communicate with this process to read the body or send a response back. The request process executes middlewares. By default, the request process executes the router and then the handlers. Like stream handlers, middlewares may also be customized. A response may be sent at almost any point in this diagram. If the response must be sent before the stream is initialized (because an error occurred early, for example) then stream handlers receive a special event indicating this error. === Protocol-specific headers Cowboy takes care of protocol-specific headers and prevents you from sending them manually. For HTTP/1.1 this includes the `transfer-encoding` and `connection` headers. For HTTP/2 this includes the colon headers like `:status`. Cowboy will also remove protocol-specific headers from requests before passing them to stream handlers. Cowboy tries to hide the implementation details of all protocols as well as possible. === Number of processes per connection By default, Cowboy will use one process per connection, plus one process per set of request/response (called a stream, internally). The reason it creates a new process for every request is due to the requirements of HTTP/2 where requests are executed concurrently and independently from the connection. The frames from the different requests end up interleaved on the single TCP connection. The request processes are never reused. There is therefore no need to perform any cleanup after the response has been sent. The process will terminate and Erlang/OTP will reclaim all memory at once. Cowboy ultimately does not require more than one process per connection. It is possible to interact with the connection directly from a stream handler, a low level interface to Cowboy. They are executed from within the connection process, and can handle the incoming requests and send responses. This is however not recommended in normal circumstances, as a stream handler taking too long to execute could have a negative impact on concurrent requests or the state of the connection itself. === Date header Because querying for the current date and time can be expensive, Cowboy generates one 'Date' header value every second, shares it to all other processes, which then simply copy it in the response. This allows compliance with HTTP/1.1 with no actual performance loss. === Binaries Cowboy makes extensive use of binaries. Binaries are more efficient than lists for representing strings because they take less memory space. Processing performance can vary depending on the operation. Binaries are known for generally getting a great boost if the code is compiled natively. Please see the HiPE documentation for more details. Binaries may end up being shared between processes. This can lead to some large memory usage when one process keeps the binary data around forever without freeing it. If you see some weird memory usage in your application, this might be the cause. ================================================ FILE: doc/src/guide/getting_started.asciidoc ================================================ [[getting_started]] == Getting started Erlang is more than a language, it is also an operating system for your applications. Erlang developers rarely write standalone modules, they write libraries or applications, and then bundle those into what is called a release. A release contains the Erlang VM plus all applications required to run the node, so it can be pushed to production directly. This chapter walks you through all the steps of setting up Cowboy, writing your first application and generating your first release. At the end of this chapter you should know everything you need to push your first Cowboy application to production. === Prerequisites We are going to use the https://github.com/ninenines/erlang.mk[Erlang.mk] build system. If you are using Windows, please check the http://erlang.mk/guide/installation.html[Installation instructions] to get your environment setup before you continue. === Bootstrap First, let's create the directory for our application. [source,bash] $ mkdir hello_erlang $ cd hello_erlang Then we need to download Erlang.mk. Either use the following command or download it manually. [source,bash] $ wget https://erlang.mk/erlang.mk We can now bootstrap our application. Since we are going to generate a release, we will also bootstrap it at the same time. [source,bash] $ make -f erlang.mk bootstrap bootstrap-rel This creates a Makefile, a base application, and the release files necessary for creating the release. We can already build and start this release. [source,bash] ---- $ make run ... (hello_erlang@127.0.0.1)1> ---- Entering the command `i().` will show the running processes, including one called `hello_erlang_sup`. This is the supervisor for our application. The release currently does nothing. In the rest of this chapter we will add Cowboy as a dependency and write a simple "Hello world!" handler. === Cowboy setup We will modify the 'Makefile' to tell the build system it needs to fetch and compile Cowboy, and that we will use releases: [source,makefile] ---- PROJECT = hello_erlang DEPS = cowboy dep_cowboy_commit = 2.14.2 REL_DEPS = relx DEP_PLUGINS = cowboy include erlang.mk ---- The `DEP_PLUGINS` line tells the build system to load the plugins Cowboy provides. These include predefined templates that we will use soon. The `REL_DEPS` line tells the build system to fetch and build `relx`, the library that will create the release. If you do `make run` now, Cowboy will be included in the release and started automatically. This is not enough however, as Cowboy doesn't do anything by default. We still need to tell Cowboy to listen for connections. === Listening for connections First we define the routes that Cowboy will use to map requests to handler modules, and then we start the listener. This is best done at application startup. Open the 'src/hello_erlang_app.erl' file and add the necessary code to the `start/2` function to make it look like this: [source,erlang] ---- start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [{"/", hello_handler, []}]} ]), {ok, _} = cowboy:start_clear(my_http_listener, [{port, 8080}], #{env => #{dispatch => Dispatch}} ), hello_erlang_sup:start_link(). ---- Routes are explained in details in the xref:routing[Routing] chapter. For this tutorial we map the path `/` to the handler module `hello_handler`. This module doesn't exist yet. Build and start the release, then open http://localhost:8080 in your browser. You will get a 500 error because the module is missing. Any other URL, like http://localhost:8080/test, will result in a 404 error. === Handling requests Cowboy features different kinds of handlers, including REST and Websocket handlers. For this tutorial we will use a plain HTTP handler. Generate a handler from a template: [source,bash] $ make new t=cowboy.http n=hello_handler Then, open the 'src/hello_handler.erl' file and modify the `init/2` function like this to send a reply. [source,erlang] ---- init(Req0, State) -> Req = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, <<"Hello Erlang!">>, Req0), {ok, Req, State}. ---- What the above code does is send a 200 OK reply, with the Content-type header set to `text/plain` and the response body set to `Hello Erlang!`. If you run the release and open http://localhost:8080 in your browser, you should get a nice `Hello Erlang!` displayed! ================================================ FILE: doc/src/guide/handlers.asciidoc ================================================ [[handlers]] == Handlers Handlers are Erlang modules that handle HTTP requests. === Plain HTTP handlers The most basic handler in Cowboy implements the mandatory `init/2` callback, manipulates the request, optionally sends a response and then returns. This callback receives the xref:req[Req object] and the initial state defined in the xref:routing[router configuration]. A handler that does nothing would look like this: [source,erlang] ---- init(Req, State) -> {ok, Req, State}. ---- Despite sending no reply, a `204 No Content` response will be sent to the client, as Cowboy makes sure that a response is sent for every request. We need to use the Req object to reply. [source,erlang] ---- init(Req0, State) -> Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain">> }, <<"Hello World!">>, Req0), {ok, Req, State}. ---- Cowboy will immediately send a response when `cowboy:reply/4` is called. We then return a 3-tuple. `ok` means that the handler ran successfully. We also give the modified Req back to Cowboy. The last value of the tuple is a state that will be used in every subsequent callbacks to this handler. Plain HTTP handlers only have one additional callback, the optional and rarely used `terminate/3`. === Other handlers The `init/2` callback can also be used to inform Cowboy that this is a different kind of handler and that Cowboy should switch to it. To do this you simply need to return the module name of the handler type you want to switch to. Cowboy comes with three handler types you can switch to: xref:rest_handlers[cowboy_rest], xref:ws_handlers[cowboy_websocket] and xref:loop_handlers[cowboy_loop]. In addition to those you can define your own handler types. Switching is simple. Instead of returning `ok`, you simply return the name of the handler type you want to use. The following snippet switches to a Websocket handler: [source,erlang] ---- init(Req, State) -> {cowboy_websocket, Req, State}. ---- === Cleaning up All handler types provide the optional `terminate/3` callback. [source,erlang] ---- terminate(_Reason, _Req, _State) -> ok. ---- This callback is strictly reserved for any required cleanup. You cannot send a response from this function. There is no other return value. This callback is optional because it is rarely necessary. Cleanup should be done in separate processes directly (by monitoring the handler process to detect when it exits). Cowboy does not reuse processes for different requests. The process will terminate soon after this call returns. ================================================ FILE: doc/src/guide/introduction.asciidoc ================================================ [[introduction]] == Introduction Cowboy is a small, fast and modern HTTP server for Erlang/OTP. Cowboy aims to provide a complete xref:modern_web[modern Web stack]. This includes HTTP/1.1, HTTP/2, Websocket, Server-Sent Events and Webmachine-based REST. Cowboy comes with functions for introspection and tracing, enabling developers to know precisely what is happening at any time. Its modular design also easily enable developers to add instrumentation. Cowboy is a high quality project. It has a small code base, is very efficient (both in latency and memory use) and can easily be embedded in another application. Cowboy is clean Erlang code. It includes hundreds of tests and its code is fully compliant with the Dialyzer. It is also well documented and features a Function Reference, a User Guide and numerous Tutorials. === Prerequisites Beginner Erlang knowledge is recommended for reading this guide. Knowledge of the HTTP protocol is recommended but not required, as it will be detailed throughout the guide. === Supported platforms Cowboy is tested and supported on Linux, FreeBSD, Windows and OSX. Cowboy has been reported to work on other platforms, but we make no guarantee that the experience will be safe and smooth. You are advised to perform the necessary testing and security audits prior to deploying on other platforms. Cowboy is developed for Erlang/OTP 24.0 and newer. === License Cowboy uses the ISC License. ---- Copyright (c) 2011-2025, Loïc Hoguin Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ---- === Versioning Cowboy uses http://semver.org/[Semantic Versioning 2.0.0]. === Conventions In the HTTP protocol, the method name is case sensitive. All standard method names are uppercase. Header names are case insensitive. When using HTTP/1.1, Cowboy converts all the request header names to lowercase. HTTP/2 requires clients to send them as lowercase. Any other header name is expected to be provided lowercased, including when querying information about the request or when sending responses. The same applies to any other case insensitive value. ================================================ FILE: doc/src/guide/listeners.asciidoc ================================================ [[listeners]] == Listeners A listener is a set of processes that listens on a port for new connections. Incoming connections get handled by Cowboy. Depending on the connection handshake, one or another protocol may be used. This chapter is specific to Cowboy. Please refer to the https://ninenines.eu/docs/en/ranch/1.8/guide/listeners/[Ranch User Guide] for more information about listeners. Cowboy provides two types of listeners: one listening for clear TCP connections, and one listening for secure TLS connections. Both of them support the HTTP/1.1 and HTTP/2 protocols. === Clear TCP listener The clear TCP listener will accept connections on the given port. A typical HTTP server would listen on port 80. Port 80 requires special permissions on most platforms however so a common alternative is port 8080. The following snippet starts listening for connections on port 8080: [source,erlang] ---- start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [{"/", hello_handler, []}]} ]), {ok, _} = cowboy:start_clear(my_http_listener, [{port, 8080}], #{env => #{dispatch => Dispatch}} ), hello_erlang_sup:start_link(). ---- The xref:getting_started[Getting Started] chapter uses a clear TCP listener. Clients connecting to Cowboy on the clear listener port are expected to use either HTTP/1.1 or HTTP/2. Cowboy supports both methods of initiating a clear HTTP/2 connection: through the Upgrade mechanism (https://tools.ietf.org/html/rfc7540#section-3.2[RFC 7540 3.2]) or by sending the preface directly (https://tools.ietf.org/html/rfc7540#section-3.4[RFC 7540 3.4]). Compatibility with HTTP/1.0 is provided by Cowboy's HTTP/1.1 implementation. === Secure TLS listener The secure TLS listener will accept connections on the given port. A typical HTTPS server would listen on port 443. Port 443 requires special permissions on most platforms however so a common alternative is port 8443. // @todo Make a complete list of restrictions. The function provided by Cowboy will ensure that the TLS options given are following the HTTP/2 RFC with regards to security. For example some TLS extensions or ciphers may be disabled. This also applies to HTTP/1.1 connections on this listener. If this is not desirable, Ranch can be used directly to set up a custom listener. [source,erlang] ---- start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [{"/", hello_handler, []}]} ]), {ok, _} = cowboy:start_tls(my_https_listener, [ {port, 8443}, {certfile, "/path/to/certfile"}, {keyfile, "/path/to/keyfile"} ], #{env => #{dispatch => Dispatch}} ), hello_erlang_sup:start_link(). ---- Clients connecting to Cowboy on the secure listener are expected to use the ALPN TLS extension to indicate what protocols they understand. Cowboy always prefers HTTP/2 over HTTP/1.1 when both are supported. When neither are supported by the client, or when the ALPN extension was missing, Cowboy expects HTTP/1.1 to be used. Cowboy also advertises HTTP/2 support through the older NPN TLS extension for compatibility. Note however that this support will likely not be enabled by default when Cowboy 2.0 gets released. Compatibility with HTTP/1.0 is provided by Cowboy's HTTP/1.1 implementation. === Stopping the listener When starting listeners along with the application it is a good idea to also stop the listener when the application stops. This can be done by calling `cowboy:stop_listener/1` in the application's stop function: [source,erlang] ---- stop(_State) -> ok = cowboy:stop_listener(my_http_listener). ---- === Protocol configuration The HTTP/1.1 and HTTP/2 protocols share the same semantics; only their framing differs. The first is a text protocol and the second a binary protocol. Cowboy doesn't separate the configuration for HTTP/1.1 and HTTP/2. Everything goes into the same map. Many options are shared. // @todo Describe good to know options for both protocols? // Maybe do that in separate chapters? ================================================ FILE: doc/src/guide/loop_handlers.asciidoc ================================================ [[loop_handlers]] == Loop handlers Loop handlers are a special kind of HTTP handlers used when the response can not be sent right away. The handler enters instead a receive loop waiting for the right message before it can send a response. Loop handlers are used for requests where a response might not be immediately available, but where you would like to keep the connection open for a while in case the response arrives. The most known example of such practice is known as long polling. Loop handlers can also be used for requests where a response is partially available and you need to stream the response body while the connection is open. The most known example of such practice is server-sent events, but it also applies to any response that takes a long time to send. While the same can be accomplished using plain HTTP handlers, it is recommended to use loop handlers because they are well-tested and allow using built-in features like hibernation and timeouts. Loop handlers essentially wait for one or more Erlang messages and feed these messages to the `info/3` callback. It also features the `init/2` and `terminate/3` callbacks which work the same as for plain HTTP handlers. === Initialization The `init/2` function must return a `cowboy_loop` tuple to enable loop handler behavior. This tuple may optionally contain the atom `hibernate` to make the process enter hibernation until a message is received. Alternatively, the tuple may optionally contain a positive integer to create a `timeout` message when the process has not received messages for too long. This snippet enables the loop handler: [source,erlang] ---- init(Req, State) -> {cowboy_loop, Req, State}. ---- This also makes the process hibernate: [source,erlang] ---- init(Req, State) -> {cowboy_loop, Req, State, hibernate}. ---- This makes the process time out after 1000ms of idle time. [source,erlang] ---- init(Req, State) -> {cowboy_loop, Req, State, 1000}. ---- === Receive loop Once initialized, Cowboy will wait for messages to arrive in the process' mailbox. When a message arrives, Cowboy calls the `info/3` function with the message, the Req object and the handler's state. The following snippet sends a reply when it receives a `reply` message from another process, or waits for another message otherwise. [source,erlang] ---- info({reply, Body}, Req, State) -> cowboy_req:reply(200, #{}, Body, Req), {stop, Req, State}; info(_Msg, Req, State) -> {ok, Req, State, hibernate}. ---- Do note that the `reply` tuple here may be any message and is simply an example. This callback may perform any necessary operation including sending all or parts of a reply, and will subsequently return a tuple indicating if more messages are to be expected. The callback may also choose to do nothing at all and just skip the message received. If a reply is sent, then the `stop` tuple should be returned. This will instruct Cowboy to end the request. Otherwise an `ok` tuple should be returned. === Streaming loop Another common case well suited for loop handlers is streaming data received in the form of Erlang messages. This can be done by initiating a chunked reply in the `init/2` callback and then using `cowboy_req:chunk/2` every time a message is received. The following snippet does exactly that. As you can see a chunk is sent every time an `event` message is received, and the loop is stopped by sending an `eof` message. [source,erlang] ---- init(Req, State) -> Req2 = cowboy_req:stream_reply(200, Req), {cowboy_loop, Req2, State}. info(eof, Req, State) -> {stop, Req, State}; info({event, Data}, Req, State) -> cowboy_req:stream_body(Data, nofin, Req), {ok, Req, State}; info(_Msg, Req, State) -> {ok, Req, State}. ---- === Cleaning up Please refer to the xref:handlers[Handlers chapter] for general instructions about cleaning up. === Hibernate To save memory, you may hibernate the process in between messages received. This is done by returning the atom `hibernate` as part of the `loop` tuple callbacks normally return. Just add the atom at the end and Cowboy will hibernate accordingly. === Idle timeout You may activate timeout events by returning a positive integer `N` as part of the `loop` tuple callbacks return. The default value is `infinity`. The `info` callback will be called with the atom `timeout` unless a message is received within `N` milliseconds: [source,erlang] ---- info(timeout, Req, State) -> %% Do something... {ok, Req, State, 1000}. ---- ================================================ FILE: doc/src/guide/middlewares.asciidoc ================================================ [[middlewares]] == Middlewares Cowboy delegates the request processing to middleware components. By default, two middlewares are defined, for the routing and handling of the request, as is detailed in most of this guide. Middlewares give you complete control over how requests are to be processed. You can add your own middlewares to the mix or completely change the chain of middlewares as needed. Cowboy will execute all middlewares in the given order, unless one of them decides to stop processing. === Usage Middlewares only need to implement a single callback: `execute/2`. It is defined in the `cowboy_middleware` behavior. This callback has two arguments. The first is the `Req` object. The second is the environment. Middlewares can return one of three different values: * `{ok, Req, Env}` to continue the request processing * `{suspend, Module, Function, Args}` to hibernate * `{stop, Req}` to stop processing and move on to the next request Of note is that when hibernating, processing will resume on the given MFA, discarding all previous stacktrace. Make sure you keep the `Req` and `Env` in the arguments of this MFA for later use. If an error happens during middleware processing, Cowboy will not try to send an error back to the socket, the process will just crash. It is up to the middleware to make sure that a reply is sent if something goes wrong. === Configuration The middleware environment is defined as the `env` protocol option. In the previous chapters we saw it briefly when we needed to pass the routing information. It is a list of tuples with the first element being an atom and the second any Erlang term. Two values in the environment are reserved: * `listener` contains the name of the listener * `result` contains the result of the processing The `listener` value is always defined. The `result` value can be set by any middleware. If set to anything other than `ok`, Cowboy will not process any subsequent requests on this connection. The middlewares that come with Cowboy may define or require other environment values to perform. You can update the environment by calling the `cowboy:set_env/3` convenience function, adding or replacing a value in the environment. === Routing middleware The routing middleware requires the `dispatch` value. If routing succeeds, it will put the handler name and options in the `handler` and `handler_opts` values of the environment, respectively. === Handler middleware The handler middleware requires the `handler` and `handler_opts` values. It puts the result of the request handling into `result`. ================================================ FILE: doc/src/guide/migrating_from_1.0.asciidoc ================================================ [appendix] == Migrating from Cowboy 1.0 to 2.0 A lot has changed between Cowboy 1.0 and 2.0. The `cowboy_req` interface in particular has seen a massive revamp. Hooks are gone, their functionality can now be achieved via stream handlers. The documentation has seen great work, in particular the manual. Each module and each function now has its own dedicated manual page with full details and examples. === Compatibility Compatibility with Erlang/OTP R16, 17 and 18 has been dropped. Erlang/OTP 19.0 or above is required. It is non-trivial to make Cowboy 2.0 work with older Erlang/OTP versions. Cowboy 2.0 is not compatible with Cowlib versions older than 2.0. It should be compatible with Ranch 1.0 or above, however it has not been tested with Ranch versions older than 1.4. Cowboy 2.0 is tested on Arch Linux, Ubuntu, FreeBSD, Windows and OSX. It is tested with every point release (latest patch release) and also with HiPE on the most recent release. Cowboy 2.0 now comes with Erlang.mk templates. === Features added * The HTTP/2 protocol is now supported. * Cowboy no longer uses only one process per connection. It now uses one process per connection plus one process per request by default. This is necessary for HTTP/2. There might be a slight drop in performance for HTTP/1.1 connections due to this change. * Cowboy internals have largely been reworked in order to support HTTP/2. This opened the way to stream handlers, which are a chain of modules that are called whenever something happens relating to a request/response. * The `cowboy_stream_h` stream handler has been added. It provides most of Cowboy's default behavior. * The `cowboy_compress_h` stream handler has been added. It compresses responses when possible. It's worth noting that it compresses in more cases than Cowboy 1.0 ever did. * Because of the many changes in the internals of Cowboy, many options have been added or modified. Of note is that the Websocket options are now given per handler rather than for the entire listener. * Websocket permessage-deflate compression is now supported via the `compress` option. * Static file handlers will now correctly find files found in '.ez' archives. * Constraints have been generalized and are now used not only in the router but also in some `cowboy_req` functions. Their interface has also been modified to allow for reverse operations and formatting of errors. === Features removed * SPDY support has been removed. Use HTTP/2 instead. * Hooks have been removed. Use xref:streams[stream handlers] instead. * The undocumented `waiting_stream` hack has been removed. It allowed disabling chunked transfer-encoding for HTTP/1.1. It has no equivalent in Cowboy 2.0. Open a ticket if necessary. * Sub protocols still exist, but their interface has largely changed and they are no longer documented for the time being. === Changed behaviors * The handler behaviors have been renamed and are now `cowboy_handler`, `cowboy_loop`, `cowboy_rest` and `cowboy_websocket`. * Plain HTTP, loop, REST and Websocket handlers have had their init and terminate callbacks unified. They now all use the `init/2` and `terminate/3` callbacks. The latter is now optional. The terminate reason has now been documented for all handlers. * The tuple returned to switch to a different handler type has changed. It now takes the form `{Module, Req, State}` or `{Module, Req, State, Opts}`, where `Opts` is a map of options to configure the handler. The timeout and hibernate options must now be specified using this map, where applicable. * All behaviors that used to accept `halt` or `shutdown` tuples as a return value no longer do so. The return value is now a `stop` tuple, consistent across Cowboy. * Middlewares can no longer return an `error` tuple. They have to send the response and return a `stop` tuple instead. * The `known_content_type` REST handler callback has been removed as it was found unnecessary. * Websocket handlers have both the normal `init/2` and an optional `websocket_init/1` function. The reason for that exception is that the `websocket_*` callbacks execute in a separate process from the `init/2` callback, and it was therefore not obvious how timers or monitors should be setup properly. They are effectively initializing the handler before and after the HTTP/1.1 upgrade. * Websocket handlers can now send frames directly from `websocket_init/1`. The frames will be sent immediately after the handshake. * Websocket handler callbacks no longer receive the Req argument. The `init/2` callback still receives it and can be used to extract relevant information. The `terminate/3` callback, if implemented, may still receive the Req (see next bullet point). * Websocket handlers have a new `req_filter` option that can be used to customize how much information should be discarded from the Req object after the handshake. Note that the Req object is only available in `terminate/3` past that point. * Websocket handlers have their timeout default changed from infinity to 60 seconds. === New functions * The `cowboy_req:scheme/1` function has been added. * The `cowboy_req:uri/1,2` function has been added, replacing the less powerful functions `cowboy_req:url/1` and `cowboy_req:host_url/1`. * The functions `cowboy_req:match_qs/2` and `cowboy_req:match_cookies/2` allow matching query string and cookies against constraints. * The function `cowboy_req:set_resp_cookie/3` has been added to complement `cowboy_req:set_resp_cookie/4`. * The functions `cowboy_req:resp_header/2,3` and `cowboy_req:resp_headers/1` have been added. They can be used to retrieve response headers that were previously set. * The function `cowboy_req:set_resp_headers/2` has been added. It allows setting many response headers at once. * The functions `cowboy_req:push/3,4` can be used to push resources for protocols that support it (by default only HTTP/2). === Changed functions * The `cowboy:start_http/4` function was renamed to `cowboy:start_clear/3`. * The `cowboy:start_https/4` function was renamed to `cowboy:start_tls/3`. * Most, if not all, functions in the `cowboy_req` module have been modified. Please consult the changelog of each individual functions. The changes are mainly about simplifying and clarifying the interface. The Req is no longer returned when not necessary, maps are used wherever possible, and some functions have been renamed. * The position of the `Opts` argument for `cowboy_req:set_resp_cookie/4` has changed to improve consistency. It is now the last argument. === Removed functions * The functions `cowboy_req:url/1` and `cowboy_req:host_url/1` have been removed in favor of the new function `cowboy_req:uri/1,2`. * The functions `cowboy_req:meta/2,3` and `cowboy_req:set_meta/3` have been removed. The Req object is now a public map, therefore they became unnecessary. * The functions `cowboy_req:set_resp_body_fun/2,3` have been removed. For sending files, the function `cowboy_req:set_resp_body/2` can now take a sendfile tuple. * Remove many undocumented functions from `cowboy_req`, including the functions `cowboy_req:get/2` and `cowboy_req:set/3`. === Other changes * The correct percent-decoding algorithm is now used for path elements during routing. It will no longer decode `+` characters. * The router will now properly handle path segments `.` and `..`. * Routing behavior has changed for URIs containing latin1 characters. They are no longer allowed. URIs are expected to be in UTF-8 once they are percent-decoded. * Clients that send multiple headers of the same name will have the values of those headers concatenated into a comma-separated list. This is of special importance in the case of the content-type header, as previously only the first value was used in the `content_types_accepted/2` step in REST handlers. * Etag comparison in REST handlers has been fixed. Some requests may now fail when they succeeded in the past. * The `If-*-Since` headers are now ignored in REST handlers if the corresponding `If*-Match` header exist. The former is largely a backward compatible header and this shouldn't create any issue. The new behavior follows the current RFCs more closely. * The static file handler has been improved to handle more special characters on systems that accept them. ================================================ FILE: doc/src/guide/migrating_from_2.0.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.0 to 2.1 Cowboy 2.1 focused on adding features that were temporarily removed in Cowboy 2.0. A number of bugs found in the 2.0 release were also fixed. === Features added * It is now possible to obtain the client TLS certificate and the local IP/port for the connection from the Req object. * Informational responses (1XX responses) can now be sent. They must be sent before initiating the final response. * The `expect: 100-continue` header is now handled automatically. The 100 response will be sent on the first `cowboy_req:read_body/2,3,4` call. This only applies when using the default `cowboy_stream_h` stream handler. === Experimental features added Experimental features are previews of features that will be added in a future release. They are not documented and their interface may change at any time. You are welcome to try them and provide feedback. * The `cowboy_metrics_h` stream handler can be used to extract metrics out of Cowboy. It must be used first in the list of stream handlers, and will record all events related to requests, responses and spawned processes. When the stream terminates it will pass this information to a user-defined callback. * The `cowboy_tracer_h` stream handler can be used to setup automatic tracing of specific requests. You can conditionally enable tracing based on a function, header, path or any other element from the request and the trace will apply to the entire connection and any processes created by it. This is meant to be used for debugging both in tests and production. === Changed behaviors * The `cowboy_rest` handler now implements a mechanism for switching to a different type of handler from any callback where `stop` is also allowed. Switch by returning `{switch_handler, Module}` or `{switch_handler, Module, Opts}`. This is especially useful for switching to `cowboy_loop` for streaming the request or response body. * REST callbacks that do not allow `stop` as a return value are now explicitly listed in the documentation. === New functions * The function `cowboy_req:sock/1` returns the IP/port of the local socket. * The function `cowboy_req:cert/1` returns the client TLS certificate or `undefined` if it isn't available. * The function `cowboy_req:inform/2,3` sends an informational response. === Bugs fixed * Ensure HTTP/2 connections are not closed prematurely when the user code does not read the request body. * Ensure HTTP/1.1 streams are not terminated too early. Their behavior is now consistent with the HTTP/2 code where the stream handler is only terminated when the `stop` command is returned. * Sending zero-sized data from stream handlers or from `cowboy_req:stream_body/3` could lead to issues with HTTP/1.1. This has been fixed. * The final chunk sent by Cowboy when it terminates a chunked body after the handler process exits was not passed through stream handlers, which could lead to issues when `cowboy_compress_h` was being used. This is now corrected. * The stream handler state was discarded in some cases where Cowboy had to send a response or response data automatically when ending a stream. This has now been corrected. * The stream handler callback `terminate/3` will now be called when switching to another protocol using the command `switch_protocol`. This doesn't apply when doing upgrades to HTTP/2 as those occur before the stream is initialized. * Cowlib has been updated to 2.0.1 to fix an issue with Websocket compression when using Erlang/OTP 20.1. Note that at the time of writing all 20.1 versions (from 20.1 to 20.1.4) have issues when compression is enabled. It is expected to work properly from 20.1.5 onward. In the meantime it is recommended to run the plain 20.1 release and disable Websocket compression, or use a release before 20.1. * Cowboy will no longer crash when the `cowboy_clock` process is not running. This can happen when Cowboy is being restarted during upgrades, for example. ================================================ FILE: doc/src/guide/migrating_from_2.1.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.1 to 2.2 Cowboy 2.2 focused on adding features required for writing gRPC servers and on completing test suites for the core HTTP RFCs, fixing many bugs along the way. === Features added * Add support for sending trailers at the end of response bodies. Trailers are additional header fields that may be sent after the body to add more information to the response. Their usage is required in gRPC servers. They are optional and may be discarded in other scenarios (for example if the request goes through an HTTP/1.0 proxy, as HTTP/1.0 does not support trailers). * The `max_skip_body_length` option was added to `cowboy_http`. It controls how much of a request body Cowboy is willing to skip when the handler did not touch it. If the remaining body size is too large Cowboy instead closes the connection. It defaults to 1MB. * The CONNECT and TRACE methods are now rejected as they are currently not implemented and must be handled differently than other methods. They will be implemented in a future release. === New functions * The function `stream_trailers/2` has been added. It terminates a stream and adds trailer fields at the end of the response. A corresponding stream handler command `{trailers, Trailers}` has also been added. === Bugs fixed * Test suites for the core HTTP RFCs RFC7230, RFC7231 and RFC7540 have been completed. Many of the bugs listed here were fixed as a result of this work. * Many HTTP/2 edge cases when clients are misbehaving have been corrected. This includes many cases where the request is malformed (for example when a pseudo-header is present twice). * When the HTTP/2 SETTINGS_INITIAL_WINDOW_SIZE value changes, Cowboy now properly updates the flow control windows. * HTTP/2 could mistakenly log stray messages that actually were expected. This is no longer the case. * We no longer send a GOAWAY frame when the HTTP/2 preface is invalid. * Some values in the Req object of pushed requests were in the wrong type. They are now the expected binary instead of iolist. * A response body was sometimes sent in response to HEAD requests when using HTTP/2. The body is now ignored. * The `max_headers` option for `cowboy_http` was not always respected depending on the contents of the buffer. The limit is now strict. * When an early error occurred on the HTTP/1.1 request line, the partial Req given to stream handlers was missing the `ref` and `peer` information. This has been corrected. * Absolute URIs with a userinfo component, or without an authority component, are now properly rejected for HTTP/1.0 and HTTP/1.1. * Whitespace is no longer allowed in header lines before the colon. * 408 responses to HTTP/1.1 requests now properly include a connection: close header indicating that we are going to close the connection. This header will also be sent for other early errors resulting in the closing of the connection. * When both the transfer-encoding and content-length headers are sent in an HTTP/1.1 request, the transfer-encoding now takes precedence over the content-length header and the latter is removed from the Req object. * A 400 response is now returned when the transfer-encoding header is invalid or contains any transfer-coding other than chunked. * Invalid chunk sizes are now rejected immediately. * Chunk extensions are now limited to 129 characters. They are not used in practice and are still ignored by Cowboy. The limit is not configurable. * The final chunk was mistakenly sent in responses to HEAD requests. This is now corrected. * `OPTIONS *` requests were broken in Cowboy 2.0. They are now working again. Both the routing and `cowboy_req:uri/1,2` have been corrected. * 204 responses no longer include a content-length header. * A packet could be lost when switching to Websocket or any other protocol via the `switch_protocol` command. This is now fixed. * A 426 response will now be sent when a handler requires the client to upgrade to Websocket and the request did not include the required headers. * Both experimental stream handlers `cowboy_metrics_h` and `cowboy_tracer_h` received a number of fixes and improvements. ================================================ FILE: doc/src/guide/migrating_from_2.10.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.10 to 2.11 Cowboy 2.11 contains a variety of new features and bug fixes. Nearly all previously experimental features are now marked as stable, including Websocket over HTTP/2. Included is a fix for an HTTP/2 protocol CVE. Cowboy 2.11 requires Erlang/OTP 24.0 or greater. Cowboy is now using GitHub Actions for CI. The main reason for the move is to reduce costs by no longer having to self-host CI runners. The downside is that GitHub runners are less reliable and timing dependent tests are now more likely to fail. === Features added * A new HTTP/2 option `max_cancel_stream_rate` has been added to control the rate of stream cancellation the server will accept. By default Cowboy will accept 500 cancelled streams every 10 seconds. * A new stream handler `cowboy_decompress_h` has been added. It allows automatically decompressing incoming gzipped request bodies. It includes options to protect against zip bombs. * Websocket over HTTP/2 is no longer considered experimental. Note that the `enable_connect_protocol` option must be set to `true` in order to use Websocket over HTTP/2 for the time being. * Automatic mode for reading request bodies has been documented. In automatic mode, Cowboy waits indefinitely for data and sends a `request_body` message when data comes in. It mirrors `{active, once}` socket modes. This is ideal for loop handlers and is also used internally for HTTP/2 Websocket. * Ranged requests support is no longer considered experimental. It was added in 2.6 to both `cowboy_static` and `cowboy_rest`. Ranged responses can be produced either automatically (for the `bytes` unit) or manually. REST flowcharts have been updated with the new callbacks and steps related to handling ranged requests. * A new HTTP/1.1 and HTTP/2 option `reset_idle_timeout_on_send` has been added. When enabled, the `idle_timeout` will be reset every time Cowboy sends data to the socket. * Loop handlers may now return a timeout value in the place of `hibernate`. Timeouts behave the same as in `gen_server`. * The `generate_etag` callback of REST handlers now accepts `undefined` as a return value to allow conditionally generating etags. * The `cowboy_compress_h` options `compress_threshold` and `compress_buffering` are no longer considered experimental. They were de facto stable since 2.6 as they already were documented. * Functions `cowboy:get_env/2,3` have been added. * Better error messages have been added when trying to send a 204 or 304 response with a body; when attempting to send two responses to a single request; when trying to push a response after the final response; when trying to send a `set-cookie` header without using `cowboy_req:set_resp_cookie/3,4`. === Features removed * Cowboy will no longer include the NPN extension when starting a TLS listener. This extension has long been deprecated and replaced with the ALPN extension. Cowboy will continue using the ALPN extension for protocol negotiation. === Bugs fixed * A fix was made to address the HTTP/2 CVE CVE-2023-44487 via the new HTTP/2 option `max_cancel_stream_rate`. * HTTP/1.1 requests that contain both a content-length and a transfer-encoding header will now be rejected to avoid security risks. Previous behavior was to ignore the content-length header as recommended by the HTTP RFC. * HTTP/1.1 connections would sometimes use the wrong timeout value to determine whether the connection should be closed. This resulted in connections staying up longer than intended. This should no longer be the case. * Cowboy now reacts to socket errors immediately for HTTP/1.1 and HTTP/2 when possible. Cowboy will notice when connections have been closed properly earlier than before. This also means that the socket option `send_timeout_close` will work as expected. * Shutting down HTTP/1.1 pipelined requests could lead to the current request being terminated before the response has been sent. This has been addressed. * When using HTTP/1.1 an invalid Connection header will now be rejected with a 400 status code instead of crashing. * The documentation now recommends increasing the HTTP/2 option `max_frame_size_received`. Cowboy currently uses the protocol default but will increase its default in a future release. Until then users are recommended to set the option to ensure larger requests are accepted and processed with acceptable performance. * Cowboy could sometimes send HTTP/2 WINDOW_UPDATE frames twice in a row. Now they should be consolidated. * Cowboy would sometimes send HTTP/2 WINDOW_UPDATE frames for streams that have stopped internally. This should no longer be the case. * The `cowboy_compress_h` stream handler will no longer attempt to compress responses that have an `etag` header to avoid caching issues. * The `cowboy_compress_h` will now always add `accept-encoding` to the `vary` header as it indicates that responses may be compressed. * Cowboy will now remove the `trap_exit` process flag when HTTP/1.1 connections upgrade to Websocket. * Exit gracefully instead of crashing when the socket gets closed when reading the PROXY header. * Missing `cowboy_stream` manual pages have been added. * A number of fixes were made to documentation and examples. ================================================ FILE: doc/src/guide/migrating_from_2.11.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.11 to 2.12 Cowboy 2.12 contains a small security improvement for the HTTP/2 protocol. Cowboy 2.12 requires Erlang/OTP 24.0 or greater. === Features added * A new HTTP/2 option `max_fragmented_header_block_size` has been added to limit the size of header blocks that are sent over multiple HEADERS and CONTINUATION frames. * Update Cowlib to 2.13.0. ================================================ FILE: doc/src/guide/migrating_from_2.12.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.12 to 2.13 Cowboy 2.13 focuses on improving the performance of Websocket, as well as the HTTP protocols. It also contains a variety of new features and bug fixes. In addition, Cowboy 2.13 is the first Cowboy version that contains the experimental HTTP/3 support. Cowboy 2.13 requires Erlang/OTP 24.0 or greater. === Features added * The option `dynamic_buffer` has been added. When enabled, Cowboy will dynamically change the `buffer` socket option based on how much data it receives. It will start at 1024 bytes and go up to 131072 bytes by default. This applies to HTTP/1.1, HTTP/2 and Websocket. The performance gains are very important depending on the scenario. * HTTP/1.1 and HTTP/2 now accept the `hibernate` option. When set the connection process will automatically hibernate to reduce memory usage at a small performance cost. * The `protocols` and `alpn_default_protocol` protocol options have been added to control exactly which HTTP protocols are allowed over clear and TLS listeners. * The Websocket `max_frame_size` option can now be set dynamically via the `set_options` command. This allows configuring a smaller max size and increase it after authentication or other checks. * `cowboy_req:set_resp_headers` now accept lists of headers. This can be used to simplify passing headers coming from client applications such as Gun. Note that the set-cookie header cannot be provided using this function. * `cowboy_rest` now always sets the allow header. * Update Ranch to 1.8.1. * Update Cowlib to 2.14.0. * When using Hex.pm, version check requirements will now be relaxed. Cowboy will accept any Ranch version from 1.8.0 to 2.2.0 as well as future 2.x versions. Similarly, any Cowlib 2.x version from 2.14.0 will be accepted. === Experimental features added * Experimental support for HTTP/3 has been added, including Websocket over HTTP/3. HTTP/3 support is disabled by default; to enable, the environment variable COWBOY_QUICER must be set at compile-time. === Features deprecated * The `inactivity_timeout` option is now deprecated for all protocols. It is de facto ignored when `hibernate` is enabled. === Optimisation-related changes * The behavior of the `idle_timeout` timer has been changed for HTTP/2 and Websocket. Cowboy used to reset the timer on every data packet received from the socket. Now Cowboy will check periodically whether new data was received in the interval. * URI and query string hex encoding and decoding has been optimised. * Websocket UTF-8 validation of text frames has been optimised. * Websocket unmasking has been optimised. === Bugs fixed * HTTP/1.1 upgrade to HTTP/2 is now disabled over TLS, as HTTP/2 over TLS must be negotiated via ALPN. * `cowboy_req:filter_cookies` could miss valid cookies. It has been corrected. * HTTP/1.1 could get to a state where it would stop receiving data from the socket, or buffer the data without processing it, and the connection eventually time out. This has been fixed. * Websocket did not compress zero-length frames properly. This resulted in decompression errors in the client. This has been corrected. * Websocket compression will now be disabled when only the server sets `client_max_window_bits`, as otherwise decompression errors will occur. * Websocket will now apply `max_frame_size` both to compressed frames as well as the uncompressed payload. Cowboy will stop decompressing when the limit is reached. * Cowboy now properly handles exits of request processes that occurred externally (e.g. via `exit/2`). * Invalid return values from `content_types_provided` could result in an atom sent to the socket, leading to a cryptic error message. The invalid value will now result in a better error message. ================================================ FILE: doc/src/guide/migrating_from_2.13.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.13 to 2.14 Cowboy 2.14 adds experimental support for HTTP/3 WebTransport based on the most recent draft. It also has a new data delivery mechanism for HTTP/2 and HTTP/3 Websocket, providing better performance. Cowboy 2.14 requires Erlang/OTP 24.0 or greater. === Features added * The `relay` data delivery mechanism has been added to HTTP/2 and HTTP/3 protocols. Using this mechanism lets the Websocket protocol bypass stream handlers to forward data from the connection process to the Websocket session process, as well as better manage HTTP/2's flow control. This results in a noticeable performance improvement. This new mechanism can be used by all sub-protocols built on top of HTTP/2 or HTTP/3 such as Websocket or the upcoming HTTP/2 WebTransport. * The `last_modified` callback of REST handlers now accepts `undefined` as a return value to allow conditionally providing a timestamp. === Experimental features added * Experimental support for HTTP/3 WebTransport has been added, based on the most recent RFC drafts. The implementation should also be compatible with earlier drafts that are currently in use by some browsers. Both HTTP/3 and HTTP/3 WebTransport are disabled by default; to enable, the environment variable COWBOY_QUICER must be set at compile-time, and a number of options must be provided at run time, including `enable_connect_protocol`, `h3_datagram`, `wt_max_sessions` and for earlier drafts `enable_webtransport`. The test suite is the best place to get started at this time. === Optimisation-related changes * The `dynamic_buffer` option introduced in the previous release has been tweaked to start at 512 bytes and have its value changed less abruptly. This is based on additional work done implementing the same feature in RabbitMQ. * The static file handler will now use `raw` mode to read file information to avoid a bottleneck when querying the file server. === Bugs fixed * It was possible for Websocket to fail to enable active mode again after it had been disabled. This has been fixed. ================================================ FILE: doc/src/guide/migrating_from_2.14.asciidoc ================================================ [appendix] == Changes since Cowboy 2.14 The following patch versions were released since Cowboy 2.14: === Cowboy 2.14.2 Cowboy compiled without `COWBOY_QUICER` set would have a number of Dialyzer errors. Now in that scenario the HTTP/3 code is fully behind ifdefs and Dialyzer no longer complains. Now when `COWBOY_QUICER` isn't set: * `cowboy:start_quic/3` is no longer defined. * `cowboy_http3` compiles to an empty module. * `cowboy_quicer` compiles to an empty module. === Cowboy 2.14.1 HTTP/2 Websocket did not call `terminate/3` on abrupt socket close (without a close frame being sent first). This is now fixed. Do note however that the Websocket session process must trap exits to call `terminate/3`. ================================================ FILE: doc/src/guide/migrating_from_2.2.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.2 to 2.3 Cowboy 2.3 focused on making the Cowboy processes behave properly according to OTP principles. This version is a very good milestone toward that goal and most of everything should now work. Release upgrades and a few details will be improved in future versions. === Features added * Add support for all functions from the module `sys`. Note that Cowboy currently does not implement the `sys` debugging mechanisms as tracing is recommended instead. * Add a `max_frame_size` option for Websocket handlers to close the connection when the client attempts to send a frame that's too large. It currently defaults to `infinity` to avoid breaking existing code but will be changed in a future version. * Update Cowlib to 2.2.1. * Add support for the 308 status code and a test suite for RFC7538 where it is defined. === Bugs fixed * Ensure timeout options accept the value `infinity` as documented. * Properly reject HTTP/2 requests with an invalid content-length header instead of simply crashing. * When switching from HTTP/1.1 to Websocket or user protocols all the messages in the mailbox were flushed. Only messages specific to `cowboy_http` should now be flushed. * Parsing of the x-forwarded-for header has been corrected. It now supports IPv6 addresses both with and without port. * Websocket subprotocol tokens are now parsed in a case insensitive manner, according to the spec. * Cookies without values are now allowed. For example `Cookie: foo`. * Colons are now allowed within path segments in routes provided to `cowboy_router:compile/1` as long as they are not the first character of the path segment. * The `cowboy_req:delete_resp_header/2` function will no longer crash when no response header was set before calling it. * A miscount of the output HTTP/2 flow control window has been fixed. It prevented sending the response body fully to some clients. The issue only affected response bodies sent as iolists. ================================================ FILE: doc/src/guide/migrating_from_2.3.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.3 to 2.4 Cowboy 2.4 focused on improving the HTTP/2 implementation. All existing tests from RFC7540 and the h2spec test suite now all pass. Numerous options have been added to control SETTINGS and related behavior. In addition experimental support for Websocket over HTTP/2 was added. === Features added * Add experimental support for Websocket over HTTP/2. You can use the `enable_connect_protocol` option to enable. It implements the following draft: https://tools.ietf.org/html/draft-ietf-httpbis-h2-websockets-01 * Add options `max_decode_table_size` and `max_encode_table_size` to restrict the size of the HPACK compression dictionary. * Add option `max_concurrent_streams` to restrict the number of HTTP/2 streams that can be opened concurrently. * Add options `initial_connection_window_size` and `initial_stream_window_size` to restrict the size of the HTTP/2 request body buffers for the whole connection and per stream, respectively. * Add options `max_frame_size_received` and `max_frame_size_sent` to restrict the size of HTTP/2 frames. * Add option `settings_timeout` to reject clients that did not send a SETTINGS ack. Note that this currently may only occur at the beginning of the connection. * Update Ranch to 1.5.0 * Update Cowlib to 2.3.0 === Bugs fixed * Fix the END_STREAM flag for informational responses when using HTTP/2. * Receive and ignore HTTP/2 request trailers if any for HTTP/2 requests. Request trailer information will be propagated to the user code in a future release. * Reject WINDOW_UPDATE frames that are sent after the client sent an RST_STREAM. Note that Cowboy will not keep state information about terminated streams forever and so the behavior might differ depending on when the stream was reset. * Reject streams that depend on themselves. Note that Cowboy currently does not implement HTTP/2's priority mechanisms so this issue was harmless. * Reject HTTP/2 requests where the body size is different than the content-length value. Note that due to how Cowboy works some requests might go through regardless, for example when the user code does not read the request body. * Fix all existing test failures from RFC7540. This was mostly incorrect test cases or intermittent failures. ================================================ FILE: doc/src/guide/migrating_from_2.4.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.4 to 2.5 Cowboy 2.5 focused on making the test suites pass. A variety of new features, fixes and improvements have also been worked on. === Features added * Add option `linger_timeout` to control how long Cowboy will wait before closing the socket when shutting down the connection. This helps avoid the TCP reset problem HTTP/1.1 suffers from. The default is now 1000 ms. * It is now possible to stream a response body without using chunked transfer-encoding when the protocol is HTTP/1.1. To enable this behavior, simply pass the content-length header with the expected size when initiating the streamed response. * Update Ranch to 1.6.2 * Update Cowlib to 2.6.0 === Experimental features added * Websocket handlers now feature a commands-based interface. The return value from the callbacks can now take the form `{Commands, State}` where `Commands` can be frames to be sent or commands yet to be introduced. New commands will be available only through this new interface. * Add the `{active, boolean()}` Websocket handler command. It allows disabling reading from the socket when `false` is returned. `true` reenables reading from the socket. * Add the protocol option `logger` that allows configuring which logger module will be used. The logger module must follow the interface of the new `logger` module in Erlang/OTP 21, or be set to `error_logger` to keep the old behavior. A similar transport option exists in Ranch 1.6; both options are necessary to override Cowboy's default behavior completely. * Add the `{log, Level, Format, Args}` stream handler command. Making it a command rather than a direct call will simplify silencing particular log messages. === New functions * The function `cowboy_req:stream_events/3` streams one or more text/event-stream events, encoding them automatically. * The functions `cowboy_req:read_and_match_urlencoded_body/2,3` can be used to read, parse and match application/x-www-form-urlencoded request bodies, in a similar way to `cowboy_req:match_qs/2`. === Bugs fixed * Fix Erlang/OTP 21 warnings. * Ensure that the port number is always defined in the Req object. When it is not provided in the request, the default port number for the protocol being used will be set. * Ensure stream handlers can run after `cowboy_stream_h`. * Honor the SETTINGS_ENABLE_PUSH HTTP/2 setting: don't send PUSH frames to clients that disabled it. * Fix HTTP/2 `settings_timeout` option when the value is set to `infinity`. * HTTP/1.1 responses will no longer include a trailer header when the request had no te header. * HTTP/1.1 204 responses no longer send the transfer-encoding header when `cowboy_req:stream_reply/2,3` is used to send a response. * Improve HTTP/1.1 keepalive handling to avoid processing requests that follow the final request that will receive a response. * Improve the validation of HTTP/1.1 absolute-form requests. * When the `switch_protocol` is used after a response was sent, Cowboy will no longer attempt to send the 101 informational response for the protocol upgrade. This caused a crash of the connection previously. * Errors that occur when a callback returned by `content_types_provided` does not exist have been improved. * Prevent annoying error logs when using sendfile in Erlang/OTP 20 and lower. * Add missing frame types to `websocket_handle`. * A test suite has been added for RFC8297 to ensure that 103 informational responses can be sent. * Numerous test cases have been fixed, improved or removed in order to make the test suites pass. Most of the failures were caused by broken tests. * Some misguiding or incorrect statements in the documentation have been removed or clarified. ================================================ FILE: doc/src/guide/migrating_from_2.5.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.5 to 2.6 Cowboy 2.6 greatly refactored the HTTP/2 code, a large part of which was moved to Cowlib and is now used by both the Cowboy server and the Gun client. A large number of tickets were also closed which resulted in many bugs fixed and many features and options added, although some of them are still experimental. === Features added * Add support for the PROXY protocol header. It can be enabled via the `proxy_header` option. The proxy information can then be found under the `proxy_info` key in the Req object. * Allow using sendfile tuples in `cowboy_req:stream_body/3` and in the data command in stream handlers. The only caveat is that when using `cowboy_compress_h` the sendfile tuples may have to be converted to in-memory data in order to compress them. This is the case for gzip compression. * The stream handlers `cowboy_stream_h` and `cowboy_compress_h` are now documented. * Add the `chunked` option to allow disabling chunked transfer-encoding for HTTP/1.1 connections. * Add the `http10_keepalive` option to allow disabling keep-alive for HTTP/1.0 connections. * Add the `idle_timeout` option for HTTP/2. * Add the `sendfile` option to both HTTP/1.1 and HTTP/2. It allows disabling the sendfile syscall entirely for all connections. It is recommended to disable sendfile when using VirtualBox shared folders. * Add the `rate_limited/2` callback to REST handlers. * Add the `deflate_opts` option to Websocket handlers that allows configuring deflate options for the permessage-deflate extension. * Add the `charset` option to `cowboy_static`. * Add support for the SameSite cookie attribute. * Update Ranch to 1.7.0 * Update Cowlib to 2.7.0 === Experimental features added * Add support for range requests (RFC7233) in REST handlers. This adds two new callbacks: `ranges_accepted/2` and `range_satisfiable/2` along with the user-specified `ProvideRangeCallback/2`. * Add automatic handling of range requests to REST handlers that return the callback `auto` from `ranges_accepted/2`. Cowboy will call the configured `ProvideCallback` and then split the output automatically for the ranged response. * Enable range requests support in `cowboy_static`. * Add the `{deflate, boolean()}` Websocket handler command to disable permessage-deflate compression temporarily. * Add the `compress_threshold` option which allows configuring how much data must be present in a response body to compress it. This only applies to non-streamed bodies at this time. * Add the `compress_buffering` option which allows controlling whether some buffering may be done when streaming a response body. Change the default behavior to not buffer to make sure it works by default in all scenarios. * Add the `{set_options, map()}` command to stream handlers and Websocket handlers. This can be used to update options on a per-request basis. Allow overriding the `idle_timeout` option for both HTTP/1.1 and Websocket, the `cowboy_compress_h` options for HTTP/1.1 and HTTP/2 and the `chunked` option for HTTP/1.1. === Bugs fixed * Do not send a content-length automatically with 304 responses. This status code allows a content-length that corresponds to what would have been sent for a 200 response, but is never followed by a body. * HTTP/2 streams are now terminated once the body has been sent fully, instead of immediately once the stop command is returned (by default when the request process exits). Metrics will therefore more accurately represent when a stream ended. * Terminate connection processes gracefully when the parent process exists or when sys:terminate/2,3 is called. * Automatically ignore the boundary parameter of multipart media types when using REST handlers. This is a special parameter that may change with all requests and cannot be predicted. * Fix parsing of the accept header when it contains charset parameters. They are case insensitive and will now be lowercased, like for accept-charset and content-type. * Handle the charset parameter using `charsets_provided` when it is present in the accept header when using REST handlers. * Don't select charsets when the q-value is 0 in REST handlers. * Handle accept-charset headers that include a wildcard in REST handlers. * Only send a charset header when the content-type negotiated is of type text in REST handlers. * Remove the default charset iso-8859-1 from REST handlers when no other is provided. This has been removed from the HTTP specifications for a long time. * Many cases where a content-type header was sent unnecessarily in the REST handlers response have been fixed. * Handle error_response commands in `cowboy_metrics_h`. * A number of types and function specifications were fixed or improved. Dialyzer is now run against both the code and tests to help uncover issues. * An undefined `cowboy_router` behavior has been documented. ================================================ FILE: doc/src/guide/migrating_from_2.6.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.6 to 2.7 Cowboy 2.7 improves the HTTP/2 code with optimizations around the sending of DATA and WINDOW_UPDATE frames; graceful shutdown of the connection when the client is going away; and rate limiting mechanisms. New options and mechanisms have also been added to control the amount of memory Cowboy ends up using with both HTTP/1.1 and HTTP/2. Much, but not all, of this work was done to address HTTP/2 CVEs about potential denial of service. In addition, many of the experimental features introduced in previous releases have been marked stable and are now documented. Cowboy 2.7 requires Erlang/OTP 20.0 or greater. === Features added * Cowboy is now compatible with both Ranch 1.7 and the upcoming Ranch 2.0. * The number of HTTP/2 WINDOW_UPDATE frames Cowboy sends has been greatly reduced. Cowboy now applies heuristics to determine whether it is necessary to update the window, based on the current window size and the amount of data requested by streams (the `cowboy_req:read_body/2` length for example). Six new options have been added to control this behavior: `connection_window_margin_size`, `connection_window_update_threshold`, `max_connection_window_size`, `max_stream_window_size`, `stream_window_margin_size` and `stream_window_update_threshold`. * HTTP/2 connections will now be shut down gracefully when receiving a GOAWAY frame. Cowboy will simply wait for existing streams to finish before closing the connection. * Functions that stream the response body now have backpressure applied. They now wait for a message to be sent back. The message will be held off when using HTTP/2 and the buffer sizes exceed either `max_connection_buffer_size` or `max_stream_buffer_size`. For HTTP/1.1 the data is sent synchronously and we rely instead on the TCP backpressure. * A new HTTP/2 option `stream_window_data_threshold` can be used to control how little the DATA frames that Cowboy sends can get. By default Cowboy will wait for the window to be large enough to send either everything queued or to reach the default maximum frame size of 16384 bytes. * A new HTTP/2 option `max_receive_frame_rate` can be used to control how fast the server is willing to receive frames. By default it will accept 1000 frames every 10 seconds. * A new HTTP/2 option `max_reset_stream_rate` can be used to control the rate of errors the server is willing to accept. By default it will accept 10 stream resets every 10 seconds. * Flow control for incoming data has been implemented for HTTP/1.1. Cowboy will now wait for the user code to ask for the request body before reading it from the socket. The option `initial_stream_flow_size` controls how much data Cowboy will read without being asked. * The HTTP/1.1 and HTTP/2 option `logger` is now documented. * The Websocket option `validate_utf8` has been added. It can be used to disable the expensive UTF-8 validation for incoming text and close frames. * The experimental commands based Websocket interface is now considered stable and has been documented. The old interface is now deprecated. * A new Websocket handler command `shutdown_reason` can be used to change the normal exit reason of Websocket processes. By default `normal` is used; with this command the exit reason can be changed to `{shutdown, ShutdownReason}`. * The experimental stream handlers `cowboy_metrics_h` and `cowboy_tracer_h` are now considered stable and have been documented. * The stream handler commands `set_options` and `log` are now considered stable and have been documented. * The router is now capable of retrieving dispatch rules directly from the `persistent_term` storage (available starting from Erlang/OTP 21.2). * Support for the status codes 208 and 508 has been added. * Update Ranch to 1.7.1. * Update Cowlib to 2.8.0. === Experimental features added * It is now possible to read the response body from any process, as well as doing any other `cowboy_req` operations. Since this is not recommended due to race condition concerns this feature will always remain experimental. === New functions * The function `cowboy_req:filter_cookies/2` has been added. It can be called before parsing/matching cookies in order to filter out undesirables. The main reason for doing this is to avoid most parse errors that may occur when dealing with Web browsers (which have a string-based Javascript interface to cookies that is very permissive of invalid content) and to be able to recover in other cases. * The function `cowboy_req:cast/2` has been added. It can be used to send events to stream handlers. === Bugs fixed * A number of fixes and additions were made to address the HTTP/2 CVEs CVE-2019-9511 through CVE-2019-9518, except for CVE-2019-9513 which required no intervention as the relevant protocol feature is not implemented by Cowboy. * The HTTP/2 connection window could become larger than the protocol allows, leading to errors. This has been corrected. * The presence of empty header names in HTTP/2 requests now results in the request to be rejected. * Cowboy will now remove headers specific to HTTP/1.1 (the hop by hop headers such as connection or upgrade) when building an HTTP/2 response. * A bug in the HTTP/2 code that resulted in the failure to fully send iolist response bodies has been fixed. Cowboy would just wait indefinitely in those cases. * It was possible for a final empty HTTP/2 DATA frame to get stuck and never sent when the window reached 0 and the remote end did not increase the window anymore. This has been corrected. * Cowboy now uses the host header when the HTTP/2 :authority pseudo header is missing. A common scenario where this occurs is when proxies translate incoming HTTP/1.1 requests to HTTP/2. * HTTP/1.1 connections are now properly closed when the user code sends less data than advertised in the response headers. * Cowboy will now close HTTP/1.1 connections immediately when a header line is missing a colon separator. Previously it was waiting for more data. * It was possible for Cowboy to receive stray timeout messages for HTTP/1.1 connections, resulting in crashes. The timeout handling in HTTP/1.1 has been reworked and the issue should no longer occur. * The type for the Req object has been updated to accept custom fields as was already documented. * The authentication scheme returned when parsing the authorization header is now case insensitive, which means it will be returned as lowercase. * Cowboy no longer discards data that follows a Websocket upgrade request. Note that the protocol does not allow sending data before receiving a successful Websocket upgrade response, so this fix is more out of principle rather than to fix a real world issue. * The `cowboy_static` handler will now properly detect the type of files that have an uppercase or mixed extension component. * The `cowboy_static` handler is now consistent across all supported platforms. It now explicitly rejects `path_info` components that include a forward slash, backward slash or NUL character. * The update to Ranch 1.7.1 fixes an issue with the PROXY protocol that would cause checksum verification to fail. * The HTTP/1.1 error reason for `stream_error` mistakenly contained an extra element. It has now been removed. * The `PartialReq` given to the `early_error` stream handler callback now includes headers when the protocol is HTTP/2. * A bug where the stacktrace was incorrect in error messages has been fixed. The problem occurred when an exception occurred in the handler's terminate callback. * The REST flowchart for POST, PATCH and PUT has received a number of fixes and had to be greatly reworked as a result. When the method is PUT, we do not check for the location header in the response. When the resource doesn't exist and the method was PUT the flowchart was largely incorrect. A 415 response may occur after the `content_types_accepted` callback and was missing from the flowchart. * The documentation for `content_types_accepted` now includes the media type wildcard that was previously missing. * The documentation for a type found in `cow_cookie` was missing. A manual page for `cow_cookie` was added and can be found in the Cowlib documentation. ================================================ FILE: doc/src/guide/migrating_from_2.7.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.7 to 2.8 Cowboy 2.8 contains many optimizations for all protocols. HTTP/1.1 has received the largest improvements and Cowboy will now be able to handle noticeably more requests. Thanks to the folks at Stressgrid for helping identify that the performance was lower than it should have been and for benchmarking my many changes and experiments. Cowboy 2.8 also contains a small number of tweaks and bug fixes. Cowboy 2.8 is the first Cowboy release, ever, to be consistently green on all tested platforms. This is mostly due to the reworking of some test cases, but a few bugs were discovered and fixed in the process. Cowboy 2.8 requires Erlang/OTP 22.0 or greater. It may also work out of the box with Erlang/OTP 21.3 but this was not tested and is not supported. === Features added * Cowboy will now use `active,N` instead of `active,once` to receive data from the socket. This greatly improves the performance and allows Cowboy to process more requests, especially for HTTP/1.1. The `active_n` protocol option can be configured to change the `active,N` value. The default is 100 for all protocols. * Add a `linger_timeout` option for HTTP/2. The default is 1000, or one second. This helps ensure that the final GOAWAY frame will be properly received by clients. * The function `cowboy_req:parse_header/2,3` will now parse the headers `access-control-request-headers`, `access-control-request-method`, `content-encoding`, `content-language`, `max-forwards`, `origin`, `proxy-authorization` and `trailer`. * A Performance chapter has been added to the guide. More content will be added in future releases. * Update Cowlib to 2.9.1. === Experimental features added * A `protocols` protocol option allows configuring which protocol will be used for clear listeners. Setting it to `[http2]` will disable HTTP/1.1 entirely. This feature will be extended in a future release. === Features modified * The default value for HTTP/1.1's `max_keepalive` option has been increased. It now allows 1000 requests before gracefully closing the connection. * The default value for HTTP/2's `max_received_frame_rate` option has been increased. It now allows 10000 frames every 10 seconds. * Cowboy will now accept whitespace in cookie names. This is in line with the recommended parsing algorithm for the upcoming cookie RFC update, and corresponds to what browsers are doing. === Bugs fixed * The number of Transport:send/2 calls has been optimized for HTTP/2. Reducing the number of calls has a noticeable impact on the number of requests that can be processed. * Trying to use `cowboy_req:reply/4` with a status code of 204 or 304 and a non-empty response body will now result in a crash. Using `cowboy_req:stream_reply/2,3` with 204 or 304 and then attempting to send a body will also result in a crash. These status codes disallow response bodies and trying to send one will break HTTP/1.1 framing. * A crash has been fixed related to HTTP/1.1 pipelining. The bug was most likely introduced in Cowboy 2.6 when flow control was added for HTTP/1.1 request bodies. * The HTTP/1.1 protocol code could get stuck because of flow control. This has been corrected. * A crash has been fixed for HTTP/1.1. It occurred when a flow control update was requested (such as reading the request body) after the body was fully read. * The timeout was incorrectly reset sometimes when a stream (a pair of request/response) terminated. This has been corrected. * Handling of hibernation for Websocket has been improved. Websocket over HTTP/2 now supports hibernating. Stray messages no longer cancel hibernation. * The `cowboy_compress_h` stream handler will now ignore malformed accept-encoding headers instead of crashing. * The manual pages for `cowboy:start_clear(3)` and `cowboy:start_tls(3)` now mentions that some protocol options may be documented in the releevant stream handler. * The manual page for `cowboy_req:parse_header(3)` was corrected. When an unsupported header is given the function crashes, it does not return an `undefined` tuple. * The routing algorithm description in the user guide has been improved. * The test suites are now consistently green on all tested platforms. Most of the test failures were caused by flaky tests. Avoiding the use of timeouts fixed most of them. A small number of tests had to be reworked. ================================================ FILE: doc/src/guide/migrating_from_2.8.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.8 to 2.9 Cowboy 2.9 implements graceful shutdown of connection processes for both HTTP/1.1 and HTTP/2 connections. Cowboy 2.9 is the first release to support the much awaited Erlang/OTP 24 out of the box. While users that were using Ranch 2.0 already were ready for OTP 24, the Ranch version used by Cowboy out of the box was not compatible and had to be updated. Cowboy 2.9 also contains a small number of tweaks and bug fixes. Cowboy 2.9 requires Erlang/OTP 22.0 or greater. === Features added * Cowboy will now gracefully shutdown HTTP/1.1 and HTTP/2 connections when the supervisor asks the connection process to exit, or when `sys:terminate/2,3` is used. Two new configuration options were added for HTTP/2 to determine the timeouts for the graceful shutdown steps. * REST handler `AcceptCallback` can now return `{created, URI}` or `{see_other, URI}` to determine what response status code should be sent (typically to differentiate between a new resource and an update). The return value `{true, URI}` is now deprecated. * Update Ranch to 1.8.0. * Update Cowlib to 2.11.0. === Bugs fixed * Fix concurrent body streaming getting stuck with HTTP/2. The alarm could get into blocking state indefinitely when two or more request processes were streaming bodies. * Fix HTTP/2 rate limiting using the wrong default values in some cases. * Don't produce an error report when the request process exited normally (`normal` or `shutdown` exit reasons). * Fix `cowboy_tracer_h` to support trace messages without timestamps. ================================================ FILE: doc/src/guide/migrating_from_2.9.asciidoc ================================================ [appendix] == Migrating from Cowboy 2.9 to 2.10 Cowboy 2.10 is a maintenance release adding support for Erlang/OTP 26. The main change is a Cowlib update to fix a compilation error that only occurs starting from OTP 26. Cowboy 2.10 requires Erlang/OTP 22.0 or greater. === Features added * Add support for `Default` value of SameSite cookie attribute. * Add support for the `stale-*` cache-control directives from RFC 5861. * Update Cowlib to 2.12.1. === Bugs fixed * Fix a compilation error in Cowlib when using Erlang/OTP 26. * Fix data sent after RST_STREAM in HTTP/2 in rare cases. * Fix parsing of RST_STREAM frames to properly handle frames that have a valid length but were not fully received yet. * Remove the obsolete `Version` cookie attribute. * Handle more edge cases for cookie parsing based on updates to the RFC 6265bis draft. * Make Basic auth parsing ignore unknown authentication parameters and generally update the code to conform to RFC 7617. * Fix URI template reserved expansion of %-encoded. * Update structured headers implementation to RFC 8941. ================================================ FILE: doc/src/guide/modern_web.asciidoc ================================================ [[modern_web]] == The modern Web Cowboy is a server for the modern Web. This chapter explains what it means and details all the standards involved. Cowboy supports all the standards listed in this document. === HTTP/2 HTTP/2 is the most efficient protocol for consuming Web services. It enables clients to keep a connection open for long periods of time; to send requests concurrently; to reduce the size of requests through HTTP headers compression; and more. The protocol is binary, greatly reducing the resources needed to parse it. HTTP/2 also enables the server to push messages to the client. This can be used for various purposes, including the sending of related resources before the client requests them, in an effort to reduce latency. This can also be used to enable bidirectional communication. Cowboy provides transparent support for HTTP/2. Clients that know it can use it; others fall back to HTTP/1.1 automatically. HTTP/2 is compatible with the HTTP/1.1 semantics. HTTP/2 is defined by RFC 7540 and RFC 7541. === HTTP/1.1 HTTP/1.1 is the previous version of the HTTP protocol. The protocol itself is text-based and suffers from numerous issues and limitations. In particular it is not possible to execute requests concurrently (though pipelining is sometimes possible), and it's also sometimes difficult to detect that a client disconnected. HTTP/1.1 does provide very good semantics for interacting with Web services. It defines the standard methods, headers and status codes used by HTTP/1.1 and HTTP/2 clients and servers. HTTP/1.1 also defines compatibility with an older version of the protocol, HTTP/1.0, which was never really standardized across implementations. The core of HTTP/1.1 is defined by RFC 7230, RFC 7231, RFC 7232, RFC 7233, RFC 7234 and RFC 7235. Numerous RFCs and other specifications exist defining additional HTTP methods, status codes, headers or semantics. === Websocket xref:ws_protocol[Websocket] is a protocol built on top of HTTP/1.1 that provides a two-ways communication channel between the client and the server. Communication is asynchronous and can occur concurrently. It consists of a Javascript object allowing setting up a Websocket connection to the server, and a binary based protocol for sending data to the server or the client. Websocket connections can transfer either UTF-8 encoded text data or binary data. The protocol also includes support for implementing a ping/pong mechanism, allowing the server and the client to have more confidence that the connection is still alive. A Websocket connection can be used to transfer any kind of data, small or big, text or binary. Because of this Websocket is sometimes used for communication between systems. Websocket messages have no semantics on their own. Websocket is closer to TCP in that aspect, and requires you to design and implement your own protocol on top of it; or adapt an existing protocol to Websocket. Cowboy provides an interface known as xref:ws_handlers[Websocket handlers] that gives complete control over a Websocket connection. The Websocket protocol is defined by RFC 6455. === Long-lived requests Cowboy provides an interface that can be used to support long-polling or to stream large amounts of data reliably, including using Server-Sent Events. Long-polling is a mechanism in which the client performs a request which may not be immediately answered by the server. It allows clients to request resources that may not currently exist, but are expected to be created soon, and which will be returned as soon as they are. Long-polling is essentially a hack, but it is widely used to overcome limitations on older clients and servers. Server-Sent Events is a small protocol defined as a media type, `text/event-stream`, along with a new HTTP header, `Last-Event-ID`. It is defined in the EventSource W3C specification. Cowboy provides an interface known as xref:loop_handlers[loop handlers] that facilitates the implementation of long-polling or stream mechanisms. It works regardless of the underlying protocol. === REST xref:rest_principles[REST, or REpresentational State Transfer], is a style of architecture for loosely connected distributed systems. It can easily be implemented on top of HTTP. REST is essentially a set of constraints to be followed. Many of these constraints are purely architectural and solved by simply using HTTP. Some constraints must be explicitly followed by the developer. Cowboy provides an interface known as xref:rest_handlers[REST handlers] that simplifies the implementation of a REST API on top of the HTTP protocol. ================================================ FILE: doc/src/guide/multipart.asciidoc ================================================ [[multipart]] == Multipart requests Multipart originates from MIME, an Internet standard that extends the format of emails. A multipart message is a list of parts. A part contains headers and a body. The body of the parts may be of any media type, and contain text or binary data. It is possible for parts to contain a multipart media type. In the context of HTTP, multipart is most often used with the `multipart/form-data` media type. It is what browsers use to upload files through HTML forms. The `multipart/byteranges` is also common. It is the media type used to send arbitrary bytes from a resource, enabling clients to resume downloads. === Form-data In the normal case, when a form is submitted, the browser will use the `application/x-www-form-urlencoded` content-type. This type is just a list of keys and values and is therefore not fit for uploading files. That's where the `multipart/form-data` content-type comes in. When the form is configured to use this content-type, the browser will create a multipart message where each part corresponds to a field on the form. For files, it also adds some metadata in the part headers, like the file name. A form with a text input, a file input and a select choice box will result in a multipart message with three parts, one for each field. The browser does its best to determine the media type of the files it sends this way, but you should not rely on it for determining the contents of the file. Proper investigation of the contents is recommended. === Checking for multipart messages The content-type header indicates the presence of a multipart message: [source,erlang] ---- {<<"multipart">>, <<"form-data">>, _} = cowboy_req:parse_header(<<"content-type">>, Req). ---- === Reading a multipart message Cowboy provides two sets of functions for reading request bodies as multipart messages. The `cowboy_req:read_part/1,2` functions return the next part's headers, if any. The `cowboy_req:read_part_body/1,2` functions return the current part's body. For large bodies you may need to call the function multiple times. To read a multipart message you need to iterate over all its parts: [source,erlang] ---- multipart(Req0) -> case cowboy_req:read_part(Req0) of {ok, _Headers, Req1} -> {ok, _Body, Req} = cowboy_req:read_part_body(Req1), multipart(Req); {done, Req} -> Req end. ---- When part bodies are too large, Cowboy will return a `more` tuple, and allow you to loop until the part body has been fully read. The function `cow_multipart:form_data/1` can be used to quickly obtain information about a part from a `multipart/form-data` message. The function returns a `data` or a `file` tuple depending on whether this is a normal field or a file being uploaded. The following snippet will use this function and use different strategies depending on whether the part is a file: [source,erlang] ---- multipart(Req0) -> case cowboy_req:read_part(Req0) of {ok, Headers, Req1} -> Req = case cow_multipart:form_data(Headers) of {data, _FieldName} -> {ok, _Body, Req2} = cowboy_req:read_part_body(Req1), Req2; {file, _FieldName, _Filename, _CType} -> stream_file(Req1) end, multipart(Req); {done, Req} -> Req end. stream_file(Req0) -> case cowboy_req:read_part_body(Req0) of {ok, _LastBodyChunk, Req} -> Req; {more, _BodyChunk, Req} -> stream_file(Req) end. ---- Both the part header and body reading functions can take options that will be given to the request body reading functions. By default, `cowboy_req:read_part/1` reads up to 64KB for up to 5 seconds. `cowboy_req:read_part_body/1` has the same defaults as `cowboy_req:read_body/1`. To change the defaults for part headers: [source,erlang] cowboy_req:read_part(Req, #{length => 128000}). And for part bodies: [source,erlang] cowboy_req:read_part_body(Req, #{length => 1000000, period => 7000}). === Skipping unwanted parts Part bodies do not have to be read. Cowboy will automatically skip it when you request the next part's body. The following snippet reads all part headers and skips all bodies: [source,erlang] ---- multipart(Req0) -> case cowboy_req:read_part(Req0) of {ok, _Headers, Req} -> multipart(Req); {done, Req} -> Req end. ---- Similarly, if you start reading the body and it ends up being too big, you can simply continue with the next part. Cowboy will automatically skip what remains. While Cowboy can skip part bodies automatically, the read rate is not configurable. Depending on your application you may want to skip manually, in particular if you observe poor performance while skipping. You do not have to read all parts either. You can stop reading as soon as you find the data you need. // @todo Cover the building of multipart messages. ================================================ FILE: doc/src/guide/performance.asciidoc ================================================ [[performance]] == Performance This chapter describes the performance characteristics of Cowboy and offers suggestions to get the most performance out of your application. === One process per connection The first version of Cowboy featured a single process per connection, whereas the current version of Cowboy features one process per connection plus one process per request. This has a negative impact on performance, but is necessary in order to provide a common interface for both HTTP/1.1 and HTTP/2 (as well as future HTTP versions). It is still possible to use a single process per connection, and avoid the creation of additional processes for each request, by implementing a stream handler to process the requests. This can be done for all requests, or just for a single endpoint depending on the application's needs. Stream handlers provide an asynchronous interface and must not block, so the implementation will be very different from normal Cowboy handlers, but the performance gains are important enough to justify it in some cases. ================================================ FILE: doc/src/guide/req.asciidoc ================================================ [[req]] == The Req object The Req object is a variable used for obtaining information about a request, read its body or send a response. It is not really an object in the object-oriented sense. It is a simple map that can be directly accessed or used when calling functions from the `cowboy_req` module. The Req object is the subject of a few different chapters. In this chapter we will learn about the Req object and look at how to retrieve information about the request. === Direct access The Req map contains a number of fields which are documented and can be accessed directly. They are the fields that have a direct mapping to HTTP: the request `method`; the HTTP `version` used; the effective URI components `scheme`, `host`, `port`, `path` and `qs`; the request `headers`; the connection `peer` address and port; and the TLS certificate `cert` when applicable. Note that the `version` field can be used to determine whether a connection is using HTTP/2. To access a field, you can simply match in the function head. The following example sends a simple "Hello world!" response when the `method` is GET, and a 405 error otherwise. [source,erlang] ---- init(Req0=#{method := <<"GET">>}, State) -> Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain">> }, <<"Hello world!">>, Req0), {ok, Req, State}; init(Req0, State) -> Req = cowboy_req:reply(405, #{ <<"allow">> => <<"GET">> }, Req0), {ok, Req, State}. ---- Any other field is internal and should not be accessed. They may change in future releases, including maintenance releases, without notice. Modifying the Req object is allowed, but extra caution must be used when modifying existing fields. You can add as many new fields as necessary, however. Just make sure to namespace the field names so that no conflict can occur with future Cowboy updates or with third party projects. === Introduction to the cowboy_req interface // @todo Link to cowboy_req manual Functions in the `cowboy_req` module provide access to the request information but also various operations that are common when dealing with HTTP requests. All the functions that begin with a verb indicate an action. Other functions simply return the corresponding value (sometimes that value does need to be built, but the cost of the operation is equivalent to retrieving a value). Some of the `cowboy_req` functions return an updated Req object. They are the read, reply, set and delete functions. While ignoring the returned Req will not cause incorrect behavior for some of them, it is highly recommended to always keep and use the last returned Req object. The manual for `cowboy_req` details these functions and what modifications are done to the Req object. Some of the calls to `cowboy_req` have side effects. This is the case of the read and reply functions. Cowboy reads the request body or replies immediately when the function is called. All functions will crash if something goes wrong. There is usually no need to catch these errors, Cowboy will send the appropriate 4xx or 5xx response depending on where the crash occurred. === Request method The request method can be retrieved directly: [source, erlang] #{method := Method} = Req. Or using a function: [source,erlang] Method = cowboy_req:method(Req). The method is a case sensitive binary string. Standard methods include GET, HEAD, OPTIONS, PATCH, POST, PUT or DELETE. === HTTP version The HTTP version is informational. It does not indicate that the client implements the protocol well or fully. There is typically no need to change behavior based on the HTTP version: Cowboy already does it for you. It can be useful in some cases, though. For example, one may want to redirect HTTP/1.1 clients to use Websocket, while HTTP/2 clients keep using HTTP/2. The HTTP version can be retrieved directly: [source,erlang] #{version := Version} = Req. Or using a function: [source,erlang] Version = cowboy_req:version(Req). Cowboy defines the `'HTTP/1.0'`, `'HTTP/1.1'` and `'HTTP/2'` versions. Custom protocols can define their own values as atoms. === Effective request URI The scheme, host, port, path and query string components of the effective request URI can all be retrieved directly: [source,erlang] ---- #{ scheme := Scheme, host := Host, port := Port, path := Path, qs := Qs } = Req. ---- Or using the related functions: [source,erlang] Scheme = cowboy_req:scheme(Req), Host = cowboy_req:host(Req), Port = cowboy_req:port(Req), Path = cowboy_req:path(Req). Qs = cowboy_req:qs(Req). The scheme and host are lowercased case insensitive binary strings. The port is an integer representing the port number. The path and query string are case sensitive binary strings. Cowboy defines only the `<<"http">>` and `<<"https">>` schemes. They are chosen so that the scheme will only be `<<"https">>` for requests on secure HTTP/1.1 or HTTP/2 connections. // @todo Is that tested well? The effective request URI itself can be reconstructed with the `cowboy_req:uri/1,2` function. By default, an absolute URI is returned: [source,erlang] %% scheme://host[:port]/path[?qs] URI = cowboy_req:uri(Req). Options are available to either disable or replace some or all of the components. Various URIs or URI formats can be generated this way, including the origin form: [source,erlang] %% /path[?qs] URI = cowboy_req:uri(Req, #{host => undefined}). The protocol relative form: [source,erlang] %% //host[:port]/path[?qs] URI = cowboy_req:uri(Req, #{scheme => undefined}). The absolute URI without a query string: [source,erlang] URI = cowboy_req:uri(Req, #{qs => undefined}). A different host: [source,erlang] URI = cowboy_req:uri(Req, #{host => <<"example.org">>}). And any other combination. === Bindings Bindings are the host and path components that you chose to extract when defining the routes of your application. They are only available after the routing. Cowboy provides functions to retrieve one or all bindings. To retrieve a single value: [source,erlang] Value = cowboy_req:binding(userid, Req). When attempting to retrieve a value that was not bound, `undefined` will be returned. A different default value can be provided: [source,erlang] Value = cowboy_req:binding(userid, Req, 42). To retrieve everything that was bound: [source,erlang] Bindings = cowboy_req:bindings(Req). They are returned as a map, with keys being atoms. The Cowboy router also allows you to capture many host or path segments at once using the `...` qualifier. To retrieve the segments captured from the host name: [source,erlang] HostInfo = cowboy_req:host_info(Req). And the path segments: [source,erlang] PathInfo = cowboy_req:path_info(Req). Cowboy will return `undefined` if `...` was not used in the route. === Query parameters Cowboy provides two functions to access query parameters. You can use the first to get the entire list of parameters. [source,erlang] QsVals = cowboy_req:parse_qs(Req), {_, Lang} = lists:keyfind(<<"lang">>, 1, QsVals). Cowboy will only parse the query string, and not do any transformation. This function may therefore return duplicates, or parameter names without an associated value. The order of the list returned is undefined. When a query string is `key=1&key=2`, the list returned will contain two parameters of name `key`. The same is true when trying to use the PHP-style suffix `[]`. When a query string is `key[]=1&key[]=2`, the list returned will contain two parameters of name `key[]`. Cowboy does not require the `[]` suffix to properly handle repeated key names. When a query string is simply `key`, Cowboy will return the list `[{<<"key">>, true}]`, using `true` to indicate that the parameter `key` was defined, but with no value. The second function Cowboy provides allows you to match out only the parameters you are interested in, and at the same time do any post processing you require using xref:constraints[constraints]. This function returns a map. [source,erlang] #{id := ID, lang := Lang} = cowboy_req:match_qs([id, lang], Req). Constraints can be applied automatically. The following snippet will crash when the `id` parameter is not an integer, or when the `lang` parameter is empty. At the same time, the value for `id` will be converted to an integer term: [source,erlang] QsMap = cowboy_req:match_qs([{id, int}, {lang, nonempty}], Req). A default value may also be provided. The default will be used if the `lang` key is not found. It will not be used if the key is found but has an empty value. [source,erlang] #{lang := Lang} = cowboy_req:match_qs([{lang, [], <<"en-US">>}], Req). If no default is provided and the value is missing, the query string is deemed invalid and the process will crash. When the query string is `key=1&key=2`, the value for `key` will be the list `[<<"1">>, <<"2">>]`. Parameter names do not need to include the PHP-style suffix. Constraints may be used to ensure that only one value was given. Constraints do not automatically look inside the list, a custom constraint must be written if that is necessary. === Headers Header values can be retrieved either as a binary string or parsed into a more meaningful representation. The get the raw value: [source,erlang] HeaderVal = cowboy_req:header(<<"content-type">>, Req). Cowboy expects all header names to be provided as lowercase binary strings. This is true for both requests and responses, regardless of the underlying protocol. When the header is missing from the request, `undefined` will be returned. A different default can be provided: [source,erlang] HeaderVal = cowboy_req:header(<<"content-type">>, Req, <<"text/plain">>). All headers can be retrieved at once, either directly: [source,erlang] #{headers := AllHeaders} = Req. Or using a function: [source,erlang] AllHeaders = cowboy_req:headers(Req). Cowboy provides equivalent functions to parse individual headers. There is no function to parse all headers at once. To parse a specific header: [source,erlang] ParsedVal = cowboy_req:parse_header(<<"content-type">>, Req). An exception will be thrown if it doesn't know how to parse the given header, or if the value is invalid. The list of known headers and default values can be found in the manual. When the header is missing, `undefined` is returned. You can change the default value. Note that it should be the parsed value directly: [source,erlang] ---- ParsedVal = cowboy_req:parse_header(<<"content-type">>, Req, {<<"text">>, <<"plain">>, []}). ---- === Peer The peer address and port number for the connection can be retrieved either directly or using a function. To retrieve the peer directly: [source,erlang] #{peer := {IP, Port}} = Req. And using a function: [source,erlang] {IP, Port} = cowboy_req:peer(Req). Note that the peer corresponds to the remote end of the connection to the server, which may or may not be the client itself. It may also be a proxy or a gateway. ================================================ FILE: doc/src/guide/req_body.asciidoc ================================================ [[req_body]] == Reading the request body The request body can be read using the Req object. Cowboy will not attempt to read the body until requested. You need to call the body reading functions in order to retrieve it. Cowboy will not cache the body, it is therefore only possible to read it once. You are not required to read it, however. If a body is present and was not read, Cowboy will either cancel or skip its download, depending on the protocol. Cowboy provides functions for reading the body raw, and read and parse form urlencoded or xref:multipart[multipart bodies]. The latter is covered in its own chapter. === Request body presence Not all requests come with a body. You can check for the presence of a request body with this function: [source,erlang] cowboy_req:has_body(Req). It returns `true` if there is a body; `false` otherwise. In practice, this function is rarely used. When the method is `POST`, `PUT` or `PATCH`, the request body is often required by the application, which should just attempt to read it directly. === Request body length You can obtain the length of the body: [source,erlang] Length = cowboy_req:body_length(Req). Note that the length may not be known in advance. In that case `undefined` will be returned. This can happen with HTTP/1.1's chunked transfer-encoding, or HTTP/2 when no content-length was provided. Cowboy will update the body length in the Req object once the body has been read completely. A length will always be returned when attempting to call this function after reading the body completely. === Reading the body You can read the entire body with one function call: [source,erlang] {ok, Data, Req} = cowboy_req:read_body(Req0). Cowboy returns an `ok` tuple when the body has been read fully. By default, Cowboy will attempt to read up to 8MB of data, for up to 15 seconds. The call will return once Cowboy has read at least 8MB of data, or at the end of the 15 seconds period. These values can be customized. For example, to read only up to 1MB for up to 5 seconds: [source,erlang] ---- {ok, Data, Req} = cowboy_req:read_body(Req0, #{length => 1000000, period => 5000}). ---- These two options can effectively be used to control the rate of transmission of the request body. It is also possible to asynchronously read the request body using auto mode: [source,erlang] ---- Ref = make_ref(), cowboy_req:cast({read_body, self(), Ref, auto, infinity}, Req). ---- Cowboy will wait indefinitely for data and then send a `request_body` message as soon as it has data available, regardless of length. [source,erlang] ---- receive {request_body, Ref, nofin, Data} -> do_something(Data); {request_body, Ref, fin, _BodyLen, Data} -> do_something(Data) end. ---- Asynchronous reading of data pairs well with loop handlers. === Streaming the body When the body is too large, the first call will return a `more` tuple instead of `ok`. You can call the function again to read more of the body, reading it one chunk at a time. [source,erlang] ---- read_body_to_console(Req0) -> case cowboy_req:read_body(Req0) of {ok, Data, Req} -> io:format("~s", [Data]), Req; {more, Data, Req} -> io:format("~s", [Data]), read_body_to_console(Req) end. ---- The `length` and `period` options can also be used. They need to be passed for every call. === Reading a form urlencoded body Cowboy provides a convenient function for reading and parsing bodies sent as application/x-www-form-urlencoded. [source,erlang] {ok, KeyValues, Req} = cowboy_req:read_urlencoded_body(Req0). This function returns a list of key/values, exactly like the function `cowboy_req:parse_qs/1`. The defaults for this function are different. Cowboy will read for up to 64KB and up to 5 seconds. They can be modified: [source,erlang] ---- {ok, KeyValues, Req} = cowboy_req:read_urlencoded_body(Req0, #{length => 4096, period => 3000}). ---- ================================================ FILE: doc/src/guide/resource_design.asciidoc ================================================ [[resource_design]] == Designing a resource handler This chapter aims to provide you with a list of questions you must answer in order to write a good resource handler. It is meant to be usable as a step by step guide. === The service Can the service become unavailable, and when it does, can we detect it? For example, database connectivity problems may be detected early. We may also have planned outages of all or parts of the system. Implement the `service_available` callback. What HTTP methods does the service implement? Do we need more than the standard OPTIONS, HEAD, GET, PUT, POST, PATCH and DELETE? Are we not using one of those at all? Implement the `known_methods` callback. === Type of resource handler Am I writing a handler for a collection of resources, or for a single resource? The semantics for each of these are quite different. You should not mix collection and single resource in the same handler. === Collection handler Skip this section if you are not doing a collection. Is the collection hardcoded or dynamic? For example, if you use the route `/users` for the collection of users then the collection is hardcoded; if you use `/forums/:category` for the collection of threads then it isn't. When the collection is hardcoded you can safely assume the resource always exists. What methods should I implement? OPTIONS is used to get some information about the collection. It is recommended to allow it even if you do not implement it, as Cowboy has a default implementation built-in. HEAD and GET are used to retrieve the collection. If you allow GET, also allow HEAD as there's no extra work required to make it work. POST is used to create a new resource inside the collection. Creating a resource by using POST on the collection is useful when resources may be created before knowing their URI, usually because parts of it are generated dynamically. A common case is some kind of auto incremented integer identifier. The next methods are more rarely allowed. PUT is used to create a new collection (when the collection isn't hardcoded), or replace the entire collection. DELETE is used to delete the entire collection. PATCH is used to modify the collection using instructions given in the request body. A PATCH operation is atomic. The PATCH operation may be used for such things as reordering; adding, modifying or deleting parts of the collection. === Single resource handler Skip this section if you are doing a collection. What methods should I implement? OPTIONS is used to get some information about the resource. It is recommended to allow it even if you do not implement it, as Cowboy has a default implementation built-in. HEAD and GET are used to retrieve the resource. If you allow GET, also allow HEAD as there's no extra work required to make it work. POST is used to update the resource. PUT is used to create a new resource (when it doesn't already exist) or replace the resource. DELETE is used to delete the resource. PATCH is used to modify the resource using instructions given in the request body. A PATCH operation is atomic. The PATCH operation may be used for adding, removing or modifying specific values in the resource. === The resource Following the above discussion, implement the `allowed_methods` callback. Does the resource always exist? If it may not, implement the `resource_exists` callback. Do I need to authenticate the client before they can access the resource? What authentication mechanisms should I provide? This may include form-based, token-based (in the URL or a cookie), HTTP basic, HTTP digest, SSL certificate or any other form of authentication. Implement the `is_authorized` callback. Do I need fine-grained access control? How do I determine that they are authorized access? Handle that in your `is_authorized` callback. Can access to a resource be forbidden regardless of access being authorized? A simple example of that is censorship of a resource. Implement the `forbidden` callback. Can access be rate-limited for authenticated users? Use the `rate_limited` callback. Are there any constraints on the length of the resource URI? For example, the URI may be used as a key in storage and may have a limit in length. Implement `uri_too_long`. === Representations What media types do I provide? If text based, what charsets are provided? What languages do I provide? Implement the mandatory `content_types_provided`. Prefix the callbacks with `to_` for clarity. For example, `to_html` or `to_text`. For resources that don't implement methods GET or HEAD, you must still accept at least one media type, but you can leave the callback as `undefined` since it will never be called. Implement the `languages_provided` or `charsets_provided` callbacks if applicable. Does the resource accept ranged requests? If it does, implement the `ranges_provided` callback. Resources that only accept `bytes` units can use the callback name `auto` and let Cowboy automatically do ranged responses. Other callbacks should have a name prefix of `ranged_` for clarity. For example, `ranged_bytes` or `ranged_pages`. If the resource needs to perform additional checks before accepting to do a ranged responses, implement the `range_satisfiable` callback. Is there any other header that may make the representation of the resource vary? Implement the `variances` callback. Depending on your choices for caching content, you may want to implement one or more of the `generate_etag`, `last_modified` and `expires` callbacks. Do I want the user or user agent to actively choose a representation available? Send a list of available representations in the response body and implement the `multiple_choices` callback. === Redirections Do I need to keep track of what resources were deleted? For example, you may have a mechanism where moving a resource leaves a redirect link to its new location. Implement the `previously_existed` callback. Was the resource moved, and is the move temporary? If it is explicitly temporary, for example due to maintenance, implement the `moved_temporarily` callback. Otherwise, implement the `moved_permanently` callback. === The request Do you need to read the query string? Individual headers? Implement `malformed_request` and do all the parsing and validation in this function. Note that the body should not be read at this point. May there be a request body? Will I know its size? What's the maximum size of the request body I'm willing to accept? Implement `valid_entity_length`. Finally, take a look at the sections corresponding to the methods you are implementing. === OPTIONS method Cowboy by default will send back a list of allowed methods. Do I need to add more information to the response? Implement the `options` method. === GET and HEAD methods If you implement the methods GET and/or HEAD, you must implement one `ProvideCallback` callback for each content-type returned by the `content_types_provided` callback. When range requests are accepted, you must implement one `RangeCallback` for each range unit returned by `ranges_provided` (unless `auto` was used). This is in addition to the `ProvideCallback` callback. === PUT, POST and PATCH methods If you implement the methods PUT, POST and/or PATCH, you must implement the `content_types_accepted` callback, and one `AcceptCallback` callback for each content-type it returns. Prefix the `AcceptCallback` callback names with `from_` for clarity. For example, `from_html` or `from_json`. Do we want to allow the POST method to create individual resources directly through their URI (like PUT)? Implement the `allow_missing_post` callback. It is recommended to explicitly use PUT in these cases instead. May there be conflicts when using PUT to create or replace a resource? Do we want to make sure that two updates around the same time are not cancelling one another? Implement the `is_conflict` callback. === DELETE methods If you implement the method DELETE, you must implement the `delete_resource` callback. When `delete_resource` returns, is the resource completely removed from the server, including from any caching service? If not, and/or if the deletion is asynchronous and we have no way of knowing it has been completed yet, implement the `delete_completed` callback. ================================================ FILE: doc/src/guide/resp.asciidoc ================================================ [[resp]] == Sending a response The response must be sent using the Req object. Cowboy provides two different ways of sending responses: either directly or by streaming the body. Response headers and body may be set in advance. The response is sent as soon as one of the reply or stream reply function is called. Cowboy also provides a simplified interface for sending files. It can also send only specific parts of a file. While only one response is allowed for every request, HTTP/2 introduced a mechanism that allows the server to push additional resources related to the response. This chapter also describes how this feature works in Cowboy. === Reply Cowboy provides three functions for sending the entire reply, depending on whether you need to set headers and body. In all cases, Cowboy will add any headers required by the protocol (for example the date header will always be sent). When you need to set only the status code, use `cowboy_req:reply/2`: [source,erlang] Req = cowboy_req:reply(200, Req0). When you need to set response headers at the same time, use `cowboy_req:reply/3`: [source,erlang] ---- Req = cowboy_req:reply(303, #{ <<"location">> => <<"https://ninenines.eu">> }, Req0). ---- Note that the header name must always be a lowercase binary. When you also need to set the response body, use `cowboy_req:reply/4`: [source,erlang] ---- Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain">> }, "Hello world!", Req0). ---- You should always set the content-type header when the response has a body. There is however no need to set the content-length header; Cowboy does it automatically. The response body and the header values must be either a binary or an iolist. An iolist is a list containing binaries, characters, strings or other iolists. This allows you to build a response from different parts without having to do any concatenation: [source,erlang] ---- Title = "Hello world!", Body = <<"Hats off!">>, Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/html">> }, ["", Title, "", "

", Body, "

"], Req0). ---- This method of building responses is more efficient than concatenating. Behind the scenes, each element of the list is simply a pointer, and those pointers are used directly when writing to the socket. === Stream reply Cowboy provides two functions for initiating a response, and an additional function for streaming the response body. Cowboy will add any required headers to the response. // @todo For HTTP/1.1 Cowboy should probably not use chunked transfer-encoding if the content-length is set. When you need to set only the status code, use `cowboy_req:stream_reply/2`: [source,erlang] ---- Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body("Hello...", nofin, Req), cowboy_req:stream_body("chunked...", nofin, Req), cowboy_req:stream_body("world!!", fin, Req). ---- The second argument to `cowboy_req:stream_body/3` indicates whether this data terminates the body. Use `fin` for the final flag, and `nofin` otherwise. This snippet does not set a content-type header. This is not recommended. All responses with a body should have a content-type. The header can be set beforehand, or using the `cowboy_req:stream_reply/3`: [source,erlang] ---- Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/html">> }, Req0), cowboy_req:stream_body("Hello world!", nofin, Req), cowboy_req:stream_body("

Hats off!

", fin, Req). ---- HTTP provides a few different ways to stream response bodies. Cowboy will select the most appropriate one based on the HTTP version and the request and response headers. While not required by any means, it is recommended that you set the content-length header in the response if you know it in advance. This will ensure that the best response method is selected and help clients understand when the response is fully received. Cowboy also provides a function to send response trailers. Response trailers are semantically equivalent to the headers you send in the response, only they are sent at the end. This is especially useful to attach information to the response that could not be generated until the response body was fully generated. Trailer fields must be listed in the trailer header. Any field not listed might be dropped by the client or an intermediary. [source,erlang] ---- Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/html">>, <<"trailer">> => <<"expires, content-md5">> }, Req0), cowboy_req:stream_body("Hello world!", nofin, Req), cowboy_req:stream_body("

Hats off!

", nofin, Req), cowboy_req:stream_trailers(#{ <<"expires">> => <<"Sun, 10 Dec 2017 19:13:47 GMT">>, <<"content-md5">> => <<"c6081d20ff41a42ce17048ed1c0345e2">> }, Req). ---- The stream ends with trailers. It is no longer possible to send data after sending trailers. You cannot send trailers after setting the `fin` flag when streaming the body. === Preset response headers Cowboy provides functions to set response headers without immediately sending them. They are stored in the Req object and sent as part of the response when a reply function is called. To set response headers: [source,erlang] Req = cowboy_req:set_resp_header(<<"allow">>, "GET", Req0). Header names must be a lowercase binary. Do not use this function for setting cookies. Refer to the xref:cookies[Cookies] chapter for more information. To check if a response header has already been set: [source,erlang] cowboy_req:has_resp_header(<<"allow">>, Req). It returns `true` if the header was set, `false` otherwise. To delete a response header that was set previously: [source,erlang] Req = cowboy_req:delete_resp_header(<<"allow">>, Req0). === Overriding headers As Cowboy provides different ways of setting response headers and body, clashes may occur, so it's important to understand what happens when a header is set twice. Headers come from five different origins: * Protocol-specific headers (for example HTTP/1.1's connection header) * Other required headers (for example the date header) * Preset headers * Headers given to the reply function * Set-cookie headers Cowboy does not allow overriding protocol-specific headers. Set-cookie headers will always be appended at the end of the list of headers before sending the response. Headers given to the reply function will always override preset headers and required headers. If a header is found in two or three of these, then the one in the reply function is picked and the others are dropped. Similarly, preset headers will always override required headers. To illustrate, look at the following snippet. Cowboy by default sends the server header with the value "Cowboy". We can override it: [source,erlang] ---- Req = cowboy_req:reply(200, #{ <<"server">> => <<"yaws">> }, Req0). ---- === Preset response body Cowboy provides functions to set the response body without immediately sending it. It is stored in the Req object and sent when the reply function is called. To set the response body: [source,erlang] Req = cowboy_req:set_resp_body("Hello world!", Req0). // @todo Yeah we probably should add that function that // also sets the content-type at the same time... To check if a response body has already been set: [source,erlang] cowboy_req:has_resp_body(Req). It returns `true` if the body was set and is non-empty, `false` otherwise. // @todo We probably should also have a function that // properly removes the response body, including any // content-* headers. The preset response body is only sent if the reply function used is `cowboy_req:reply/2` or `cowboy_req:reply/3`. === Sending files Cowboy provides a shortcut for sending files. When using `cowboy_req:reply/4`, or when presetting the response header, you can give a `sendfile` tuple to Cowboy: [source,erlang] {sendfile, Offset, Length, Filename} Depending on the values for `Offset` or `Length`, the entire file may be sent, or just a part of it. The length is required even for sending the entire file. Cowboy sends it in the content-length header. To send a file while replying: [source,erlang] ---- Req = cowboy_req:reply(200, #{ <<"content-type">> => "image/png" }, {sendfile, 0, 12345, "path/to/logo.png"}, Req0). ---- // @todo An example of presetting a file would be useful, // but let's wait for the function that can set the // content-type at the same time. // @todo What about streaming many files? For example // it should be possible to build a tar file on the fly // while still using sendfile. Another example could be // proper support for multipart byte ranges. Yet another // example would be automatic concatenation of CSS or JS // files. === Informational responses Cowboy allows you to send informational responses. Informational responses are responses that have a status code between 100 and 199. Any number can be sent before the proper response. Sending an informational response does not change the behavior of the proper response, and clients are expected to ignore any informational response they do not understand. The following snippet sends a 103 informational response with some headers that are expected to be in the final response. [source,erlang] ---- Req = cowboy_req:inform(103, #{ <<"link">> => <<"; rel=preload; as=style, ; rel=preload; as=script">> }, Req0). ---- === Push The HTTP/2 protocol introduced the ability to push resources related to the one sent in the response. Cowboy provides two functions for that purpose: `cowboy_req:push/3,4`. Push is only available for HTTP/2. Cowboy will automatically ignore push requests if the protocol doesn't support it. The push function must be called before any of the reply functions. Doing otherwise will result in a crash. To push a resource, you need to provide the same information as a client performing a request would. This includes the HTTP method, the URI and any necessary request headers. Cowboy by default only requires you to give the path to the resource and the request headers. The rest of the URI is taken from the current request (excluding the query string, set to empty) and the method is GET by default. The following snippet pushes a CSS file that is linked to in the response: [source,erlang] ---- cowboy_req:push("/static/style.css", #{ <<"accept">> => <<"text/css">> }, Req0), Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/html">> }, ["My web page", "", "

Welcome to Erlang!

"], Req0). ---- To override the method, scheme, host, port or query string, simply pass in a fourth argument. The following snippet uses a different host name: [source,erlang] ---- cowboy_req:push("/static/style.css", #{ <<"accept">> => <<"text/css">> }, #{host => <<"cdn.example.org">>}, Req), ---- Pushed resources don't have to be files. As long as the push request is cacheable, safe and does not include a body, the resource can be pushed. Under the hood, Cowboy handles pushed requests the same as normal requests: a different process is created which will ultimately send a response to the client. ================================================ FILE: doc/src/guide/rest_flowcharts.asciidoc ================================================ [[rest_flowcharts]] == REST flowcharts This chapter will explain the REST handler state machine through a number of different diagrams. There are four main paths that requests may follow. One for the method OPTIONS; one for the methods GET and HEAD; one for the methods PUT, POST and PATCH; and one for the method DELETE. All paths start with the "Start" diagram, and all paths excluding the OPTIONS path go through the "Content negotiation" diagram and optionally the "Conditional requests" diagram if the resource exists. The red squares refer to another diagram. The light green squares indicate a response. Other squares may be either a callback or a question answered by Cowboy itself. Green arrows tend to indicate the default behavior if the callback is undefined. The star next to values indicate that the value is descriptive rather than exact. === Start All requests start from here. image::rest_start.png[REST starting flowchart] A series of callbacks are called in succession to perform a general checkup of the service, the request line and request headers. The request body, if any, is not expected to have been received for any of these steps. It is only processed at the end of the "PUT, POST and PATCH methods" diagram, when all conditions have been met. The `known_methods` and `allowed_methods` callbacks return a list of methods. Cowboy then checks if the request method is in the list, and stops otherwise. The `is_authorized` callback may be used to check that access to the resource is authorized. Authentication may also be performed as needed. When authorization is denied, the return value from the callback must include a challenge applicable to the requested resource, which will be sent back to the client in the www-authenticate header. This diagram is immediately followed by either the "OPTIONS method" diagram when the request method is OPTIONS, or the "Content negotiation" diagram otherwise. === OPTIONS method This diagram only applies to OPTIONS requests. image::rest_options.png[REST OPTIONS method flowchart] The `options` callback may be used to add information about the resource, such as media types or languages provided; allowed methods; any extra information. A response body may also be set, although clients should not be expected to read it. If the `options` callback is not defined, Cowboy will send a response containing the list of allowed methods by default. === Content negotiation This diagram applies to all request methods other than OPTIONS. It is executed right after the "Start" diagram is completed. image::rest_conneg.png[REST content negotiation flowchart] The purpose of these steps is to determine an appropriate representation to be sent back to the client. The request may contain any of the accept header; the accept-language header; or the accept-charset header. When present, Cowboy will parse the headers and then call the corresponding callback to obtain the list of provided content-type, language or charset for this resource. It then automatically select the best match based on the request. If a callback is not defined, Cowboy will select the content-type, language or charset that the client prefers. The `content_types_provided` also returns the name of a callback for every content-type it accepts. This callback will only be called at the end of the "GET and HEAD methods" diagram, when all conditions have been met. Optionally, the `ranges_provided` also returns the name of a callback for every range unit it accepts. This will be called at the end of the "GET and HEAD methods" diagram in the case of ranged requests. The selected content-type, language and charset are saved as meta values in the Req object. You *should* use the appropriate representation if you set a response body manually (alongside an error code, for example). This diagram is immediately followed by the "GET and HEAD methods" diagram, the "PUT, POST and PATCH methods" diagram, or the "DELETE method" diagram, depending on the method. === GET and HEAD methods This diagram only applies to GET and HEAD requests. For a description of the `cond` step, please see the "Conditional requests" diagram. image::rest_get_head.png[REST GET/HEAD methods flowchart] When the resource exists, and the conditional steps succeed, the resource can be retrieved. Cowboy prepares the response by first retrieving metadata about the representation, then by calling the `ProvideCallback` callback. This is the callback you defined for each content-types you returned from `content_types_provided`. This callback returns the body that will be sent back to the client. For ranged requests, but only when the `ranges_provided` callback was defined earlier, Cowboy will add the selected `range` information to the Req object and call the `range_satisfiable` callback. After confirming that the range can be provided, Cowboy calls the `RangeResource` callback and produces a ranged response using the ranged data from the callback. When the resource does not exist, Cowboy will figure out whether the resource existed previously, and if so whether it was moved elsewhere in order to redirect the client to the new URI. The `moved_permanently` and `moved_temporarily` callbacks must return the new location of the resource if it was in fact moved. === PUT, POST and PATCH methods This diagram only applies to PUT, POST and PATCH requests. For a description of the `cond` step, please see the "Conditional requests" diagram. image::rest_put_post_patch.png[REST PUT/POST/PATCH methods flowchart] When the resource exists, first the conditional steps are executed. When that succeeds, and the method is PUT, Cowboy will call the `is_conflict` callback. This function can be used to prevent potential race conditions, by locking the resource for example. Then all three methods reach the `content_types_accepted` step that we will describe in a few paragraphs. When the resource does not exist, and the method is PUT, Cowboy will check for conflicts and then move on to the `content_types_accepted` step. For other methods, Cowboy will figure out whether the resource existed previously, and if so whether it was moved elsewhere. If the resource is truly non-existent, the method is POST and the call for `allow_missing_post` returns `true`, then Cowboy will move on to the `content_types_accepted` step. Otherwise the request processing ends there. The `moved_permanently` and `moved_temporarily` callbacks must return the new location of the resource if it was in fact moved. The `content_types_accepted` returns a list of content-types it accepts, but also the name of a callback for each of them. Cowboy will select the appropriate callback for processing the request body and call it. This callback may return one of three different return values. If an error occurred while processing the request body, it must return `false` and Cowboy will send an appropriate error response. If the method is POST, then you may return `true` with an URI of where the resource has been created. This is especially useful for writing handlers for collections. Otherwise, return `true` to indicate success. Cowboy will select the appropriate response to be sent depending on whether a resource has been created, rather than modified, and on the availability of a location header or a body in the response. === DELETE method This diagram only applies to DELETE requests. For a description of the `cond` step, please see the "Conditional requests" diagram. image::rest_delete.png[REST DELETE method flowchart] When the resource exists, and the conditional steps succeed, the resource can be deleted. Deleting the resource is a two steps process. First the callback `delete_resource` is executed. Use this callback to delete the resource. Because the resource may be cached, you must also delete all cached representations of this resource in the system. This operation may take a while though, so you may return before it finished. Cowboy will then call the `delete_completed` callback. If you know that the resource has been completely deleted from your system, including from caches, then you can return `true`. If any doubts persist, return `false`. Cowboy will assume `true` by default. To finish, Cowboy checks if you set a response body, and depending on that, sends the appropriate response. When the resource does not exist, Cowboy will figure out whether the resource existed previously, and if so whether it was moved elsewhere in order to redirect the client to the new URI. The `moved_permanently` and `moved_temporarily` callbacks must return the new location of the resource if it was in fact moved. === Conditional requests This diagram applies to all request methods other than OPTIONS. It is executed right after the `resource_exists` callback, when the resource exists. image::rest_cond.png[REST conditional requests flowchart] A request becomes conditional when it includes either of the if-match header; the if-unmodified-since header; the if-none-match header; or the if-modified-since header. If the condition fails, the request ends immediately without any retrieval or modification of the resource. The `generate_etag` and `last_modified` are called as needed. Cowboy will only call them once and then cache the results for subsequent use. ================================================ FILE: doc/src/guide/rest_handlers.asciidoc ================================================ [[rest_handlers]] == REST handlers REST is implemented in Cowboy as a sub protocol. The request is handled as a state machine with many optional callbacks describing the resource and modifying the machine's behavior. The REST handler is the recommended way to handle HTTP requests. === Initialization First, the `init/2` callback is called. This callback is common to all handlers. To use REST for the current request, this function must return a `cowboy_rest` tuple. [source,erlang] ---- init(Req, State) -> {cowboy_rest, Req, State}. ---- Cowboy will then switch to the REST protocol and start executing the state machine. After reaching the end of the flowchart, the `terminate/3` callback will be called if it is defined. === Methods The REST component has code for handling the following HTTP methods: HEAD, GET, POST, PATCH, PUT, DELETE and OPTIONS. Other methods can be accepted, however they have no specific callback defined for them at this time. === Callbacks All callbacks are optional. Some may become mandatory depending on what other defined callbacks return. The various flowcharts in the next chapter should be a useful to determine which callbacks you need. All callbacks take two arguments, the Req object and the State, and return a three-element tuple of the form `{Value, Req, State}`. Nearly all callbacks can also return `{stop, Req, State}` to stop execution of the request, and `{{switch_handler, Module}, Req, State}` or `{{switch_handler, Module, Opts}, Req, State}` to switch to a different handler type. The exceptions are `expires` `generate_etag`, `last_modified` and `variances`. The following table summarizes the callbacks and their default values. If the callback isn't defined, then the default value will be used. Please look at the flowcharts to find out the result of each return value. In the following table, "skip" means the callback is entirely skipped if it is undefined, moving directly to the next step. Similarly, "none" means there is no default value for this callback. [cols="<,^",options="header"] |=== | Callback name | Default value | allowed_methods | `[<<"GET">>, <<"HEAD">>, <<"OPTIONS">>]` | allow_missing_post | `true` | charsets_provided | skip | content_types_accepted | none // @todo Space required for the time being: https://github.com/spf13/hugo/issues/2398 | content_types_provided | `[{{ <<"text">>, <<"html">>, '*'}, to_html}]` | delete_completed | `true` | delete_resource | `false` | expires | `undefined` | forbidden | `false` | generate_etag | `undefined` | is_authorized | `true` | is_conflict | `false` | known_methods | `[<<"GET">>, <<"HEAD">>, <<"POST">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>]` | languages_provided | skip | last_modified | `undefined` | malformed_request | `false` | moved_permanently | `false` | moved_temporarily | `false` | multiple_choices | `false` | options | `ok` | previously_existed | `false` | ranges_provided | skip | range_satisfiable | `true` | rate_limited | `false` | resource_exists | `true` | service_available | `true` | uri_too_long | `false` | valid_content_headers | `true` | valid_entity_length | `true` | variances | `[]` |=== As you can see, Cowboy tries to move on with the request whenever possible by using well thought out default values. In addition to these, there can be any number of user-defined callbacks that are specified through `content_types_accepted/2`, `content_types_provided/2` or `ranges_provided/2`. They can take any name (except `auto` for range callbacks), however it is recommended to use a separate prefix for the callbacks of each function. For example, `from_html` and `to_html` indicate in the first case that we're accepting a resource given as HTML, and in the second case that we send one as HTML. === Meta data Cowboy will set informative values to the Req object at various points of the execution. You can retrieve them by matching the Req object directly. The values are defined in the following table: [cols="<,<",options="header"] |=== | Key | Details | media_type | The content-type negotiated for the response entity | language | The language negotiated for the response entity | charset | The charset negotiated for the response entity | range | The range selected for the ranged response |=== They can be used to send a proper body with the response to a request that used a method other than HEAD or GET. === Response headers Cowboy will set response headers automatically over the execution of the REST code. They are listed in the following table. [cols="<,<",options="header"] |=== | Header name | Details | accept-ranges | Range units accepted by the resource | allow | HTTP methods allowed by the resource | content-language | Language used in the response body | content-range | Range of the content found in the response | content-type | Media type and charset of the response body | etag | Etag of the resource | expires | Expiration date of the resource | last-modified | Last modification date for the resource | location | Relative or absolute URI to the requested resource | retry-after | Delay or time the client should wait before accessing the resource | vary | List of headers that may change the representation of the resource | www-authenticate | Authentication information to access the resource |=== ================================================ FILE: doc/src/guide/rest_principles.asciidoc ================================================ [[rest_principles]] == REST principles This chapter will attempt to define the concepts behind REST and explain what makes a service RESTful. REST is often confused with performing a distinct operation depending on the HTTP method, while using more than the GET and POST methods. That's highly misguided at best. We will first attempt to define REST and will look at what it means in the context of HTTP and the Web. For a more in-depth explanation of REST, you can read http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm[Roy T. Fielding's dissertation] as it does a great job explaining where it comes from and what it achieves. === REST architecture REST is a *client-server* architecture. The client and the server both have a different set of concerns. The server stores and/or manipulates information and makes it available to the user in an efficient manner. The client takes that information and displays it to the user and/or uses it to perform subsequent requests for information. This separation of concerns allows both the client and the server to evolve independently as it only requires that the interface stays the same. REST is *stateless*. That means the communication between the client and the server always contains all the information needed to perform the request. There is no session state in the server, it is kept entirely on the client's side. If access to a resource requires authentication, then the client needs to authenticate itself with every request. REST is *cacheable*. The client, the server and any intermediary components can all cache resources in order to improve performance. REST provides a *uniform interface* between components. This simplifies the architecture, as all components follow the same rules to speak to one another. It also makes it easier to understand the interactions between the different components of the system. A number of constraints are required to achieve this. They are covered in the rest of the chapter. REST is a *layered system*. Individual components cannot see beyond the immediate layer with which they are interacting. This means that a client connecting to an intermediate component, like a proxy, has no knowledge of what lies beyond. This allows components to be independent and thus easily replaceable or extendable. REST optionally provides *code on demand*. Code may be downloaded to extend client functionality. This is optional however because the client may not be able to download or run this code, and so a REST component cannot rely on it being executed. === Resources and resource identifiers A resource is an abstract concept. In a REST system, any information that can be named may be a resource. This includes documents, images, a collection of resources and any other information. Any information that can be the target of an hypertext link can be a resource. A resource is a conceptual mapping to a set of entities. The set of entities evolves over time; a resource doesn't. For example, a resource can map to "users who have logged in this past month" and another to "all users". At some point in time they may map to the same set of entities, because all users logged in this past month. But they are still different resources. Similarly, if nobody logged in recently, then the first resource may map to the empty set. This resource exists regardless of the information it maps to. Resources are identified by uniform resource identifiers, also known as URIs. Sometimes internationalized resource identifiers, or IRIs, may also be used, but these can be directly translated into a URI. In practice we will identify two kinds of resources. Individual resources map to a set of one element, for example "user Joe". Collection of resources map to a set of 0 to N elements, for example "all users". === Resource representations The representation of a resource is a sequence of bytes associated with metadata. The metadata comes as a list of key-value pairs, where the name corresponds to a standard that defines the value's structure and semantics. With HTTP, the metadata comes in the form of request or response headers. The headers' structure and semantics are well defined in the HTTP standard. Metadata includes representation metadata, resource metadata and control data. The representation metadata gives information about the representation, such as its media type, the date of last modification, or even a checksum. Resource metadata could be link to related resources or information about additional representations of the resource. Control data allows parameterizing the request or response. For example, we may only want the representation returned if it is more recent than the one we have in cache. Similarly, we may want to instruct the client about how it should cache the representation. This isn't restricted to caching. We may, for example, want to store a new representation of a resource only if it wasn't modified since we first retrieved it. The data format of a representation is also known as the media type. Some media types are intended for direct rendering to the user, while others are intended for automated processing. The media type is a key component of the REST architecture. === Self-descriptive messages Messages must be self-descriptive. That means that the data format of a representation must always come with its media type (and similarly requesting a resource involves choosing the media type of the representation returned). If you are sending HTML, then you must say it is HTML by sending the media type with the representation. In HTTP this is done using the content-type header. The media type is often an IANA registered media type, like `text/html` or `image/png`, but does not need to be. Exactly two things are important for respecting this constraint: that the media type is well specified, and that the sender and recipient agree about what the media type refers to. This means that you can create your own media types, like `application/x-mine`, and that as long as you write the specifications for it and that both endpoints agree about it then the constraint is respected. === Hypermedia as the engine of application state The last constraint is generally where services that claim to be RESTful fail. Interactions with a server must be entirely driven by hypermedia. The client does not need any prior knowledge of the service in order to use it, other than an entry point and of course basic understanding of the media type of the representations, at the very least enough to find and identify hyperlinks and link relations. To give a simple example, if your service only works with the `application/json` media type then this constraint cannot be respected (as there are no concept of links in JSON) and thus your service isn't RESTful. This is the case for the majority of self-proclaimed REST services. On the other hand if you create a JSON based media type that has a concept of links and link relations, then your service might be RESTful. Respecting this constraint means that the entirety of the service becomes self-discoverable, not only the resources in it, but also the operations you can perform on it. This makes clients very thin as there is no need to implement anything specific to the service to operate on it. ================================================ FILE: doc/src/guide/routing.asciidoc ================================================ [[routing]] == Routing Cowboy does nothing by default. To make Cowboy useful, you need to map URIs to Erlang modules that will handle the requests. This is called routing. Cowboy routes requests using the following algorithm: * If no configured host matches the request URI, a 400 response is returned. * Otherwise, the first configured host that matches the request URI will be used. Only the paths configured for this host will be considered. * If none of the configured paths found in the previous step match the request URI, a 404 response is returned. * Otherwise, the handler and its initial state are added to the environment and the request continues to be processed. NOTE: It is possible to run into a situation where two hosts match a request URI, but only the paths on the second host match the request URI. In this case the expected result is a 404 response because the only paths used during routing are the paths from the first configured host that matches the request URI. Routes need to be compiled before they can be used by Cowboy. The result of the compilation is the dispatch rules. === Syntax The general structure for the routes is defined as follow. [source,erlang] Routes = [Host1, Host2, ... HostN]. Each host contains matching rules for the host along with optional constraints, and a list of routes for the path component. [source,erlang] Host1 = {HostMatch, PathsList}. Host2 = {HostMatch, Constraints, PathsList}. The list of routes for the path component is defined similar to the list of hosts. [source,erlang] PathsList = [Path1, Path2, ... PathN]. Finally, each path contains matching rules for the path along with optional constraints, and gives us the handler module to be used along with its initial state. [source,erlang] Path1 = {PathMatch, Handler, InitialState}. Path2 = {PathMatch, Constraints, Handler, InitialState}. Continue reading to learn more about the match syntax and the optional constraints. === Match syntax The match syntax is used to associate host names and paths with their respective handlers. The match syntax is the same for host and path with a few subtleties. Indeed, the segments separator is different, and the host is matched starting from the last segment going to the first. All examples will feature both host and path match rules and explain the differences when encountered. Excluding special values that we will explain at the end of this section, the simplest match value is a host or a path. It can be given as either a `string()` or a `binary()`. [source,erlang] ---- PathMatch1 = "/". PathMatch2 = "/path/to/resource". HostMatch1 = "cowboy.example.org". ---- As you can see, all paths defined this way must start with a slash character. Note that these two paths are identical as far as routing is concerned. [source,erlang] PathMatch2 = "/path/to/resource". PathMatch3 = "/path/to/resource/". Hosts with and without a trailing dot are equivalent for routing. Similarly, hosts with and without a leading dot are also equivalent. [source,erlang] HostMatch1 = "cowboy.example.org". HostMatch2 = "cowboy.example.org.". HostMatch3 = ".cowboy.example.org". It is possible to extract segments of the host and path and to store the values in the `Req` object for later use. We call these kind of values bindings. The syntax for bindings is very simple. A segment that begins with the `:` character means that what follows until the end of the segment is the name of the binding in which the segment value will be stored. [source,erlang] PathMatch = "/hats/:name/prices". HostMatch = ":subdomain.example.org". If these two end up matching when routing, you will end up with two bindings defined, `subdomain` and `name`, each containing the segment value where they were defined. For example, the URL `http://test.example.org/hats/wild_cowboy_legendary/prices` will result in having the value `test` bound to the name `subdomain` and the value `wild_cowboy_legendary` bound to the name `name`. They can later be retrieved using `cowboy_req:binding/{2,3}`. The binding name must be given as an atom. There is a special binding name you can use to mimic the underscore variable in Erlang. Any match against the `_` binding will succeed but the data will be discarded. This is especially useful for matching against many domain names in one go. [source,erlang] HostMatch = "ninenines.:_". Similarly, it is possible to have optional segments. Anything between brackets is optional. [source,erlang] PathMatch = "/hats/[page/:number]". HostMatch = "[www.]ninenines.eu". You can also have imbricated optional segments. [source,erlang] PathMatch = "/hats/[page/[:number]]". While Cowboy does not reject multiple brackets in a route, the behavior may be undefined if the route is under-specified. For example, this route requires constraints to determine what is a chapter and what is a page, since they are both optional: [source,erlang] PathMatch = "/book/[:chapter]/[:page]". You can retrieve the rest of the host or path using `[...]`. In the case of hosts it will match anything before, in the case of paths anything after the previously matched segments. It is a special case of optional segments, in that it can have zero, one or many segments. You can then find the segments using `cowboy_req:host_info/1` and `cowboy_req:path_info/1` respectively. They will be represented as a list of segments. [source,erlang] PathMatch = "/hats/[...]". HostMatch = "[...]ninenines.eu". If a binding appears twice in the routing rules, then the match will succeed only if they share the same value. This copies the Erlang pattern matching behavior. [source,erlang] PathMatch = "/hats/:name/:name". This is also true when an optional segment is present. In this case the two values must be identical only if the segment is available. [source,erlang] PathMatch = "/hats/:name/[:name]". If a binding is defined in both the host and path, then they must also share the same value. [source,erlang] PathMatch = "/:user/[...]". HostMatch = ":user.github.com". Finally, there are two special match values that can be used. The first is the atom `'_'` which will match any host or path. [source,erlang] PathMatch = '_'. HostMatch = '_'. The second is the special host match `"*"` which will match the wildcard path, generally used alongside the `OPTIONS` method. [source,erlang] HostMatch = "*". === Constraints After the matching has completed, the resulting bindings can be tested against a set of constraints. Constraints are only tested when the binding is defined. They run in the order you defined them. The match will succeed only if they all succeed. If the match fails, then Cowboy tries the next route in the list. The format used for constraints is the same as match functions in `cowboy_req`: they are provided as a list of fields which may have one or more constraints. While the router accepts the same format, it will skip fields with no constraints and will also ignore default values, if any. Read more about xref:constraints[constraints]. === Compilation The routes must be compiled before Cowboy can use them. The compilation step normalizes the routes to simplify the code and speed up the execution, but the routes are still looked up one by one in the end. Faster compilation strategies could be to compile the routes directly to Erlang code, but would require heavier dependencies. To compile routes, just call the appropriate function: [source,erlang] ---- Dispatch = cowboy_router:compile([ %% {HostMatch, list({PathMatch, Handler, InitialState})} {'_', [{'_', my_handler, #{}}]} ]), %% Name, TransOpts, ProtoOpts cowboy:start_clear(my_http_listener, [{port, 8080}], #{env => #{dispatch => Dispatch}} ). ---- === Using persistent_term The routes can be stored in `persistent_term` starting from Erlang/OTP 21.2. This may give a performance improvement when there are a large number of routes. To use this functionality you need to compile the routes, store them in `persistent_term` and then inform Cowboy: [source,erlang] ---- Dispatch = cowboy_router:compile([ {'_', [{'_', my_handler, #{}}]} ]), persistent_term:put(my_app_dispatch, Dispatch), cowboy:start_clear(my_http_listener, [{port, 8080}], #{env => #{dispatch => {persistent_term, my_app_dispatch}}} ). ---- === Live update You can use the `cowboy:set_env/3` function for updating the dispatch list used by routing. This will apply to all new connections accepted by the listener: [source,erlang] Dispatch = cowboy_router:compile(Routes), cowboy:set_env(my_http_listener, dispatch, Dispatch). Note that you need to compile the routes again before updating. When using `persistent_term` there is no need to call this function, you can simply put the new routes in the storage. ================================================ FILE: doc/src/guide/specs.asciidoc ================================================ [appendix] == HTTP and other specifications This chapter intends to list all the specification documents for or related to HTTP. === HTTP ==== IANA Registries * https://www.iana.org/assignments/http-methods/http-methods.xhtml[HTTP Method Registry] * https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml[HTTP Status Code Registry] * https://www.iana.org/assignments/message-headers/message-headers.xhtml[Message Headers] * https://www.iana.org/assignments/http-parameters/http-parameters.xhtml[HTTP Parameters] * https://www.iana.org/assignments/http-alt-svc-parameters/http-alt-svc-parameters.xhtml[HTTP Alt-Svc Parameter Registry] * https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml[HTTP Authentication Scheme Registry] * https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml[HTTP Cache Directive Registry] * https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml[HTTP Digest Algorithm Values] * https://www.iana.org/assignments/hoba-device-identifiers/hoba-device-identifiers.xhtml[HTTP Origin-Bound Authentication Device Identifier Types] * https://www.iana.org/assignments/http-upgrade-tokens/http-upgrade-tokens.xhtml[HTTP Upgrade Token Registry] * https://www.iana.org/assignments/http-warn-codes/http-warn-codes.xhtml[HTTP Warn Codes] * https://www.iana.org/assignments/http2-parameters/http2-parameters.xhtml[HTTP/2 Parameters] * https://www.ietf.org/assignments/websocket/websocket.xml[WebSocket Protocol Registries] ==== Current * http://www.w3.org/TR/cors/[CORS]: Cross-Origin Resource Sharing * http://www.w3.org/TR/CSP2/[CSP2]: Content Security Policy Level 2 * http://www.w3.org/TR/tracking-dnt/[DNT]: Tracking Preference Expression (DNT) * http://www.w3.org/TR/eventsource/[eventsource]: Server-Sent Events * https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4[Form content types]: Form content types * https://www.w3.org/TR/preload/[Preload]: Preload * https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt[PROXY]: The PROXY protocol * http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm[REST]: Fielding's Dissertation * https://tools.ietf.org/html/rfc1945[RFC 1945]: HTTP/1.0 * https://tools.ietf.org/html/rfc1951[RFC 1951]: DEFLATE Compressed Data Format Specification version 1.3 * https://tools.ietf.org/html/rfc1952[RFC 1952]: GZIP file format specification version 4.3 * https://tools.ietf.org/html/rfc2046#section-5.1[RFC 2046]: Multipart media type (in MIME Part Two: Media Types) * https://tools.ietf.org/html/rfc2295[RFC 2295]: Transparent Content Negotiation in HTTP * https://tools.ietf.org/html/rfc2296[RFC 2296]: HTTP Remote Variant Selection Algorithm: RVSA/1.0 * https://tools.ietf.org/html/rfc2817[RFC 2817]: Upgrading to TLS Within HTTP/1.1 * https://tools.ietf.org/html/rfc2818[RFC 2818]: HTTP Over TLS * https://tools.ietf.org/html/rfc3230[RFC 3230]: Instance Digests in HTTP * https://tools.ietf.org/html/rfc4559[RFC 4559]: SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft Windows * https://tools.ietf.org/html/rfc5789[RFC 5789]: PATCH Method for HTTP * https://tools.ietf.org/html/rfc5843[RFC 5843]: Additional Hash Algorithms for HTTP Instance Digests * https://tools.ietf.org/html/rfc5861[RFC 5861]: HTTP Cache-Control Extensions for Stale Content * https://tools.ietf.org/html/rfc6265[RFC 6265]: HTTP State Management Mechanism * https://tools.ietf.org/html/rfc6266[RFC 6266]: Use of the Content-Disposition Header Field * https://tools.ietf.org/html/rfc6454[RFC 6454]: The Web Origin Concept * https://tools.ietf.org/html/rfc6455[RFC 6455]: The WebSocket Protocol * https://tools.ietf.org/html/rfc6585[RFC 6585]: Additional HTTP Status Codes * https://tools.ietf.org/html/rfc6750[RFC 6750]: The OAuth 2.0 Authorization Framework: Bearer Token Usage * https://tools.ietf.org/html/rfc6797[RFC 6797]: HTTP Strict Transport Security (HSTS) * https://tools.ietf.org/html/rfc6903[RFC 6903]: Additional Link Relation Types * https://tools.ietf.org/html/rfc7034[RFC 7034]: HTTP Header Field X-Frame-Options * https://tools.ietf.org/html/rfc7089[RFC 7089]: Time-Based Access to Resource States: Memento * https://tools.ietf.org/html/rfc7230[RFC 7230]: HTTP/1.1 Message Syntax and Routing * https://tools.ietf.org/html/rfc7231[RFC 7231]: HTTP/1.1 Semantics and Content * https://tools.ietf.org/html/rfc7232[RFC 7232]: HTTP/1.1 Conditional Requests * https://tools.ietf.org/html/rfc7233[RFC 7233]: HTTP/1.1 Range Requests * https://tools.ietf.org/html/rfc7234[RFC 7234]: HTTP/1.1 Caching * https://tools.ietf.org/html/rfc7235[RFC 7235]: HTTP/1.1 Authentication * https://tools.ietf.org/html/rfc7239[RFC 7239]: Forwarded HTTP Extension * https://tools.ietf.org/html/rfc7240[RFC 7240]: Prefer Header for HTTP * https://tools.ietf.org/html/rfc7469[RFC 7469]: Public Key Pinning Extension for HTTP * https://tools.ietf.org/html/rfc7486[RFC 7486]: HTTP Origin-Bound Authentication (HOBA) * https://tools.ietf.org/html/rfc7538[RFC 7538]: HTTP Status Code 308 (Permanent Redirect) * https://tools.ietf.org/html/rfc7540[RFC 7540]: Hypertext Transfer Protocol Version 2 (HTTP/2) * https://tools.ietf.org/html/rfc7541[RFC 7541]: HPACK: Header Compression for HTTP/2 * https://tools.ietf.org/html/rfc7578[RFC 7578]: Returning Values from Forms: multipart/form-data * https://tools.ietf.org/html/rfc7615[RFC 7615]: HTTP Authentication-Info and Proxy-Authentication-Info Response Header Fields * https://tools.ietf.org/html/rfc7616[RFC 7616]: HTTP Digest Access Authentication * https://tools.ietf.org/html/rfc7617[RFC 7617]: The 'Basic' HTTP Authentication Scheme * https://tools.ietf.org/html/rfc7639[RFC 7639]: The ALPN HTTP Header Field * https://tools.ietf.org/html/rfc7692[RFC 7692]: Compression Extensions for WebSocket * https://tools.ietf.org/html/rfc7694[RFC 7694]: HTTP Client-Initiated Content-Encoding * https://tools.ietf.org/html/rfc7725[RFC 7725]: An HTTP Status Code to Report Legal Obstacles * https://tools.ietf.org/html/rfc7804[RFC 7804]: Salted Challenge Response HTTP Authentication Mechanism * https://tools.ietf.org/html/rfc7838[RFC 7838]: HTTP Alternative Services * https://tools.ietf.org/html/rfc7932[RFC 7932]: Brotli Compressed Data Format * https://tools.ietf.org/html/rfc7936[RFC 7936]: Clarifying Registry Procedures for the WebSocket Subprotocol Name Registry * https://tools.ietf.org/html/rfc8053[RFC 8053]: HTTP Authentication Extensions for Interactive Clients * https://tools.ietf.org/html/rfc8164[RFC 8164]: Opportunistic Security for HTTP/2 * https://tools.ietf.org/html/rfc8187[RFC 8187]: Indicating Character Encoding and Language for HTTP Header Field Parameters * https://tools.ietf.org/html/rfc8188[RFC 8188]: Encrypted Content-Encoding for HTTP * https://tools.ietf.org/html/rfc8246[RFC 8246]: HTTP Immutable Responses * https://tools.ietf.org/html/rfc8288[RFC 8288]: Web Linking * https://tools.ietf.org/html/rfc8297[RFC 8297]: An HTTP Status Code for Indicating Hints * https://tools.ietf.org/html/rfc8336[RFC 8336]: The ORIGIN HTTP/2 Frame * https://tools.ietf.org/html/rfc8441[RFC 8441]: Bootstrapping WebSockets with HTTP/2 * https://tools.ietf.org/html/rfc8470[RFC 8470]: Using Early Data in HTTP * https://tools.ietf.org/html/rfc8473[RFC 8473]: Token Binding over HTTP * https://tools.ietf.org/html/rfc8586[RFC 8586]: Loop Detection in Content Delivery Networks (CDNs) * https://tools.ietf.org/html/rfc8594[RFC 8594]: The Sunset HTTP Header Field * https://tools.ietf.org/html/rfc8673[RFC 8673]: HTTP Random Access and Live Content * https://tools.ietf.org/html/rfc8674[RFC 8674]: The "safe" HTTP Preference * https://tools.ietf.org/html/rfc8740[RFC 8740]: Using TLS 1.3 with HTTP/2 * https://tools.ietf.org/html/rfc8941[RFC 8941]: Structured Field Values for HTTP * https://tools.ietf.org/html/rfc8942[RFC 8942]: HTTP Client Hints * https://www.w3.org/TR/trace-context/[Trace Context]: Trace Context * https://www.w3.org/TR/webmention/[Webmention]: Webmention ==== Upcoming * https://www.w3.org/TR/clear-site-data/[Clear Site Data] * https://www.w3.org/TR/csp-cookies/[Content Security Policy: Cookie Controls] * https://www.w3.org/TR/csp-embedded-enforcement/[Content Security Policy: Embedded Enforcement] * https://www.w3.org/TR/CSP3/[Content Security Policy Level 3] * https://www.w3.org/TR/csp-pinning/[Content Security Policy Pinning] * http://www.w3.org/TR/referrer-policy/[Referrer Policy] * http://www.w3.org/TR/UISecurity/[User Interface Security Directives for Content Security Policy] ==== Informative * http://www.w3.org/TR/webarch/[Architecture of the World Wide Web] * https://tools.ietf.org/html/rfc2936[RFC 2936]: HTTP MIME Type Handler Detection * https://tools.ietf.org/html/rfc2964[RFC 2964]: Use of HTTP State Management * https://tools.ietf.org/html/rfc3143[RFC 3143]: Known HTTP Proxy/Caching Problems * https://tools.ietf.org/html/rfc6202[RFC 6202]: Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP * https://tools.ietf.org/html/rfc6838[RFC 6838]: Media Type Specifications and Registration Procedures * https://tools.ietf.org/html/rfc7478[RFC 7478]: Web Real-Time Communication Use Cases and Requirements ==== Related * http://www.w3.org/TR/app-uri/[app: URL Scheme] * http://www.w3.org/TR/beacon/[Beacon] * http://www.w3.org/TR/FileAPI/[File API] * https://tools.ietf.org/html/rfc8030[Generic Event Delivery Using HTTP Push] * http://www.w3.org/TR/capability-urls/[Good Practices for Capability URLs] * https://html.spec.whatwg.org/multipage/[HTML Living Standard] * https://developers.whatwg.org/[HTML Living Standard for Web developers] * http://www.w3.org/TR/html401/[HTML4.01] * http://www.w3.org/TR/html5/[HTML5] * http://www.w3.org/TR/html51/[HTML5.1] * https://www.w3.org/TR/html52/[HTML5.2] * http://www.w3.org/TR/media-frags/[Media Fragments URI 1.0] * https://tools.ietf.org/html/rfc5829[RFC 5829]: Link Relation Types for Simple Version Navigation between Web Resources * https://tools.ietf.org/html/rfc6657[RFC 6657]: Update to MIME regarding "charset" Parameter Handling in Textual Media Types * https://tools.ietf.org/html/rfc6690[RFC 6690]: Constrained RESTful Environments (CoRE) Link Format * https://tools.ietf.org/html/rfc7807[RFC 7807]: Problem Details for HTTP APIs * https://tools.ietf.org/html/rfc6906[RFC 6906]: The 'profile' Link Relation Type * https://tools.ietf.org/html/rfc8631[RFC 8631]: Link Relation Types for Web Services * http://www.w3.org/TR/SRI/[Subresource Integrity] * http://www.w3.org/TR/tracking-compliance/[Tracking Compliance and Scope] * http://www.w3.org/TR/media-frags-reqs/[Use cases and requirements for Media Fragments] * http://www.w3.org/TR/webrtc/[WebRTC 1.0: Real-time Communication Between Browsers] * http://www.w3.org/TR/websockets/[Websocket API] * http://www.w3.org/TR/XMLHttpRequest/[XMLHttpRequest Level 1] * https://xhr.spec.whatwg.org/[XMLHttpRequest Living Standard] ==== Seemingly obsolete * https://tools.ietf.org/html/rfc2227[RFC 2227]: Simple Hit-Metering and Usage-Limiting for HTTP * https://tools.ietf.org/html/rfc2310[RFC 2310]: The Safe Response Header Field * https://tools.ietf.org/html/rfc2324[RFC 2324]: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0) * https://tools.ietf.org/html/rfc2660[RFC 2660]: The Secure HyperText Transfer Protocol * https://tools.ietf.org/html/rfc2774[RFC 2774]: An HTTP Extension Framework * https://tools.ietf.org/html/rfc2965[RFC 2965]: HTTP State Management Mechanism (Cookie2) * https://tools.ietf.org/html/rfc3229[RFC 3229]: Delta encoding in HTTP * https://tools.ietf.org/html/rfc7168[RFC 7168]: The Hyper Text Coffee Pot Control Protocol for Tea Efflux Appliances (HTCPCP-TEA) * https://tools.ietf.org/html/rfc8565[RFC 8565]: Hypertext Jeopardy Protocol (HTJP/1.0) * http://dev.chromium.org/spdy/spdy-protocol[SPDY]: SPDY Protocol * https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06[x-webkit-deflate-frame]: Deprecated Websocket compression === URL * https://tools.ietf.org/html/rfc3986[RFC 3986]: URI Generic Syntax * https://tools.ietf.org/html/rfc6570[RFC 6570]: URI Template * https://tools.ietf.org/html/rfc6874[RFC 6874]: Representing IPv6 Zone Identifiers in Address Literals and URIs * https://tools.ietf.org/html/rfc7320[RFC 7320]: URI Design and Ownership * https://tools.ietf.org/html/rfc8615[RFC 8615]: Well-Known URIs * http://www.w3.org/TR/url-1/[URL] * https://url.spec.whatwg.org/[URL Living Standard] === WebDAV * https://tools.ietf.org/html/rfc3253[RFC 3253]: Versioning Extensions to WebDAV * https://tools.ietf.org/html/rfc3648[RFC 3648]: WebDAV Ordered Collections Protocol * https://tools.ietf.org/html/rfc3744[RFC 3744]: WebDAV Access Control Protocol * https://tools.ietf.org/html/rfc4316[RFC 4316]: Datatypes for WebDAV Properties * https://tools.ietf.org/html/rfc4331[RFC 4331]: Quota and Size Properties for DAV Collections * https://tools.ietf.org/html/rfc4437[RFC 4437]: WebDAV Redirect Reference Resources * https://tools.ietf.org/html/rfc4709[RFC 4709]: Mounting WebDAV Servers * https://tools.ietf.org/html/rfc4791[RFC 4791]: Calendaring Extensions to WebDAV (CalDAV) * https://tools.ietf.org/html/rfc4918[RFC 4918]: HTTP Extensions for WebDAV * https://tools.ietf.org/html/rfc5323[RFC 5323]: WebDAV SEARCH * https://tools.ietf.org/html/rfc5397[RFC 5397]: WebDAV Current Principal Extension * https://tools.ietf.org/html/rfc5689[RFC 5689]: Extended MKCOL for WebDAV * https://tools.ietf.org/html/rfc5842[RFC 5842]: Binding Extensions to WebDAV * https://tools.ietf.org/html/rfc5995[RFC 5995]: Using POST to Add Members to WebDAV Collections * https://tools.ietf.org/html/rfc6352[RFC 6352]: CardDAV: vCard Extensions to WebDAV * https://tools.ietf.org/html/rfc6578[RFC 6578]: Collection Synchronization for WebDAV * https://tools.ietf.org/html/rfc6638[RFC 6638]: Scheduling Extensions to CalDAV * https://tools.ietf.org/html/rfc6764[RFC 6764]: Locating Services for Calendaring Extensions to WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV) * https://tools.ietf.org/html/rfc7809[RFC 7809]: Calendaring Extensions to WebDAV (CalDAV): Time Zones by Reference * https://tools.ietf.org/html/rfc7953[RFC 7953]: Calendar Availability * https://tools.ietf.org/html/rfc8144[RFC 8144]: Use of the Prefer Header Field in WebDAV * https://tools.ietf.org/html/rfc8607[RFC 8607]: Calendaring Extensions to WebDAV (CalDAV): Managed Attachments === CoAP * https://tools.ietf.org/html/rfc7252[RFC 7252]: The Constrained Application Protocol (CoAP) * https://tools.ietf.org/html/rfc7390[RFC 7390]: Group Communication for CoAP * https://tools.ietf.org/html/rfc7641[RFC 7641]: Observing Resources in CoAP * https://tools.ietf.org/html/rfc7650[RFC 7650]: A CoAP Usage for REsource LOcation And Discovery (RELOAD) * https://tools.ietf.org/html/rfc7959[RFC 7959]: Block-Wise Transfers in CoAP * https://tools.ietf.org/html/rfc7967[RFC 7967]: CoAP Option for No Server Response * https://tools.ietf.org/html/rfc8075[RFC 8075]: Guidelines for Mapping Implementations: HTTP to CoAP * https://tools.ietf.org/html/rfc8132[RFC 8132]: PATCH and FETCH Methods for CoAP * https://tools.ietf.org/html/rfc8323[RFC 8323]: CoAP over TCP, TLS, and WebSockets * https://tools.ietf.org/html/rfc8516[RFC 8516]: "Too Many Requests" Response Code for CoAP * https://tools.ietf.org/html/rfc8613[RFC 8613]: Object Security for Constrained RESTful Environments * https://tools.ietf.org/html/rfc8710[RFC 8710]: Multipart Content-Format for CoAP * https://tools.ietf.org/html/rfc8768[RFC 8768]: CoAP Hop-Limit Option ================================================ FILE: doc/src/guide/static_files.asciidoc ================================================ [[static_files]] == Static files Cowboy comes with a ready to use handler for serving static files. It is provided as a convenience for serving files during development. For systems in production, consider using one of the many Content Distribution Network (CDN) available on the market, as they are the best solution for serving files. The static handler can serve either one file or all files from a given directory. The etag generation and mime types can be configured. === Serve one file You can use the static handler to serve one specific file from an application's private directory. This is particularly useful to serve an 'index.html' file when the client requests the `/` path, for example. The path configured is relative to the given application's private directory. The following rule will serve the file 'static/index.html' from the application `my_app`'s priv directory whenever the path `/` is accessed: [source,erlang] {"/", cowboy_static, {priv_file, my_app, "static/index.html"}} You can also specify the absolute path to a file, or the path to the file relative to the current directory: [source,erlang] {"/", cowboy_static, {file, "/var/www/index.html"}} === Serve all files from a directory You can also use the static handler to serve all files that can be found in the configured directory. The handler will use the `path_info` information to resolve the file location, which means that your route must end with a `[...]` pattern for it to work. All files are served, including the ones that may be found in subfolders. You can specify the directory relative to the application's private directory (e.g. `my_app/priv`). The following rule will serve any file found in the `my_app` application's private directory in the `my_app/priv/static/assets` folder whenever the requested path begins with `/assets/`: [source,erlang] {"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets"}} You can also specify the absolute path to the directory or set it relative to the current directory: [source,erlang] {"/assets/[...]", cowboy_static, {dir, "/var/www/assets"}} === Customize the mimetype detection By default, Cowboy will attempt to recognize the mimetype of your static files by looking at the extension. You can override the function that figures out the mimetype of the static files. It can be useful when Cowboy is missing a mimetype you need to handle, or when you want to reduce the list to make lookups faster. You can also give a hard-coded mimetype that will be used unconditionally. Cowboy comes with two functions built-in. The default function only handles common file types used when building Web applications. The other function is an extensive list of hundreds of mimetypes that should cover almost any need you may have. You can of course create your own function. To use the default function, you should not have to configure anything, as it is the default. If you insist, though, the following will do the job: [source,erlang] ---- {"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{mimetypes, cow_mimetypes, web}]}} ---- As you can see, there is an optional field that may contain a list of less used options, like mimetypes or etag. All option types have this optional field. To use the function that will detect almost any mimetype, the following configuration will do: [source,erlang] ---- {"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{mimetypes, cow_mimetypes, all}]}} ---- You probably noticed the pattern by now. The configuration expects a module and a function name, so you can use any of your own functions instead: [source,erlang] ---- {"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{mimetypes, Module, Function}]}} ---- The function that performs the mimetype detection receives a single argument that is the path to the file on disk. It is recommended to return the mimetype in tuple form, although a binary string is also allowed (but will require extra processing). If the function can't figure out the mimetype, then it should return `{<<"application">>, <<"octet-stream">>, []}`. When the static handler fails to find the extension, it will send the file as `application/octet-stream`. A browser receiving such file will attempt to download it directly to disk. Finally, the mimetype can be hard-coded for all files. This is especially useful in combination with the `file` and `priv_file` options as it avoids needless computation: [source,erlang] ---- {"/", cowboy_static, {priv_file, my_app, "static/index.html", [{mimetypes, {<<"text">>, <<"html">>, []}}]}} ---- === Generate an etag By default, the static handler will generate an etag header value based on the size and modified time. This solution can not be applied to all systems though. It would perform rather poorly over a cluster of nodes, for example, as the file metadata will vary from server to server, giving a different etag on each server. You can however change the way the etag is calculated: [source,erlang] ---- {"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{etag, Module, Function}]}} ---- This function will receive three arguments: the path to the file on disk, the size of the file and the last modification time. In a distributed setup, you would typically use the file path to retrieve an etag value that is identical across all your servers. You can also completely disable etag handling: [source,erlang] ---- {"/assets/[...]", cowboy_static, {priv_dir, my_app, "static/assets", [{etag, false}]}} ---- ================================================ FILE: doc/src/guide/streams.asciidoc ================================================ [[streams]] == Streams A stream is the set of messages that form an HTTP request/response pair. The term stream comes from HTTP/2. In Cowboy, it is also used when talking about HTTP/1.1 or HTTP/1.0. It should not be confused with streaming the request or response body. All versions of HTTP allow clients to initiate streams. HTTP/2 is the only one also allowing servers, through its server push feature. Both client and server-initiated streams go through the same process in Cowboy. === Stream handlers link:man:cowboy_stream(3)[Stream handlers] must implement five different callbacks. Four of them are directly related; one is special. All callbacks receives the stream ID as first argument. Most of them can return a list of commands to be executed by Cowboy. When callbacks are chained, it is possible to intercept and modify these commands. This can be useful for modifying responses for example. The `init/3` callback is invoked when a new request comes in. It receives the Req object and the protocol options for this listener. The `data/4` callback is invoked when data from the request body is received. It receives both this data and a flag indicating whether more is to be expected. The `info/3` callback is invoked when an Erlang message is received for this stream. They will typically be messages sent by the request process. Finally the `terminate/3` callback is invoked with the terminate reason for the stream. The return value is ignored. Note that as with all terminate callbacks in Erlang, there is no strong guarantee that it will be called. The special callback `early_error/5` is called when an error occurs before the request headers were fully received and Cowboy is sending a response. It receives the partial Req object, the error reason, the protocol options and the response Cowboy will send. This response must be returned, possibly modified. === Built-in handlers Cowboy comes with four handlers. link:man:cowboy_stream_h(3)[cowboy_stream_h] is the default stream handler. It is the core of much of the functionality of Cowboy. All chains of stream handlers should call it last. link:man:cowboy_compress_h(3)[cowboy_compress_h] will automatically compress responses when possible. It is not enabled by default. It is a good example for writing your own handlers that will modify responses. link:man:cowboy_decompress_h(3)[cowboy_decompress_h] will automatically decompress request bodies when possible. It is not enabled by default. It is a good example for writing your own handlers that will modify requests. link:man:cowboy_metrics_h(3)[cowboy_metrics_h] gathers metrics about a stream then passes them to a configurable function. It is not enabled by default. link:man:cowboy_tracer_h(3)[cowboy_tracer_h] can be used to conditionally trace streams based on the contents of the request or its origin. Trace events are passed to a configurable function. It is not enabled by default. ================================================ FILE: doc/src/guide/ws_handlers.asciidoc ================================================ [[ws_handlers]] == Websocket handlers Websocket handlers provide an interface for upgrading HTTP/1.1 connections to Websocket and sending or receiving frames on the Websocket connection. As Websocket connections are established through the HTTP/1.1 upgrade mechanism, Websocket handlers need to be able to first receive the HTTP request for the upgrade, before switching to Websocket and taking over the connection. They can then receive or send Websocket frames, handle incoming Erlang messages or close the connection. === Upgrade The `init/2` callback is called when the request is received. To establish a Websocket connection, you must switch to the `cowboy_websocket` module: [source,erlang] ---- init(Req, State) -> {cowboy_websocket, Req, State}. ---- Cowboy will perform the Websocket handshake immediately. Note that the handshake will fail if the client did not request an upgrade to Websocket. The Req object becomes unavailable after this function returns. Any information required for proper execution of the Websocket handler must be saved in the state. === Subprotocol The client may provide a list of Websocket subprotocols it supports in the sec-websocket-protocol header. The server *must* select one of them and send it back to the client or the handshake will fail. For example, a client could understand both STOMP and MQTT over Websocket, and provide the header: ---- sec-websocket-protocol: v12.stomp, mqtt ---- If the server only understands MQTT it can return: ---- sec-websocket-protocol: mqtt ---- This selection must be done in `init/2`. An example usage could be: [source,erlang] ---- init(Req0, State) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req0) of undefined -> {cowboy_websocket, Req0, State}; Subprotocols -> case lists:member(<<"mqtt">>, 1, Subprotocols) of true -> Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req0), {cowboy_websocket, Req, State}; false -> Req = cowboy_req:reply(400, Req0), {ok, Req, State} end end. ---- === Post-upgrade initialization Cowboy has separate processes for handling the connection and requests. Because Websocket takes over the connection, the Websocket protocol handling occurs in a different process than the request handling. This is reflected in the different callbacks Websocket handlers have. The `init/2` callback is called from the temporary request process and the `websocket_` callbacks from the connection process. This means that some initialization cannot be done from `init/2`. Anything that would require the current pid, or be tied to the current pid, will not work as intended. The optional `websocket_init/1` can be used instead: [source,erlang] ---- websocket_init(State) -> erlang:start_timer(1000, self(), <<"Hello!">>), {ok, State}. ---- All Websocket callbacks share the same return values. This means that we can send frames to the client right after the upgrade: [source,erlang] ---- websocket_init(State) -> {[{text, <<"Hello!">>}], State}. ---- === Receiving frames Cowboy will call `websocket_handle/2` whenever a text, binary, ping or pong frame arrives from the client. The handler can handle or ignore the frames. It can also send frames back to the client or stop the connection. The following snippet echoes back any text frame received and ignores all others: [source,erlang] ---- websocket_handle(Frame = {text, _}, State) -> {[Frame], State}; websocket_handle(_Frame, State) -> {ok, State}. ---- Note that ping and pong frames require no action from the handler as Cowboy will automatically reply to ping frames. They are provided for informative purposes only. === Receiving Erlang messages Cowboy will call `websocket_info/2` whenever an Erlang message arrives. The handler can handle or ignore the messages. It can also send frames to the client or stop the connection. The following snippet forwards log messages to the client and ignores all others: [source,erlang] ---- websocket_info({log, Text}, State) -> {[{text, Text}], State}; websocket_info(_Info, State) -> {ok, State}. ---- === Sending frames // @todo This will be deprecated and eventually replaced with a // {Commands, State} interface that allows providing more // functionality easily. All `websocket_` callbacks share return values. They may send zero, one or many frames to the client. To send nothing, just return an ok tuple: [source,erlang] ---- websocket_info(_Info, State) -> {ok, State}. ---- To send one frame, return the frame to be sent: [source,erlang] ---- websocket_info(_Info, State) -> {[{text, <<"Hello!">>}], State}. ---- You can send frames of any type: text, binary, ping, pong or close frames. You can send many frames at the same time: [source,erlang] ---- websocket_info(_Info, State) -> {[ {text, "Hello"}, {text, <<"world!">>}, {binary, <<0:8000>>} ], State}. ---- They are sent in the given order. === Keeping the connection alive Cowboy will automatically respond to ping frames sent by the client. They are still forwarded to the handler for informative purposes, but no further action is required. Cowboy does not send ping frames itself. The handler can do it if required. A better solution in most cases is to let the client handle pings. Doing it from the handler would imply having an additional timer per connection and this can be a considerable cost for servers that need to handle large numbers of connections. Cowboy can be configured to close idle connections automatically. It is highly recommended to configure a timeout here, to avoid having processes linger longer than needed. The `init/2` callback can set the timeout to be used for the connection. For example, this would make Cowboy close connections idle for more than 30 seconds: [source,erlang] ---- init(Req, State) -> {cowboy_websocket, Req, State, #{ idle_timeout => 30000}}. ---- This value cannot be changed once it is set. It defaults to `60000`. === Limiting frame sizes Cowboy accepts frames of any size by default. You should limit the size depending on what your handler may handle. You can do this via the `init/2` callback: [source,erlang] ---- init(Req, State) -> {cowboy_websocket, Req, State, #{ max_frame_size => 8000000}}. ---- The lack of limit is historical. A future version of Cowboy will have a more reasonable default. === Saving memory The Websocket connection process can be set to hibernate after the callback returns. Simply add an `hibernate` field to the returned tuple: [source,erlang] ---- websocket_init(State) -> {[], State, hibernate}. websocket_handle(_Frame, State) -> {[], State, hibernate}. websocket_info(_Info, State) -> {[{text, <<"Hello!">>}], State, hibernate}. ---- It is highly recommended to write your handlers with hibernate enabled, as this allows to greatly reduce the memory usage. Do note however that an increase in the CPU usage or latency can be observed instead, in particular for the more busy connections. === Closing the connection The connection can be closed at any time, either by telling Cowboy to stop it or by sending a close frame. To tell Cowboy to close the connection, use a stop tuple: [source,erlang] ---- websocket_info(_Info, State) -> {stop, State}. ---- Sending a `close` frame will immediately initiate the closing of the Websocket connection. Note that when sending a list of frames that include a close frame, any frame found after the close frame will not be sent. The following example sends a close frame with a reason message: [source,erlang] ---- websocket_info(_Info, State) -> {[{close, 1000, <<"some-reason">>}], State}. ---- ================================================ FILE: doc/src/guide/ws_protocol.asciidoc ================================================ [[ws_protocol]] == The Websocket protocol This chapter explains what Websocket is and why it is a vital component of soft realtime Web applications. === Description Websocket is an extension to HTTP that emulates plain TCP connections between the client, typically a Web browser, and the server. It uses the HTTP Upgrade mechanism to establish the connection. Websocket connections are fully asynchronous, unlike HTTP/1.1 (synchronous) and HTTP/2 (asynchronous, but the server can only initiate streams in response to requests). With Websocket, the client and the server can both send frames at any time without any restriction. It is closer to TCP than any of the HTTP protocols. Websocket is an IETF standard. Cowboy supports the standard and all drafts that were previously implemented by browsers, excluding the initial flawed draft sometimes known as "version 0". === Websocket vs HTTP/2 For a few years Websocket was the only way to have a bidirectional asynchronous connection with the server. This changed when HTTP/2 was introduced. While HTTP/2 requires the client to first perform a request before the server can push data, this is only a minor restriction as the client can do so just as it connects. Websocket was designed as a kind-of-TCP channel to a server. It only defines the framing and connection management and lets the developer implement a protocol on top of it. For example you could implement IRC over Websocket and use a Javascript IRC client to speak to the server. HTTP/2 on the other hand is just an improvement over the HTTP/1.1 connection and request/response mechanism. It has the same semantics as HTTP/1.1. If all you need is to access an HTTP API, then HTTP/2 should be your first choice. On the other hand, if what you need is a different protocol, then you can use Websocket to implement it. === Implementation Cowboy implements Websocket as a protocol upgrade. Once the upgrade is performed from the `init/2` callback, Cowboy switches to Websocket. Please consult the next chapter for more information on initiating and handling Websocket connections. The implementation of Websocket in Cowboy is validated using the Autobahn test suite, which is an extensive suite of tests covering all aspects of the protocol. Cowboy passes the suite with 100% success, including all optional tests. Cowboy's Websocket implementation also includes the permessage-deflate and x-webkit-deflate-frame compression extensions. Cowboy will automatically use compression when the `compress` option is returned from the `init/2` function. ================================================ FILE: doc/src/manual/cowboy.asciidoc ================================================ = cowboy(3) == Name cowboy - HTTP server == Description The module `cowboy` provides convenience functions for manipulating Ranch listeners. == Exports * link:man:cowboy:start_clear(3)[cowboy:start_clear(3)] - Listen for connections using plain TCP * link:man:cowboy:start_tls(3)[cowboy:start_tls(3)] - Listen for connections using TLS * link:man:cowboy:stop_listener(3)[cowboy:stop_listener(3)] - Stop the given listener * link:man:cowboy:get_env(3)[cowboy:get_env(3)] - Retrieve a listener's environment value * link:man:cowboy:set_env(3)[cowboy:set_env(3)] - Update a listener's environment value == Types === fields() [source,erlang] ---- fields() :: [Name | {Name, Constraints} | {Name, Constraints, Default}] Name :: atom() Constraints :: Constraint | [Constraint] Constraint :: cowboy_constraints:constraint() Default :: any() ---- Fields description for match operations. This type is used in link:man:cowboy_router(3)[cowboy_router(3)] for matching bindings and in the match functions found in link:man:cowboy_req(3)[cowboy_req(3)]. === http_headers() [source,erlang] ---- http_headers() :: #{binary() => iodata()} ---- HTTP headers. === http_status() [source,erlang] ---- http_status() :: non_neg_integer() | binary() ---- HTTP response status. A binary status can be used to set a reason phrase. Note however that HTTP/2 only sends the status code and drops the reason phrase entirely. === http_version() [source,erlang] ---- http_version() :: 'HTTP/2' | 'HTTP/1.1' | 'HTTP/1.0' ---- HTTP version. Note that semantically, HTTP/1.1 and HTTP/2 are equivalent. === opts() [source,erlang] ---- opts() :: map() ---- Options for the HTTP/1.1, HTTP/2 and Websocket protocols. The protocol options are in a map containing all the options for the different protocols that may be involved when connecting to the listener, including HTTP/1.1 and HTTP/2. The HTTP/1.1 options are documented in the link:man:cowboy_http(3)[cowboy_http(3)] manual and the HTTP/2 options in link:man:cowboy_http2(3)[cowboy_http2(3)]. == See also link:man:cowboy(7)[cowboy(7)], link:man:ranch(3)[ranch(3)] ================================================ FILE: doc/src/manual/cowboy.get_env.asciidoc ================================================ = cowboy:get_env(3) == Name cowboy:get_env - Retrieve a listener's environment value == Description [source,erlang] ---- get_env(Name :: ranch:ref(), Key :: atom()) -> any() get_env(Name :: ranch:ref(), Key :: atom(), Default :: any()) -> any() ---- Retrieve an environment value for a previously started listener. This function may crash when the key is missing from the environment and a default value is not provided. == Arguments Name:: The name of the listener to access. + The name of the listener is the first argument given to the link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)] or link:man:ranch:start_listener(3)[ranch:start_listener(3)] function. Key:: The key in the environment map. Common keys include `dispatch` and `middlewares`. Default:: The default value if the key is missing. == Return value The environment value is returned on success. If a default was provided and the key is missing, then the default value is returned. An `exit:badarg` exception is thrown when the listener does not exist. An `exit:{badkey, Key}` exception is thrown when the key requested is missing and no default was provided. == Changelog * *2.11*: Function introduced. == Examples .Retrieve a listener's routes [source,erlang] ---- Dispatch = cowboy:get_env(example, dispatch). ---- == See also link:man:cowboy(3)[cowboy(3)], link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)], link:man:cowboy:set_env(3)[cowboy:set_env(3)], link:man:ranch:get_protocol_options(3)[ranch:get_protocol_options(3)] ================================================ FILE: doc/src/manual/cowboy.set_env.asciidoc ================================================ = cowboy:set_env(3) == Name cowboy:set_env - Update a listener's environment value == Description [source,erlang] ---- set_env(Name :: ranch:ref(), Key :: atom(), Value :: any()) -> ok ---- Set or update an environment value for a previously started listener. This is most useful for updating the routes dynamically, without having to restart the listener. The new value will only be available to new connections. Pre-existing connections will still use the old value. == Arguments Name:: The name of the listener to update. + The name of the listener is the first argument given to the link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)] or link:man:ranch:start_listener(3)[ranch:start_listener(3)] function. Key:: The key in the environment map. Common keys include `dispatch` and `middlewares`. Value:: The new value. + The type of the value differs depending on the key. == Return value The atom `ok` is returned on success. An `exit:badarg` exception is thrown when the listener does not exist. == Changelog * *1.0*: Function introduced. == Examples .Update a listener's routes [source,erlang] ---- Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []}, {"/ws", websocket_h, []} ]} ]), cowboy:set_env(example, dispatch, Dispatch). ---- == See also link:man:cowboy(3)[cowboy(3)], link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)], link:man:cowboy:get_env(3)[cowboy:get_env(3)], link:man:ranch:set_protocol_options(3)[ranch:set_protocol_options(3)] ================================================ FILE: doc/src/manual/cowboy.start_clear.asciidoc ================================================ = cowboy:start_clear(3) == Name cowboy:start_clear - Listen for connections using plain TCP == Description [source,erlang] ---- start_clear(Name :: ranch:ref(), TransportOpts :: ranch_tcp:opts(), ProtocolOpts :: opts()) -> {ok, ListenerPid :: pid()} | {error, any()} ---- Start listening for connections over a clear TCP channel. Both HTTP/1.1 and HTTP/2 are supported on this listener. HTTP/2 has two methods of establishing a connection over a clear TCP channel. Both the upgrade and the prior knowledge methods are supported. == Arguments Name:: The listener name is used to refer to this listener in future calls, for example when stopping it or when updating the routes defined. + It can be any Erlang term. An atom is generally good enough, for example `api`, `my_app_clear` or `my_app_tls`. TransportOpts:: The transport options are where the TCP options, including the listener's port number, are defined. Transport options are provided as a list of keys and values, for example `[{port, 8080}]`. + The available options are documented in the link:man:ranch_tcp(3)[ranch_tcp(3)] manual. ProtocolOpts:: The protocol options are in a map containing all the options for the different protocols that may be involved when connecting to the listener, including HTTP/1.1 and HTTP/2. + The HTTP/1.1 options are documented in the link:man:cowboy_http(3)[cowboy_http(3)] manual; and the HTTP/2 options in link:man:cowboy_http2(3)[cowboy_http2(3)]. Stream handlers such as link:man:cowboy_stream_h(3)[cowboy_stream_h(3)] (which is enabled by default) may also define options. == Return value An ok tuple is returned on success. It contains the pid of the top-level supervisor for the listener. An error tuple is returned on error. The error reason may be any Erlang term. A common error is `eaddrinuse`. It indicates that the port configured for Cowboy is already in use. == Changelog * *2.0*: HTTP/2 support added. * *2.0*: Function introduced. Replaces `cowboy:start_http/4`. == Examples .Start a listener [source,erlang] ---- Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(example, [{port, 8080}], #{ env => #{dispatch => Dispatch} }). ---- .Start a listener on a random port [source,erlang] ---- Name = example, {ok, _} = cowboy:start_clear(Name, [], #{ env => #{dispatch => Dispatch} }), Port = ranch:get_port(Name). ---- == See also link:man:cowboy(3)[cowboy(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)], link:man:cowboy:stop_listener(3)[cowboy:stop_listener(3)], link:man:ranch(3)[ranch(3)] ================================================ FILE: doc/src/manual/cowboy.start_tls.asciidoc ================================================ = cowboy:start_tls(3) == Name cowboy:start_tls - Listen for connections using TLS == Description [source,erlang] ---- start_tls(Name :: ranch:ref(), TransportOpts :: ranch_ssl:opts(), ProtocolOpts :: opts()) -> {ok, ListenerPid :: pid()} | {error, any()} ---- Start listening for connections over a secure TLS channel. Both HTTP/1.1 and HTTP/2 are supported on this listener. The ALPN TLS extension must be used to initiate an HTTP/2 connection. == Arguments Name:: The listener name is used to refer to this listener in future calls, for example when stopping it or when updating the routes defined. + It can be any Erlang term. An atom is generally good enough, for example `api`, `my_app_clear` or `my_app_tls`. TransportOpts:: The transport options are where the TCP options, including the listener's port number, are defined. They also contain the TLS options, like the server's certificate. Transport options are provided as a list of keys and values, for example `[{port, 8443}, {certfile, "path/to/cert.pem"}]`. + The available options are documented in the link:man:ranch_ssl(3)[ranch_ssl(3)] manual. ProtocolOpts:: The protocol options are in a map containing all the options for the different protocols that may be involved when connecting to the listener, including HTTP/1.1 and HTTP/2. + The HTTP/1.1 options are documented in the link:man:cowboy_http(3)[cowboy_http(3)] manual; and the HTTP/2 options in link:man:cowboy_http2(3)[cowboy_http2(3)]. Stream handlers such as link:man:cowboy_stream_h(3)[cowboy_stream_h(3)] (which is enabled by default) may also define options. == Return value An ok tuple is returned on success. It contains the pid of the top-level supervisor for the listener. An error tuple is returned on error. The error reason may be any Erlang term. A common error is `eaddrinuse`. It indicates that the port configured for Cowboy is already in use. == Changelog * *2.0*: HTTP/2 support added. * *2.0*: Function introduced. Replaces `cowboy:start_https/4`. == Examples .Start a listener [source,erlang] ---- Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_tls(example, [ {port, 8443}, {certfile, "path/to/cert.pem"} ], #{ env => #{dispatch => Dispatch} }). ---- .Start a listener on a random port [source,erlang] ---- Name = example, {ok, _} = cowboy:start_tls(Name, [ {certfile, "path/to/cert.pem"} ], #{ env => #{dispatch => Dispatch} }), Port = ranch:get_port(Name). ---- == See also link:man:cowboy(3)[cowboy(3)], link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:stop_listener(3)[cowboy:stop_listener(3)], link:man:ranch(3)[ranch(3)] ================================================ FILE: doc/src/manual/cowboy.stop_listener.asciidoc ================================================ = cowboy:stop_listener(3) == Name cowboy:stop_listener - Stop the given listener == Description [source,erlang] ---- stop_listener(Name :: ranch:ref()) -> ok | {error, not_found}. ---- Stop a previously started listener. Alias of link:man:ranch:stop_listener(3)[ranch:stop_listener(3)]. == Arguments Name:: The name of the listener to be stopped. + The name of the listener is the first argument given to the link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)] or link:man:ranch:start_listener(3)[ranch:start_listener(3)] function. == Return value The atom `ok` is returned on success. The `{error, not_found}` tuple is returned when the listener does not exist. == Changelog * *1.0*: Function introduced. == Examples .Stop a listener [source,erlang] ---- ok = cowboy:stop_listener(example). ---- == See also link:man:cowboy(3)[cowboy(3)], link:man:cowboy:start_clear(3)[cowboy:start_clear(3)], link:man:cowboy:start_tls(3)[cowboy:start_tls(3)], link:man:ranch(3)[ranch(3)], link:man:ranch:start_listener(3)[ranch:start_listener(3)] ================================================ FILE: doc/src/manual/cowboy_app.asciidoc ================================================ = cowboy(7) == Name cowboy - Small, fast, modern HTTP server for Erlang/OTP == Description Cowboy is an HTTP server for Erlang/OTP with support for the HTTP/1.1, HTTP/2 and Websocket protocols. Cowboy aims to provide a complete HTTP stack. This includes the implementation of the HTTP RFCs but also any directly related standards, like Websocket or Server-Sent Events. == Modules Functions: * link:man:cowboy(3)[cowboy(3)] - Listener management * link:man:cowboy_req(3)[cowboy_req(3)] - Request and response * link:man:cowboy_router(3)[cowboy_router(3)] - Router * link:man:cowboy_constraints(3)[cowboy_constraints(3)] - Constraints Protocols: * link:man:cowboy_http(3)[cowboy_http(3)] - HTTP/1.1 * link:man:cowboy_http2(3)[cowboy_http2(3)] - HTTP/2 * link:man:cowboy_websocket(3)[cowboy_websocket(3)] - Websocket Handlers: * link:man:cowboy_static(3)[cowboy_static(3)] - Static file handler Stream handlers: * link:man:cowboy_stream_h(3)[cowboy_stream_h(3)] - Default stream handler * link:man:cowboy_compress_h(3)[cowboy_compress_h(3)] - Compress stream handler * link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)] - Decompress stream handler * link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)] - Metrics stream handler * link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)] - Tracer stream handler Behaviors: * link:man:cowboy_handler(3)[cowboy_handler(3)] - Plain HTTP handlers * link:man:cowboy_loop(3)[cowboy_loop(3)] - Loop handlers * link:man:cowboy_middleware(3)[cowboy_middleware(3)] - Middlewares * link:man:cowboy_rest(3)[cowboy_rest(3)] - REST handlers * link:man:cowboy_stream(3)[cowboy_stream(3)] - Stream handlers * link:man:cowboy_websocket(3)[cowboy_websocket(3)] - Websocket handlers Middlewares: * link:man:cowboy_router(3)[cowboy_router(3)] - Router middleware * link:man:cowboy_handler(3)[cowboy_handler(3)] - Handler middleware // @todo http_status_codes is not linked to; what to do with it? == Dependencies * link:man:ranch(7)[ranch(7)] - Socket acceptor pool for TCP protocols * link:man:cowlib(7)[cowlib(7)] - Support library for manipulating Web protocols * ssl - Secure communication over sockets * crypto - Crypto functions All these applications must be started before the `cowboy` application. To start Cowboy and all dependencies at once: [source,erlang] ---- {ok, _} = application:ensure_all_started(cowboy). ---- == Environment The `cowboy` application does not define any application environment configuration parameters. == See also link:man:ranch(7)[ranch(7)], link:man:cowlib(7)[cowlib(7)] ================================================ FILE: doc/src/manual/cowboy_compress_h.asciidoc ================================================ = cowboy_compress_h(3) == Name cowboy_compress_h - Compress stream handler == Description The module `cowboy_compress_h` compresses response bodies automatically when the client supports it. It will not try to compress responses that already have a content encoding or that have an etag header defined. Normal responses will only be compressed when their size is lower than the configured threshold. Streamed responses are always compressed, including when the sendfile command is used. Because the file must be read in memory to be compressed, this module is *not* suitable for automatically compressing large files. == Options [source,erlang] ---- opts() :: #{ compress_buffering => boolean(), compress_threshold => non_neg_integer() } ---- Configuration for the compress stream handler. The default value is given next to the option name: compress_buffering (false):: Whether the output will be buffered. By default no buffering is done to provide maximum compatibility at the cost of a lower compression rate. + This option can be updated at any time using the `set_options` stream handler command. compress_threshold (300):: How large the response body must be to be compressed when the response isn't streamed. + This option can be updated at any time using the `set_options` stream handler command. == Events The compress stream handler does not produce any event. == Changelog * *2.11*: Compression is now disabled when the etag header is in the response headers. * *2.11*: The vary: accept-encoding header is now always set when this handler is enabled. * *2.6*: The options `compress_buffering` and `compress_threshold` were added. * *2.0*: Module introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)], link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)], link:man:cowboy_stream_h(3)[cowboy_stream_h(3)], link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)] ================================================ FILE: doc/src/manual/cowboy_constraints.asciidoc ================================================ = cowboy_constraints(3) == Name cowboy_constraints - Constraints == Description The module `cowboy_constraints` defines the built-in constraints in Cowboy and provides an interface for manipulating these constraints. Constraints are functions that define what type of input is allowed. They are used throughout Cowboy, from the router to query strings to cookies. == Exports Built-in constraints: * link:man:cowboy_constraints:int(3)[cowboy_constraints:int(3)] - Integer constraint * link:man:cowboy_constraints:nonempty(3)[cowboy_constraints:nonempty(3)] - Non-empty constraint == Types === constraint() [source,erlang] ---- constraint() :: int | nonempty | fun() ---- A constraint function. The atom constraints are built-in, see the corresponding function in the exports list above. === reason() [source,erlang] ---- reason() :: {constraint(), Reason, Value} Reason :: any() Value :: any() ---- Reason for the constraint failure. It includes the constraint function in question, a machine-readable error reason and the value that made the constraint fail. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy(3)[cowboy(3)], link:man:cowboy_router(3)[cowboy_router(3)], link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)], link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] ================================================ FILE: doc/src/manual/cowboy_constraints.int.asciidoc ================================================ = cowboy_constraints:int(3) == Name cowboy_constraints:int - Integer constraint == Description Constraint functions implement a number of different operations. [source,erlang] ---- int(forward, Bin) -> {ok, Int} | {error, not_an_integer} Bin :: binary() Int :: integer() ---- Validate and convert the text representation of an integer. [source,erlang] ---- int(reverse, Int) -> {ok, Bin} | {error, not_an_integer} ---- Convert an integer back to its text representation. [source,erlang] ---- int(format_error, Error) -> HumanReadable Error :: {not_an_integer, Bin | Int} HumanReadable :: iolist() ---- Generate a human-readable error message. == Arguments Arguments vary depending on the operation. Constraint functions always take the operation type as first argument, and the value as second argument. == Return value The return value varies depending on the operation. == Changelog * *2.0*: Interface modified to allow for a variety of operations. * *1.0*: Constraint introduced. == Examples This function is not meant to be called directly. == See also link:man:cowboy_constraints(3)[cowboy_constraints(3)], link:man:cowboy_constraints:nonempty(3)[cowboy_constraints:nonempty(3)], link:man:cowboy_router(3)[cowboy_router(3)], link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)], link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] ================================================ FILE: doc/src/manual/cowboy_constraints.nonempty.asciidoc ================================================ = cowboy_constraints:nonempty(3) == Name cowboy_constraints:nonempty - Non-empty constraint == Description Constraint functions implement a number of different operations. [source,erlang] ---- nonempty(forward | reverse, <<>>) -> {error, empty} ---- Reject empty values. [source,erlang] ---- nonempty(forward | reverse, Bin) -> {ok, Bin} Bin :: binary() ---- Accept any other binary values. [source,erlang] ---- nonempty(format_error, Error) -> HumanReadable Error :: {empty, Bin} HumanReadable :: iolist() ---- Generate a human-readable error message. == Arguments Arguments vary depending on the operation. Constraint functions always take the operation type as first argument, and the value as second argument. == Return value The return value varies depending on the operation. == Changelog * *2.0*: Interface modified to allow for a variety of operations. * *1.0*: Constraint introduced. == Examples This function is not meant to be called directly. == See also link:man:cowboy_constraints(3)[cowboy_constraints(3)], link:man:cowboy_constraints:int(3)[cowboy_constraints:int(3)], link:man:cowboy_router(3)[cowboy_router(3)], link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)], link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] ================================================ FILE: doc/src/manual/cowboy_decompress_h.asciidoc ================================================ = cowboy_decompress_h(3) == Name cowboy_decompress_h - Decompress stream handler == Description The module `cowboy_decompress_h` decompresses request bodies automatically when the server supports it. The only compression algorithm currently supported is the gzip algorithm. Another limitation is that decompression is only attempted when gzip is the only content-encoding in the request. This stream handler always adds a field to the Req object with the name `content_decoded` which is treated as a list of decoded content-encoding values. Currently this list may only contain the `<<"gzip">>` binary if content was decoded; or be empty otherwise. == Options [source,erlang] ---- opts() :: #{ decompress_enabled => boolean(), decompress_ratio_limit => non_neg_integer() } ---- Configuration for the decompress stream handler. The default value is given next to the option name: decompress_ratio_limit (20):: The max ratio of the compressed and decompressed body before it is rejected with a `413 Payload Too Large` error response. + This option can be updated at any time using the `set_options` stream handler command. decompress_enabled (true):: Whether the handler is enabled by default. + This option can be updated using the `set_options` stream handler command. This allows disabling decompression for the current stream. Attempts to enable or disable decompression after starting to read the body will be ignored. == Events The decompress stream handler does not produce any event. == Changelog * *2.11*: Module introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_compress_h(3)[cowboy_compress_h(3)], link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)], link:man:cowboy_stream_h(3)[cowboy_stream_h(3)], link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)] ================================================ FILE: doc/src/manual/cowboy_handler.asciidoc ================================================ = cowboy_handler(3) == Name cowboy_handler - Plain HTTP handlers == Description The `cowboy_handler` middleware executes the handler selected by the router or any other preceding middleware. This middleware takes the handler module and initial state from the `handler` and `handler_opts` environment values, respectively. On completion, it adds a `result` value to the middleware environment, containing the return value of the `terminate/3` callback (if defined) and `ok` otherwise. This module also defines a callback interface for handling HTTP requests. == Callbacks Plain HTTP handlers implement the following interface: [source,erlang] ---- init(Req, State) -> {ok, Req, State} terminate(Reason, Req, State) -> ok %% optional Req :: cowboy_req:req() State :: any() Reason :: normal | {crash, error | exit | throw, any()} ---- These two callbacks are common to all handlers. Plain HTTP handlers do all their work in the `init/2` callback. Returning `ok` terminates the handler. If no response is sent, Cowboy will send a `204 No Content`. The optional `terminate/3` callback will ultimately be called with the reason for the termination of the handler. Cowboy will terminate the process right after this. There is no need to perform any cleanup in this callback. The following terminate reasons are defined for plain HTTP handlers: normal:: The connection was closed normally. {crash, Class, Reason}:: A crash occurred in the handler. `Class` and `Reason` can be used to obtain more information about the crash. == Exports The following function should be called by modules implementing custom handlers to execute the optional terminate callback: * link:man:cowboy_handler:terminate(3)[cowboy_handler:terminate(3)] - Terminate the handler == See also link:man:cowboy(7)[cowboy(7)] ================================================ FILE: doc/src/manual/cowboy_handler.terminate.asciidoc ================================================ = cowboy_handler:terminate(3) == Name cowboy_handler:terminate - Terminate the handler == Description [source,erlang] ---- terminate(Reason, PartialReq, State, Handler) -> ok Reason :: any() PartialReq :: map() State :: any() Handler :: module() ---- Call the optional terminate callback if it is defined. Make sure to use this function at the end of the execution of modules that implement custom handler behaviors. == Arguments Reason:: Reason for termination. PartialReq:: The Req object. + It is possible to remove fields from the Req object to save memory when the handler has no concept of requests/responses. The only requirement is that a map is provided. State:: Handler state. Handler:: Handler module. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.0*: Function introduced. == Examples .Terminate a handler normally [source,erlang] ---- cowboy_handler:terminate(normal, Req, State, Handler). ---- == See also link:man:cowboy_handler(3)[cowboy_handler(3)] ================================================ FILE: doc/src/manual/cowboy_http.asciidoc ================================================ = cowboy_http(3) == Name cowboy_http - HTTP/1.1 == Description The module `cowboy_http` implements HTTP/1.1 and HTTP/1.0 as a Ranch protocol. == Options // @todo Might be worth moving cowboy_clear/tls options // to their respective manual, when they are added. [source,erlang] ---- opts() :: #{ active_n => pos_integer(), alpn_default_protocol => http | http2, chunked => boolean(), connection_type => worker | supervisor, dynamic_buffer => false | {pos_integer(), pos_integer()}, hibernate => boolean(), http10_keepalive => boolean(), idle_timeout => timeout(), inactivity_timeout => timeout(), initial_stream_flow_size => non_neg_integer(), linger_timeout => timeout(), logger => module(), max_authorization_header_value_length => non_neg_integer(), max_cookie_header_value_length => non_neg_integer(), max_empty_lines => non_neg_integer(), max_header_name_length => non_neg_integer(), max_header_value_length => non_neg_integer(), max_headers => non_neg_integer(), max_keepalive => non_neg_integer(), max_method_length => non_neg_integer(), max_request_line_length => non_neg_integer(), max_skip_body_length => non_neg_integer(), protocols => [http | http2], proxy_header => boolean(), request_timeout => timeout(), reset_idle_timeout_on_send => boolean(), sendfile => boolean(), stream_handlers => [module()] } ---- Configuration for the HTTP/1.1 protocol. This configuration is passed to Cowboy when starting listeners using `cowboy:start_clear/3` or `cowboy:start_tls/3` functions. It can be updated without restarting listeners using the Ranch functions `ranch:get_protocol_options/1` and `ranch:set_protocol_options/2`. The default value is given next to the option name: active_n (1):: The number of packets Cowboy will request from the socket at once. This can be used to tweak the performance of the server. Higher values reduce the number of times Cowboy need to request more packets from the port driver at the expense of potentially higher memory being used. alpn_default_protocol (http):: Default protocol to use when the client connects over TLS without ALPN. Can be set to `http2` to disable HTTP/1.1 entirely. chunked (true):: Whether chunked transfer-encoding is enabled for HTTP/1.1 connections. Note that a response streamed to the client without the chunked transfer-encoding and without a content-length header will result in the connection being closed at the end of the response body. + This option can be updated at any time using the `set_options` stream handler command. connection_type (supervisor):: Whether the connection process also acts as a supervisor. dynamic_buffer ({1024, 131072}):: Cowboy will dynamically change the socket's `buffer` size depending on the size of the data it receives from the socket. This lets Cowboy use the optimal buffer size for the current workload. + The dynamic buffer size functionality can be disabled by setting this option to `false`. Cowboy will also disable it by default when the `buffer` transport option is configured. hibernate (false):: Whether the connection process will hibernate automatically. http10_keepalive (true):: Whether keep-alive is enabled for HTTP/1.0 connections. idle_timeout (60000):: Time in ms with no data received before Cowboy closes the connection. + This option can be updated at any time using the `set_options` stream handler command. inactivity_timeout (300000):: **DEPRECATED** Time in ms with nothing received at all before Cowboy closes the connection. initial_stream_flow_size (65535):: Amount of data in bytes Cowboy will read from the socket right after a request was fully received. This is a soft limit. linger_timeout (1000):: Time in ms that Cowboy will wait when closing the connection. This is necessary to avoid the TCP reset problem as described in the https://tools.ietf.org/html/rfc7230#section-6.6[section 6.6 of RFC7230]. logger (error_logger):: The module that will be used to write log messages. max_authorization_header_value_length:: Maximum length of the `authorization` header value. Defaults to the value of the option `max_header_value_length`. max_cookie_header_value_length:: Maximum length of the `cookie` header value. Defaults to the value of the option `max_header_value_length`. max_empty_lines (5):: Maximum number of empty lines before a request. max_header_name_length (64):: Maximum length of header names. max_header_value_length (4096):: Maximum length of header values. max_headers (100):: Maximum number of headers allowed per request. max_keepalive (1000):: Maximum number of requests allowed per connection. max_method_length (32):: Maximum length of the method. max_request_line_length (8000):: Maximum length of the request line. max_skip_body_length (1000000):: Maximum length Cowboy is willing to skip when the user code did not read the body fully. When the remaining length is too large or unknown Cowboy will close the connection. protocols ([http2, http]):: Protocols that may be used when the client connects over cleartext TCP. The default is to allow both HTTP/1.1 and HTTP/2. HTTP/1.1 and HTTP/2 can be disabled entirely by omitting them from the list. proxy_header (false):: Whether incoming connections have a PROXY protocol header. The proxy information will be passed forward via the `proxy_header` key of the Req object. request_timeout (5000):: Time in ms with no requests before Cowboy closes the connection. reset_idle_timeout_on_send (false):: Whether the `idle_timeout` gets reset when sending data to the socket. sendfile (true):: Whether the sendfile syscall may be used. It can be useful to disable it on systems where the syscall has a buggy implementation, for example under VirtualBox when using shared folders. stream_handlers ([cowboy_stream_h]):: Ordered list of stream handlers that will handle all stream events. == Changelog * *2.13*: The `inactivity_timeout` option was deprecated. * *2.13*: The `active_n` default value was changed to `1`. * *2.13*: The `dynamic_buffer` and `hibernate` options were added. * *2.11*: The `reset_idle_timeout_on_send` option was added. * *2.8*: The `active_n` option was added. * *2.7*: The `initial_stream_flow_size` and `logger` options were added. * *2.6*: The `chunked`, `http10_keepalive`, `proxy_header` and `sendfile` options were added. * *2.5*: The `linger_timeout` option was added. * *2.2*: The `max_skip_body_length` option was added. * *2.0*: The `timeout` option was renamed `request_timeout`. * *2.0*: The `idle_timeout`, `inactivity_timeout` and `shutdown_timeout` options were added. * *2.0*: The `max_method_length` option was added. * *2.0*: The `max_request_line_length` default was increased from 4096 to 8000. * *2.0*: The `connection_type` option was added. * *2.0*: The `env` option is now a map instead of a proplist. * *2.0*: The `stream_handlers` option was added. * *2.0*: The `compress` option was removed in favor of the `cowboy_compress_h` stream handler. * *2.0*: Options are now a map instead of a proplist. * *2.0*: Protocol introduced. Replaces `cowboy_protocol`. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_http2(3)[cowboy_http2(3)], link:man:cowboy_websocket(3)[cowboy_websocket(3)] ================================================ FILE: doc/src/manual/cowboy_http2.asciidoc ================================================ = cowboy_http2(3) == Name cowboy_http2 - HTTP/2 == Description The module `cowboy_http2` implements HTTP/2 as a Ranch protocol. == Options // @todo Might be worth moving cowboy_clear/tls/stream_h options // to their respective manual, when they are added. [source,erlang] ---- opts() :: #{ active_n => pos_integer(), alpn_default_protocol => http | http2, connection_type => worker | supervisor, connection_window_margin_size => 0..16#7fffffff, connection_window_update_threshold => 0..16#7fffffff, dynamic_buffer => false | {pos_integer(), pos_integer()}, enable_connect_protocol => boolean(), goaway_initial_timeout => timeout(), goaway_complete_timeout => timeout(), hibernate => boolean(), idle_timeout => timeout(), inactivity_timeout => timeout(), initial_connection_window_size => 65535..16#7fffffff, initial_stream_window_size => 0..16#7fffffff, linger_timeout => timeout(), logger => module(), max_concurrent_streams => non_neg_integer() | infinity, max_connection_buffer_size => non_neg_integer(), max_connection_window_size => 0..16#7fffffff, max_decode_table_size => non_neg_integer(), max_encode_table_size => non_neg_integer(), max_fragmented_header_block_size => 16384..16#7fffffff, max_frame_size_received => 16384..16777215, max_frame_size_sent => 16384..16777215 | infinity, max_received_frame_rate => {pos_integer(), timeout()}, max_reset_stream_rate => {pos_integer(), timeout()}, max_cancel_stream_rate => {pos_integer(), timeout()}, max_stream_buffer_size => non_neg_integer(), max_stream_window_size => 0..16#7fffffff, preface_timeout => timeout(), protocols => [http | http2], proxy_header => boolean(), reset_idle_timeout_on_send => boolean(), sendfile => boolean(), settings_timeout => timeout(), stream_handlers => [module()], stream_window_data_threshold => 0..16#7fffffff, stream_window_margin_size => 0..16#7fffffff, stream_window_update_threshold => 0..16#7fffffff } ---- Configuration for the HTTP/2 protocol. This configuration is passed to Cowboy when starting listeners using `cowboy:start_clear/3` or `cowboy:start_tls/3` functions. It can be updated without restarting listeners using the Ranch functions `ranch:get_protocol_options/1` and `ranch:set_protocol_options/2`. The default value is given next to the option name: active_n (1):: The number of packets Cowboy will request from the socket at once. This can be used to tweak the performance of the server. Higher values reduce the number of times Cowboy need to request more packets from the port driver at the expense of potentially higher memory being used. alpn_default_protocol (http):: Default protocol to use when the client connects over TLS without ALPN. Can be set to `http2` to disable HTTP/1.1 entirely. connection_type (supervisor):: Whether the connection process also acts as a supervisor. connection_window_margin_size (65535):: Extra amount in bytes to be added to the window size when updating the connection window. This is used to ensure that there is always some space available in the window. connection_window_update_threshold (163840):: The connection window will only get updated when its size becomes lower than this threshold, in bytes. This is to avoid sending too many `WINDOW_UPDATE` frames. dynamic_buffer ({1024, 131072}):: Cowboy will dynamically change the socket's `buffer` size depending on the size of the data it receives from the socket. This lets Cowboy use the optimal buffer size for the current workload. + The dynamic buffer size functionality can be disabled by setting this option to `false`. Cowboy will also disable it by default when the `buffer` transport option is configured. enable_connect_protocol (false):: Whether to enable the extended CONNECT method to allow protocols like Websocket to be used over an HTTP/2 stream. + For backward compatibility reasons, this option is disabled by default. It must be enabled to use Websocket over HTTP/2. It will be enabled by default in a future release. goaway_initial_timeout (1000):: Time in ms to wait for any in-flight stream creations before stopping to accept new streams on an existing connection during a graceful shutdown. goaway_complete_timeout (3000):: Time in ms to wait for ongoing streams to complete before closing the connection during a graceful shutdown. hibernate (false):: Whether the connection process will hibernate automatically. idle_timeout (60000):: Time in ms with no data received before Cowboy closes the connection. inactivity_timeout (300000):: **DEPRECATED** Time in ms with nothing received at all before Cowboy closes the connection. initial_connection_window_size (65535):: Initial window size in bytes for the connection. This is the total amount of data (from request bodies for example) that may be buffered by the connection across all streams before the user code explicitly requests it. + Note that this value cannot be lower than the default. initial_stream_window_size (65535):: Initial window size in bytes for new streams. This is the total amount of data (from request bodies for example) that may be buffered by a single stream before the user code explicitly requests it. linger_timeout (1000):: Time in ms that Cowboy will wait when closing the connection. This is necessary to avoid the TCP reset problem as described in the https://tools.ietf.org/html/rfc7230#section-6.6[section 6.6 of RFC7230]. In HTTP/2's case the GOAWAY message might also be lost when closing the connection immediately. logger (error_logger):: The module that will be used to write log messages. max_concurrent_streams (infinity):: Maximum number of concurrent streams allowed on the connection. max_connection_buffer_size (16000000):: Maximum size of all stream buffers for this connection, in bytes. This is a soft limit used to apply backpressure to handlers that send data faster than the HTTP/2 connection allows. max_connection_window_size (16#7fffffff):: Maximum connection window size in bytes. This is used as an upper bound when calculating the window size, either when reading the request body or receiving said body. max_decode_table_size (4096):: Maximum header table size in bytes used by the decoder. This is the value advertised to the client. The client can then choose a header table size equal or lower to the advertised value. max_encode_table_size (4096):: Maximum header table size in bytes used by the encoder. The server will compare this value to what the client advertises and choose the smallest one as the encoder's header table size. max_fragmented_header_block_size (32768):: Maximum header block size when headers are split over multiple HEADERS and CONTINUATION frames. Clients that attempt to send header blocks larger than this value will receive an ENHANCE_YOUR_CALM connection error. Note that this value is not advertised and should be large enough for legitimate requests. max_frame_size_received (16384):: Maximum size in bytes of the frames received by the server. This value is advertised to the remote endpoint which can then decide to use any value lower or equal for its frame sizes. + It is highly recommended to increase this value for performance reasons. In a future Cowboy version the default will be increased to 1MB (1048576). Too low values may result in very large file uploads failing because Cowboy will detect the large number of frames as flood and close the connection. max_frame_size_sent (infinity):: Maximum size in bytes of the frames sent by the server. This option allows setting an upper limit to the frame sizes instead of blindly following the client's advertised maximum. + Note that actual frame sizes may be lower than the limit when there is not enough space left in the flow control window. max_received_frame_rate ({10000, 10000}):: Maximum frame rate allowed per connection. The rate is expressed as a tuple `{NumFrames, TimeMs}` indicating how many frames are allowed over the given time period. This is similar to a supervisor restart intensity/period. max_reset_stream_rate ({10, 10000}):: Maximum reset stream rate per connection. This can be used to protect against misbehaving or malicious peers that do not follow the protocol, leading to the server resetting streams, by limiting the number of streams that can be reset over a certain time period. The rate is expressed as a tuple `{NumResets, TimeMs}`. This is similar to a supervisor restart intensity/period. max_cancel_stream_rate ({500, 10000}):: Maximum cancel stream rate per connection. This can be used to protect against misbehaving or malicious peers, by limiting the number of streams that the peer can reset over a certain time period. The rate is expressed as a tuple `{NumCancels, TimeMs}`. This is similar to a supervisor restart intensity/period. max_stream_buffer_size (8000000):: Maximum stream buffer size in bytes. This is a soft limit used to apply backpressure to handlers that send data faster than the HTTP/2 connection allows. max_stream_window_size (16#7fffffff):: Maximum stream window size in bytes. This is used as an upper bound when calculating the window size, either when reading the request body or receiving said body. preface_timeout (5000):: Time in ms Cowboy is willing to wait for the connection preface. protocols ([http2, http]):: Protocols that may be used when the client connects over cleartext TCP. The default is to allow both HTTP/1.1 and HTTP/2. HTTP/1.1 and HTTP/2 can be disabled entirely by omitting them from the list. proxy_header (false):: Whether incoming connections have a PROXY protocol header. The proxy information will be passed forward via the `proxy_header` key of the Req object. reset_idle_timeout_on_send (false):: Whether the `idle_timeout` gets reset when sending data to the socket. sendfile (true):: Whether the sendfile syscall may be used. It can be useful to disable it on systems where the syscall has a buggy implementation, for example under VirtualBox when using shared folders. settings_timeout (5000):: Time in ms Cowboy is willing to wait for a SETTINGS ack. stream_handlers ([cowboy_stream_h]):: Ordered list of stream handlers that will handle all stream events. stream_window_data_threshold (16384):: Window threshold in bytes below which Cowboy will not attempt to send data, with one exception. When Cowboy has data to send and the window is high enough, Cowboy will always send the data, regardless of this option. stream_window_margin_size (65535):: Extra amount in bytes to be added to the window size when updating a stream's window. This is used to ensure that there is always some space available in the window. stream_window_update_threshold (163840):: A stream's window will only get updated when its size becomes lower than this threshold, in bytes. This is to avoid sending too many `WINDOW_UPDATE` frames. == Changelog * *2.13*: The `inactivity_timeout` option was deprecated. * *2.13*: The `active_n` default value was changed to `1`. * *2.13*: The `dynamic_buffer` and `hibernate` options were added. * *2.11*: Websocket over HTTP/2 is now considered stable. * *2.11*: The `reset_idle_timeout_on_send` option was added. * *2.11*: Add the option `max_cancel_stream_rate` to protect against another flood scenario. * *2.9*: The `goaway_initial_timeout` and `goaway_complete_timeout` options were added. * *2.8*: The `active_n` option was added. * *2.8*: The `linger_timeout` option was added. * *2.8*: The `max_received_frame_rate` default value has been multiplied by 10 as the default was too low. * *2.7*: Add the options `connection_window_margin_size`, `connection_window_update_threshold`, `max_connection_window_size`, `max_stream_window_size`, `stream_window_margin_size` and `stream_window_update_threshold` to configure behavior on sending WINDOW_UPDATE frames; `max_connection_buffer_size` and `max_stream_buffer_size` to apply backpressure when sending data too fast; `max_received_frame_rate` and `max_reset_stream_rate` to protect against various flood scenarios; and `stream_window_data_threshold` to control how small the DATA frames that Cowboy sends can get. * *2.7*: The `logger` option was added. * *2.6*: The `proxy_header` and `sendfile` options were added. * *2.4*: Add the options `initial_connection_window_size`, `initial_stream_window_size`, `max_concurrent_streams`, `max_decode_table_size`, `max_encode_table_size`, `max_frame_size_received`, `max_frame_size_sent` and `settings_timeout` to configure HTTP/2 SETTINGS and related behavior. * *2.4*: Add the option `enable_connect_protocol`. * *2.0*: Protocol introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_http(3)[cowboy_http(3)], link:man:cowboy_websocket(3)[cowboy_websocket(3)] ================================================ FILE: doc/src/manual/cowboy_loop.asciidoc ================================================ = cowboy_loop(3) == Name cowboy_loop - Loop handlers == Description The module `cowboy_loop` defines a callback interface for long running HTTP connections. You should switch to this behavior for long polling, server-sent events and similar long-running requests. There are generally two usage patterns: * Loop until receiving a specific message, then send a response and stop execution (for example long polling); * Or initiate a response in `init/2` and stream the body in `info/3` as necessary (for example server-sent events). == Callbacks Loop handlers implement the following interface: [source,erlang] ---- init(Req, State) -> {cowboy_loop, Req, State} | {cowboy_loop, Req, State, hibernate | timeout()} info(Info, Req, State) -> {ok, Req, State} | {ok, Req, State, hibernate | timeout()} | {stop, Req, State} terminate(Reason, Req, State) -> ok %% optional Req :: cowboy_req:req() State :: any() Info :: any() Reason :: stop | {crash, error | exit | throw, any()} ---- The `init/2` callback is common to all handlers. To switch to the loop behavior, it must return `cowboy_loop` as the first element of the tuple. The `info/3` callback will be called for every Erlang message received. It may choose to continue the receive loop or stop it. The optional `terminate/3` callback will ultimately be called with the reason for the termination of the handler. Cowboy will terminate the process right after this. There is no need to perform any cleanup in this callback. The following terminate reasons are defined for loop handlers: stop:: The handler requested to close the connection by returning a `stop` tuple. {crash, Class, Reason}:: A crash occurred in the handler. `Class` and `Reason` can be used to obtain more information about the crash. == Changelog * *2.11*: A timeout may be returned instead of `hibernate`. It functions the same way as the `gen_server` timeout. * *2.0*: Loop handlers no longer need to handle socket events. * *1.0*: Behavior introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_handler(3)[cowboy_handler(3)] ================================================ FILE: doc/src/manual/cowboy_metrics_h.asciidoc ================================================ = cowboy_metrics_h(3) == Name cowboy_metrics_h - Metrics stream handler == Description The module `cowboy_metrics_h` gathers metrics and other information about a stream. It then calls the configured callback with this data. == Types === metrics() [source,erlang] ---- metrics() :: #{ %% The identifier for this listener. ref := ranch:ref(), %% The pid for this connection. pid := pid(), %% The streamid also indicates the total number of requests on %% this connection (StreamID div 2 + 1). streamid := cowboy_stream:streamid(), %% The terminate reason is always useful. reason := cowboy_stream:reason(), %% A filtered Req object or a partial Req object %% depending on how far the request got to. req => cowboy_req:req(), partial_req => cowboy_stream:partial_req(), %% Response status. resp_status := cowboy:http_status(), %% Filtered response headers. resp_headers := cowboy:http_headers(), %% Start/end of the processing of the request. %% %% This represents the time from this stream handler's init %% to terminate. req_start => integer(), req_end => integer(), %% Start/end of the receiving of the request body. %% Begins when the first packet has been received. req_body_start => integer(), req_body_end => integer(), %% Start/end of the sending of the response. %% Begins when we send the headers and ends on the final %% packet of the response body. If everything is sent at %% once these values are identical. resp_start => integer(), resp_end => integer(), %% For early errors all we get is the time we received it. early_error_time => integer(), %% Start/end of spawned processes. This is where most of %% the user code lies, excluding stream handlers. On a %% default Cowboy configuration there should be only one %% process: the request process. procs => ProcMetrics, %% Informational responses sent before the final response. informational => [InformationalMetrics], %% Length of the request and response bodies. This does %% not include the framing. req_body_length => non_neg_integer(), resp_body_length => non_neg_integer(), %% Additional metadata set by the user. user_data => map() } InformationalMetrics :: #{ %% Informational response status. status := cowboy:http_status(), %% Headers sent with the informational response. headers := cowboy:http_headers(), %% Time when the informational response was sent. time := integer() } ProcMetrics :: #{pid() => #{ %% Time at which the process spawned. spawn := integer(), %% Time at which the process exited. exit => integer(), %% Reason for the process exit. reason => any() }} ---- Metrics given to the callback function. Depending on the life of the stream the metrics may include more or less information. The `set_options` command can be used to add additional metadata in the `user_data` metric. This can be used for example to add the handler module which was selected by the router. The option to be set is `metrics_user_data`. It takes a map which will be merged in the existing `user_data` map. == Options [source,erlang] ---- opts() :: #{ metrics_callback => fun((metrics()) -> any()), metrics_req_filter => fun((cowboy_req:req()) -> map()), metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()) } ---- Configuration for the metrics stream handler. metrics_callback - mandatory:: The function that will be called upon completion of the stream. It only takes a single argument, the `metrics()`. metrics_req_filter:: A function applied to the Req to compact it and only keep required information. By default no filtering is done. metrics_resp_headers_filter:: A function applied to the response headers to filter them and only keep required information. By default no filtering is done. == Events The metrics stream handler does not produce any event. == Changelog * *2.7*: Module introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_compress_h(3)[cowboy_compress_h(3)], link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)], link:man:cowboy_stream_h(3)[cowboy_stream_h(3)], link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)] ================================================ FILE: doc/src/manual/cowboy_middleware.asciidoc ================================================ = cowboy_middleware(3) == Name cowboy_middleware - Middlewares == Description The module `cowboy_middleware` defines a callback interface for Cowboy middlewares. Middlewares process the request sequentially in the order they are configured. == Callbacks Middlewares implement the following interface: [source,erlang] ---- execute(Req, Env) -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} Req :: cowboy_req:req() Env :: cowboy_middleware:env() ---- The `execute/2` is the only callback that needs to be implemented. It must execute the middleware and return with instructions for Cowboy. ok:: Cowboy should continue processing the request using the returned Req object and environment. suspend:: Cowboy will hibernate the process. When resuming, Cowboy will apply the returned module, function and arguments. stop:: Cowboy will stop middleware execution. No other middleware will be executed. This effectively ends the processing of the request. // @todo No need to return the Req when stopping. Fix in 3.0. == Types === env() [source,erlang] ---- env() :: #{atom() => any()} ---- Middleware environment. A new environment is created for every request. The initial environment contained the user configured environment values (like `dispatch` for example) plus the `listener` value which contains the name of the listener for this connection. Middlewares may modify the environment as necessary. == Changelog * *2.0*: The `env` type is now a map instead of a proplist. * *1.0*: Behavior introduced. == See also link:man:cowboy(7)[cowboy(7)] ================================================ FILE: doc/src/manual/cowboy_req.asciidoc ================================================ = cowboy_req(3) == Name cowboy_req - HTTP request and response == Description The module `cowboy_req` provides functions to access, manipulate and respond to requests. There are four types of functions in this module. They can be differentiated by their name and their return type: [options="header"] |=== | Type | Name pattern | Return type | access | no verb, parse_*, match_* | `Value` | question | has_* | `boolean()` | modification | set_* | `Req` | action | any other verb | `ok \| {Result, Value, Req}` |=== Any `Req` returned must be used in place of the one passed as argument. Functions that perform an action in particular write state in the Req object to make sure you are using the function correctly. For example, it's only possible to send one response, and to read the body once. == Exports Connection: * link:man:cowboy_req:peer(3)[cowboy_req:peer(3)] - Peer address and port * link:man:cowboy_req:sock(3)[cowboy_req:sock(3)] - Socket address and port * link:man:cowboy_req:cert(3)[cowboy_req:cert(3)] - Client TLS certificate Raw request: * link:man:cowboy_req:method(3)[cowboy_req:method(3)] - HTTP method * link:man:cowboy_req:version(3)[cowboy_req:version(3)] - HTTP version * link:man:cowboy_req:scheme(3)[cowboy_req:scheme(3)] - URI scheme * link:man:cowboy_req:host(3)[cowboy_req:host(3)] - URI host name * link:man:cowboy_req:port(3)[cowboy_req:port(3)] - URI port number * link:man:cowboy_req:path(3)[cowboy_req:path(3)] - URI path * link:man:cowboy_req:qs(3)[cowboy_req:qs(3)] - URI query string * link:man:cowboy_req:uri(3)[cowboy_req:uri(3)] - Reconstructed URI * link:man:cowboy_req:header(3)[cowboy_req:header(3)] - HTTP header * link:man:cowboy_req:headers(3)[cowboy_req:headers(3)] - HTTP headers Processed request: * link:man:cowboy_req:parse_qs(3)[cowboy_req:parse_qs(3)] - Parse the query string * link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] - Match the query string against constraints * link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(3)] - Parse the given HTTP header * link:man:cowboy_req:filter_cookies(3)[cowboy_req:filter_cookies(3)] - Filter cookie headers * link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)] - Parse cookie headers * link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)] - Match cookies against constraints * link:man:cowboy_req:binding(3)[cowboy_req:binding(3)] - Access a value bound from the route * link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)] - Access all values bound from the route * link:man:cowboy_req:host_info(3)[cowboy_req:host_info(3)] - Access the route's heading host segments * link:man:cowboy_req:path_info(3)[cowboy_req:path_info(3)] - Access the route's trailing path segments Request body: * link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)] - Is there a request body? * link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)] - Body length * link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] - Read the request body * link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)] - Read and parse a urlencoded request body * link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)] - Read, parse and match a urlencoded request body against constraints * link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)] - Read the next multipart headers * link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] - Read the current part's body Response: * link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)] - Set a cookie * link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)] - Set a response header * link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)] - Set several response headers * link:man:cowboy_req:has_resp_header(3)[cowboy_req:has_resp_header(3)] - Is the given response header set? * link:man:cowboy_req:resp_header(3)[cowboy_req:resp_header(3)] - Response header * link:man:cowboy_req:resp_headers(3)[cowboy_req:resp_headers(3)] - Response headers * link:man:cowboy_req:delete_resp_header(3)[cowboy_req:delete_resp_header(3)] - Delete a response header * link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)] - Set the response body * link:man:cowboy_req:has_resp_body(3)[cowboy_req:has_resp_body(3)] - Is there a response body? * link:man:cowboy_req:inform(3)[cowboy_req:inform(3)] - Send an informational response * link:man:cowboy_req:reply(3)[cowboy_req:reply(3)] - Send the response * link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] - Send the response headers * link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)] - Stream the response body * link:man:cowboy_req:stream_events(3)[cowboy_req:stream_events(3)] - Stream events * link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)] - Send the response trailers * link:man:cowboy_req:push(3)[cowboy_req:push(3)] - Push a resource to the client Stream handlers: * link:man:cowboy_req:cast(3)[cowboy_req:cast(3)] - Cast a stream handler event == Types === push_opts() [source,erlang] ---- push_opts() :: #{ method => binary(), %% case sensitive scheme => binary(), %% lowercase; case insensitive host => binary(), %% lowercase; case insensitive port => inet:port_number(), qs => binary() %% case sensitive } ---- Push options. By default, Cowboy will use the GET method, an empty query string, and take the scheme, host and port directly from the current request's URI. === read_body_opts() [source,erlang] ---- read_body_opts() :: #{ length => non_neg_integer() | auto, period => non_neg_integer() | infinity, timeout => timeout() } ---- Body reading options. The defaults are function-specific. Auto mode can be enabled by setting `length` to `auto` and `period` to `infinity`. The period cannot be set to `infinity` when auto mode isn't used. === req() [source,erlang] ---- req() :: #{ method := binary(), %% case sensitive version := cowboy:http_version() | atom(), scheme := binary(), %% lowercase; case insensitive host := binary(), %% lowercase; case insensitive port := inet:port_number(), path := binary(), %% case sensitive qs := binary(), %% case sensitive headers := cowboy:http_headers(), peer := {inet:ip_address(), inet:port_number()}, sock := {inet:ip_address(), inet:port_number()}, cert := binary() | undefined } ---- The Req object. Contains information about the request and response. While some fields are publicly documented, others aren't and shouldn't be used. You may add custom fields if required. Make sure to namespace them by prepending an underscore and the name of your application: .Setting a custom field [source,erlang] ---- Req#{'_myapp_auth_method' => pubkey}. ---- === resp_body() [source,erlang] ---- resp_body() :: iodata() | {sendfile, Offset, Length, Filename} Offset :: non_neg_integer() Length :: non_neg_integer() Filename :: file:name_all() ---- Response body. It can take two forms: the actual data to be sent or a tuple indicating which file to send. When sending data directly, the type is either a binary or an iolist. Iolists are an efficient way to build output. Instead of concatenating strings or binaries, you can simply build a list containing the fragments you want to send in the order they should be sent: .Example iolists usage [source,erlang] ---- 1> RespBody = ["Hello ", [<<"world">>, $!]]. ["Hello ",[<<"world">>,33]] 2> io:format("~s~n", [RespBody]). Hello world! ---- Note that the length must be greater than zero for any data to be sent. Cowboy will send an empty body when the length is zero. == See also link:man:cowboy(7)[cowboy(7)] ================================================ FILE: doc/src/manual/cowboy_req.binding.asciidoc ================================================ = cowboy_req:binding(3) == Name cowboy_req:binding - Access a value bound from the route == Description [source,erlang] ---- binding(Name, Req) -> binding(Name, Req, undefined) binding(Name, Req, Default) -> any() | Default Name :: atom() Req :: cowboy_req:req() Default :: any() ---- Return the value for the given binding. == Arguments Name:: Desired binding name as an atom. Req:: The Req object. Default:: Default value returned when the binding is missing. == Return value By default the value is a case sensitive binary string, however constraints may change the type of this value (for example automatically converting numbers to integer). == Changelog * *2.0*: Only the value is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the username from the path [source,erlang] ---- %% Route is "/users/:user" Username = cowboy_req:binding(user, Req). ---- .Get the branch name, with a default [source,erlang] ---- %% Route is "/log[/:branch]" Branch = cowboy_req:binding(branch, Req, <<"master">>) ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)], link:man:cowboy_req:host_info(3)[cowboy_req:host_info(3)], link:man:cowboy_req:path_info(3)[cowboy_req:path_info(3)], link:man:cowboy_router(3)[cowboy_router(3)] ================================================ FILE: doc/src/manual/cowboy_req.bindings.asciidoc ================================================ = cowboy_req:bindings(3) == Name cowboy_req:bindings - Access all values bound from the route == Description [source,erlang] ---- bindings(Req :: cowboy_req:req()) -> cowboy_router:bindings() ---- Return a map containing all bindings. == Arguments Req:: The Req object. == Return value By default values are case sensitive binary strings, however constraints may change the type of this value (for example automatically converting numbers to integer). == Changelog * *2.0*: Only the values are returned, they are no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get all bindings [source,erlang] ---- Bindings = cowboy_req:bindings(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:binding(3)[cowboy_req:binding(3)], link:man:cowboy_req:host_info(3)[cowboy_req:host_info(3)], link:man:cowboy_req:path_info(3)[cowboy_req:path_info(3)], link:man:cowboy_router(3)[cowboy_router(3)] ================================================ FILE: doc/src/manual/cowboy_req.body_length.asciidoc ================================================ = cowboy_req:body_length(3) == Name cowboy_req:body_length - Body length == Description [source,erlang] ---- body_length(Req :: cowboy_req:req()) -> undefined | non_neg_integer() ---- Return the length of the request body. The length is not always known before reading the body. In those cases Cowboy will return `undefined`. The body length is available after the body has been fully read. == Arguments Req:: The Req object. == Return value The length of the request body, or `undefined` if it is not known. == Changelog * *2.0*: Only the length is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the body length [source,erlang] ---- Length = cowboy_req:body_length(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.cast.asciidoc ================================================ = cowboy_req:cast(3) == Name cowboy_req:cast - Cast a stream handler event == Description [source,erlang] ---- cast(Event :: any(), Req :: cowboy_req:req()) -> ok ---- Cast a stream handler event. The event will be passed to stream handlers through the `info/3` callback. == Arguments Event:: The event to be sent to stream handlers. Req:: The Req object. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.7*: Function introduced. == Examples .Read the body using auto mode [source,erlang] ---- read_body_auto_async(Req) -> read_body_auto_async(Req, make_ref(), <<>>). read_body_auto_async(Req, Ref, Acc) -> cowboy_req:cast({read_body, self(), Ref, auto, infinity}, Req), receive {request_body, Ref, nofin, Data} -> read_body_auto_async(Req, Ref, <>); {request_body, Ref, fin, _BodyLen, Data} -> {ok, <>, Req} end. ---- .Increase the HTTP/1.1 idle timeout [source,erlang] ---- cowboy_req:cast({set_options, #{ idle_timeout => 3600000 }}, Req). ---- .Add user data to metrics ---- cowboy_req:cast({set_options, #{ metrics_user_data => #{handler => ?MODULE} }}, Req). ---- .Enable compression buffering ---- cowboy_req:cast({set_options, #{ compress_buffering => true }}, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_stream(3)[cowboy_stream(3)] ================================================ FILE: doc/src/manual/cowboy_req.cert.asciidoc ================================================ = cowboy_req:cert(3) == Name cowboy_req:cert - Client TLS certificate == Description [source,erlang] ---- cert(Req :: cowboy_req:req()) -> binary() | undefined ---- Return the peer's TLS certificate. Using the default configuration this function will always return `undefined`. You need to explicitly configure Cowboy to request the client certificate. To do this you need to set the `verify` transport option to `verify_peer`: [source,erlang] ---- {ok, _} = cowboy:start_tls(example, [ {port, 8443}, {certfile, "path/to/cert.pem"}, {verify, verify_peer} ], #{ env => #{dispatch => Dispatch} }). ---- You may also want to customize the `verify_fun` function. Please consult the `ssl` application's manual for more details. TCP connections do not allow a certificate and this function will therefore always return `undefined`. The certificate can also be obtained using pattern matching: [source,erlang] ---- #{cert := Cert} = Req. ---- == Arguments Req:: The Req object. == Return value The client TLS certificate. == Changelog * *2.1*: Function introduced. == Examples .Get the client TLS certificate. [source,erlang] ---- Cert = cowboy_req:cert(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:peer(3)[cowboy_req:peer(3)], link:man:cowboy_req:sock(3)[cowboy_req:sock(3)] ================================================ FILE: doc/src/manual/cowboy_req.delete_resp_header.asciidoc ================================================ = cowboy_req:delete_resp_header(3) == Name cowboy_req:delete_resp_header - Delete a response header == Description [source,erlang] ---- delete_resp_header(Name, Req :: cowboy_req:req()) -> Req Name :: binary() %% lowercase; case insensitive ---- Delete the given response header. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. == Arguments Name:: Header name as a lowercase binary string. Req:: The Req object. == Return value A new Req object is returned. The returned Req object must be used from that point onward, otherwise the header will still be sent in the response. == Changelog * *1.0*: Function introduced. == Examples .Remove the content-type header from the response [source,erlang] ---- Req = cowboy_req:delete_resp_header(<<"content-type">>, Req0), ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:has_resp_header(3)[cowboy_req:has_resp_header(3)], link:man:cowboy_req:resp_header(3)[cowboy_req:resp_header(3)], link:man:cowboy_req:resp_headers(3)[cowboy_req:resp_headers(3)] ================================================ FILE: doc/src/manual/cowboy_req.filter_cookies.asciidoc ================================================ = cowboy_req:filter_cookies(3) == Name cowboy_req:filter_cookies - Filter cookie headers == Description [source,erlang] ---- filter_cookies(Names, Req) -> Req Names :: [atom() | binary()] ---- Filter cookie headers. This function is meant to be used before attempting to parse or match cookies in order to remove cookies that are not relevant and are potentially malformed. Because Cowboy by default crashes on malformed cookies, this function allows processing requests that would otherwise result in a 400 error. Malformed cookies are unfortunately fairly common due to the string-based interface provided by browsers and this function provides a middle ground between Cowboy's strict behavior and chaotic real world use cases. Note that there may still be crashes even after filtering cookies because this function does not correct malformed values. Cookies that have malformed values should probably be unset in an error response or in a redirect. This function can be called even if there are no cookies in the request. == Arguments Names:: The cookies that should be kept. Req:: The Req object. == Return value The Req object is returned with its cookie header value filtered. == Changelog * *2.7*: Function introduced. == Examples .Filter then parse cookies [source,erlang] ---- Req = cowboy_req:filter_cookies([session_id, token], Req0), Cookies = cowboy_req:parse_cookies(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)], link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)] ================================================ FILE: doc/src/manual/cowboy_req.has_body.asciidoc ================================================ = cowboy_req:has_body(3) == Name cowboy_req:has_body - Is there a request body? == Description [source,erlang] ---- has_body(Req :: cowboy_req:req()) -> boolean() ---- Return whether the request has a body. == Arguments Req:: The Req object. == Return value A boolean indicating whether the request has a body. == Changelog * *1.0*: Function introduced. == Examples .Ensure the request has a body [source,erlang] ---- true = cowboy_req:has_body(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.has_resp_body.asciidoc ================================================ = cowboy_req:has_resp_body(3) == Name cowboy_req:has_resp_body - Is there a response body? == Description [source,erlang] ---- has_resp_body(Req :: cowboy_req:req()) -> boolean() ---- Return whether a response body has been set. == Arguments Req:: The Req object. == Return value A boolean indicating whether a response body has been set. This function will return `false` when an empty response body has been set. == Changelog * *1.0*: Function introduced. == Examples .Check whether a body has been set [source,erlang] ---- false = cowboy_req:has_resp_body(Req0), Req1 = cowboy_req:set_resp_body(<<"Hello!">>, Req0), true = cowboy_req:has_resp_body(Req1), Req = cowboy_req:set_resp_body(<<>>, Req1), false = cowboy_req:has_resp_body(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.has_resp_header.asciidoc ================================================ = cowboy_req:has_resp_header(3) == Name cowboy_req:has_resp_header - Is the given response header set? == Description [source,erlang] ---- has_resp_header(Name, Req :: cowboy_req:req()) -> boolean() Name :: binary() %% lowercase; case insensitive ---- Return whether the given response header has been set. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. == Arguments Name:: Header name as a lowercase binary string. Req:: The Req object. == Return value A boolean indicating whether the given response header has been set. == Changelog * *1.0*: Function introduced. == Examples .Check whether the content-type header has been set [source,erlang] ---- false = cowboy_req:has_resp_header(<<"content-type">>, Req0), Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/html">>, Req0), true = cowboy_req:has_resp_header(<<"content-type">>, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:resp_header(3)[cowboy_req:resp_header(3)], link:man:cowboy_req:resp_headers(3)[cowboy_req:resp_headers(3)], link:man:cowboy_req:delete_resp_header(3)[cowboy_req:delete_resp_header(3)] ================================================ FILE: doc/src/manual/cowboy_req.header.asciidoc ================================================ = cowboy_req:header(3) == Name cowboy_req:header - HTTP header == Description [source,erlang] ---- header(Name, Req) -> header(Name, Req, undefined) header(Name, Req, Default) -> binary() | Default Name :: binary() %% lowercase; case insensitive Req :: cowboy_req:req() Default :: any() ---- Return the value for the given HTTP header. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. Headers can also be obtained using pattern matching: [source,erlang] ---- #{headers := #{Name := Value}} = Req. ---- Note that this snippet will crash if the header is missing. == Arguments Name:: Desired HTTP header name as a lowercase binary string. Req:: The Req object. Default:: Default value returned when the header is missing. == Return value The header value is returned as a binary string. When the header is missing, the default argument is returned. == Changelog * *2.0*: Only the header value is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the accept header [source,erlang] ---- Accept = cowboy_req:header(<<"accept">>, Req). ---- .Get the content-length header with a default value [source,erlang] ---- Length = cowboy_req:header(<<"content-length">>, Req, <<"0">>). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:headers(3)[cowboy_req:headers(3)], link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(3)] ================================================ FILE: doc/src/manual/cowboy_req.headers.asciidoc ================================================ = cowboy_req:headers(3) == Name cowboy_req:headers - HTTP headers == Description [source,erlang] ---- headers(Req :: cowboy_req:req()) -> cowboy:http_headers() ---- Return all request headers. Request headers can also be obtained using pattern matching: [source,erlang] ---- #{headers := Headers} = Req. ---- == Arguments Req:: The Req object. == Return value Headers are returned as a map with keys being lowercase binary strings, and values as binary strings. == Changelog * *2.0*: Only the headers are returned, they are no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get all headers [source,erlang] ---- Headers = cowboy_req:headers(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:header(3)[cowboy_req:header(3)], link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(3)] ================================================ FILE: doc/src/manual/cowboy_req.host.asciidoc ================================================ = cowboy_req:host(3) == Name cowboy_req:host - URI host name == Description [source,erlang] ---- host(Req :: cowboy_req:req()) -> Host :: binary() ---- Return the host name of the effective request URI. The host name can also be obtained using pattern matching: [source,erlang] ---- #{host := Host} = Req. ---- == Arguments Req:: The Req object. == Return value The host name is returned as a lowercase binary string. It is case insensitive. == Changelog * *2.0*: Only the host name is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the effective request URI's host name [source,erlang] ---- Host = cowboy_req:host(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:binding(3)[cowboy_req:binding(3)], link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)], link:man:cowboy_req:host_info(3)[cowboy_req:host_info(3)] ================================================ FILE: doc/src/manual/cowboy_req.host_info.asciidoc ================================================ = cowboy_req:host_info(3) == Name cowboy_req:host_info - Access the route's heading host segments == Description [source,erlang] ---- host_info(Req :: cowboy_req:req()) -> cowboy_router:tokens() ---- Return the tokens for the heading host segments. This is the part of the host name that was matched using the `...` notation. == Arguments Req:: The Req object. == Return value The tokens are returned as a list of case insensitive binary strings. == Changelog * *2.0*: Only the tokens are returned, they are no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the host_info tokens [source,erlang] ---- HostInfo = cowboy_req:host_info(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:binding(3)[cowboy_req:binding(3)], link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)], link:man:cowboy_req:path_info(3)[cowboy_req:path_info(3)], link:man:cowboy_router(3)[cowboy_router(3)] ================================================ FILE: doc/src/manual/cowboy_req.inform.asciidoc ================================================ = cowboy_req:inform(3) == Name cowboy_req:inform - Send an informational response == Description [source,erlang] ---- inform(Status, Req :: cowboy_req:req()) -> inform(StatusCode, #{}, Req) inform(Status, Headers, Req :: cowboy_req:req()) -> ok Status :: cowboy:http_status() Headers :: cowboy:http_headers() ---- Send an informational response. Informational responses use a status code between 100 and 199. They cannot include a body. This function will not use any of the previously set headers. All headers to be sent must be given directly. Any number of informational responses can be sent as long as they are sent before the proper response. Attempting to use this function after sending a normal response will result in an error. The header names must be given as lowercase binary strings. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. == Arguments Status:: The status code for the response. Headers:: The response headers. + Header names must be given as lowercase binary strings. Req:: The Req object. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.1*: Function introduced. == Examples .Send an informational response [source,erlang] ---- Req = cowboy_req:inform(102, Req0). ---- .Send an informational response with headers [source,erlang] ---- Req = cowboy_req:inform(103, #{ <<"link">> => <<"; rel=preload; as=style, " "; rel=preload; as=script">> }, Req0). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)], link:man:cowboy_req:push(3)[cowboy_req:push(3)] ================================================ FILE: doc/src/manual/cowboy_req.match_cookies.asciidoc ================================================ = cowboy_req:match_cookies(3) == Name cowboy_req:match_cookies - Match cookies against constraints == Description [source,erlang] ---- match_cookies(Fields :: cowboy:fields(), Req :: cowboy_req:req()) -> #{atom() => any()} ---- Parse the cookies and match specific values against constraints. Cowboy will only return the cookie values specified in the fields list, and ignore all others. Fields can be either the name of the cookie requested; the name along with a list of constraints; or the name, a list of constraints and a default value in case the cookie is missing. This function will crash if the cookie is missing and no default value is provided. This function will also crash if a constraint fails. The name of the cookie must be provided as an atom. The key of the returned map will be that atom. The value may be converted through the use of constraints, making this function able to extract, validate and convert values all in one step. This function will crash on invalid cookie data. How to handle this is explained in details in the manual page for link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)]. == Arguments Fields:: Cookies to retrieve. + See link:man:cowboy(3)[cowboy(3)] for a complete description. Req:: The Req object. == Return value Desired values are returned as a map. The key is the atom that was given in the list of fields, and the value is the optionally converted value after applying constraints. The map contains the same keys that were given in the fields. An exception is triggered when the match fails. == Changelog * *2.0*: Function introduced. == Examples .Match fields [source,erlang] ---- %% ID and Lang are binaries. #{id := ID, lang := Lang} = cowboy_req:match_cookies([id, lang], Req). ---- .Match fields and apply constraints [source,erlang] ---- %% ID is an integer and Lang a non-empty binary. #{id := ID, lang := Lang} = cowboy_req:match_cookies([{id, int}, {lang, nonempty}], Req). ---- .Match fields with default values [source,erlang] ---- #{lang := Lang} = cowboy_req:match_cookies([{lang, [], <<"en-US">>}], Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:filter_cookies(3)[cowboy_req:filter_cookies(3)], link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)] ================================================ FILE: doc/src/manual/cowboy_req.match_qs.asciidoc ================================================ = cowboy_req:match_qs(3) == Name cowboy_req:match_qs - Match the query string against constraints == Description [source,erlang] ---- match_qs(Fields :: cowboy:fields(), Req :: cowboy_req:req()) -> #{atom() => any()} ---- Parse the query string and match specific values against constraints. Cowboy will only return the query string values specified in the fields list, and ignore all others. Fields can be either the key requested; the key along with a list of constraints; or the key, a list of constraints and a default value in case the key is missing. This function will crash if the key is missing and no default value is provided. This function will also crash if a constraint fails. The key must be provided as an atom. The key of the returned map will be that atom. The value may be converted through the use of constraints, making this function able to extract, validate and convert values all in one step. == Arguments Fields:: Fields to retrieve from the query string. + See link:man:cowboy(3)[cowboy(3)] for a complete description. Req:: The Req object. == Return value Desired values are returned as a map. The key is the atom that was given in the list of fields, and the value is the optionally converted value after applying constraints. The map contains the same keys that were given in the fields. An exception is triggered when the match fails. == Changelog * *2.0*: Function introduced. == Examples .Match fields [source,erlang] ---- %% ID and Lang are binaries. #{id := ID, lang := Lang} = cowboy_req:match_qs([id, lang], Req). ---- .Match fields and apply constraints [source,erlang] ---- %% ID is an integer and Lang a non-empty binary. #{id := ID, lang := Lang} = cowboy_req:match_qs([{id, int}, {lang, nonempty}], Req). ---- .Match fields with default values [source,erlang] ---- #{lang := Lang} = cowboy_req:match_qs([{lang, [], <<"en-US">>}], Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:qs(3)[cowboy_req:qs(3)], link:man:cowboy_req:parse_qs(3)[cowboy_req:parse_qs(3)] ================================================ FILE: doc/src/manual/cowboy_req.method.asciidoc ================================================ = cowboy_req:method(3) == Name cowboy_req:method - HTTP method == Description [source,erlang] ---- method(Req :: cowboy_req:req()) -> Method :: binary() ---- Return the request's HTTP method. The method can also be obtained using pattern matching: [source,erlang] ---- #{method := Method} = Req. ---- == Arguments Req:: The Req object. == Return value The request's HTTP method is returned as a binary string. While methods are case sensitive, standard methods are always uppercase. == Changelog * *2.0*: Only the method is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Ensure the request's method is GET [source,erlang] ---- <<"GET">> = cowboy_req:method(Req). ---- .Allow methods from list [source,erlang] ---- init(Req, State) -> case lists:member(cowboy_req:method(Req), [<<"GET">>, <<"POST">>]) of true -> handle(Req, State); false -> method_not_allowed(Req, State) end. ---- == See also link:man:cowboy_req(3)[cowboy_req(3)] ================================================ FILE: doc/src/manual/cowboy_req.parse_cookies.asciidoc ================================================ = cowboy_req:parse_cookies(3) == Name cowboy_req:parse_cookies - Parse cookie headers == Description [source,erlang] ---- parse_cookies(Req) -> [{Name, Value}] Name :: binary() %% case sensitive Value :: binary() %% case sensitive ---- Parse cookie headers. Alias for link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(<<"cookie">>, Req)]. When the cookie header is missing or empty, `[]` is returned. This function will crash on invalid cookie data. Because invalid cookies are fairly common when dealing with browsers (because of the string interface that the Javascript API provides), it is recommended to filter the cookie header value before attempting to parse it. This can be accomplished by calling the function link:man:cowboy_req:filter_cookies(3)[cowboy_req:filter_cookies(3)] first. This does not guarantee that parsing succeeds. If it still fails it is recommended to send an error response or redirect with instructions to delete the relevant cookies: .Recover from cookie parsing errors [source,erlang] ---- Req1 = cowboy_req:filter_cookies([session_id, token], Req0), try cowboy_req:parse_cookies(Req1) of Cookies -> do_something(Req1, Cookies) catch _:_ -> %% We can't parse the cookies we need, unset them %% otherwise the browser will continue sending them. Req2 = cowboy_req:set_resp_cookie(<<"session_id">>, <<>>, Req1, #{max_age => 0}), Req = cowboy_req:set_resp_cookie(<<"token">>, <<>>, Req2, #{max_age => 0}), cowboy_req:reply(500, Req) end. ---- == Arguments Req:: The Req object. == Return value The cookies are returned as a list of key/values. Keys and values are case sensitive binary strings. == Changelog * *2.0*: Only the parsed header value is returned, it is no longer wrapped in a tuple. * *2.0*: Function introduced. Replaces `cookie/2,3` and `cookies/1`. == Examples .Look for a specific cookie [source,erlang] ---- Cookies = cowboy_req:parse_cookies(Req), {_, Token} = lists:keyfind(<<"token">>, 1, Cookies). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(3)], link:man:cowboy_req:filter_cookies(3)[cowboy_req:filter_cookies(3)], link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)] ================================================ FILE: doc/src/manual/cowboy_req.parse_header.asciidoc ================================================ = cowboy_req:parse_header(3) == Name cowboy_req:parse_header - Parse the given HTTP header == Description [source,erlang] ---- parse_header(Name, Req) -> ParsedValue | Default parse_header(Name, Req, Default) -> ParsedValue | Default Name :: binary() Req :: cowboy_req:req() ParsedValue :: any() Default :: any() ---- Parse the given HTTP header. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. The type of the parsed value varies depending on the header. Similarly, the default value when calling `cowboy_req:parse_header/2` differs depending on the header. == Arguments Name:: Desired HTTP header name as a lowercase binary string. Req:: The Req object. Default:: Default value returned when the header is missing. == Return value The parsed header value varies depending on the header. When the header is missing, the default argument is returned. == Headers The following snippets detail the types returned by the different headers. Unless mentioned otherwise, the default value when the header is missing will be `undefined`: .accept [source,erlang] ---- parse_header(<<"accept">>, Req) -> [{{Type, SubType, Params}, Quality, AcceptExt}] Type :: binary() %% case insensitive SubType :: binary() %% case insensitive Params :: [{Key, Value}] Quality :: 0..1000 AcceptExt :: [Key | {Key, Value}] Key :: binary() %% case insensitive Value :: binary() %% case sensitive ---- .accept-charset, accept-encoding and accept-language [source,erlang] ---- parse_header(Name, Req) -> [{Value, Quality}] Name :: <<"accept-charset">> | <<"accept-encoding">> | <<"accept-language">> Value :: binary() %% case insensitive Quality :: 0..1000 ---- .access-control-request-headers [source,erlang] ---- parse_header(<<"access-control-request-headers">>, Req) -> [Header] Header :: binary() %% case insensitive ---- .access-control-request-method [source,erlang] ---- parse_header(<<"access-control-request-method">>) -> Method Method :: binary() %% case sensitive ---- .authorization and proxy-authorization [source,erlang] ---- parse_header(<<"authorization">>, Req) -> {basic, Username :: binary(), Password :: binary()} | {bearer, Token :: binary()} | {digest, [{Key :: binary(), Value :: binary()}]} ---- // @todo Currently also parses connection. Do we want this? Should it be documented? Use case? .content-encoding and content-language [source,erlang] ---- parse_header(Name, Req) -> [Value] Name :: <<"content-encoding">> | <<"content-language">> Value :: binary() %% case insensitive ---- .content-length [source,erlang] ---- parse_header(<<"content-length">>, Req) -> non_neg_integer() ---- When the content-length header is missing, `0` is returned. .content-type [source,erlang] ---- parse_header(<<"content-type">>, Req) -> {Type, SubType, Params} Type :: binary() %% case insensitive SubType :: binary() %% case insensitive Params :: [{Key, Value}] Key :: binary() %% case insensitive Value :: binary() %% case sensitive; ---- Note that the value for the charset parameter is case insensitive and returned as a lowercase binary string. .cookie [source,erlang] ---- parse_header(<<"cookie">>, Req) -> [{Name, Value}] Name :: binary() %% case sensitive Value :: binary() %% case sensitive ---- When the cookie header is missing, `[]` is returned. While an empty cookie header is not valid, some clients do send it. Cowboy will in this case also return `[]`. .expect [source,erlang] ---- parse_header(<<"expect">>, Req) -> continue ---- .if-match and if-none-match [source,erlang] ---- parse_header(Name, Req) -> '*' | [{weak | strong, OpaqueTag}] Name :: <<"if-match">> | <<"if-none-match">> OpaqueTag :: binary() %% case sensitive ---- .if-modified-since and if-unmodified-since [source,erlang] ---- parse_header(Name, Req) -> calendar:datetime() ---- .max-forwards [source,erlang] ---- parse_header(<<"max-forwards">>, Req) -> non_neg_integer() ---- .origin [source,erlang] ---- parse_header(<<"origin">>, Req) -> [{Scheme, Host, Port} | GUID] Scheme :: <<"http">> | <<"https">> Host :: binary() %% case insensitive Port :: 0..65535 GUID :: reference() ---- Cowboy generates a reference in place of a GUID when the URI uses an unsupported uri-scheme or is not an absolute URI. [source,erlang] ---- parse_header(<<"range">>, Req) -> {From, To} | Final From :: non_neg_integer() To :: non_neg_integer() | infinity Final :: neg_integer() ---- .sec-websocket-extensions [source,erlang] ---- parse_header(<<"sec-websocket-extensions">>, Req) -> [{Extension, Params}] Extension :: binary() %% case sensitive Params :: [Key | {Key, Value}] Key :: binary() %% case sensitive Value :: binary() %% case sensitive ---- .sec-websocket-protocol and upgrade [source,erlang] ---- parse_header(Name, Req) -> [Token] Name :: <<"sec-websocket-protocol">> | <<"upgrade">> Token :: binary() %% case insensitive ---- .trailer [source,erlang] ---- parse_header(Name, Req) -> [Header] Header :: binary() %% case insensitive ---- .x-forwarded-for [source,erlang] ---- parse_header(<<"x-forwarded-for">>, Req) -> [Token] Token :: binary() %% case sensitive ---- This function will crash when attempting to parse a header Cowboy does not currently understand. == Changelog * *2.8*: The function now parses `access-control-request-headers`, `access-control-request-method`, `content-encoding`, `content-language`, `max-forwards`, `origin`, `proxy-authorization` and `trailer`. * *2.0*: Only the parsed header value is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Parse the accept header with a custom default value [source,erlang] ---- %% Accept everything when header is missing. Accept = cowboy_req:parse_header(<<"accept">>, Req, [{{ <<"*">>, <<"*">>, []}, 1000, []}]). ---- .Parse the content-length header [source,erlang] ---- %% Default content-length is 0. Length = cowboy_req:header(<<"content-length">>, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:header(3)[cowboy_req:header(3)], link:man:cowboy_req:headers(3)[cowboy_req:headers(3)] ================================================ FILE: doc/src/manual/cowboy_req.parse_qs.asciidoc ================================================ = cowboy_req:parse_qs(3) == Name cowboy_req:parse_qs - Parse the query string == Description [source,erlang] ---- parse_qs(Req :: cowboy_req:req()) -> [{Key :: binary(), Value :: binary() | true}] ---- Parse the query string as a list of key/value pairs. == Arguments Req:: The Req object. == Return value The parsed query string is returned as a list of key/value pairs. The key is a binary string. The value is either a binary string, or the atom `true`. Both key and value are case sensitive. The atom `true` is returned when a key is present in the query string without a value. For example, in the following URIs the key `<<"edit">>` will always have the value `true`: * `/posts/42?edit` * `/posts/42?edit&exclusive=1` * `/posts/42?exclusive=1&edit` * `/posts/42?exclusive=1&edit&from=web` == Changelog * *2.0*: The parsed value is not longer cached in the Req object. * *2.0*: Only the parsed query string is returned, it is no longer wrapped in a tuple. * *2.0*: Function introduced. Replaces `qs_val/1` and `qs_vals/1`. == Examples .Parse the query string and convert the keys to atoms [source,erlang] ---- ParsedQs = cowboy_req:parse_qs(Req), AtomsQs = [{binary_to_existing_atom(K, latin1), V} || {K, V} <- ParsedQs]. ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:qs(3)[cowboy_req:qs(3)], link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] ================================================ FILE: doc/src/manual/cowboy_req.path.asciidoc ================================================ = cowboy_req:path(3) == Name cowboy_req:path - URI path == Description [source,erlang] ---- path(Req :: cowboy_req:req()) -> Path :: binary() ---- Return the path of the effective request URI. The path can also be obtained using pattern matching: [source,erlang] ---- #{path := Path} = Req. ---- == Arguments Req:: The Req object. == Return value The path is returned as a binary string. It is case sensitive. == Changelog * *2.0*: Only the path is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the effective request URI's path [source,erlang] ---- Path = cowboy_req:path(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:binding(3)[cowboy_req:binding(3)], link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)], link:man:cowboy_req:path_info(3)[cowboy_req:path_info(3)] ================================================ FILE: doc/src/manual/cowboy_req.path_info.asciidoc ================================================ = cowboy_req:path_info(3) == Name cowboy_req:path_info - Access the route's trailing path segments == Description [source,erlang] ---- path_info(Req :: cowboy_req:req()) -> cowboy_router:tokens() ---- Return the tokens for the trailing path segments. This is the part of the host name that was matched using the `...` notation. == Arguments Req:: The Req object. == Return value The tokens are returned as a list of case sensitive binary strings. == Changelog * *2.0*: Only the tokens are returned, they are no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the path_info tokens [source,erlang] ---- PathInfo = cowboy_req:path_info(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:binding(3)[cowboy_req:binding(3)], link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)], link:man:cowboy_req:host_info(3)[cowboy_req:host_info(3)], link:man:cowboy_router(3)[cowboy_router(3)] ================================================ FILE: doc/src/manual/cowboy_req.peer.asciidoc ================================================ = cowboy_req:peer(3) == Name cowboy_req:peer - Peer address and port == Description [source,erlang] ---- peer(Req :: cowboy_req:req()) -> Info Info :: {inet:ip_address(), inet:port_number()} ---- Return the peer's IP address and port number. The peer information can also be obtained using pattern matching: [source,erlang] ---- #{peer := {IP, Port}} = Req. ---- == Arguments Req:: The Req object. == Return value The peer's IP address and port number. The peer is not necessarily the client's IP address and port. It is the IP address of the endpoint connecting directly to the server, which may be a gateway or a proxy. The forwarded header can be used to get better information about the different endpoints from the client to the server. Note however that it is only informative; there is no reliable way of determining the source of an HTTP request. == Changelog * *2.0*: Only the peer is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the peer IP address and port number. [source,erlang] ---- {IP, Port} = cowboy_req:peer(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:sock(3)[cowboy_req:sock(3)], link:man:cowboy_req:cert(3)[cowboy_req:cert(3)] ================================================ FILE: doc/src/manual/cowboy_req.port.asciidoc ================================================ = cowboy_req:port(3) == Name cowboy_req:port - URI port number == Description [source,erlang] ---- port(Req :: cowboy_req:req()) -> Port :: inet:port_number() ---- Return the port number of the effective request URI. Note that the port number returned by this function is obtained by parsing the host header. It may be different from the port the peer used to connect to Cowboy. The port number can also be obtained using pattern matching: [source,erlang] ---- #{port := Port} = Req. ---- == Arguments Req:: The Req object. == Return value The port number is returned as an integer. == Changelog * *2.0*: Only the port number is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the effective request URI's port number [source,erlang] ---- Port = cowboy_req:port(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)] ================================================ FILE: doc/src/manual/cowboy_req.push.asciidoc ================================================ = cowboy_req:push(3) == Name cowboy_req:push - Push a resource to the client == Description [source,erlang] ---- push(Path, Headers, Req :: cowboy_req:req()) -> push(Path, Headers, Req, #{}) push(Path, Headers, Req :: cowboy_req:req(), Opts) -> ok Path :: iodata() %% case sensitive Headers :: cowboy:http_headers() Opts :: cowboy_req:push_opts() ---- Push a resource to the client. Cowboy handles push requests the same way as if they came from the client, including the creation of a request handling process, routing and middlewares and so on. This function does nothing when the HTTP/1.1 protocol is used. You may call it safely without first checking whether the connection uses HTTP/2. The header names must be given as lowercase binary strings. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. Note that the headers must be the headers the client is expected to send if it were to perform the request. They are therefore request headers, and not response headers. By default, Cowboy will use the GET method, an empty query string, and take the scheme, host and port directly from the current request's URI. You can override them by passing options. Note that clients may cancel the push or ignore it entirely. For example browsers may ignore the resource when the connection is not considered secure. It is not possible to push resources after sending a response. Any attempt will result in an error. == Arguments Path:: The status code for the response. Headers:: The response headers. + Header names must be given as lowercase binary strings. Req:: The Req object. Opts:: Customize the HTTP method or the URI scheme, host, port or query string. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.0*: Function introduced. == Examples .Push a resource [source,erlang] ---- cowboy_req:push("/static/style.css", #{ <<"accept">> => <<"text/css">> }, Req), ---- .Push a resource with a custom host [source,erlang] ---- cowboy_req:push("/static/style.css", #{ <<"accept">> => <<"text/css">> }, #{host => <<"cdn.example.org">>}, Req), ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:inform(3)[cowboy_req:inform(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] ================================================ FILE: doc/src/manual/cowboy_req.qs.asciidoc ================================================ = cowboy_req:qs(3) == Name cowboy_req:qs - URI query string == Description [source,erlang] ---- qs(Req :: cowboy_req:req()) -> Qs :: binary() ---- Return the query string of the effective request URI. The query string can also be obtained using pattern matching: [source,erlang] ---- #{qs := Qs} = Req. ---- == Arguments Req:: The Req object. == Return value The query string is returned as a binary string. It is case sensitive. == Changelog * *2.0*: Only the query string is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the effective request URI's query string [source,erlang] ---- Qs = cowboy_req:qs(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:parse_qs(3)[cowboy_req:parse_qs(3)], link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] ================================================ FILE: doc/src/manual/cowboy_req.read_and_match_urlencoded_body.asciidoc ================================================ = cowboy_req:read_and_match_urlencoded_body(3) == Name cowboy_req:read_and_match_urlencoded_body - Read, parse and match a urlencoded request body against constraints == Description [source,erlang] ---- read_and_match_urlencoded_body(Fields, Req) -> read_and_match_urlencoded_body(Fields, Req, #{}) read_and_match_urlencoded_body(Fields, Req, Opts) -> {ok, Body, Req} Fields :: cowboy:fields() Req :: cowboy_req:req() Opts :: cowboy_req:read_body_opts() Body :: #{atom() => any()} ---- Read, parse and match a urlencoded request body against constraints. This function reads the request body and parses it as `application/x-www-form-urlencoded`. It then applies the given field constraints to the urlencoded data and returns the result as a map. The urlencoded media type is used by Web browsers when submitting HTML forms using the POST method. Cowboy will only return the values specified in the fields list, and ignore all others. Fields can be either the key requested; the key along with a list of constraints; or the key, a list of constraints and a default value in case the key is missing. This function will crash if the key is missing and no default value is provided. This function will also crash if a constraint fails. The key must be provided as an atom. The key of the returned map will be that atom. The value may be converted through the use of constraints, making this function able to extract, validate and convert values all in one step. Cowboy needs to read the full body before parsing. By default it will read bodies of size up to 64KB. It is possible to provide options to read larger bodies if required. Cowboy will automatically handle protocol details including the expect header, chunked transfer-encoding and others. Once the body has been read, Cowboy sets the content-length header if it was not previously provided. This function can only be called once. Calling it again will result in undefined behavior. == Arguments Fields:: Fields to retrieve from the urlencoded body. + See link:man:cowboy(3)[cowboy(3)] for a complete description. Req:: The Req object. Opts:: A map of body reading options. Please refer to link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] for details about each option. + This function defaults the `length` to 64KB and the `period` to 5 seconds. == Return value An `ok` tuple is returned. Desired values are returned as a map. The key is the atom that was given in the list of fields, and the value is the optionally converted value after applying constraints. The map contains the same keys that were given in the fields. An exception is triggered when the match fails. The Req object returned in the tuple must be used from that point onward. It contains a more up to date representation of the request. For example it may have an added content-length header once the body has been read. == Changelog * *2.5*: Function introduced. == Examples .Match fields [source,erlang] ---- %% ID and Lang are binaries. #{id := ID, lang := Lang} = cowboy_req:read_and_match_urlencoded_body( [id, lang], Req). ---- .Match fields and apply constraints [source,erlang] ---- %% ID is an integer and Lang a non-empty binary. #{id := ID, lang := Lang} = cowboy_req:read_and_match_urlencoded_body( [{id, int}, {lang, nonempty}], Req). ---- .Match fields with default values [source,erlang] ---- #{lang := Lang} = cowboy_req:read_and_match_urlencoded_body( [{lang, [], <<"en-US">>}], Req). ---- .Allow large urlencoded bodies [source,erlang] ---- {ok, Body, Req} = cowboy_req:read_and_match_urlencoded_body( Fields, Req0, #{length => 1000000}). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.read_body.asciidoc ================================================ = cowboy_req:read_body(3) == Name cowboy_req:read_body - Read the request body == Description [source,erlang] ---- read_body(Req :: cowboy_req:req()) -> read_body(Req, #{}) read_body(Req :: cowboy_req:req(), Opts) -> {ok, Data :: binary(), Req} | {more, Data :: binary(), Req} Opts :: cowboy_req:read_body_opts() ---- Read the request body. This function reads a chunk of the request body. A `more` tuple is returned when more data remains to be read. Call the function repeatedly until an `ok` tuple is returned to read the entire body. An `ok` tuple with empty data is returned when the request has no body, or when calling this function again after the body has already been read. It is therefore safe to call this function directly. Note that the body can only be read once. This function reads the request body from the connection process. The connection process is responsible for reading from the socket. The exact behavior varies depending on the protocol. The options therefore are only related to the communication between the request process and the connection process. Cowboy will automatically handle protocol details including the expect header, chunked transfer-encoding and others. Once the body has been read fully, Cowboy sets the content-length header if it was not previously provided. == Arguments Req:: The Req object. Opts:: A map of body reading options. + The `length` option can be used to request smaller or bigger chunks of data to be sent. It is a best effort approach, Cowboy may send more data than configured on occasions. It defaults to 8MB. + The `period` indicates how long the connection process will wait before it provides us with the data it received. It defaults to 15 seconds. + The connection process sends data to the request process when either the `length` of data or the `period` of time is reached. + The `timeout` option is a safeguard in case the connection process becomes unresponsive. The function will crash if no message was received in that interval. The timeout should be larger than the period. It defaults to the period + 1 second. + Auto mode can be enabled by setting the `length` to `auto` and the `period` to `infinity`. When auto mode is used, Cowboy will send data to the handler as soon as it receives it, regardless of its size. It will wait indefinitely until data is available. Auto mode's main purpose is asynchronous body reading using link:man:cowboy_req:cast(3)[cowboy_req:cast(3)]. == Return value A `more` tuple is returned when there are more data to be read. An `ok` tuple is returned when there are no more data to be read, either because this is the last chunk of data, the body has already been read, or there was no body to begin with. The data is always returned as a binary. The Req object returned in the tuple must be used from that point onward. It contains a more up to date representation of the request. For example it may have an added content-length header once the body has been read. == Changelog * *2.11*: The `length` option now accepts `auto` and the period now accepts `infinity`. This adds support for reading the body in auto mode. * *2.0*: Function introduced. Replaces `body/1,2`. == Examples .Read the entire body [source,erlang] ---- read_body(Req0, Acc) -> case cowboy_req:read_body(Req0) of {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req}; {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>) end. ---- .Read the body in small chunks [source,erlang] ---- cowboy_req:read_body(Req, #{length => 64000}). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.read_part.asciidoc ================================================ = cowboy_req:read_part(3) == Name cowboy_req:read_part - Read the next multipart headers == Description [source,erlang] ---- read_part(Req :: cowboy_req:req()) -> read_part(Req, #{}) read_part(Req :: cowboy_req:req(), Opts) -> {ok, Headers, Req} | {done, Req} Opts :: cowboy_req:read_body_opts() Headers :: #{binary() => binary()} ---- Read the next part of a multipart body. This function reads the request body and parses it as multipart. Each parts of a multipart representation have their own headers and body. This function parses and returns headers. Examples of multipart media types are `multipart/form-data` and `multipart/byteranges`. Cowboy will skip any data remaining until the beginning of the next part. This includes the preamble to the multipart message but also the body of a previous part if it hasn't been read. Both are skipped automatically when calling this function. Cowboy will read the body before parsing in chunks of size up to 64KB, with a period of 5 seconds. This is tailored for reading part headers and might not be the most efficient for skipping the previous part's body. The headers returned are MIME headers, *NOT* HTTP headers. They can be parsed using the functions from the `cow_multipart` module. In addition, the `cow_multipart:form_data/1` function can be used to quickly extract information from `multipart/form-data` representations. // @todo Proper link to cow_multipart:form_data. Once a part has been read, it can not be read again. Once the body has been read, Cowboy sets the content-length header if it was not previously provided. // @todo Limit the maximum size of multipart headers. == Arguments Req:: The Req object. Opts:: A map of body reading options. Please refer to link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] for details about each option. + This function defaults the `length` to 64KB and the `period` to 5 seconds. == Return value An `ok` tuple is returned containing the next part's headers as a map. A `done` tuple is returned if there are no more parts to read. The Req object returned in the tuple must be used from that point onward. It contains a more up to date representation of the request. For example it may have an added content-length header once the body has been read. == Changelog * *2.0*: Function introduced. Replaces `part/1,2`. == Examples .Read all parts [source,erlang] ---- acc_multipart(Req0, Acc) -> case cowboy_req:read_part(Req0) of {ok, Headers, Req1} -> {ok, Body, Req} = stream_body(Req1, <<>>), acc_multipart(Req, [{Headers, Body}|Acc]); {done, Req} -> {lists:reverse(Acc), Req} end. stream_body(Req0, Acc) -> case cowboy_req:read_part_body(Req0) of {more, Data, Req} -> stream_body(Req, << Acc/binary, Data/binary >>); {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req} end. ---- .Read all part headers, skipping bodies [source,erlang] ---- skip_body_multipart(Req0, Acc) -> case cowboy_req:read_part(Req0) of {ok, Headers, Req} -> skip_body_multipart(Req, [Headers|Acc]); {done, Req} -> {lists:reverse(Acc), Req} end. ---- .Read a part header in larger chunks [source,erlang] ---- {ok, Headers, Req} = cowboy_req:read_part(Req0, #{length => 1000000}). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.read_part_body.asciidoc ================================================ = cowboy_req:read_part_body(3) == Name cowboy_req:read_part_body - Read the current part's body == Description [source,erlang] ---- read_part_body(Req :: cowboy_req:req()) -> read_part_body(Req, #{}) read_part_body(Req :: cowboy_req:req(), Opts) -> {ok, Data :: binary(), Req} | {more, Data :: binary(), Req} Opts :: cowboy_req:read_body_opts() ---- Read the body of the current part of the multipart message. This function reads the request body and parses it as multipart. Each parts of a multipart representation have their own headers and body. This function returns the body of the current part. Examples of multipart media types are `multipart/form-data` and `multipart/byteranges`. This function reads a chunk of the part's body. A `more` tuple is returned when more data remains to be read. Call the function repeatedly until an `ok` tuple is returned to read the entire body. Once a part has been read, it can not be read again. Once the body has been read, Cowboy sets the content-length header if it was not previously provided. // @todo Limit the maximum size of multipart headers. == Arguments Req:: The Req object. Opts:: A map of body reading options. Please refer to link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] for details about each option. + This function uses the same default options as the link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] function. == Return value A `more` tuple is returned when there are more data to be read. An `ok` tuple is returned when there are no more data to be read. The data is always returned as a binary. The Req object returned in the tuple must be used from that point onward. It contains a more up to date representation of the request. For example it may have an added content-length header once the body has been read. == Changelog * *2.0*: Function introduced. Replaces `part_body/1,2`. == Examples .Read a full part's body [source,erlang] ---- stream_body(Req0, Acc) -> case cowboy_req:read_part_body(Req0) of {more, Data, Req} -> stream_body(Req, << Acc/binary, Data/binary >>); {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req} end. ---- .Ensure a part's body is smaller than 64KB [source,erlang] ---- {ok, Body, Req} = cowboy_req:read_part_body(Req0, #{length => 64000}). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_urlencoded_body(3)[cowboy_req:read_urlencoded_body(3)], link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)] ================================================ FILE: doc/src/manual/cowboy_req.read_urlencoded_body.asciidoc ================================================ = cowboy_req:read_urlencoded_body(3) == Name cowboy_req:read_urlencoded_body - Read and parse a urlencoded request body == Description [source,erlang] ---- read_urlencoded_body(Req :: cowboy_req:req()) -> read_urlencoded_body(Req, #{}) read_urlencoded_body(Req :: cowboy_req:req(), Opts) -> {ok, Body, Req} Opts :: cowboy_req:read_body_opts() Body :: [{Key :: binary(), Value :: binary() | true}] ---- Read and parse a urlencoded request body. This function reads the request body and parses it as `application/x-www-form-urlencoded`. It returns a list of key/values. The urlencoded media type is used by Web browsers when submitting HTML forms using the POST method. Cowboy needs to read the full body before parsing. By default it will read bodies of size up to 64KB. It is possible to provide options to read larger bodies if required. Cowboy will automatically handle protocol details including the expect header, chunked transfer-encoding and others. Once the body has been read, Cowboy sets the content-length header if it was not previously provided. This function can only be called once. Calling it again will result in undefined behavior. == Arguments Req:: The Req object. Opts:: A map of body reading options. Please refer to link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] for details about each option. + This function defaults the `length` to 64KB and the `period` to 5 seconds. == Return value An `ok` tuple is returned containing a list of key/values found in the body. The Req object returned in the tuple must be used from that point onward. It contains a more up to date representation of the request. For example it may have an added content-length header once the body has been read. == Changelog * *2.0*: Function introduced. Replaces `body_qs/1,2`. == Examples .Read a urlencoded body [source,erlang] ---- {ok, Body, Req} = cowboy_req:read_urlencoded_body(Req0), {_, Lang} = lists:keyfind(<<"lang">>, 1, Body). ---- .Allow large urlencoded bodies [source,erlang] ---- {ok, Body, Req} = cowboy_req:read_urlencoded_body(Req0, #{length => 1000000}). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:has_body(3)[cowboy_req:has_body(3)], link:man:cowboy_req:body_length(3)[cowboy_req:body_length(3)], link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)], link:man:cowboy_req:read_and_match_urlencoded_body(3)[cowboy_req:read_and_match_urlencoded_body(3)], link:man:cowboy_req:read_part(3)[cowboy_req:read_part(3)], link:man:cowboy_req:read_part_body(3)[cowboy_req:read_part_body(3)] ================================================ FILE: doc/src/manual/cowboy_req.reply.asciidoc ================================================ = cowboy_req:reply(3) == Name cowboy_req:reply - Send the response == Description [source,erlang] ---- reply(Status, Req :: cowboy_req:req()) -> reply(StatusCode, #{}, Req) reply(Status, Headers, Req :: cowboy_req:req()) -> Req reply(Status, Headers, Body, Req :: cowboy_req:req()) -> Req Status :: cowboy:http_status() Headers :: cowboy:http_headers() Body :: cowboy_req:resp_body() ---- Send the response. The header names must be given as lowercase binary strings. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. Cowboy does not allow duplicate header names. Headers set by this function may overwrite those set by `set_resp_header/3` and `set_resp_headers/2`. Use link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)] instead of this function to set cookies. The `reply/2,3` functions will send the body set previously, if any. The `reply/4` function always sends the given body, overriding any previously set. You do not need to set the content-length header when sending a response body. Cowboy takes care of it automatically. You should however provide a content-type header. No further data can be transmitted after this function returns. This includes the push mechanism. Attempting to send two replies, or to push resources after a reply has been sent, will result in an error. == Arguments Status:: The status code for the response. Headers:: The response headers. + Header names must be given as lowercase binary strings. Body:: The body can be either a binary value, an iolist or a `sendfile` tuple telling Cowboy to send the contents of a file. Req:: The Req object. == Return value A new Req object is returned. The returned Req object should be used from that point onward as it contains updated information about the state of the request. == Changelog * *2.0*: Only the Req is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Reply [source,erlang] ---- Req = cowboy_req:reply(404, Req0). ---- .Reply with custom headers [source,erlang] ---- Req = cowboy_req:reply(401, #{ <<"www-authenticate">> => <<"Basic realm=\"erlang.org\"">> }, Req0). ---- .Reply with custom headers and a body [source,erlang] ---- Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain">> }, "Hello world!", Req0). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)], link:man:cowboy_req:inform(3)[cowboy_req:inform(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)], link:man:cowboy_req:push(3)[cowboy_req:push(3)] ================================================ FILE: doc/src/manual/cowboy_req.resp_header.asciidoc ================================================ = cowboy_req:resp_header(3) == Name cowboy_req:resp_header - Response header == Description [source,erlang] ---- resp_header(Name, Req) -> resp_header(Name, Req, undefined) resp_header(Name, Req, Default) -> binary() | Default Name :: binary() %% lowercase; case insensitive Req :: cowboy_req:req() Default :: any() ---- Return the value for the given response header. The response header must have been set previously using link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)] or link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)]. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. == Arguments Name:: Desired response header name as a lowercase binary string. Req:: The Req object. Default:: Default value returned when the header is missing. == Return value The header value is returned as a binary string. When the header is missing, the default argument is returned. == Changelog * *2.0*: Function introduced. == Examples .Get the content-type response header [source,erlang] ---- Type = cowboy_req:resp_header(<<"content-type">>, Req). ---- .Get the content-type response header with a default value [source,erlang] ---- Type = cowboy_req:resp_header(<<"content-type">>, Req, <<"text/html">>). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:resp_headers(3)[cowboy_req:resp_headers(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)] ================================================ FILE: doc/src/manual/cowboy_req.resp_headers.asciidoc ================================================ = cowboy_req:resp_headers(3) == Name cowboy_req:resp_headers - Response headers == Description [source,erlang] ---- resp_headers(Req :: cowboy_req:req()) -> cowboy:http_headers() ---- Return all response headers. == Arguments Req:: The Req object. == Return value Headers are returned as a map with keys being lowercase binary strings, and values as binary strings. == Changelog * *2.0*: Function introduced. == Examples .Get all response headers [source,erlang] ---- Headers = cowboy_req:resp_headers(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:resp_header(3)[cowboy_req:resp_header(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)] ================================================ FILE: doc/src/manual/cowboy_req.scheme.asciidoc ================================================ = cowboy_req:scheme(3) == Name cowboy_req:scheme - URI scheme == Description [source,erlang] ---- scheme(Req :: cowboy_req:req()) -> Scheme :: binary() ---- Return the scheme of the effective request URI. The scheme can also be obtained using pattern matching: [source,erlang] ---- #{scheme := Scheme} = Req. ---- == Arguments Req:: The Req object. == Return value The scheme is returned as a binary. It is case insensitive. Cowboy will only set the scheme to `<<"http">>` or `<<"https">>`. == Changelog * *2.0*: Function introduced. == Examples .Redirect HTTP to HTTPS [source,erlang] ---- init(Req0=#{scheme := <<"http">>}, State) -> Req = cowboy_req:reply(302, #{ <<"location">> => cowboy_req:uri(Req, #{scheme => <<"https">>}) }, Req0), {ok, Req, State}; init(Req, State) -> {cowboy_rest, Req, State}. ---- == See also link:man:cowboy_req(3)[cowboy_req(3)] ================================================ FILE: doc/src/manual/cowboy_req.set_resp_body.asciidoc ================================================ = cowboy_req:set_resp_body(3) == Name cowboy_req:set_resp_body - Set the response body == Description [source,erlang] ---- set_resp_body(Body, Req :: cowboy_req:req()) -> Req Body :: cowboy_req:resp_body() ---- Set the response body. The response body will be sent when a reply is initiated. Note that the functions `stream_reply/2,3` and `reply/4` will override the body set by this function. This function can also be used to remove a response body that was set previously. To do so, simply call this function with an empty body. == Arguments Body:: The body can be either a binary value, an iolist or a `sendfile` tuple telling Cowboy to send the contents of a file. Req:: The Req object. == Return value A new Req object is returned. The returned Req object must be used from that point onward, otherwise the body will not be sent in the response. == Changelog * *2.0*: The function now accepts a `sendfile` tuple. * *2.0*: The `set_resp_body_fun/2,3` functions were removed. * *1.0*: Function introduced. == Examples .Set the response body [source,erlang] ---- Req = cowboy_req:set_resp_body(<<"Hello world!">>, Req0). ---- .Set the response body as an iolist [source,erlang] ---- Req = cowboy_req:set_resp_body([ "", page_title(), "", page_body(), "" ], Req0). ---- .Tell Cowboy to send data from a file [source,erlang] ---- {ok, #file_info{size=Size}} = file:read_file_info(Filename), Req = cowboy_req:set_resp_body({sendfile, 0, Size, Filename}, Req0). ---- .Clear any previously set response body [source,erlang] ---- Req = cowboy_req:set_resp_body(<<>>, Req0). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] ================================================ FILE: doc/src/manual/cowboy_req.set_resp_cookie.asciidoc ================================================ = cowboy_req:set_resp_cookie(3) == Name cowboy_req:set_resp_cookie - Set a cookie == Description [source,erlang] ---- set_resp_cookie(Name, Value, Req :: cowboy_req:req()) -> set_resp_cookie(Name, Value, Req, #{}) set_resp_cookie(Name, Value, Req :: cowboy_req:req(), Opts) -> Req Name :: binary() %% case sensitive Value :: iodata() %% case sensitive Opts :: cow_cookie:cookie_opts() ---- Set a cookie to be sent with the response. Note that cookie names are case sensitive. == Arguments Name:: Cookie name. Value:: Cookie value. Req:: The Req object. Opts:: Cookie options. == Return value A new Req object is returned. The returned Req object must be used from that point onward, otherwise the cookie will not be sent in the response. == Changelog * *2.0*: `set_resp_cookie/3` introduced as an alias to `set_resp_cookie/4` with no options. * *2.0*: The first argument type is now `binary()` instead of `iodata()`. * *1.0*: Function introduced. == Examples .Set a session cookie [source,erlang] ---- SessionID = base64:encode(crypto:strong_rand_bytes(32)), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0). ---- .Set a cookie with an expiration time [source,erlang] ---- Req = cowboy_req:set_resp_cookie(<<"lang">>, <<"fr-FR">>, Req0, #{max_age => 3600}). ---- .Delete a cookie [source,erlang] ---- Req = cowboy_req:set_resp_cookie(<<"sessionid">>, <<>>, Req0, #{max_age => 0}). ---- .Set a cookie for a specific domain and path [source,erlang] ---- Req = cowboy_req:set_resp_cookie(<<"inaccount">>, <<"1">>, Req0, #{domain => "my.example.org", path => "/account"}). ---- .Restrict a cookie to HTTPS [source,erlang] ---- SessionID = base64:encode(crypto:strong_rand_bytes(32)), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0, #{secure => true}). ---- .Restrict a cookie to HTTP [source,erlang] ---- SessionID = base64:encode(crypto:strong_rand_bytes(32)), Req = cowboy_req:set_resp_cookie(<<"sessionid">>, SessionID, Req0, #{http_only => true}). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] ================================================ FILE: doc/src/manual/cowboy_req.set_resp_header.asciidoc ================================================ = cowboy_req:set_resp_header(3) == Name cowboy_req:set_resp_header - Set a response header == Description [source,erlang] ---- set_resp_header(Name, Value, Req :: cowboy_req:req()) -> Req Name :: binary() %% lowercase; case insensitive Value :: iodata() %% case depends on header ---- Set a header to be sent with the response. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. Cowboy does not allow duplicate header names. Headers set by this function may be overwritten by those set from the reply functions. Use link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)] instead of this function to set cookies. == Arguments Name:: Header name as a lowercase binary string. Value:: Header value. Req:: The Req object. == Return value A new Req object is returned. The returned Req object must be used from that point onward, otherwise the header will not be sent in the response. == Changelog * *1.0*: Function introduced. == Examples .Set a header in the response [source,erlang] ---- Req = cowboy_req:set_resp_header(<<"allow">>, "GET", Req0). ---- .Construct a header using iolists [source,erlang] ---- Req = cowboy_req:set_resp_header(<<"allow">>, [allowed_methods(), ", OPTIONS"], Req0). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:has_resp_header(3)[cowboy_req:has_resp_header(3)], link:man:cowboy_req:resp_header(3)[cowboy_req:resp_header(3)], link:man:cowboy_req:resp_headers(3)[cowboy_req:resp_headers(3)], link:man:cowboy_req:delete_resp_header(3)[cowboy_req:delete_resp_header(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] ================================================ FILE: doc/src/manual/cowboy_req.set_resp_headers.asciidoc ================================================ = cowboy_req:set_resp_headers(3) == Name cowboy_req:set_resp_headers - Set several response headers == Description [source,erlang] ---- set_resp_headers(Headers, Req :: cowboy_req:req()) -> Req Headers :: cowboy:http_headers() | [{binary(), iodata()}] ---- Set several headers to be sent with the response. The header name must be given as a lowercase binary string. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. Cowboy does not allow duplicate header names. Headers set by this function may be overwritten by those set from the reply functions. Likewise, headers set by this function may overwrite headers that were set previously. Use link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)] instead of this function to set cookies. == Arguments Headers:: Headers as a map with names being lowercase binary strings, and values as iodata; or as a list with the same requirements for names and values. + When a list is given it is converted to its equivalent map, with duplicate headers concatenated with a comma inserted in-between. Support for lists is meant to simplify using data from clients or other applications. + The set-cookie header must not be set using this function. Req:: The Req object. == Return value A new Req object is returned. The returned Req object must be used from that point onward, otherwise the headers will not be sent in the response. == Changelog * *2.13*: The function now accepts a list of headers. * *2.0*: Function introduced. == Examples .Set several response headers [source,erlang] ---- Req = cowboy_req:set_resp_headers(#{ <<"content-type">> => <<"text/html">>, <<"content-encoding">> => <<"gzip">> }, Req0). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:has_resp_header(3)[cowboy_req:has_resp_header(3)], link:man:cowboy_req:resp_header(3)[cowboy_req:resp_header(3)], link:man:cowboy_req:resp_headers(3)[cowboy_req:resp_headers(3)], link:man:cowboy_req:delete_resp_header(3)[cowboy_req:delete_resp_header(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] ================================================ FILE: doc/src/manual/cowboy_req.sock.asciidoc ================================================ = cowboy_req:sock(3) == Name cowboy_req:sock - Socket address and port == Description [source,erlang] ---- sock(Req :: cowboy_req:req()) -> Info Info :: {inet:ip_address(), inet:port_number()} ---- Return the socket's IP address and port number. The socket information can also be obtained using pattern matching: [source,erlang] ---- #{sock := {IP, Port}} = Req. ---- == Arguments Req:: The Req object. == Return value The socket's local IP address and port number. == Changelog * *2.1*: Function introduced. == Examples .Get the socket's IP address and port number. [source,erlang] ---- {IP, Port} = cowboy_req:sock(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:peer(3)[cowboy_req:peer(3)], link:man:cowboy_req:cert(3)[cowboy_req:cert(3)] ================================================ FILE: doc/src/manual/cowboy_req.stream_body.asciidoc ================================================ = cowboy_req:stream_body(3) == Name cowboy_req:stream_body - Stream the response body == Description [source,erlang] ---- stream_body(Data, IsFin, Req :: cowboy_req:req()) -> ok Data :: cowboy_req:resp_body() IsFin :: fin | nofin ---- Stream the response body. This function may be called as many times as needed after initiating a response using the link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] function. The second argument indicates if this call is the final call. Use the `nofin` value until you know no more data will be sent. The final call should use `fin` (possibly with an empty data value) or be a call to the link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)] function. Note that not using `fin` for the final call is not an error; Cowboy will take care of it when the request handler terminates if needed. Depending on the resource it may however be more efficient to do it as early as possible. You do not need to handle HEAD requests specifically as Cowboy will ensure no data is sent when you call this function. == Arguments Data:: The data to be sent. IsFin:: A flag indicating whether this is the final piece of data to be sent. Req:: The Req object. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.6*: The `Data` argument can now be a sendfile tuple. * *2.0*: Function introduced. Replaces `chunk/2`. == Examples .Stream the response body [source,erlang] ---- Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/plain">> }, Req0), cowboy_req:stream_body(<<"Hello\n">>, nofin, Req), timer:sleep(1000), cowboy_req:stream_body(<<"World!\n">>, fin, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)], link:man:cowboy_req:stream_events(3)[cowboy_req:stream_events(3)], link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)] ================================================ FILE: doc/src/manual/cowboy_req.stream_events.asciidoc ================================================ = cowboy_req:stream_events(3) == Name cowboy_req:stream_events - Stream events == Description [source,erlang] ---- stream_events(Events, IsFin, Req :: cowboy_req:req()) -> ok Events :: Event | [Event] IsFin :: fin | nofin Event :: #{ comment => iodata(), data => iodata(), event => iodata() | atom(), id => iodata(), retry => non_neg_integer() } ---- Stream events. This function should only be used for `text/event-stream` responses when using server-sent events. Cowboy will automatically encode the given events to their text representation. This function may be called as many times as needed after initiating a response using the link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] function. The second argument indicates if this call is the final call. Use the `nofin` value until you know no more data will be sent. The final call should use `fin` (possibly with an empty data value) or be a call to the link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)] function. Note that not using `fin` for the final call is not an error; Cowboy will take care of it when the request handler terminates if needed. Depending on the resource it may however be more efficient to do it as early as possible. You do not need to handle HEAD requests specifically as Cowboy will ensure no data is sent when you call this function. == Arguments Events:: Events to be sent. All fields are optional. IsFin:: A flag indicating whether this is the final piece of data to be sent. Req:: The Req object. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.5*: Function introduced. == Examples .Stream events [source,erlang] ---- Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/event-stream">> }, Req0), cowboy_req:stream_events(#{ id => <<"comment-123">>, event => <<"add_comment">>, data => <<"Hello,\n\nI noticed something wrong in ...">> }, nofin, Req), timer:sleep(1000), cowboy_req:stream_events(#{ event => <<"debug">>, data => io_lib:format("An error occurred: ~p~n", [Error]) }, fin, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)], link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)], link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)] ================================================ FILE: doc/src/manual/cowboy_req.stream_reply.asciidoc ================================================ = cowboy_req:stream_reply(3) == Name cowboy_req:stream_reply - Send the response headers == Description [source,erlang] ---- stream_reply(Status, Req :: cowboy_req:req()) -> stream_reply(StatusCode, #{}, Req) stream_reply(Status, Headers, Req :: cowboy_req:req()) -> Req Status :: cowboy:http_status() Headers :: cowboy:http_headers() ---- Send the response headers. The header names must be given as lowercase binary strings. While header names are case insensitive, Cowboy requires them to be given as lowercase to function properly. Cowboy does not allow duplicate header names. Headers set by this function may overwrite those set by `set_resp_header/3`. Use link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)] instead of this function to set cookies. If a response body was set before calling this function, it will not be sent. Use link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)] to stream the response body and optionally link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)] to send response trailer field values. You may want to set the content-length header when using this function, if it is known in advance. This will allow clients using HTTP/2 and HTTP/1.0 to process the response more efficiently. The streaming method varies depending on the protocol being used. HTTP/2 will use the usual DATA frames. HTTP/1.1 will use chunked transfer-encoding, if the content-length response header is set the body will be sent without chunked chunked transfer-encoding. HTTP/1.0 will send the body unmodified and close the connection at the end if no content-length was set. It is not possible to push resources after this function returns. Any attempt will result in an error. == Arguments Status:: The status code for the response. Headers:: The response headers. + Header names must be given as lowercase binary strings. Req:: The Req object. == Return value A new Req object is returned. The returned Req object must be used from that point onward in order to be able to stream the response body. == Changelog * *2.0*: Only the Req is returned, it is no longer wrapped in a tuple. * *2.0*: Function introduced. Replaces `chunked_reply/1,2`. == Examples .Initiate the response [source,erlang] ---- Req = cowboy_req:stream_reply(200, Req0). ---- .Stream the response with custom headers [source,erlang] ---- Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/plain">> }, Req0), cowboy_req:stream_body(<<"Hello\n">>, nofin, Req), timer:sleep(1000), cowboy_req:stream_body(<<"World!\n">>, fin, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:set_resp_cookie(3)[cowboy_req:set_resp_cookie(3)], link:man:cowboy_req:set_resp_header(3)[cowboy_req:set_resp_header(3)], link:man:cowboy_req:set_resp_headers(3)[cowboy_req:set_resp_headers(3)], link:man:cowboy_req:inform(3)[cowboy_req:inform(3)], link:man:cowboy_req:reply(3)[cowboy_req:reply(3)], link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)], link:man:cowboy_req:stream_events(3)[cowboy_req:stream_events(3)], link:man:cowboy_req:stream_trailers(3)[cowboy_req:stream_trailers(3)], link:man:cowboy_req:push(3)[cowboy_req:push(3)] ================================================ FILE: doc/src/manual/cowboy_req.stream_trailers.asciidoc ================================================ = cowboy_req:stream_trailers(3) == Name cowboy_req:stream_trailers - Send the response trailers == Description [source,erlang] ---- stream_trailers(Trailers, Req :: cowboy_req:req()) -> ok Trailers :: cowboy:http_headers() ---- Send the response trailers and terminate the stream. This function can only be called once, after initiating a response using link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)] and sending zero or more body chunks using link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)] with the `nofin` argument set. The function `stream_trailers/2` implies `fin` and automatically terminate the response. You must list all field names sent in trailers in the trailer header, otherwise they might be dropped by intermediaries or clients. == Arguments Trailers:: Trailer field values to be sent. Req:: The Req object. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.2*: Function introduced. == Examples .Stream a response body with trailers [source,erlang] ---- Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/plain">>, <<"trailer">> => <<"expires, content-md5">> }, Req0), cowboy_req:stream_body(<<"Hello\n">>, nofin, Req), timer:sleep(1000), cowboy_req:stream_body(<<"World!\n">>, nofin, Req). cowboy_req:stream_trailers(#{ <<"expires">> => <<"Sun, 10 Dec 2017 19:13:47 GMT">>, <<"content-md5">> => <<"fbf68a8e34b2ded53bba54e68794b4fe">> }, Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:stream_reply(3)[cowboy_req:stream_reply(3)], link:man:cowboy_req:stream_body(3)[cowboy_req:stream_body(3)], link:man:cowboy_req:stream_events(3)[cowboy_req:stream_events(3)] ================================================ FILE: doc/src/manual/cowboy_req.uri.asciidoc ================================================ = cowboy_req:uri(3) == Name cowboy_req:uri - Reconstructed URI == Description [source,erlang] ---- uri(Req :: cowboy_req:req()) -> uri(Req, #{}) uri(Req :: cowboy_req:req(), Opts) -> URI :: iodata() Opts :: #{ scheme => iodata() | undefined, host => iodata() | undefined, port => inet:port_number() | undefined, path => iodata() | undefined, qs => iodata() | undefined, fragment => iodata() | undefined } ---- Reconstruct the effective request URI, optionally modifying components. By default Cowboy will build a URI using the components found in the request. Options allow disabling or replacing individual components. == Arguments Req:: The Req object. Opts:: Map for overriding individual components. + To replace a component, provide its new value as a binary string or an iolist. To disable a component, set its value to `undefined`. + As this function always returns a valid URI, there are some things to note: + * Disabling the host also disables the scheme and port. * There is no fragment component by default as these are not sent with the request. * The port number may not appear in the resulting URI if it is the default port for the given scheme (http: 80; https: 443). == Return value The reconstructed URI is returned as an iolist or a binary string. == Changelog * *2.0*: Individual components can be replaced or disabled. * *2.0*: Only the URI is returned, it is no longer wrapped in a tuple. * *2.0*: Function introduced. Replaces `host_url/1` and `url/1`. == Examples With an effective request URI http://example.org/path/to/res?edit=1 we can have: .Protocol relative form [source,erlang] ---- %% //example.org/path/to/res?edit=1 cowboy_req:uri(Req, #{scheme => undefined}). ---- .Serialized origin for use in the origin header [source,erlang] ---- %% http://example.org cowboy_req:uri(Req, #{path => undefined, qs => undefined}). ---- .HTTP/1.1 origin form (path and query string only) [source,erlang] ---- %% /path/to/res?edit=1 cowboy_req:uri(Req, #{host => undefined}). ---- .Add a fragment to the URI [source,erlang] ---- %% http://example.org/path/to/res?edit=1#errors cowboy_req:uri(Req, #{fragment => <<"errors">>}). ---- .Ensure the scheme is HTTPS [source,erlang] ---- %% https://example.org/path/to/res?edit=1 cowboy_req:uri(Req, #{scheme => <<"https">>}). ---- .Convert the URI to a binary string [source,erlang] ---- iolist_to_binary(cowboy_req:uri(Req)). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)], link:man:cowboy_req:scheme(3)[cowboy_req:scheme(3)], link:man:cowboy_req:host(3)[cowboy_req:host(3)], link:man:cowboy_req:port(3)[cowboy_req:port(3)], link:man:cowboy_req:path(3)[cowboy_req:path(3)], link:man:cowboy_req:qs(3)[cowboy_req:qs(3)] ================================================ FILE: doc/src/manual/cowboy_req.version.asciidoc ================================================ = cowboy_req:version(3) == Name cowboy_req:version - HTTP version == Description [source,erlang] ---- version(Req :: cowboy_req:req()) -> Version :: cowboy:http_version() ---- Return the HTTP version used for the request. The version can also be obtained using pattern matching: [source,erlang] ---- #{version := Version} = Req. ---- == Arguments Req:: The Req object. == Return value The HTTP version used for the request is returned as an atom. It is provided for informative purposes only. == Changelog * *2.0*: Only the version is returned, it is no longer wrapped in a tuple. * *1.0*: Function introduced. == Examples .Get the HTTP version [source,erlang] ---- Version = cowboy_req:version(Req). ---- == See also link:man:cowboy_req(3)[cowboy_req(3)] ================================================ FILE: doc/src/manual/cowboy_rest.asciidoc ================================================ = cowboy_rest(3) == Name cowboy_rest - REST handlers == Description The module `cowboy_rest` implements the HTTP state machine. Implementing REST handlers is not enough to provide a REST interface; this interface must also follow the REST constraints including HATEOAS (hypermedia as the engine of application state). == Callbacks REST handlers implement the following interface: [source,erlang] ---- init(Req, State) -> {cowboy_rest, Req, State} Callback(Req, State) -> {Result, Req, State} | {stop, Req, State} | {{switch_handler, Module}, Req, State} | {{switch_handler, Module, Opts}, Req, State} terminate(Reason, Req, State) -> ok %% optional Req :: cowboy_req:req() State :: any() Module :: module() Opts :: any() Reason :: normal | {crash, error | exit | throw, any()} Callback - see below Result - see below Default - see below ---- The `init/2` callback is common to all handlers. To switch to the REST handler behavior, it must return `cowboy_rest` as the first element of the tuple. The `Callback/2` above represents all the REST-specific callbacks. They are described in the following section of this manual. REST-specific callbacks differ by their name, semantics, result and default values. The default value is the one used when the callback has not been implemented. They otherwise all follow the same interface. The `stop` tuple can be returned to stop REST processing. If no response was sent before then, Cowboy will send a '204 No Content'. The `stop` tuple can be returned from any callback, excluding `expires`, `generate_etag`, `last_modified` and `variances`. A `switch_handler` tuple can be returned from these same callbacks to stop REST processing and switch to a different handler type. This is very useful to, for example, to stream the response body. The optional `terminate/3` callback will ultimately be called with the reason for the termination of the handler. Cowboy will terminate the process right after this. There is no need to perform any cleanup in this callback. The following terminate reasons are defined for loop handlers: normal:: The handler terminated normally. {crash, Class, Reason}:: A crash occurred in the handler. `Class` and `Reason` can be used to obtain more information about the crash. == REST callbacks === AcceptCallback [source,erlang] ---- AcceptCallback(Req, State) -> {Result, Req, State} Result :: true | {created, URI :: iodata()} | {see_other, URI :: iodata()} | false Default - crash ---- Process the request body. This function should create or update the resource using the request body. For PUT requests, the body is a representation of the resource that is being created or replaced. For POST requests, the body is typically application-specific instructions on how to process the request, but it may also be a representation of the resource. When creating a new resource with POST at a different location, return `{created, URI}` or `{see_other, URI}` with `URI` the new location. The `see_other` tuple will redirect the client to the new location automatically. For PATCH requests, the body is a series of instructions on how to update the resource. Patch files or JSON Patch are examples of such media types. A response body may be sent. The appropriate media type, charset and language for the response can be retrieved from the Req object using the `media_type`, `charset` and `language` keys, respectively. The body can be set using link:man:cowboy_req:set_resp_body(3)[cowboy_req:set_resp_body(3)]. === allowed_methods [source,erlang] ---- allowed_methods(Req, State) -> {Result, Req, State} Result :: [binary()] %% case sensitive Default :: [<<"GET">>, <<"HEAD">>, <<"OPTIONS">>] ---- Return the list of allowed methods. === allow_missing_post [source,erlang] ---- allow_missing_post(Req, State) -> {Result, Req, State} Result :: boolean() Default :: true ---- Return whether POST is allowed when the resource doesn't exist. Returning `true` here means that a new resource will be created. The URI for the newly created resource should be returned from the `AcceptCallback` function. === charsets_provided [source,erlang] ---- charsets_provided(Req, State) -> {Result, Req, State} Result :: [binary()] %% lowercase; case insensitive Default - skip this step ---- Return the list of charsets the resource provides in order of preference. During content negotiation Cowboy will pick the most appropriate charset for the client. The client advertises charsets it prefers with the accept-charset header. When that header is missing, Cowboy picks the first charset from the resource. // @todo We should explain precisely how charsets are picked. Cowboy will add the negotiated `charset` to the Req object after this step completes: [source,erlang] ---- req() :: #{ charset => binary() %% lowercase; case insensitive } ---- Note that Cowboy will only append the charset to the content-type header of the response if the media type is text. === content_types_accepted [source,erlang] ---- content_types_accepted(Req, State) -> {Result, Req, State} Result :: [{'*' | binary() | ParsedMime, AcceptCallback :: atom()}] ParsedMime :: {Type :: binary(), SubType :: binary(), '*' | Params} Params :: [{Key :: binary(), Value :: binary()}] Default - crash ---- // @todo Case sensitivity of parsed mime content? Return the list of media types the resource accepts in order of preference. A media type is made of different parts. The media type `text/html;charset=utf-8` is of type `text`, subtype `html` and has a single parameter `charset` with value `utf-8`. The special value `'*'` can be used to accept any media type. // @todo Cowboy needs to ignore the boundary parameter for // multipart, as we never want to match against it. Or allow // ignoring specific parameters at the very least. Cowboy will match the content-type request header against the media types the server accepts and select the appropriate callback. When that header is missing, or when the server does not accept this media type, the request fails and an error response is returned. Cowboy will execute the callback immediately otherwise. // @todo We should explain precisely how media types are picked. An empty parameters list `[]` means that no parameters will be accepted. When any parameter is acceptable, the tuple form should be used with parameters as the atom `'*'`. Cowboy treats all parameters as case sensitive, except for the `charset` parameter, which is known to be case insensitive. You should therefore always provide the charset as a lowercase binary string. // @todo Maybe this should be in the user guide instead. //This function will be called for POST, PUT and PATCH requests. //It is entirely possible to define different callbacks for different //methods if the handling of the request differs. Simply verify //what the method is with `cowboy_req:method/1` and return a //different list for each methods. === content_types_provided [source,erlang] ---- content_types_provided(Req, State) -> {Result, Req, State} Result :: [{binary() | ParsedMime, ProvideCallback :: atom()}] ParsedMime :: {Type :: binary(), SubType :: binary(), '*' | Params} Params :: [{Key :: binary(), Value :: binary()}] Default - [{{ <<"text">>, <<"html">>, '*'}, to_html}] ---- // @todo Case sensitivity of parsed mime content? // @todo Space required for the time being: https://github.com/spf13/hugo/issues/2398 Return the list of media types the resource provides in order of preference. A media type is made of different parts. The media type `text/html;charset=utf-8` is of type `text`, subtype `html` and has a single parameter `charset` with value `utf-8`. // @todo Cowboy needs to ignore the boundary parameter for // multipart, as we never want to match against it. Or allow // ignoring specific parameters at the very least. During content negotiation Cowboy will pick the most appropriate media type for the client. The client advertises media types it prefers with the accept header. When that header is missing, the content negotiation fails and an error response is returned. The callback given for the selected media type will be called at the end of the execution of GET and HEAD requests when a representation must be sent to the client. // @todo We should explain precisely how media types are picked. An empty parameters list `[]` means that no parameters will be accepted. When any parameter is acceptable, the tuple form should be used with parameters as the atom `'*'`. Cowboy treats all parameters as case sensitive, except for the `charset` parameter, which is known to be case insensitive. You should therefore always provide the charset as a lowercase binary string. When a charset is given in the media type parameters in the accept header, Cowboy will do some additional checks to confirm that it can use this charset. When the wildcard is used then Cowboy will immediately call `charsets_provided` to confirm the charset is acceptable. If the callback is undefined Cowboy assumes any charset is acceptable. When the wildcard is not used and the charset given in the accept header matches one of the configured media types Cowboy will use that charset and skip the `charsets_provided` step entirely. Cowboy will add the negotiated `media_type` to the Req object after this step completes: [source,erlang] ---- req() :: #{ media_type => ParsedMime } ---- // @todo Case sensitivity of parsed mime content? Cowboy may also add the negotiated `charset` to the Req object after this step completes: [source,erlang] ---- req() :: #{ charset => binary() %% lowercase; case insensitive } ---- === delete_completed [source,erlang] ---- delete_completed(Req, State) -> {Result, Req, State} Result :: boolean() Default :: true ---- Return whether the resource has been fully deleted from the system, including from any internal cache. Returning `false` will result in a '202 Accepted' response being sent instead of a '200 OK' or '204 No Content'. === delete_resource [source,erlang] ---- delete_resource(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Delete the resource. Cowboy will send an error response when this function returns `false`. === expires [source,erlang] ---- expires(Req, State) -> {Result, Req, State} Result :: calendar:datetime() | binary() | undefined Default :: undefined ---- Return the resource's expiration date. === forbidden [source,erlang] ---- forbidden(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Return whether access to the resource is forbidden. A '403 Forbidden' response will be sent if this function returns `true`. This status code means that access is forbidden regardless of authentication, and that the request shouldn't be repeated. === generate_etag [source,erlang] ---- generate_etag(Req, State) -> {Result, Req, State} Result :: binary() | {weak | strong, binary()} | undefined Default - no etag value ---- Return the entity tag of the resource. When a binary is returned, the value is automatically parsed to a tuple. The binary must be in the same format as the etag header, including quotes. It is possible to conditionally generate an etag. When no etag can be generated, `undefined` should be returned. === is_authorized [source,erlang] ---- is_authorized(Req, State) -> {Result, Req, State} Result :: true | {false, AuthHeader :: iodata()} Default - true ---- Return whether the user is authorized to perform the action. This function should be used to perform any necessary authentication of the user before attempting to perform any action on the resource. When authentication fails, the `AuthHeader` value will be sent in the www-authenticate header for the '401 Unauthorized' response. === is_conflict [source,erlang] ---- is_conflict(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Return whether the PUT request results in a conflict. A '409 Conflict' response is sent when `true`. === known_methods [source,erlang] ---- known_methods(Req, State) -> {Result, Req, State} Result :: [binary()] %% case sensitive Default :: [<<"GET">>, <<"HEAD">>, <<"POST">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>] ---- Return the list of known methods. The full list of methods known by the server should be returned, regardless of their use in the resource. The default value lists the methods Cowboy knows and implement in `cowboy_rest`. === languages_provided [source,erlang] ---- languages_provided(Req, State) -> {Result, Req, State} Result :: [binary()] %% lowercase; case insensitive Default - skip this step ---- Return the list of languages the resource provides in order of preference. During content negotiation Cowboy will pick the most appropriate language for the client. The client advertises languages it prefers with the accept-language header. When that header is missing, Cowboy picks the first language from the resource. // @todo We should explain precisely how languages are picked. Cowboy will add the negotiated `language` to the Req object after this step completes: [source,erlang] ---- req() :: #{ language => binary() %% lowercase; case insensitive } ---- === last_modified [source,erlang] ---- last_modified(Req, State) -> {Result, Req, State} Result :: calendar:datetime() | undefined Default - no last modified value ---- Return the resource's last modification date. This date will be used to test against the if-modified-since and if-unmodified-since headers, and sent as the last-modified header in the response to GET and HEAD requests. When `undefined` is returned, no last-modified header is added to response. Can be useful if you save timestamp on store action in memory and lose it after restart. === malformed_request [source,erlang] ---- malformed_request(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Return whether the request is malformed. A request is malformed when a component required by the resource is invalid. This may include the query string or individual headers. They should be parsed and validated in this function. The body should not be read at this point. === moved_permanently [source,erlang] ---- moved_permanently(Req, State) -> {Result, Req, State} Result :: {true, URI :: iodata()} | false Default :: false ---- Return whether the resource was permanently moved, and what its new location is. === moved_temporarily [source,erlang] ---- moved_temporarily(Req, State) -> {Result, Req, State} Result :: {true, URI :: iodata()} | false Default :: false ---- Return whether the resource was temporarily moved, and what its new location is. === multiple_choices [source,erlang] ---- multiple_choices(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Return whether the client should engage in reactive negotiation. Return `true` when the server has multiple representations of a resource, each with their specific identifier, but is unable to determine which is best for the client. For example an image might have different sizes and the server is unable to determine the capabilities of the client. When returning `true` the server should send a body with links to the different representations. If the server has a preferred representation it can send its link inside a location header. Note that when replying manually in this callback you should either call `cowboy_req:reply/4` or remove the response body that Cowboy sets to avoid surprises. === options [source,erlang] ---- options(Req, State) -> {ok, Req, State} ---- Respond to an OPTIONS request. The response should inform the client the communication options available for this resource. By default Cowboy will send a '200 OK' response with the allow header set. === previously_existed [source,erlang] ---- previously_existed(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Return whether the resource existed previously. === ProvideCallback [source,erlang] ---- ProvideCallback(Req, State) -> {Result, Req, State} Result :: cowboy_req:resp_body() Default - crash ---- Return the response body. The response body can be provided either as the actual data to be sent or a tuple indicating which file to send. This function is called for both GET and HEAD requests. For the latter the body is not sent: it is only used to calculate the content length. // @todo Perhaps we can optimize HEAD requests and just // allow calculating the length instead of returning the // whole thing. It is possible to stream the response body either by manually sending the response and returning a `stop` value; or by switching to a different handler (for example a loop handler) and manually sending the response. All headers already set by Cowboy will also be included in the response. == RangeCallback [source,erlang] ---- RangeCallback(Req, State) -> {Result, Req, State} Result :: [{Range, Body}] Range :: {From, To, Total} | binary() From :: non_neg_integer() To :: non_neg_integer() Total :: non_neg_integer() | '*' Body :: cowboy_req:resp_body() Default - crash ---- Return a list of ranges for the response body. The range selected can be found in the key `range` in the Req object, as indicated in `range_satisfiable`. Instead of returning the full response body as would be done in the `ProvideCallback`, a list of ranges must be returned. There can be one or more range. When one range is returned, a normal ranged response is sent. When multiple ranges are returned, Cowboy will automatically send a multipart/byteranges response. When the total is not known the atom `'*'` can be returned. == ranges_provided [source,erlang] ---- ranges_provided(Req, State) -> {Result, Req, State} Result :: [Range | Auto] Range :: { binary(), %% lowercase; case insensitive RangeCallback :: atom() } Auto :: {<<"bytes">>, auto} Default - skip this step ---- Return the list of range units the resource provides. During content negotiation Cowboy will build an accept-ranges response header with the list of ranges provided. Cowboy does not choose a range at this time; ranges are choosen when it comes time to call the `ProvideCallback`. By default ranged requests will be handled the same as normal requests: the `ProvideCallback` will be called and the full response body will be sent. It is possible to let Cowboy handle ranged responses automatically when the range unit is bytes and the atom returned is `auto` (instead of a callback name). In that case Cowboy will call the `ProvideCallback` and split the response automatically, including by producing a multipart/byteranges response if necessary. == range_satisfiable [source,erlang] ---- range_satisfiable(Req, State) -> {Result, Req, State} Result :: boolean() | {false, non_neg_integer() | iodata()} Default :: true ---- Whether the range request is satisfiable. When the time comes to send the response body, and when ranges have been provided via the `ranges_provided` callback, Cowboy will process the if-range and the range request headers and ensure it is satisfiable. This callback allows making resource-specific checks before sending the ranged response. The default is to accept sending a ranged response. Cowboy adds the requested `range` to the Req object just before calling this callback: [source,erlang] ---- req() :: #{ range => { binary(), %% lowercase; case insensitive Range } } Range :: ByteRange | binary() ByteRange :: [{FirstByte, LastByte | infinity} | SuffixLen] FirstByte :: non_neg_integer() LastByte :: non_neg_integer() SuffixLen :: neg_integer() ---- Only byte ranges are parsed. Other ranges are provided as binary. Byte ranges may either be requested from first to last bytes (inclusive); from first bytes to the end (`infinity` is used to represent the last byte); or the last bytes of the representation via a negative integer (so -500 means the last 500 bytes). Returning `false` will result in a 416 Range Not Satisfiable response being sent. The content-range header will be set automatically in the response if a tuple is returned. The integer value represents the total size (in the choosen unit) of the resource. An iodata value may also be returned and will be used as-is to build the content range header, prepended with the unit choosen. === rate_limited [source,erlang] ---- rate_limited(Req, State) -> {Result, Req, State} Result :: false | {true, RetryAfter} RetryAfter :: non_neg_integer() | calendar:datetime() Default :: false ---- Return whether the user is rate limited. This function can be used to temporarily restrict access to a resource when the user has issued too many requests. When the resource is rate limited the `RetryAfter` value will be sent in the retry-after header for the '429 Too Many Requests' response. It indicates when the resource will become available again and can be specified as a number of seconds in the future or a specific date/time. === resource_exists [source,erlang] ---- resource_exists(Req, State) -> {Result, Req, State} Result :: boolean() Default :: true ---- Return whether the resource exists. === service_available [source,erlang] ---- service_available(Req, State) -> {Result, Req, State} Result :: boolean() Default :: true ---- Return whether the service is available. A '503 Service Unavailable' response will be sent when this function returns `false`. === uri_too_long [source,erlang] ---- uri_too_long(Req, State) -> {Result, Req, State} Result :: boolean() Default :: false ---- Return whether the requested URI is too long. This function can be used to further restrict the length of the URI for this specific resource. === valid_content_headers [source,erlang] ---- valid_content_headers(Req, State) -> {Result, Req, State} Result :: boolean() Default :: true ---- Return whether the content headers are valid. This callback can be used to reject requests that have invalid content header values, for example an unsupported content-encoding. === valid_entity_length [source,erlang] ---- valid_entity_length(Req, State) -> {Result, Req, State} Result :: boolean() Default :: true ---- Return whether the request body length is within acceptable boundaries. A '413 Request Entity Too Large' response will be sent if this function returns `false`. === variances [source,erlang] ---- variances(Req, State) -> {Result, Req, State} Result :: [binary()] %% case insensitive Default :: [] ---- Return the list of request headers that affect the representation of the resource. Cowboy automatically adds the accept, accept-charset and accept-language headers when necessary. It's also useful to note that some standard headers also do not need to be listed here, like the authorization header. == Changelog * *2.14*: The `last_modified` callback is now type correct when returning `undefined` to avoid responding a last-modified header. * *2.11*: The `ranges_provided`, `range_satisfiable` and the `RangeCallback` callbacks have been added. * *2.11*: The `generate_etag` callback can now return `undefined` to conditionally avoid generating an etag. * *2.9*: An `AcceptCallback` can now return `{created, URI}` or `{see_other, URI}`. The return value `{true, URI}` is deprecated. * *2.7*: The media type wildcard in `content_types_accepted` is now documented. * *2.6*: The callback `rate_limited` was added. * *2.1*: The `switch_handler` return value was added. * *1.0*: Behavior introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_handler(3)[cowboy_handler(3)] ================================================ FILE: doc/src/manual/cowboy_router.asciidoc ================================================ = cowboy_router(3) == Name cowboy_router - Router middleware == Description The `cowboy_router` middleware maps the requested host and path to the handler to be used for processing the request. The router takes the `dispatch` rules as input from the middleware environment. Dispatch rules are generated by calling the link:man:cowboy_router:compile(3)[cowboy_router:compile(3)] function. The environment can contain the rules directly or a tuple `{persistent_term, Key}`, in which case Cowboy will call `persistent_term:get(Key)` to retrieve the dispatch rules. When a route matches, the router sets the `handler` and `handler_opts` middleware environment values containing the handler module and initial state, respectively. The router will stop execution when no route matches. It will send a 400 response if no host was found, and a 404 response otherwise. == Exports * link:man:cowboy_router:compile(3)[cowboy_router:compile(3)] - Compile routes to the resources == Types === bindings() [source,erlang] ---- bindings() :: #{atom() => any()} ---- Bindings found during routing. === dispatch_rules() Opaque type containing the compiled routes. === routes() [source,erlang] ---- routes() = [ {Host, PathList} | {Host, Fields, PathList} ] PathList :: [ {Path, Handler, InitialState} | {Path, Fields, Handler, InitialState} ] Host :: '_' | iodata() Path :: '_' | iodata() Fields :: cowboy:fields() Handler :: module() InitialState :: any() ---- Human readable list of routes to handlers. Cowboy uses this list to map hosts and paths, optionally augmented with constraints applied to the bindings, to handler modules. The syntax for routes is currently defined in the user guide. // @todo The syntax should probably be in this module, // and the user guide show more practical examples. === tokens() [source,erlang] ---- tokens() :: [binary()] ---- List of `host_info` and `path_info` tokens that were found using the `...` syntax. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_req:binding(3)[cowboy_req:binding(3)], link:man:cowboy_req:bindings(3)[cowboy_req:bindings(3)], link:man:cowboy_req:host_info(3)[cowboy_req:host_info(3)], link:man:cowboy_req:path_info(3)[cowboy_req:path_info(3)] ================================================ FILE: doc/src/manual/cowboy_router.compile.asciidoc ================================================ = cowboy_router:compile(3) == Name cowboy_router:compile - Compile routes to the resources == Description [source,erlang] ---- compile(cowboy_router:routes()) -> cowboy_router:dispatch_rules() ---- Compile routes to the resources. Takes a human readable list of routes and transforms it into a form more efficient to process. == Arguments Routes:: Human readable list of routes. == Return value An opaque dispatch rules value is returned. This value must be given to Cowboy as a middleware environment value. == Changelog * *1.0*: Function introduced. == Examples .Compile routes and start a listener [source,erlang] ---- Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []}, {"/[...]", cowboy_static, {priv_dir, my_example_app, ""}} ]} ]), {ok, _} = cowboy:start_clear(example, [{port, 8080}], #{ env => #{dispatch => Dispatch} }). ---- == See also link:man:cowboy_router(3)[cowboy_router(3)] ================================================ FILE: doc/src/manual/cowboy_static.asciidoc ================================================ = cowboy_static(3) == Name cowboy_static - Static file handler == Description The module `cowboy_static` implements file serving capabilities using the REST semantics provided by `cowboy_rest`. The static file handler is a pre-written handler coming with Cowboy. To serve files, use it in your routes. == Options [source,erlang] ---- opts() :: {priv_file, App, Path} | {priv_file, App, Path, Extra} | {file, Path} | {file, Path, Extra} | {priv_dir, App, Path} | {priv_dir, App, Path, Extra} | {dir, Path} | {dir, Path, Extra} App :: atom() Path :: binary() | string() Extra :: [Charset | Etag | Mimetypes] Charset :: {charset, module(), function()} | {charset, binary()} Etag :: {etag, module(), function()} | {etag, false} Mimetypes :: {mimetypes, module(), function()} | {mimetypes, binary() | ParsedMime} ParsedMime :: {Type :: binary(), SubType :: binary(), Params} Params :: [{Key :: binary(), Value :: binary()}] ---- Static handler configuration. priv_file:: Send a file. + The path is relative to the given application's private directory. file:: Send a file. + The path is either absolute or relative to the Erlang node's current directory. priv_dir:: Recursively serve files from a directory. + The path is relative to the given application's private directory. dir:: Recursively serve files from a directory. + The path is either absolute or relative to the Erlang node's current directory. The extra options allow you to define how the etag should be calculated and how the MIME type of files should be detected. By default the static handler will not send a charset with the response. You can provide a specific charset that will be used for all files using the text media type, or provide a module and function that will be called when needed: [source,erlang] ---- detect_charset(Path :: binary()) -> Charset :: binary() ---- A charset must always be returned even if it doesn't make sense considering the media type of the file. A good default is `<<"utf-8">>`. By default the static handler will generate an etag based on the size and modification time of the file. You may disable the etag entirely with `{etag, false}` or provide a module and function that will be called when needed: [source,erlang] ---- generate_etag(Path, Size, Mtime) -> {strong | weak, binary()} Path :: binary() Size :: non_neg_integer() Mtime :: file:date_time() ---- By default the static handler will detect Web-related MIME types by looking at the file extension. You can provide a specific MIME type that will always be used, or a module and function that will be called when needed: [source,erlang] ---- detect_mimetype(Path) -> ParsedMime Path :: binary() ParsedMime :: {Type :: binary(), SubType :: binary(), Params} Params :: [{Key :: binary(), Value :: binary()}] ---- // @todo Case sensitivity of parsed mime content? Cowboy comes with two such functions; the default function `cow_mimetypes:web/1`, and a second function generated from the Apache 'mime.types' file, `cow_mimetypes:all/1`. The MIME type function should return `{<<"application">>, <<"octet-stream">>, []}` when it fails to detect a file's MIME type. == Changelog * *2.11*: Support for range requests was added in 2.6 and is now considered stable. * *2.6*: The `charset` extra option was added. * *1.0*: Handler introduced. == Examples .Custom etag function [source,erlang] ---- generate_etag(Path, Size, Mtime) -> {strong, integer_to_binary( erlang:phash2({Path, Size, Mtime}, 16#ffffffff))}. ---- .Custom MIME type function [source,erlang] ---- always_octet_stream(_Path) -> case filename:extension(Path) of <<".erl">> -> {<<"text">>, <<"plain">>, []}; _ -> {<<"application">>, <<"octet-stream">>, []} end. ---- == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_router(3)[cowboy_router(3)] ================================================ FILE: doc/src/manual/cowboy_stream.asciidoc ================================================ = cowboy_stream(3) == Name cowboy_stream - Stream handlers == Description The module `cowboy_stream` defines a callback interface and a protocol for handling HTTP streams. An HTTP request and its associated response is called a stream. A connection may have many streams. In HTTP/1.1 they are executed sequentially, while in HTTP/2 they are executed concurrently. Cowboy calls the stream handler for nearly all events related to a stream. Exceptions vary depending on the protocol. Extra care must be taken when implementing stream handlers to ensure compatibility. While some modification of the events and commands is allowed, it is generally not a good idea to completely discard them. == Callbacks Stream handlers must implement the following interface: [source,erlang] ---- init(StreamID, Req, Opts) -> {Commands, State} data(StreamID, IsFin, Data, State) -> {Commands, State} info(StreamID, Info, State) -> {Commands, State} terminate(StreamID, Reason, State) -> any() early_error(StreamID, Reason, PartialReq, Resp, Opts) -> Resp StreamID :: cowboy_stream:streamid() Req :: cowboy_req:req() Opts :: cowboy:opts() Commands :: cowboy_stream:commands() State :: any() IsFin :: cowboy_stream:fin() Data :: binary() Info :: any() Reason :: cowboy_stream:reason() PartialReq - cowboy_req:req(), except all fields are optional Resp :: cowboy_stream:resp_command() ---- HTTP/1.1 will initialize a stream only when the request-line and all headers have been received. When errors occur before that point Cowboy will call the callback `early_error/5` with a partial request, the error reason and the response Cowboy intends to send. All other events go through the stream handler using the normal callbacks. HTTP/2 will initialize the stream when the `HEADERS` block has been fully received and decoded. Any protocol error occurring before that will not result in a response being sent and will therefore not go through the stream handler. In addition Cowboy may terminate streams without sending an HTTP response back. The stream is initialized by calling `init/3`. All streams that are initialized will eventually be terminated by calling `terminate/3`. When Cowboy receives data for the stream it will call `data/4`. The data given is the request body after any transfer decoding has been applied. When Cowboy receives a message addressed to a stream, or when Cowboy needs to inform the stream handler that an internal event has occurred, it will call `info/3`. [[commands]] == Commands Stream handlers can return a list of commands to be executed from the `init/3`, `data/4` and `info/3` callbacks. In addition, the `early_error/5` callback must return a response command. // @todo The logger option and the {log, Level, Format, Args} // options need to be documented and tested. The order in which the commands are given matters. For example, when sending a response and at the same time creating a new child process, the first command should be the `spawn` and the second the `response`. The reason for that is that the sending of the response may result in a socket error which leads to the termination of the connection before the rest of the commands are executed. The following commands are defined: [[inform_command]] === inform Send an informational response to the client. [source,erlang] ---- {inform, cowboy:http_status(), cowboy:http_headers()} ---- Any number of informational responses may be sent, but only until the final response is sent. [[response_command]] === response Send a response to the client. [source,erlang] ---- {response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()} ---- No more data can be sent after this command. Note that in Cowboy it is the `cowboy_req` module that sets the date and server headers. When using the command directly those headers will not be added. [[headers_command]] === headers Initiate a response to the client. [source,erlang] ---- {headers, cowboy:http_status(), cowboy:http_headers()} ---- This initiates a response to the client. The stream will end when a data command with the `fin` flag or a trailer command is returned. Note that in Cowboy it is the `cowboy_req` module that sets the date and server headers. When using the command directly those headers will not be added. [[data_command]] === data Send data to the client. [source,erlang] ---- {data, fin(), cowboy_req:resp_body()} ---- [[trailers_command]] === trailers Send response trailers to the client. [source,erlang] ---- {trailers, cowboy:http_headers()} ---- [[push_command]] === push Push a resource to the client. [source,erlang] ---- {push, Method, Scheme, Host, inet:port_number(), Path, Qs, cowboy:http_headers()} Method = Scheme = Host = Path = Qs = binary() ---- The command will be ignored if the protocol does not provide any server push mechanism. === flow [source,erlang] ---- {flow, pos_integer()} ---- Request more data to be read from the request body. The exact behavior depends on the protocol. === spawn Inform Cowboy that a process was spawned and should be supervised. [source,erlang] ---- {spawn, pid(), timeout()} ---- === error_response Send an error response if no response was sent previously. [source,erlang] ---- {error_response, cowboy:http_status(), cowboy:http_headers(), iodata()} ---- [[switch_protocol_command]] === switch_protocol Switch to a different protocol. [source,erlang] ---- {switch_protocol, cowboy:http_headers(), module(), state()} ---- Contains the headers that will be sent in the 101 response, along with the module implementing the protocol we are switching to and its initial state. Note that the 101 informational response will not be sent after a final response. === stop Stop the stream. [source,erlang] ---- stop ---- While no more data can be sent after the `fin` flag was set, the stream is still tracked by Cowboy until it is stopped by the handler. The behavior when stopping a stream for which no response has been sent will vary depending on the protocol. The stream will end successfully as far as the client is concerned. To indicate that an error occurred, either use `error_response` before stopping, or use `internal_error`. No other command can be executed after the `stop` command. === internal_error Stop the stream with an error. [source,erlang] ---- {internal_error, Reason, HumanReadable} Reason = any() HumanReadable = atom() ---- This command should be used when the stream cannot continue because of an internal error. An `error_response` command may be sent before that to advertise to the client why the stream is dropped. === log Log a message. [source,erlang] ---- {log, logger:level(), io:format(), list()} ---- This command can be used to log a message using the configured `logger` module. === set_options Set protocol options. [source,erlang] ---- {set_options, map()} ---- This can also be used to override stream handler options. For example this is supported by link:man:cowboy_compress_h(3)[cowboy_compress_h(3)]. Not all options can be overridden. Please consult the relevant option's documentation for details. == Predefined events Cowboy will forward all messages sent to the stream to the `info/3` callback. To send a message to a stream, the function link:man:cowboy_req:cast(3)[cowboy_req:cast(3)] can be used. Cowboy will also forward the exit signals for the processes that the stream spawned. When Cowboy needs to send a response it will trigger an event that looks exactly like the corresponding command. This event must be returned to be processed by Cowboy (which is done automatically when using link:man:cowboy_stream_h(3)[cowboy_stream_h(3)]). Cowboy may trigger the following events on its own, regardless of the stream handlers configured: xref:inform_command[inform] (to send a 101 informational response when upgrading to HTTP/2 or Websocket), xref:response_command[response], xref:headers_command[headers], xref:data_command[data] and xref:switch_protocol_command[switch_protocol]. == Exports The following function should be called by modules implementing stream handlers to execute the next stream handler in the list: * link:man:cowboy_stream:init(3)[cowboy_stream:init(3)] - Initialize a stream * link:man:cowboy_stream:data(3)[cowboy_stream:data(3)] - Handle data for a stream * link:man:cowboy_stream:info(3)[cowboy_stream:info(3)] - Handle a message for a stream * link:man:cowboy_stream:terminate(3)[cowboy_stream:terminate(3)] - Terminate a stream * link:man:cowboy_stream:early_error(3)[cowboy_stream:early_error(3)] - Handle an early error for a stream == Types === commands() [source,erlang] ---- commands() :: [Command] ---- See the xref:commands[list of commands] for details. === fin() [source,erlang] ---- fin() :: fin | nofin ---- Used in commands and events to indicate that this is the end of a direction of a stream. === partial_req() [source,erlang] ---- req() :: #{ method => binary(), %% case sensitive version => cowboy:http_version() | atom(), scheme => binary(), %% lowercase; case insensitive host => binary(), %% lowercase; case insensitive port => inet:port_number(), path => binary(), %% case sensitive qs => binary(), %% case sensitive headers => cowboy:http_headers(), peer => {inet:ip_address(), inet:port_number()} } ---- Partial request information received when an early error is detected. === reason() [source,erlang] ---- reason() :: normal | switch_protocol | {internal_error, timeout | {error | exit | throw, any()}, HumanReadable} | {socket_error, closed | atom(), HumanReadable} | {stream_error, Error, HumanReadable} | {connection_error, Error, HumanReadable} | {stop, cow_http2:frame() | {exit, any()}, HumanReadable} Error = atom() HumanReadable = atom() ---- Reason for the stream termination. === resp_command() [source,erlang] ---- resp_command() :: {response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()} ---- See the xref:response_command[response command] for details. === streamid() [source,erlang] ---- streamid() :: any() ---- The identifier for this stream. The identifier is unique over the connection process. It is possible to form a unique identifier node-wide and cluster-wide by wrapping it in a `{self(), StreamID}` tuple. == Changelog * *2.7*: The `log` and `set_options` commands were introduced. * *2.6*: The `data` command can now contain a sendfile tuple. * *2.6*: The `{stop, {exit, any()}, HumanReadable}` terminate reason was added. * *2.2*: The `trailers` command was introduced. * *2.0*: Module introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_http(3)[cowboy_http(3)], link:man:cowboy_http2(3)[cowboy_http2(3)], link:man:cowboy_req:cast(3)[cowboy_req:cast(3)] ================================================ FILE: doc/src/manual/cowboy_stream.data.asciidoc ================================================ = cowboy_stream:data(3) == Name cowboy_stream:data - Handle data for a stream == Description [source,erlang] ---- data(StreamID, IsFin, Data, State) -> {Commands, State} StreamID :: cowboy_stream:stream_id() IsFin :: cowboy_stream:fin() Data :: binary() Commands :: cowboy_stream:commands() State - opaque ---- Handle data for a stream. This function should be called by all stream handlers. It will propagate data to the next configured stream handler. Handlers do not have to propagate data that has been fully handled. == Arguments StreamID:: The stream ID. IsFin:: Whether this is the end of the request body. Data:: The data received. Commands:: The commands to be executed. State:: The state for the next stream handler. == Return value A list of commands and an opaque state is returned. The list of commands returned should be included in the commands returned from the current stream handler. It can be modified if necessary. The state should be stored in the current stream handler's state and passed to `cowboy_stream` when necessary. The state should be treated as opaque. == Changelog * *2.0*: Function introduced. == Examples .Propagate data to the next stream handler [source,erlang] ---- data(StreamID, IsFin, Data, State=#state{next=Next0}) -> MyCommands = my_commands(), {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), {MyCommands ++ Commands, #state{next=Next}}. ---- == See also link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_stream:init(3)[cowboy_stream:init(3)], link:man:cowboy_stream:info(3)[cowboy_stream:info(3)], link:man:cowboy_stream:terminate(3)[cowboy_stream:terminate(3)], link:man:cowboy_stream:early_error(3)[cowboy_stream:early_error(3)] ================================================ FILE: doc/src/manual/cowboy_stream.early_error.asciidoc ================================================ = cowboy_stream:early_error(3) == Name cowboy_stream:early_error - Handle an early error for a stream == Description [source,erlang] ---- early_error(StreamID, Reason, PartialReq, Resp, Opts) -> Resp StreamID :: cowboy_stream:stream_id() Reason :: cowboy_stream:reason() PartialReq :: cowboy_stream:partial_req() Resp :: cowboy_stream:resp_command() Opts :: cowboy:opts() ---- Handle an early error for a stream. This function should be called by all stream handlers. It will propagate the early error to the next configured stream handler. == Arguments StreamID:: The stream ID. Reason:: Reason for termination. PartialReq:: The request data that has been received so far. Resp:: The response that will be sent as a result of the early error. + It may be modified by the stream handler before or after being propagated to the next handler. Opts:: The protocol options. == Return value The response to be sent as a result of the early error. == Changelog * *2.0*: Function introduced. == Examples .Propagate the early error to the next stream handler [source,erlang] ---- early_error(StreamID, Reason, PartialReq, Resp, Opts) -> cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). ---- == See also link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_stream:init(3)[cowboy_stream:init(3)], link:man:cowboy_stream:data(3)[cowboy_stream:data(3)], link:man:cowboy_stream:info(3)[cowboy_stream:info(3)], link:man:cowboy_stream:terminate(3)[cowboy_stream:terminate(3)] ================================================ FILE: doc/src/manual/cowboy_stream.info.asciidoc ================================================ = cowboy_stream:info(3) == Name cowboy_stream:info - Handle a message for a stream == Description [source,erlang] ---- info(StreamID, Info, State) -> {Commands, State} StreamID :: cowboy_stream:stream_id() Info :: any() Commands :: cowboy_stream:commands() State - opaque ---- Handle a message for a stream. This function should be called by all stream handlers. It will propagate the event to the next configured stream handler. Handlers do not have to propagate events that have been fully handled. == Arguments StreamID:: The stream ID. Info:: The event received. Commands:: The commands to be executed. State:: The state for the next stream handler. == Return value A list of commands and an opaque state is returned. The list of commands returned should be included in the commands returned from the current stream handler. It can be modified if necessary. The state should be stored in the current stream handler's state and passed to `cowboy_stream` when necessary. The state should be treated as opaque. == Changelog * *2.0*: Function introduced. == Examples .Propagate an event to the next stream handler [source,erlang] ---- info(StreamID, Info, State=#state{next=Next0}) -> MyCommands = my_commands(), {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), {MyCommands ++ Commands, #state{next=Next}}. ---- == See also link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_stream:init(3)[cowboy_stream:init(3)], link:man:cowboy_stream:data(3)[cowboy_stream:data(3)], link:man:cowboy_stream:terminate(3)[cowboy_stream:terminate(3)], link:man:cowboy_stream:early_error(3)[cowboy_stream:early_error(3)] ================================================ FILE: doc/src/manual/cowboy_stream.init.asciidoc ================================================ = cowboy_stream:init(3) == Name cowboy_stream:init - Initialize a stream == Description [source,erlang] ---- init(StreamID, Req, Opts) -> {Commands, State} StreamID :: cowboy_stream:stream_id() Req :: cowboy_req:req() Opts :: cowboy:opts() Commands :: cowboy_stream:commands() State - opaque ---- Initialize a stream. This function must be called by all stream handlers. It will initialize the next configured stream handler. == Arguments StreamID:: The stream ID. Req:: The Req object. Opts:: The protocol options. Commands:: The commands to be executed. State:: The state for the next stream handler. == Return value A list of commands and an opaque state is returned. The list of commands returned should be included in the commands returned from the current stream handler. It can be modified if necessary. The state should be stored in the current stream handler's state and passed to `cowboy_stream` when necessary. The state should be treated as opaque. == Changelog * *2.0*: Function introduced. == Examples .Initialize the next stream handler [source,erlang] ---- init(StreamID, Req, Opts) -> MyCommands = my_commands(), {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), {MyCommands ++ Commands, #state{next=Next}}. ---- == See also link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_stream:data(3)[cowboy_stream:data(3)], link:man:cowboy_stream:info(3)[cowboy_stream:info(3)], link:man:cowboy_stream:terminate(3)[cowboy_stream:terminate(3)], link:man:cowboy_stream:early_error(3)[cowboy_stream:early_error(3)] ================================================ FILE: doc/src/manual/cowboy_stream.terminate.asciidoc ================================================ = cowboy_stream:terminate(3) == Name cowboy_stream:terminate - Terminate a stream == Description [source,erlang] ---- terminate(StreamID, Reason, State) -> ok StreamID :: cowboy_stream:stream_id() Reason :: cowboy_stream:reason() State - opaque ---- Terminate a stream. This function must be called by all stream handlers. It will terminate the next configured stream handler. == Arguments StreamID:: The stream ID. Reason:: Reason for termination. State:: The state for the next stream handler. == Return value The atom `ok` is always returned. It can be safely ignored. == Changelog * *2.0*: Function introduced. == Examples .Terminate the next stream handler [source,erlang] ---- terminate(StreamID, Reason, State=#state{next=Next0}) -> my_termination(State), cowboy_stream:terminate(StreamID, Reason, Next0). ---- == See also link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_stream:init(3)[cowboy_stream:init(3)], link:man:cowboy_stream:data(3)[cowboy_stream:data(3)], link:man:cowboy_stream:info(3)[cowboy_stream:info(3)], link:man:cowboy_stream:early_error(3)[cowboy_stream:early_error(3)] ================================================ FILE: doc/src/manual/cowboy_stream_h.asciidoc ================================================ = cowboy_stream_h(3) == Name cowboy_stream_h - Default stream handler == Description The module `cowboy_stream_h` is Cowboy's default stream handler and defines much of its behavior. It is responsible for managing the request process, sending it the request body and translating its messages into commands that Cowboy understands. == Options [source,erlang] ---- opts() :: #{ env => cowboy_middleware:env(), middlewares => [module()], shutdown_timeout => timeout() } ---- Configuration for the default stream handler. The default value is given next to the option name: env (#{}):: Middleware environment. middlewares ([cowboy_router, cowboy_handler]):: Middlewares to run for every request. shutdown_timeout (5000):: Time in ms Cowboy will wait for child processes to shut down before killing them. == Events The default stream handler spawns the request process and receives its exit signal when it terminates. It will stop the stream once its receives it. Because this stream handler converts events from the request process into commands, other stream handlers may not work properly if they are executed after the default stream handler. Always be mindful of in which order stream handlers will get executed. === Request body The default stream handler implements the `read_body` mechanism. In addition to reading the body, the handler will automatically handle the `expect: 100-continue` header and send a 100 Continue response. Normally one would use link:man:cowboy_req:read_body(3)[cowboy_req:read_body(3)] to read the request body. The default stream handler will buffer data until the amount gets larger than the requested length before sending it. Alternatively, it will send whatever data it has when the period timeout triggers. Depending on the protocol, the flow control window is updated to allow receiving data for the requested length. The default stream handler also comes with an automatic mode for reading the request body. This can be used by sending the event message `{read_body, Pid, Ref, auto, infinity}` using link:man:cowboy_req:cast(3)[cowboy_req:cast(3)]. The default stream handler will then send data as soon as some becomes available using one of these two messages depending on whether body reading was completed: * `{request_body, Ref, nofin, Data}` * `{request_body, Ref, fin, BodyLen, Data}` Depending on the protocol, Cowboy will update the flow control window using the size of the data that was read. Auto mode automatically gets disabled after data has been sent to the handler. Therefore in order to continue reading data a `read_body` event message must be sent after each `request_body` message. === Response In addition it returns a command for any event message looking like one of the following commands: `inform`, `response`, `headers`, `data`, `trailers`, `push`, `switch_protocol`. This is what allows the request process to send a response. == Changelog * *2.11*: Introduce body reading using auto mode. * *2.0*: Module introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_compress_h(3)[cowboy_compress_h(3)], link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)], link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)], link:man:cowboy_tracer_h(3)[cowboy_tracer_h(3)], link:man:cowboy_req:cast(3)[cowboy_req:cast(3)] ================================================ FILE: doc/src/manual/cowboy_tracer_h.asciidoc ================================================ = cowboy_tracer_h(3) == Name cowboy_tracer_h - Tracer stream handler == Description The module `cowboy_tracer_h` can be used to conditionally trace streams based on information found in the request. Trace messages are given to the configured callback. == Options [source,erlang] ---- opts() :: #{ tracer_callback => Callback, tracer_flags => [atom()], tracer_match_specs => [MatchSpec] } Callback :: fun((init | terminate | tuple(), State) -> State) MatchSpec :: MatchPredicate | {method, binary()} | {host, binary()} | {path, binary()} | {path_start, binary()} | {header, binary()} | {header, binary(), binary()} | {peer_ip, inet:ip_address()} MatchPredicate :: fun((cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> boolean()) } ---- Configuration for the tracer stream handler. This module will not set trace patterns. Those must be set by the user directly, either from the callback's `init` or, preferably, in advance. tracer_callback:: The function that will be called for each trace events. It will also be called before any trace event with an argument `init`, and when the stream is terminated with an argument `terminate`. + This option is required for tracing to be enabled. The tracer stream handler does nothing otherwise. tracer_flags:: Trace flags to enable. See the documentation of `erlang:trace/3` for details. Note that all trace flags are allowed except for the `tracer` flag. tracer_match_specs:: A list of match conditions that must all be fulfilled for the stream to be traced. Cowboy will compare these with the information found in the request and only enable tracing if all matches succeed. + This option is required for tracing to be enabled. The tracer stream handler does nothing otherwise. == Events The tracer stream handler does not produce any event. == Changelog * *2.7*: Module introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_stream(3)[cowboy_stream(3)], link:man:cowboy_compress_h(3)[cowboy_compress_h(3)], link:man:cowboy_decompress_h(3)[cowboy_decompress_h(3)], link:man:cowboy_metrics_h(3)[cowboy_metrics_h(3)], link:man:cowboy_stream_h(3)[cowboy_stream_h(3)] ================================================ FILE: doc/src/manual/cowboy_websocket.asciidoc ================================================ = cowboy_websocket(3) == Name cowboy_websocket - Websocket == Description The module `cowboy_websocket` implements Websocket as a Ranch protocol. It also defines a callback interface for handling Websocket connections. == Callbacks Websocket handlers must implement the following callback interface: [source,erlang] ---- init(Req, State) -> {cowboy_websocket, Req, State} | {cowboy_websocket, Req, State, Opts} websocket_init(State) -> CallResult %% optional websocket_handle(InFrame, State) -> CallResult websocket_info(Info, State) -> CallResult terminate(Reason, PartialReq, State) -> ok %% optional Req :: cowboy_req:req() PartialReq :: map() State :: any() Opts :: cowboy_websocket:opts() InFrame :: ping | pong | {text | binary | ping | pong, binary()} Info :: any() CallResult :: {commands(), State} | {commands(), State, hibernate} | Deprecated Deprecated :: {ok, State} | {ok, State, hibernate} | {reply, OutFrame | [OutFrame], State} | {reply, OutFrame | [OutFrame], State, hibernate} | {stop, State} OutFrame :: cow_ws:frame() %% see types below Reason :: normal | stop | timeout | remote | {remote, cow_ws:close_code(), binary()} | {error, badencoding | badframe | closed | atom()} | {crash, error | exit | throw, any()} ---- The `init/2` callback is common to all handlers. To upgrade the connection to Websocket, it must return `cowboy_websocket` as the first element of the tuple. Any operation requiring the HTTP request must be done in the `init/2` function, as the Req object will not be available after it returns. Websocket sub-protocol selection should therefore be done in this function. The optional `websocket_init/1` callback will be called once the connection has been upgraded to Websocket. It can be used to perform any required initialization of the handler. Note that the `init/2` function does not run in the same process as the Websocket callbacks. Any Websocket-specific initialization must be done in `websocket_init/1`. The `websocket_handle/2` callback will be called for every frame received. The `websocket_info/2` callback will be called for every Erlang message received. All three Websocket callbacks may send one or more frames back to the client, including close frames to terminate the connection; enable/disable active mode; enable/disable compression for subsequent frames; or change Websocket options. The optional `terminate/3` callback will ultimately be called with the reason for the termination of the connection. This callback is common to all handlers. Note that Websocket will not provide the full Req object by default, to save memory. Cowboy will terminate the process right after closing the Websocket connection. This means that there is no need to perform any cleanup in the `terminate/3` callback. The following terminate reasons are defined for Websocket connections: normal:: The connection was closed normally before establishing a Websocket connection. This typically happens if an `ok` tuple is returned from the `init/2` callback. remote:: The remote endpoint closed the connection without giving any further details. {remote, Code, Payload}:: The remote endpoint closed the connection with the given `Code` and `Payload` as the reason. stop:: The handler requested to close the connection, either by returning a `stop` tuple or by sending a `close` frame. timeout:: The connection has been closed due to inactivity. The timeout value can be configured from `init/2`. {crash, Class, Reason}:: A crash occurred in the handler. `Class` and `Reason` can be used to obtain more information about the crash. {error, badencoding}:: A text frame was sent by the client with invalid encoding. All text frames must be valid UTF-8. {error, badframe}:: A protocol error has been detected. {error, closed}:: The socket has been closed brutally without a close frame being received first. {error, Reason}:: A socket error occurred. == Types === commands() [source,erlang] ---- commands() :: [Command] Command :: {active, boolean()} | {deflate, boolean()} | {set_options, #{ idle_timeout => timeout(), max_frame_size => non_neg_integer() | infinity}} | {shutdown_reason, any()} | Frame :: cow_ws:frame() ---- Commands that may be returned from Websocket callbacks. The following commands are defined: active:: Whether to disable or enable reading from the socket. This can be used to apply flow control to a Websocket connection. deflate:: Whether the subsequent frames should be compressed. Has no effect on connections that did not negotiate compression. set_options:: Set Websocket options. Currently only the options `idle_timeout` and `max_frame_size` may be updated from a Websocket handler. shutdown_reason:: Change the shutdown reason. The Websocket process will exit with reason `normal` by default. This command can be used to exit with reason `{shutdown, ShutdownReason}` under normal conditions. This command has no effect when the Websocket process exits abnormally, for example following a crash in a handler callback. Frame:: Send the corresponding Websocket frame. === cow_ws:frame() [source,erlang] ---- frame() :: {text, iodata()} | {binary, iodata()} | ping | {ping, iodata()} | pong | {pong, iodata()} | close | {close, iodata()} | {close, close_code(), iodata()} close_code() :: 1000..1003 | 1006..1011 | 3000..4999 ---- Websocket frames that can be sent as a response. Note that there is no need to send pong frames back as Cowboy does it automatically for you. === opts() [source,erlang] ---- opts() :: #{ active_n => pos_integer(), compress => boolean(), data_delivery => stream_handlers | relay, data_delivery_flow => pos_integer(), deflate_opts => cow_ws:deflate_opts(), dynamic_buffer => false | {pos_integer(), pos_integer()}, idle_timeout => timeout(), max_frame_size => non_neg_integer() | infinity, req_filter => fun((cowboy_req:req()) -> map()), validate_utf8 => boolean() } ---- Websocket handler options. This configuration is passed to Cowboy from the `init/2` function: [source,erlang] ---- init(Req, State) -> Opts = #{compress => true}, {cowboy_websocket, Req, State, Opts}. ---- The default value is given next to the option name: active_n (1):: The number of packets Cowboy will request from the socket at once. This can be used to tweak the performance of the server. Higher values reduce the number of times Cowboy need to request more packets from the port driver at the expense of potentially higher memory being used. + This option does not apply to Websocket over HTTP/2. compress (false):: Whether to enable the Websocket frame compression extension. Frames will only be compressed for the clients that support this extension. data_delivery (stream_handlers):: HTTP/2+ only. Determines how data will be delivered to the Websocket session process. `stream_handlers` is the default and makes data go through stream handlers. `relay` is a faster method introduced in Cowboy 2.14 and sends data directly. `relay` is intended to become the default in Cowboy 3.0. data_delivery_flow (pos_integer()):: When the `relay` data delivery method is used, this value may be used to decide how much the flow control window should be for the Websocket stream. Currently only applies to HTTP/2. deflate_opts (#{}):: Configuration for the permessage-deflate Websocket extension. Allows configuring both the negotiated options and the zlib compression options. The defaults optimize the compression at the expense of some memory and CPU. dynamic_buffer ({1024, 131072}):: Cowboy will dynamically change the socket's `buffer` size depending on the size of the data it receives from the socket. This lets Cowboy use the optimal buffer size for the current workload. + The dynamic buffer size functionality can be disabled by setting this option to `false`. Cowboy will also disable it by default when the `buffer` transport option is configured. idle_timeout (60000):: Time in milliseconds that Cowboy will keep the connection open without receiving anything from the client. + This option can be updated at any time using the `set_options` command. max_frame_size (infinity):: Maximum frame size in bytes allowed by this Websocket handler. Cowboy will close the connection when a client attempts to send a frame that goes over this limit. For fragmented frames this applies to the size of the reconstituted frame. req_filter:: A function applied to the Req to compact it and only keep required information. The Req is only given back in the `terminate/3` callback. By default it keeps the method, version, URI components and peer information. validate_utf8 (true):: Whether Cowboy should verify that the payload of `text` and `close` frames is valid UTF-8. This is required by the protocol specification but in some cases it may be more interesting to disable it in order to save resources. + Note that `binary` frames do not have this UTF-8 requirement and are what should be used under normal circumstances if necessary. == Changelog * *2.14*: The `data_delivery` and `data_delivery_flow` options were added. The `relay` data delivery mechanism provides a better way of forwarding data to HTTP/2+ Websocket session processes. * *2.13*: The `active_n` default value was changed to `1`. * *2.13*: The `dynamic_buffer` option was added. * *2.13*: The `max_frame_size` option can now be set dynamically. * *2.11*: Websocket over HTTP/2 is now considered stable. * *2.11*: HTTP/1.1 Websocket no longer traps exits by default. * *2.8*: The `active_n` option was added. * *2.7*: The commands based interface has been documented. The old interface is now deprecated. * *2.7*: The command `shutdown_reason` was introduced. * *2.7*: The option `validate_utf8` has been added. * *2.6*: Deflate options can now be configured via `deflate_opts`. * *2.0*: The Req object is no longer passed to Websocket callbacks. * *2.0*: The callback `websocket_terminate/3` was removed in favor of `terminate/3`. * *1.0*: Protocol introduced. == See also link:man:cowboy(7)[cowboy(7)], link:man:cowboy_handler(3)[cowboy_handler(3)], link:man:cowboy_http(3)[cowboy_http(3)], link:man:cowboy_http2(3)[cowboy_http2(3)] ================================================ FILE: doc/src/manual/http_status_codes.asciidoc ================================================ = HTTP status codes(7) == Name HTTP status codes - status codes used by Cowboy == Description This chapter aims to list all HTTP status codes that Cowboy may return, with details on the reasons why. The list given here only includes the replies that Cowboy sends, not user replies. == 100 Continue When the client sends an `expect: 100-continue` header, Cowboy automatically sends a this status code before trying to read the request body. This behavior can be disabled using the appropriate body option. == 101 Switching Protocols This is the status code sent when switching to the Websocket protocol. == 200 OK This status code is sent by `cowboy_rest`. == 201 Created This status code is sent by `cowboy_rest`. == 202 Accepted This status code is sent by `cowboy_rest`. == 204 No Content This status code is sent when the processing of a request ends without any reply having been sent. It may also be sent by `cowboy_rest` under normal conditions. == 300 Multiple Choices This status code is sent by `cowboy_rest`. == 301 Moved Permanently This status code is sent by `cowboy_rest`. == 303 See Other This status code is sent by `cowboy_rest`. == 304 Not Modified This status code is sent by `cowboy_rest`. == 307 Temporary Redirect This status code is sent by `cowboy_rest`. == 400 Bad Request Cowboy will send this status code for any of the following reasons: * Too many empty lines were sent before the request. * The request-line could not be parsed. * Too many headers were sent. * A header name was too long. * A header value was too long. * The host header was missing from an HTTP/1.1 request. * The host header could not be parsed. * The requested host was not found. * The requested path could not be parsed. * The accept header could not be parsed when using REST. * REST under normal conditions. * A Websocket upgrade failed. == 401 Unauthorized This status code is sent by `cowboy_rest`. == 403 Forbidden This status code is sent by `cowboy_rest`. == 404 Not Found This status code is sent when the router successfully resolved the host but didn't find a matching path for the request. It may also be sent by `cowboy_rest` under normal conditions. == 405 Method Not Allowed This status code is sent by `cowboy_rest`. == 406 Not Acceptable This status code is sent by `cowboy_rest`. == 408 Request Timeout Cowboy will send this status code to the client if the client started to send a request, indicated by the request-line being received fully, but failed to send all headers in a reasonable time. == 409 Conflict This status code is sent by `cowboy_rest`. == 410 Gone This status code is sent by `cowboy_rest`. == 412 Precondition Failed This status code is sent by `cowboy_rest`. == 413 Request Entity Too Large This status code is sent by `cowboy_rest`. == 414 Request-URI Too Long Cowboy will send this status code to the client if the request-line is too long. It may also be sent by `cowboy_rest` under normal conditions. == 415 Unsupported Media Type This status code is sent by `cowboy_rest`. == 500 Internal Server Error This status code is sent when a crash occurs in HTTP, loop or REST handlers, or when an invalid return value is returned. It may also be sent by `cowboy_rest` under normal conditions. == 501 Not Implemented This status code is sent by `cowboy_rest`. == 503 Service Unavailable This status code is sent by `cowboy_rest`. == 505 HTTP Version Not Supported Cowboy only supports the versions 1.0 and 1.1 of HTTP. In all other cases this status code is sent back to the client and the connection is closed. ================================================ FILE: doc/src/specs/index.ezdoc ================================================ ::: Cowboy Implementation Reference The implementation reference documents the behavior of Cowboy with regards to various standards and specifications. * ^"RFC6585 status codes^rfc6585 * ^"RFC7230 HTTP/1.1 server^rfc7230_server ================================================ FILE: doc/src/specs/rfc6585.ezdoc ================================================ ::: RFC6585 This document lists status codes that Cowboy implements as defined in the RFC6585 specifications. :: Status codes : 428 Precondition Required (RFC6585 3) The server requires the request to this resource to be conditional. The response should explain how to resubmit the request successfully. : 429 Too Many Requests (RFC6585 4, RFC6585 7.2) The user has sent too many requests in a given amount of time. The response should detail the rates allowed. The retry-after header can be used to indicate how long the user has to wait before making a new request. When an attack is detected it is recommended to drop the connection directly instead of sending this response. : 431 Request Header Fields Too Large (RFC6585 5, RFC6585 7.3) The request's header fields are too large. When rejecting a single header, the response should detail which header was at fault. When an attack is detected it is recommended to drop the connection directly instead of sending this response. : 511 Network Authentication Required (RFC6585 6) The user needs to authenticate into the network to gain access. This status code is meant to be used by proxies only, not by origin servers. The response should contain a link to the resource allowing the user to log in. ================================================ FILE: doc/src/specs/rfc7230_server.ezdoc ================================================ ::: RFC7230 HTTP/1.1 server This document lists the rules the Cowboy server follows based on the RFC7230 HTTP specifications. :: Listener The default port for "http" connections is 80. The connection uses plain TCP. (RFC7230 2.7.1) The default port for "https" connections is 443. The connection uses TLS. (RFC7230 2.7.2) Any other port may be used for either of them. :: Before the request A configurable number of empty lines (CRLF) preceding the request must be ignored. At least 1 empty line must be ignored. (RFC7230 3.5) When receiving a response instead of a request, identified by the status-line which starts with the HTTP version, the server must reject the message with a 501 status code and close the connection. (RFC7230 3.1) :: Request It is only necessary to parse elements required to process the request. (RFC7230 2.5) Parsed elements are subject to configurable limits. A server must be able to parse elements at least as long as it generates. (RFC7230 2.5) The request must be parsed as a sequence of octets in an encoding that is a superset of US-ASCII. (RFC7230 2.5) ``` HTTP-request = request-line *( header-field CRLF ) CRLF [ message-body ] ``` The general format of HTTP requests is strict. No empty line is allowed in-between components except for the empty line indicating the end of the list of headers. It is not necessary to read the message-body before processing the request as the message-body may be dropped depending on the outcome of the processing. The time the request (request line and headers) takes to be received by the server must be limited and subject to configuration. A server must wait at least 5 seconds before dropping the connection. A 408 status code must be sent if the request line was received fully when the timeout is triggered. An HTTP/1.1 server must understand any valid HTTP/1.0 request, and respond to those with an HTTP/1.1 message that only use features understood or safely ignored by HTTP/1.0 clients. (RFC7230 A) :: Request line It is recommended to limit the request-line length to a configurable limit of at least 8000 octets. However, as the possible line length is highly dependent on what form the request-target takes, it is preferrable to limit each individual components of the request-target. (RFC7230 3.1.1) A request line too long must be rejected with a 414 status code and the closing of the connection. (RFC7230 3.1.1) ``` method SP request-target SP version CRLF ``` :: Method ``` method = token ; case sensitive token = 1*tchar tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA ``` The request method is defined as 1+ token characters. An invalid or empty method must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.2.6) In practice the only characters in use by registered methods are uppercase letters [A-Z] and the dash "-". (IANA HTTP Method Registry) The length of the method must be subject to a configurable limit. A method too long must be rejected with a 501 status code and the closing of the connection. (RFC7230 3.1.1) A good default for the method length limit is the longest method length the server implements. (RFC7230 3.1.1) :: Between method and request-target A request that uses anything other than SP as separator between the method and the request-target must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.5) :: Request target There are four request-target forms. A server must be able to handle at least origin-form and absolute-form. The other two forms are specific to the CONNECT and site-wide OPTIONS method, respectively. (RFC7230 5.3.2) The fragment part of the target URI is not sent. It must be ignored by a server receiving it. (RFC7230 5.1) ``` request-target = origin-form / absolute-form / authority-form / asterisk-form ``` Any other form is invalid and must be rejected with a 400 status code and the closing of the connection. : origin-form origin-form is used when the client does not connect to a proxy, does not use the CONNECT method and does not issue a site-wide OPTIONS request. (RFC7230 5.3.1) ``` origin-form = absolute-path [ "?" query ] absolute-path = 1*( "/" segment ) segment = *pchar query = *( pchar / "/" / "?" ) pchar = unreserved / pct-encoded / sub-delims / ":" / "@" unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" ``` The scheme is either resolved from configuration or is "https" when on a TLS connection and "http" otherwise. (RFC7230 5.5) The authority is sent in the host header. (RFC7230 5.3.1, RFC7230 5.5) The absolute-path always starts with "/" and ends with either "?", "#" or the end of the URI. (RFC3986 3.3) The query starts with "?" and ends with "#" or the end of the URI. (RFC3986 3.4) The path and query must be subject to a configurable limit. This limit must be at least as high as what the server generates. A good default would be 8000 characters. (RFC7230 2.5, RFC7230 3.1.1) A request with a too long origin-form must be rejected with a 414 status code and the closing of the connection. (RFC7230 3.1.1) : absolute-form absolute-form is used when the client connects to a proxy, though its usage is also allowed when connecting to the server directly. (RFC7230 5.3.2) In practice the scheme will be "http" or "https". The "http" and "https" schemes based URI take the following form. (RFC7230 2.7.1, RFC7230 2.7.2) ``` http-URI = "http:" "//" authority path-abempty [ "?" query ] [ "#" fragment ] https-URI = "https:" "//" authority path-abempty [ "?" query ] [ "#" fragment ] ``` The target URI excludes the fragment component. (RFC7230 5.1) This means that the absolute-form uses a subset of absolute-URI. ``` absolute-form = ( "http" / "https" ) "://" authority path-abempty [ "?" query ] authority = host [ ":" port ] path-abempty = *( "/" segment ) query = *( pchar / "/" / "?" ) host = IP-literal / IPv4address / reg-name port = *DIGIT IP-literal = "[" ( IPv6address / IPvFuture ) "]" IPv6address = 6( h16 ":" ) ls32 / "::" 5( h16 ":" ) ls32 / [ h16 ] "::" 4( h16 ":" ) ls32 / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 / [ *4( h16 ":" ) h16 ] "::" ls32 / [ *5( h16 ":" ) h16 ] "::" h16 / [ *6( h16 ":" ) h16 ] "::" ls32 = ( h16 ":" h16 ) / IPv4address ; least-significant 32 bits of address h16 = 1*4HEXDIG ; 16 bits of address represented in hexadecimal IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet dec-octet = DIGIT / %x31-39 DIGIT / "1" 2DIGIT / "2" %x30-34 DIGIT / "25" %x30-35 IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) reg-name = *( unreserved / pct-encoded / sub-delims ) segment = *pchar segment-nz = 1*pchar pchar = unreserved / pct-encoded / sub-delims / ":" / "@" unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" ``` The scheme and host are case insensitive and normally provided in lowercase. All other components are case sensitive. (RFC7230 2.7.3) Unknown schemes must be rejected with a 400 status code and the closing of the connection. Because only a fixed number of schemes are allowed, it is not necessary to limit its length. The scheme provided with the request must be dropped. The effective scheme is either resolved from configuration or is "https" when on a TLS connection and "http" otherwise. (RFC7230 5.5) An authority component with a userinfo component (and its "@" delimiter) is invalid. The request must be rejected with a 400 status code and the closing of the connection. (RFC7230 2.7.1) A URI with a missing host identifier is invalid. The request must be rejected with a 400 status code and the closing of the connection. (RFC7230 2.7.1) The maximum length for an IPv4address is 15 characters. No configurable limit is necessary. The maximum length for an IPv6address is 47 characters. No configurable limit is necessary. The maximum length for the reg-name component must be subject to a configurable limit. A good default is 255 characters. (RFC3986 3.2.2, RFC1034 3.1) It is not possible to distinguish between an IPv4address and a reg-name before reaching the end of the string, therefore the length limit for IPv4address must be ignored until that point. The maximum length for the port component is 5. No configurable limit is necessary. The authority is sent both in the URI and in the host header. The authority from the URI must be dropped, and the host header must be used instead. (RFC7230 5.5) The path always starts with "/" and ends with either "?", "#" or the end of the URI. (RFC3986 3.3) An empty path component is equivalent to "/". (RFC7230 2.7.3) The query starts with "?" and ends with "#" or the end of the URI. (RFC3986 3.4) The path and query must be subject to a configurable limit. This limit must be at least as high as what the server generates. A good default would be 8000 characters. (RFC7230 2.5, RFC7230 3.1.1) A request with a too long component of absolute-form must be rejected with a 414 status code and the closing of the connection. (RFC7230 3.1.1) : authority-form When the method is CONNECT, authority-form must be used. This form does not apply to any other methods which must reject the request with a 400 status code and the closing of the connection. (RFC7230 5.3.3) ``` authority-form = authority authority = host [ ":" port ] host = IP-literal / IPv4address / reg-name port = *DIGIT IP-literal = "[" ( IPv6address / IPvFuture ) "]" IPv6address = 6( h16 ":" ) ls32 / "::" 5( h16 ":" ) ls32 / [ h16 ] "::" 4( h16 ":" ) ls32 / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 / [ *4( h16 ":" ) h16 ] "::" ls32 / [ *5( h16 ":" ) h16 ] "::" h16 / [ *6( h16 ":" ) h16 ] "::" ls32 = ( h16 ":" h16 ) / IPv4address ; least-significant 32 bits of address h16 = 1*4HEXDIG ; 16 bits of address represented in hexadecimal IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet dec-octet = DIGIT / %x31-39 DIGIT / "1" 2DIGIT / "2" %x30-34 DIGIT / "25" %x30-35 IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) reg-name = *( unreserved / pct-encoded / sub-delims ) unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" ``` An authority component with a userinfo component (and its "@" delimiter) is invalid. The request must be rejected with a 400 status code and the closing of the connection. (RFC7230 2.7.1) The maximum length for an IPv4address is 15 characters. No configurable limit is necessary. The maximum length for an IPv6address is 47 characters. No configurable limit is necessary. The maximum length for the reg-name component must be subject to a configurable limit. A good default is 255 characters. (RFC3986 3.2.2, RFC1034 3.1) It is not possible to distinguish between an IPv4address and a reg-name before reaching the end of the string, therefore the length limit for IPv4address must be ignored until that point. The maximum length for the port component is 5. No configurable limit is necessary. A request with a too long component of authority-form must be rejected with a 414 status code and the closing of the connection. (RFC7230 3.1.1) The authority is either resolved from configuration or is taken directly from authority-form. (RFC7230 5.5) The path and query are empty when using authority-form. (RFC7230 5.5) : asterisk-form asterisk-form is used for server-wide OPTIONS requests. It is invalid with any other methods which must reject the request with a 400 status code and the closing of the connection. (RFC7230 5.3.4) ``` asterisk-form = "*" ``` The asterisk-form always has a length of 1. No configurable limit is necessary. The authority is empty when using asterisk-form. The path and query are empty when using asterisk-form. (RFC7230 5.5) :: Between request-target and version A request that uses anything other than SP as separator between the request-target and the version must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.5) :: Request version ``` version = "HTTP/1.0" / "HTTP/1.1" ``` Any version number other than HTTP/1.0 or HTTP/1.1 must be rejected by a server or intermediary with a 505 status code. (RFC7230 2.6, RFC7230 A.2) A request that has any whitespace or characters different than CRLF following the version must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.1.1) :: Request headers ``` headers = *( header-field CRLF ) CRLF header-field = field-name ":" OWS field-value OWS field-name = token field-value = *( SP / HTAB / %21-7E / %80-FF ) OWS = *( SP / HTAB ) ``` The header field name is case insensitive. (RFC7230 3.2) HTTP/2 requires header field names to be lowercase. It is perfectly acceptable for a server supporting both to convert HTTP/1.1 header names to lowercase when they are received. (draft-ietf-httpbis-http2-15 8.1.2.1) Messages that contain whitespace before the header name must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.2.4) Messages that contain whitespace between the header name and colon must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.2.4) The header name must be subject to a configurable limit. A good default is 50 characters, well above the longest registered header. Such a request must be rejected with a 431 status code and the closing of the connection. (RFC7230 3.2.5, RFC6585 5, IANA Message Headers registry) The header value and the optional whitespace around it must be subject to a configurable limit. There is no recommendations for the default. 4096 characters is known to work well. Such a request must be rejected with a 431 status code and the closing of the connection. (RFC7230 3.2.5, RFC6585 5) Optional whitespace before and after the header value is not part of the value and must be dropped. The order of header fields with differing names is not significant. (RFC7230 3.2.2) The normal procedure for parsing headers is to read each header field into a hash table by field name until the empty line. (RFC7230 3) Requests with duplicate content-length or host headers must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.3.2) Other duplicate header fields must be combined by inserting a comma between the values in the order they were received. (RFC7230 3.2.2) Duplicate header field names are only allowed when their value is a comma-separated list. In practice there is no need to perform a check while reading the headers as the value will become invalid and the error can be handled while parsing the header later on. (RFC7230 3.2.2) The request must not be processed until all headers have arrived. (RFC7230 3.2.2) The number of headers allowed in a request must be subject to a configurable limit. There is no recommendations for the default. 100 headers is known to work well. Such a request must be rejected with a 431 status code and the closing of the connection. (RFC7230 3.2.5, RFC6585 5) When parsing header field values, the server must ignore empty list elements, and not count those as the count of elements present. (RFC7230 7) The information in the via header is largely unreliable. (RFC7230 5.7.1) :: Request body ``` message-body = *OCTET ``` The message body is the octets after decoding any transfer codings. (RFC7230 3.3) A request has a message body only if it includes a transfer-encoding header or a non-zero content-length header. (RFC7230 3.3) ``` Transfer-Encoding = 1#transfer-coding transfer-coding = "chunked" / "compress" / "deflate" / "gzip" / transfer-extension transfer-extension = token *( OWS ";" OWS transfer-parameter ) transfer-parameter = token BWS "=" BWS ( token / quoted-string ) ``` The transfer-coding is case insensitive. (RFC7230 4) There are no known other transfer-extension with the exception of deprecated aliases "x-compress" and "x-gzip". (IANA HTTP Transfer Coding Registry, RFC7230 4.2.1, RFC7230 4.2.3, RFC7230 8.4.2) A server must be able to handle at least chunked transfer-encoding. This is also the only coding that sees widespread use. (RFC7230 3.3.1, RFC7230 4.1) Messages encoded more than once with chunked transfer-encoding must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.3.1) Messages where chunked, when present, is not the last transfer-encoding must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.3.3) Some non-conformant implementations send the "deflate" compressed data without the zlib wrapper. (RFC7230 4.2.2) Messages encoded with a transfer-encoding the server does not understand must be rejected with a 501 status code and the closing of the connection. (RFC7230 3.3.1) A server can reject requests with a body and no content-length header with a 411 status code. (RFC7230 3.3.3) ``` Content-Length = 1*DIGIT ``` A request with an invalid content-length header must be rejected with a 400 status code and the closing of the connection. (RFC7230 3.3.3) The content-length header ranges from 0 to infinity. Requests with a message body too large must be rejected with a 413 status code and the closing of the connection. (RFC7230 3.3.2) When a message includes both transfer-encoding and content-length headers, the content-length header must be removed before processing the request. (RFC7230 3.3.3) If a socket error occurs while reading the body the server must send a 400 status code response and close the connection. (RFC7230 3.3.3, RFC7230 3.4) If a timeout occurs while reading the body the server must send a 408 status code response and close the connection. (RFC7230 3.3.3, RFC7230 3.4) : Body length The length of a message with a transfer-encoding header can only be determined on decoding completion. (RFC7230 3.3.3) The length of a message with a content-length header is the numeric value in octets found in the header. (RFC7230 3.3.3) A message with no transfer-encoding or content-length header has a body length of 0. (RFC7230 3.3.3) : Chunked transfer-encoding ``` chunked-body = *chunk last-chunk trailer-part CRLF chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF chunk-size = 1*HEXDIG chunk-data = 1*OCTET ; a sequence of chunk-size octets last-chunk = 1*("0") [ chunk-ext ] CRLF ``` The chunk-size field is a string of hex digits indicating the size of the chunk-data in octets. ``` chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) chunk-ext-name = token chunk-ext-val = token / quoted-string ``` Unknown chunk extensions must be ignored. (RFC7230 4.1.1) The chunk-size line length must be subject to configuration. There are no recommended defaults, although 100 octets should work. Requests with a too long line must be rejected with a 400 status code and the closing of the connection. ``` trailer-part = *( header-field CRLF ) ``` Trailing headers must not include transfer-encoding, content-length, host, cache-control, expect, max-forwards, pragma, range, te, if-match, if-none-match, if-modified-since, if-unmodified-since, if-range, www-authenticate, authorization, proxy-authenticate, proxy-authorization, age, cache-control, expires, date, location, retry-after, vary, warning, content-encoding, content-type, content-range, or trailer. (RFC7230 4.1.2) Trailer headers can be ignored safely. (RFC7230 4.1.2) When trailer headers are processed, invalid headers must be ignored. Valid headers must be added to the list of headers of the request. (RFC7230 4.1.2) The number of trailer headers must be subject to configuration. There is no known recommendations for the default. A value of 10 should cover most cases. Requests with too many trailer headers must be rejected with a 431 status code and the closing of the connection. (RFC6585 5) Upon completion of chunk decoding the server must add a content-length header with the value set to the total length of data read. (RFC7230 4.1.3) Upon completion of chunk decoding the server must remove "chunked" from the transfer-encoding header. This header must be removed if it becomes empty following this removal. (RFC7230 4.1.3) Upon completion of chunk decoding the server must remove the trailer header from the list of headers. (RFC7230 4.1.3) ``` Trailer = 1#field-name ``` The trailer header can be used to list the headers found in the trailer. A server must have the option of ignoring trailer headers that were not listed in the trailer header. (RFC7230 4.4) The trailer header must be listed in the connection header field. Trailers must be ignored otherwise. :: Connection management Never assume any two requests on a single connection come from the same user agent. (RFC7230 2.3) ``` Connection = 1#token ; case-insensitive ``` The connection token is either case insensitive "close", "keep-alive" or a header field name. There are no corresponding "close" or "keep-alive" headers. (RFC7230 8.1, RFC7230 A.2) The connection header is valid only for the immediate connection, alongside any header field it lists. (RFC7230 6.1) The server must determine if the connection is persistent for every message received by looking at the connection header and HTTP version. (RFC7230 6.3) HTTP/1.1 requests with no "close" option and HTTP/1.0 with the "keep-alive" option indicate the connection will persist. (RFC7230 6.1, RFC7230 6.3) HTTP/1.1 requests with the "close" option and HTTP/1.0 with no "keep-alive" option indicate the connection will be closed upon reception of the response by the client. (RFC7230 6.1, RFC7230 6.3) The maximum number of requests sent using a persistent connection must be subject to configuration. The connection must be closed when the limit is reached. (RFC7230 6.3) A server that doesn't want to read the entire body of a message must close the connection, if possible after sending the "close" connection option in the response. (RFC7230 6.3) A server can receive more than one request before any response is sent. This is called pipelining. The requests can be processed in parallel if they all have safe methods. Responses must be sent in the same order as the requests. (RFC7230 6.3.2) The server must reject abusive traffic by closing the connection. Abusive traffic can come from the form of too many requests in a given amount of time, or too many concurrent connections. Limits must be subject to configuration. (RFC7230 6.4) The server must close inactive connections. The timeout must be subject to configuration. (RFC7230 6.5) The server must monitor connections for the close signal and close the socket on its end accordingly. (RFC7230 6.5) A connection close may occur at any time. (RFC7230 6.5) The server must not process any request after sending or receiving the "close" connection option. (RFC7230 6.6) The server must close the connection in stages to avoid the TCP reset problem. The server starts by closing the write side of the socket. The server then reads until it detects the socket has been closed, until it can be certain its last response has been received by the client, or until a close or timeout occurs. The server then fully close the connection. (6.6) :: Routing ``` Host = authority ; same as authority-form ``` An HTTP/1.1 request that lack a host header must be rejected with a 400 status code and the closing of the connection. (RFC7230 5.4) An HTTP/1.0 request that lack a host header is valid. Behavior for these requests is configuration dependent. (RFC7230 5.5) A request with an invalid host header must be rejected with a 400 status code and the closing of the connection. (RFC7230 5.4) An authority component with a userinfo component (and its "@" delimiter) is invalid. The request must be rejected with a 400 status code and the closing of the connection. (RFC7230 2.7.1) When using absolute-form the URI authority component must be identical to the host header. Invalid requests must be rejected with a 400 status code and the closing of the connection. (RFC7230 5.4) When using authority-form the URI authority component must be identical to the host header. Invalid requests must be rejected with a 400 status code and the closing of the connection. The host header is empty when the authority component is undefined. (RFC7230 5.4) The effective request URI can be rebuilt by concatenating scheme, "://", authority, path and query components. (RFC7230 5.5) Resources with identical URI except for the scheme component must be treated as different. (RFC7230 2.7.2) :: Response A server can send more than one response per request only when a 1xx response is sent preceding the final response. (RFC7230 5.6) A server that does parallel pipelining must send responses in the same order as the requests came in. (RFC7230 5.6) ``` HTTP-response = status-line *( header-field CRLF ) CRLF [ message-body ] ``` The response format must be followed strictly. ``` status-line = HTTP-version SP status-code SP reason-phrase CRLF status-code = 3DIGIT reason-phrase = *( HTAB / SP / VCHAR / obs-text ) ``` A server must send its own version. (RFC7230 2.6) An HTTP/1.1 server may send an HTTP/1.0 version for compatibility purposes. (RFC7230 2.6) RFC6585 defines additional status code a server can use to reject messages. (RFC7230 9.3, RFC6585) :: Response headers In responses, OWS must be generated as SP or not generated at all. RWS must be generated as SP. BWS must not be generated. (RFC7230 3.2.3) ``` header-field = field-name ":" SP field-value field-name = token ; case-insensitive field-value = *( SP / %21-7E / %80-FF ) ``` In quoted-string found in field-value, quoted-pair must only be used for DQUOTE and backslash. (RFC7230 3.2.6) The server must not generate comments in header values. HTTP header values must use US-ASCII encoding and must only send printable characters or SP. (RFC7230 3.2.4, RFC7230 9.4) The server must not generate empty list elements in headers. (RFC7230 7) When encoding an URI as part of a response, only characters that are reserved need to be percent-encoded. (RFC7230 2.7.3) The set-cookie header must be handled as a special case. There must be exactly one set-cookie header field per cookie. (RFC7230 3.2.2) The server must list headers for or about the immediate connection in the connection header field. (RFC7230 6.1) A server that does not support persistent connections must send "close" in every non-1xx response. (RFC7230 6.1) A server must not send a "close" connection option in 1xx responses. (RFC7230 6.1) The "close" connection must be sent in a message when the sender knows it will close the connection after fully sending the response. (RFC7230 6.6) A server must close the connection after sending or receiving a "close" once the response has been sent. (RFC7230 6.6) A server must send a "close" in a response to a request containing a "close". (RFC7230 6.6) :: Response body Responses to HEAD requests never include a message body. (RFC7230 3.3) 2xx responses to CONNECT requests never include a message body. (RFC7230 3.3) 1xx, 204 and 304 responses never include a message body. (RFC7230 3.3) Responses to HEAD requests and 304 responses can include a content-length or transfer-encoding header. Their value must be the same as if the request was an unconditional GET. (RFC7230 3.3, RFC7230 3.3.1, RFC7230 3.3.2) 1xx, 204 responses and 2xx responses to CONNECT requests must not include a content-length or transfer-encoding header. (RFC7230 3.3.1, RFC7230 3.3.2) ``` message-body = *OCTET ``` The message body is the octets after decoding any transfer codings. (RFC7230 3.3) When the length is known in advance, the server must send a content-length header, including if the length is 0. (RFC7230 3.3.2, RFC7230 3.3.3) When the length is not known in advance, the chunked transfer-encoding must be used. (RFC7230 3.3.2, RFC7230 3.3.3) For compatibility purposes a server can send no content-length or transfer-encoding header. In this case the connection must be closed after the response has been sent fully. (RFC7230 3.3.2, RFC7230 3.3.3) The content-length header must not be sent when a transfer-encoding header already exists. (RFC7230 3.3.2) The server must not apply the chunked transfer-encoding more than once. (RFC7230 3.3.1) The server must apply the chunked transfer-encoding last. (RFC7230 3.3.1) The transfer-encoding header must not be sent in responses to HTTP/1.0 requests, or in responses that use the HTTP/1.0 version. No transfer codings must be applied in these cases. (RFC7230 3.3.1) ``` TE = #t-codings t-codings = "trailers" / ( transfer-coding [ t-ranking ] ) t-ranking = OWS ";" OWS "q=" rank rank = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) ``` Trailers can only be sent if the request includes a TE header containing "trailers". (RFC7230 4.1.2) The presence of "chunked" in a TE header must be ignored as it is always acceptable with HTTP/1.1. (RFC7230 4.3) A qvalue of 0 in the TE header means "not acceptable". (RFC7230 4.3) The lack of a TE header or an empty TE header means only "chunked" (with no trailers) or no transfer-encoding is acceptable. (RFC7230 4.3) The TE header must be listed in the connection header field, or must be ignored otherwise. Trailer headers must be listed in the trailer header field value. (RFC7230 4.4) When defined, the trailer header must also be listed in the connection header. (RFC7230 4.4) :: Upgrade ``` Upgrade = 1#protocol protocol = protocol-name ["/" protocol-version] protocol-name = token protocol-version = token ``` The upgrade header contains the list of protocols the client wishes to upgrade to, in order of preference. (RFC7230 6.7) The upgrade header can be safely ignored. (RFC7230 6.7) The upgrade header must be listed under the connection header, or must be ignored otherwise. (RFC7230 6.7) A server accepting an upgrade request must send a 101 status code with a upgrade header listing the protocol(s) it upgrades to, in layer-ascending order. In addition the upgrade header must be listed in the connection header. (RFC7230 6.7) A server must not switch to a protocol not listed in the request's upgrade header. (RFC7230 6.7) A server that sends a 426 status code must include a upgrade header listing acceptable protocols in order of preference. (RFC7230 6.7) A server can send a upgrade header to any response to advertise its support for other protocols listed in order of preference. (RFC7230 6.7) Immediately after a server responds with a 101 status code it must respond to the original request using the new protocol. (RFC7230 6.7) A server must not switch protocols unless the original message's semantics can be honored by the new protocol. OPTIONS requests can be honored by any protocol. (RFC7230 6.7) A server must ignore an upgrade header received by an HTTP/1.0 request. (RFC7230 6.7) A server receiving both an upgrade header and an expect header containing "100-continue" must send a 100 response before the 101 response. (RFC7230 6.7) The upgrade header field cannot be used for switching the connection protocol (e.g. TCP) or switching connections. (RFC7230 6.7) :: Compatibility A server can choose to be non-conformant to the specifications for the sake of compatibility. Such behavior can be enabled through configuration and/or software identification. (RFC7230 2.5) ================================================ FILE: ebin/cowboy.app ================================================ {application, 'cowboy', [ {description, "Small, fast, modern HTTP server."}, {vsn, "2.14.2"}, {modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_children','cowboy_clear','cowboy_clock','cowboy_compress_h','cowboy_constraints','cowboy_decompress_h','cowboy_handler','cowboy_http','cowboy_http2','cowboy_http3','cowboy_loop','cowboy_metrics_h','cowboy_middleware','cowboy_quicer','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket','cowboy_webtransport']}, {registered, [cowboy_sup,cowboy_clock]}, {applications, [kernel,stdlib,crypto,cowlib,ranch]}, {optional_applications, []}, {mod, {cowboy_app, []}}, {env, []} ]}. ================================================ FILE: erlang.mk ================================================ # Copyright (c) 2013-2016, Loïc Hoguin # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .PHONY: all app apps deps search rel relup docs install-docs check tests clean distclean help erlang-mk ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) export ERLANG_MK_FILENAME ERLANG_MK_VERSION = 2022.05.31-142-gba4dcff-dirty ERLANG_MK_WITHOUT = # Make 3.81 and 3.82 are deprecated. ifeq ($(MAKELEVEL)$(MAKE_VERSION),03.81) $(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html) endif ifeq ($(MAKELEVEL)$(MAKE_VERSION),03.82) $(warning Please upgrade to GNU Make 4 or later: https://erlang.mk/guide/installation.html) endif # Core configuration. PROJECT ?= $(notdir $(CURDIR)) PROJECT := $(strip $(PROJECT)) PROJECT_VERSION ?= rolling PROJECT_MOD ?= PROJECT_ENV ?= [] # Verbosity. V ?= 0 verbose_0 = @ verbose_2 = set -x; verbose = $(verbose_$(V)) ifeq ($V,3) SHELL := $(SHELL) -x endif gen_verbose_0 = @echo " GEN " $@; gen_verbose_2 = set -x; gen_verbose = $(gen_verbose_$(V)) gen_verbose_esc_0 = @echo " GEN " $$@; gen_verbose_esc_2 = set -x; gen_verbose_esc = $(gen_verbose_esc_$(V)) # Temporary files directory. ERLANG_MK_TMP ?= $(CURDIR)/.erlang.mk export ERLANG_MK_TMP # "erl" command. ERL = erl -noinput -boot no_dot_erlang -kernel start_distribution false +P 1024 +Q 1024 # Platform detection. ifeq ($(PLATFORM),) UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) PLATFORM = linux else ifeq ($(UNAME_S),Darwin) PLATFORM = darwin else ifeq ($(UNAME_S),SunOS) PLATFORM = solaris else ifeq ($(UNAME_S),GNU) PLATFORM = gnu else ifeq ($(UNAME_S),FreeBSD) PLATFORM = freebsd else ifeq ($(UNAME_S),NetBSD) PLATFORM = netbsd else ifeq ($(UNAME_S),OpenBSD) PLATFORM = openbsd else ifeq ($(UNAME_S),DragonFly) PLATFORM = dragonfly else ifeq ($(shell uname -o),Msys) PLATFORM = msys2 else $(error Unable to detect platform. Please open a ticket with the output of uname -a.) endif export PLATFORM endif # Core targets. all:: deps app rel # Noop to avoid a Make warning when there's nothing to do. rel:: $(verbose) : relup:: deps app check:: tests clean:: clean-crashdump clean-crashdump: ifneq ($(wildcard erl_crash.dump),) $(gen_verbose) rm -f erl_crash.dump endif distclean:: clean distclean-tmp $(ERLANG_MK_TMP): $(verbose) mkdir -p $(ERLANG_MK_TMP) distclean-tmp: $(gen_verbose) rm -rf $(ERLANG_MK_TMP) help:: $(verbose) printf "%s\n" \ "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ "Copyright (c) 2013-2016 Loïc Hoguin " \ "" \ "Usage: [V=1] $(MAKE) [target]..." \ "" \ "Core targets:" \ " all Run deps, app and rel targets in that order" \ " app Compile the project" \ " deps Fetch dependencies (if needed) and compile them" \ " fetch-deps Fetch dependencies recursively (if needed) without compiling them" \ " list-deps List dependencies recursively on stdout" \ " search q=... Search for a package in the built-in index" \ " rel Build a release for this project, if applicable" \ " docs Build the documentation for this project" \ " install-docs Install the man pages for this project" \ " check Compile and run all tests and analysis for this project" \ " tests Run the tests for this project" \ " clean Delete temporary and output files from most targets" \ " distclean Delete all temporary and output files" \ " help Display this help and exit" \ " erlang-mk Update erlang.mk to the latest version" # Core functions. empty := space := $(empty) $(empty) tab := $(empty) $(empty) comma := , define newline endef define comma_list $(subst $(space),$(comma),$(strip $1)) endef define escape_dquotes $(subst ",\",$1) endef # Adding erlang.mk to make Erlang scripts who call init:get_plain_arguments() happy. define erlang $(ERL) $2 -pz $(ERLANG_MK_TMP)/rebar3/_build/prod/lib/*/ebin/ -eval "$(subst $(newline),,$(call escape_dquotes,$1))" -- erlang.mk endef ifeq ($(PLATFORM),msys2) core_native_path = $(shell cygpath -m $1) else core_native_path = $1 endif core_http_get = curl -Lf$(if $(filter-out 0,$V),,s)o $(call core_native_path,$1) $2 core_eq = $(and $(findstring $1,$2),$(findstring $2,$1)) # We skip files that contain spaces because they end up causing issues. # Files that begin with a dot are already ignored by the wildcard function. core_find = $(foreach f,$(wildcard $(1:%/=%)/*),$(if $(wildcard $f/.),$(call core_find,$f,$2),$(if $(filter $(subst *,%,$2),$f),$(if $(wildcard $f),$f)))) core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) core_ls = $(filter-out $1,$(shell echo $1)) # @todo Use a solution that does not require using perl. core_relpath = $(shell perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $1 $2) define core_render printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $1)))))\n' > $2 endef # Automated update. ERLANG_MK_REPO ?= https://github.com/ninenines/erlang.mk ERLANG_MK_COMMIT ?= ERLANG_MK_BUILD_CONFIG ?= build.config ERLANG_MK_BUILD_DIR ?= .erlang.mk.build erlang-mk: WITHOUT ?= $(ERLANG_MK_WITHOUT) erlang-mk: ifdef ERLANG_MK_COMMIT $(verbose) git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) $(verbose) cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) else $(verbose) git clone --depth 1 $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) endif $(verbose) if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi $(gen_verbose) $(MAKE) --no-print-directory -C $(ERLANG_MK_BUILD_DIR) WITHOUT='$(strip $(WITHOUT))' UPGRADE=1 $(verbose) cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk $(verbose) rm -rf $(ERLANG_MK_BUILD_DIR) $(verbose) rm -rf $(ERLANG_MK_TMP) # The erlang.mk package index is bundled in the default erlang.mk build. # Search for the string "copyright" to skip to the rest of the code. # Copyright (c) 2015-2017, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-kerl KERL_INSTALL_DIR ?= $(HOME)/erlang ifeq ($(strip $(KERL)),) KERL := $(ERLANG_MK_TMP)/kerl/kerl endif KERL_DIR = $(ERLANG_MK_TMP)/kerl export KERL KERL_GIT ?= https://github.com/kerl/kerl KERL_COMMIT ?= master KERL_MAKEFLAGS ?= OTP_GIT ?= https://github.com/erlang/otp define kerl_otp_target $(KERL_INSTALL_DIR)/$1: $(KERL) $(verbose) if [ ! -d $$@ ]; then \ MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1; \ $(KERL) install $1 $(KERL_INSTALL_DIR)/$1; \ fi endef $(KERL): $(KERL_DIR) $(KERL_DIR): | $(ERLANG_MK_TMP) $(gen_verbose) git clone --depth 1 $(KERL_GIT) $(ERLANG_MK_TMP)/kerl $(verbose) cd $(ERLANG_MK_TMP)/kerl && git checkout $(KERL_COMMIT) $(verbose) chmod +x $(KERL) distclean:: distclean-kerl distclean-kerl: $(gen_verbose) rm -rf $(KERL_DIR) # Allow users to select which version of Erlang/OTP to use for a project. ifneq ($(strip $(LATEST_ERLANG_OTP)),) # In some environments it is necessary to filter out master. ERLANG_OTP := $(notdir $(lastword $(sort\ $(filter-out $(KERL_INSTALL_DIR)/master $(KERL_INSTALL_DIR)/OTP_R%,\ $(filter-out %-rc1 %-rc2 %-rc3,$(wildcard $(KERL_INSTALL_DIR)/*[^-native])))))) endif ERLANG_OTP ?= # Use kerl to enforce a specific Erlang/OTP version for a project. ifneq ($(strip $(ERLANG_OTP)),) export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_OTP)/bin:$(PATH) SHELL := env PATH=$(PATH) $(SHELL) $(eval $(call kerl_otp_target,$(ERLANG_OTP))) # Build Erlang/OTP only if it doesn't already exist. ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_OTP))$(BUILD_ERLANG_OTP),) $(info Building Erlang/OTP $(ERLANG_OTP)... Please wait...) $(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_OTP) ERLANG_OTP=$(ERLANG_OTP) BUILD_ERLANG_OTP=1 >&2) endif endif PACKAGES += asciideck pkg_asciideck_name = asciideck pkg_asciideck_description = Asciidoc for Erlang. pkg_asciideck_homepage = https://ninenines.eu pkg_asciideck_fetch = git pkg_asciideck_repo = https://github.com/ninenines/asciideck pkg_asciideck_commit = master PACKAGES += cowboy pkg_cowboy_name = cowboy pkg_cowboy_description = Small, fast and modular HTTP server. pkg_cowboy_homepage = http://ninenines.eu pkg_cowboy_fetch = git pkg_cowboy_repo = https://github.com/ninenines/cowboy pkg_cowboy_commit = master PACKAGES += cowlib pkg_cowlib_name = cowlib pkg_cowlib_description = Support library for manipulating Web protocols. pkg_cowlib_homepage = http://ninenines.eu pkg_cowlib_fetch = git pkg_cowlib_repo = https://github.com/ninenines/cowlib pkg_cowlib_commit = master PACKAGES += elixir pkg_elixir_name = elixir pkg_elixir_description = Elixir is a dynamic, functional language for building scalable and maintainable applications. pkg_elixir_homepage = https://elixir-lang.org pkg_elixir_fetch = git pkg_elixir_repo = https://github.com/elixir-lang/elixir pkg_elixir_commit = main PACKAGES += erlydtl pkg_erlydtl_name = erlydtl pkg_erlydtl_description = Django Template Language for Erlang. pkg_erlydtl_homepage = https://github.com/erlydtl/erlydtl pkg_erlydtl_fetch = git pkg_erlydtl_repo = https://github.com/erlydtl/erlydtl pkg_erlydtl_commit = master PACKAGES += gpb pkg_gpb_name = gpb pkg_gpb_description = A Google Protobuf implementation for Erlang pkg_gpb_homepage = https://github.com/tomas-abrahamsson/gpb pkg_gpb_fetch = git pkg_gpb_repo = https://github.com/tomas-abrahamsson/gpb pkg_gpb_commit = master PACKAGES += gun pkg_gun_name = gun pkg_gun_description = Asynchronous SPDY, HTTP and Websocket client written in Erlang. pkg_gun_homepage = http//ninenines.eu pkg_gun_fetch = git pkg_gun_repo = https://github.com/ninenines/gun pkg_gun_commit = master PACKAGES += hex_core pkg_hex_core_name = hex_core pkg_hex_core_description = Reference implementation of Hex specifications pkg_hex_core_homepage = https://github.com/hexpm/hex_core pkg_hex_core_fetch = git HEX_CORE_GIT ?= https://github.com/hexpm/hex_core pkg_hex_core_repo = $(HEX_CORE_GIT) pkg_hex_core_commit = e57b4fb15cde710b3ae09b1d18f148f6999a63cc PACKAGES += proper pkg_proper_name = proper pkg_proper_description = PropEr: a QuickCheck-inspired property-based testing tool for Erlang. pkg_proper_homepage = http://proper.softlab.ntua.gr pkg_proper_fetch = git pkg_proper_repo = https://github.com/manopapad/proper pkg_proper_commit = master PACKAGES += ranch pkg_ranch_name = ranch pkg_ranch_description = Socket acceptor pool for TCP protocols. pkg_ranch_homepage = http://ninenines.eu pkg_ranch_fetch = git pkg_ranch_repo = https://github.com/ninenines/ranch pkg_ranch_commit = master PACKAGES += relx pkg_relx_name = relx pkg_relx_description = Sane, simple release creation for Erlang pkg_relx_homepage = https://github.com/erlware/relx pkg_relx_fetch = git pkg_relx_repo = https://github.com/erlware/relx pkg_relx_commit = main PACKAGES += triq pkg_triq_name = triq pkg_triq_description = Trifork QuickCheck pkg_triq_homepage = https://triq.gitlab.io pkg_triq_fetch = git pkg_triq_repo = https://gitlab.com/triq/triq.git pkg_triq_commit = master # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: search define pkg_print $(verbose) printf "%s\n" \ $(if $(call core_eq,$1,$(pkg_$(1)_name)),,"Pkg name: $1") \ "App name: $(pkg_$(1)_name)" \ "Description: $(pkg_$(1)_description)" \ "Home page: $(pkg_$(1)_homepage)" \ "Fetch with: $(pkg_$(1)_fetch)" \ "Repository: $(pkg_$(1)_repo)" \ "Commit: $(pkg_$(1)_commit)" \ "" endef search: ifdef q $(foreach p,$(PACKAGES), \ $(if $(findstring $(call core_lc,$q),$(call core_lc,$(pkg_$(p)_name) $(pkg_$(p)_description))), \ $(call pkg_print,$p))) else $(foreach p,$(PACKAGES),$(call pkg_print,$p)) endif # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-deps clean-tmp-deps.log # Configuration. ifdef OTP_DEPS $(warning The variable OTP_DEPS is deprecated in favor of LOCAL_DEPS.) endif IGNORE_DEPS ?= export IGNORE_DEPS APPS_DIR ?= $(CURDIR)/apps export APPS_DIR DEPS_DIR ?= $(CURDIR)/deps export DEPS_DIR REBAR_DEPS_DIR = $(DEPS_DIR) export REBAR_DEPS_DIR # When testing Erlang.mk and updating these, make sure # to delete test/test_rebar_git before running tests again. REBAR3_GIT ?= https://github.com/erlang/rebar3 REBAR3_COMMIT ?= bde4b54248d16280b2c70a244aca3bb7566e2033 # 3.23.0 CACHE_DEPS ?= 0 CACHE_DIR ?= $(if $(XDG_CACHE_HOME),$(XDG_CACHE_HOME),$(HOME)/.cache)/erlang.mk export CACHE_DIR HEX_CONFIG ?= define hex_config.erl begin Config0 = hex_core:default_config(), Config0$(HEX_CONFIG) end endef # External "early" plugins (see core/plugins.mk for regular plugins). # They both use the core_dep_plugin macro. define core_dep_plugin ifeq ($2,$(PROJECT)) -include $$(patsubst $(PROJECT)/%,%,$1) else -include $(DEPS_DIR)/$1 $(DEPS_DIR)/$1: $(DEPS_DIR)/$2 ; endif endef DEP_EARLY_PLUGINS ?= $(foreach p,$(DEP_EARLY_PLUGINS),\ $(eval $(if $(findstring /,$p),\ $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ $(call core_dep_plugin,$p/early-plugins.mk,$p)))) # Query functions. query_fetch_method = $(if $(dep_$(1)),$(call _qfm_dep,$(word 1,$(dep_$(1)))),$(call _qfm_pkg,$1)) _qfm_dep = $(if $(dep_fetch_$(1)),$1,fail) _qfm_pkg = $(if $(pkg_$(1)_fetch),$(pkg_$(1)_fetch),fail) query_name = $(if $(dep_$(1)),$1,$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$1)) query_repo = $(call _qr,$1,$(call query_fetch_method,$1)) _qr = $(if $(query_repo_$(2)),$(call query_repo_$(2),$1),$(call query_repo_git,$1)) query_repo_default = $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo)) query_repo_git = $(patsubst git://github.com/%,https://github.com/%,$(call query_repo_default,$1)) query_repo_git-subfolder = $(call query_repo_git,$1) query_repo_git-submodule = - query_repo_hg = $(call query_repo_default,$1) query_repo_svn = $(call query_repo_default,$1) query_repo_cp = $(call query_repo_default,$1) query_repo_ln = $(call query_repo_default,$1) query_repo_hex = https://hex.pm/packages/$(if $(word 3,$(dep_$(1))),$(word 3,$(dep_$(1))),$1) query_repo_fail = - query_version = $(call _qv,$1,$(call query_fetch_method,$1)) _qv = $(if $(query_version_$(2)),$(call query_version_$(2),$1),$(call query_version_default,$1)) query_version_default = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit))) query_version_git = $(call query_version_default,$1) query_version_git-subfolder = $(call query_version_default,$1) query_version_git-submodule = - query_version_hg = $(call query_version_default,$1) query_version_svn = - query_version_cp = - query_version_ln = - query_version_hex = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_commit))) query_version_fail = - query_extra = $(call _qe,$1,$(call query_fetch_method,$1)) _qe = $(if $(query_extra_$(2)),$(call query_extra_$(2),$1),-) query_extra_git = - query_extra_git-subfolder = $(if $(dep_$(1)),subfolder=$(word 4,$(dep_$(1))),-) query_extra_git-submodule = - query_extra_hg = - query_extra_svn = - query_extra_cp = - query_extra_ln = - query_extra_hex = $(if $(dep_$(1)),package-name=$(word 3,$(dep_$(1))),-) query_extra_fail = - query_absolute_path = $(addprefix $(DEPS_DIR)/,$(call query_name,$1)) # Deprecated legacy query function. Used by RabbitMQ and its third party plugins. # Can be removed once RabbitMQ has been updated and enough time has passed. dep_name = $(call query_name,$(1)) # Application directories. LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$a),$(APPS_DIR)/$a)) # Elixir is handled specially as it must be built before all other deps # when Mix autopatching is necessary. ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call query_name,$(dep)))) # When we are calling an app directly we don't want to include it here # otherwise it'll be treated both as an apps and a top-level project. ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d))) ifdef ROOT_DIR ifndef IS_APP ALL_APPS_DIRS := $(filter-out $(APPS_DIR)/$(notdir $(CURDIR)),$(ALL_APPS_DIRS)) endif endif ifeq ($(filter $(APPS_DIR) $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),) ifeq ($(ERL_LIBS),) ERL_LIBS = $(APPS_DIR):$(DEPS_DIR) else ERL_LIBS := $(ERL_LIBS):$(APPS_DIR):$(DEPS_DIR) endif endif export ERL_LIBS export NO_AUTOPATCH # Elixir. # Elixir is automatically enabled in all cases except when # an Erlang project uses an Elixir dependency. In that case # $(ELIXIR) must be set explicitly. ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,$(if $(EX_FILES),system,disable)) export ELIXIR # Verbosity. dep_verbose_0 = @echo " DEP $1 ($(call query_version,$1))"; dep_verbose_2 = set -x; dep_verbose = $(dep_verbose_$(V)) # Optimization: don't recompile deps unless truly necessary. ifndef IS_DEP ifneq ($(MAKELEVEL),0) $(shell rm -f ebin/dep_built) endif endif # Core targets. ALL_APPS_DIRS_TO_BUILD = $(if $(LOCAL_DEPS_DIRS)$(IS_APP),$(LOCAL_DEPS_DIRS),$(ALL_APPS_DIRS)) apps:: $(ALL_APPS_DIRS) clean-tmp-deps.log | $(ERLANG_MK_TMP) # Create ebin directory for all apps to make sure Erlang recognizes them # as proper OTP applications when using -include_lib. This is a temporary # fix, a proper fix would be to compile apps/* in the right order. ifndef IS_APP ifneq ($(ALL_APPS_DIRS),) $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ mkdir -p $$dep/ebin; \ done endif endif # At the toplevel: if LOCAL_DEPS is defined with at least one local app, only # compile that list of apps. Otherwise, compile everything. # Within an app: compile all LOCAL_DEPS that are (uncompiled) local apps. ifneq ($(ALL_APPS_DIRS_TO_BUILD),) $(verbose) set -e; for dep in $(ALL_APPS_DIRS_TO_BUILD); do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \ :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \ $(MAKE) -C $$dep $(if $(IS_TEST),test-build-app) IS_APP=1; \ fi \ done endif clean-tmp-deps.log: ifeq ($(IS_APP)$(IS_DEP),) $(verbose) rm -f $(ERLANG_MK_TMP)/apps.log $(ERLANG_MK_TMP)/deps.log endif # Erlang.mk does not rebuild dependencies after they were compiled # once. If a developer is working on the top-level project and some # dependencies at the same time, he may want to change this behavior. # There are two solutions: # 1. Set `FULL=1` so that all dependencies are visited and # recursively recompiled if necessary. # 2. Set `FORCE_REBUILD=` to the specific list of dependencies that # should be recompiled (instead of the whole set). FORCE_REBUILD ?= ifeq ($(origin FULL),undefined) ifneq ($(strip $(force_rebuild_dep)$(FORCE_REBUILD)),) define force_rebuild_dep echo "$(FORCE_REBUILD)" | grep -qw "$$(basename "$1")" endef endif endif ifneq ($(SKIP_DEPS),) deps:: else ALL_DEPS_DIRS_TO_BUILD = $(if $(filter-out $(DEPS_DIR)/elixir,$(ALL_DEPS_DIRS)),$(filter-out $(DEPS_DIR)/elixir,$(ALL_DEPS_DIRS)),$(ALL_DEPS_DIRS)) deps:: $(ALL_DEPS_DIRS_TO_BUILD) apps clean-tmp-deps.log | $(ERLANG_MK_TMP) ifneq ($(ALL_DEPS_DIRS_TO_BUILD),) $(verbose) set -e; for dep in $(ALL_DEPS_DIRS_TO_BUILD); do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ :; \ else \ echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ if [ -z "$(strip $(FULL))" ] $(if $(force_rebuild_dep),&& ! ($(call force_rebuild_dep,$$dep)),) && [ ! -L $$dep ] && [ -f $$dep/ebin/dep_built ]; then \ :; \ elif [ "$$dep" = "$(DEPS_DIR)/hut" -a "$(HUT_PATCH)" ]; then \ $(MAKE) -C $$dep app IS_DEP=1; \ if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \ elif [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ $(MAKE) -C $$dep IS_DEP=1; \ if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \ else \ echo "Error: No Makefile to build dependency $$dep." >&2; \ exit 2; \ fi \ fi \ done endif endif # Deps related targets. autopatch_verbose_0 = @echo " PATCH " $(subst autopatch-,,$@) "(method: $(AUTOPATCH_METHOD))"; autopatch_verbose_2 = set -x; autopatch_verbose = $(autopatch_verbose_$(V)) define dep_autopatch_detect if [ -f $(DEPS_DIR)/$1/erlang.mk ]; then \ echo erlang.mk; \ elif [ -f $(DEPS_DIR)/$1/mix.exs -a -d $(DEPS_DIR)/$1/lib ]; then \ if [ "$(ELIXIR)" != "disable" ]; then \ echo mix; \ elif [ -f $(DEPS_DIR)/$1/rebar.lock -o -f $(DEPS_DIR)/$1/rebar.config ]; then \ echo rebar3; \ elif [ -f $(DEPS_DIR)/$1/Makefile ]; then \ echo noop; \ else \ exit 99; \ fi \ elif [ -f $(DEPS_DIR)/$1/Makefile ]; then \ if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \ echo rebar3; \ elif [ 0 != \`grep -c "include ../\w*\.mk" $(DEPS_DIR)/$1/Makefile\` ]; then \ echo rebar3; \ elif [ 0 != \`grep -ci "^[^#].*rebar" $(DEPS_DIR)/$1/Makefile\` ]; then \ echo rebar3; \ elif [ -n "\`find $(DEPS_DIR)/$1/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;\`" ]; then \ echo rebar3; \ else \ echo noop; \ fi \ elif [ ! -d $(DEPS_DIR)/$1/src/ ]; then \ echo noop; \ else \ echo rebar3; \ fi endef define dep_autopatch_for_erlang.mk rm -rf $(DEPS_DIR)/$1/ebin/; \ $(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \ $(call dep_autopatch_erlang_mk,$1) endef define dep_autopatch_for_rebar3 ! test -f $(DEPS_DIR)/$1/ebin/$1.app || \ mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \ rm -f $(DEPS_DIR)/$1/ebin/$1.app; \ if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \ $(call erlang,$(call dep_autopatch_appsrc_script.erl,$1)); \ fi; \ $(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \ if [ -f $(DEPS_DIR)/$1/rebar -o -f $(DEPS_DIR)/$1/rebar.config -o -f $(DEPS_DIR)/$1/rebar.config.script -o -f $(DEPS_DIR)/$1/rebar.lock ]; then \ $(call dep_autopatch_fetch_rebar); \ $(call dep_autopatch_rebar,$1); \ else \ $(call dep_autopatch_gen,$1); \ fi endef define dep_autopatch_for_mix $(call dep_autopatch_mix,$1) endef define dep_autopatch_for_noop test -f $(DEPS_DIR)/$1/Makefile || printf "noop:\n" > $(DEPS_DIR)/$1/Makefile endef define maybe_flock if command -v flock >/dev/null; then \ flock $1 sh -c "$2"; \ elif command -v lockf >/dev/null; then \ lockf $1 sh -c "$2"; \ else \ $2; \ fi endef # Replace "include erlang.mk" with a line that will load the parent Erlang.mk # if given. Do it for all 3 possible Makefile file names. ifeq ($(NO_AUTOPATCH_ERLANG_MK),) define dep_autopatch_erlang_mk for f in Makefile makefile GNUmakefile; do \ if [ -f $(DEPS_DIR)/$1/$$f ]; then \ sed -i.bak s/'include *erlang.mk'/'include $$(if $$(ERLANG_MK_FILENAME),$$(ERLANG_MK_FILENAME),erlang.mk)'/ $(DEPS_DIR)/$1/$$f; \ fi \ done endef else define dep_autopatch_erlang_mk : endef endif define dep_autopatch_gen printf "%s\n" \ "ERLC_OPTS = +debug_info" \ "include ../../erlang.mk" > $(DEPS_DIR)/$1/Makefile endef # We use flock/lockf when available to avoid concurrency issues. define dep_autopatch_fetch_rebar $(call maybe_flock,$(ERLANG_MK_TMP)/rebar.lock,$(call dep_autopatch_fetch_rebar2)) endef define dep_autopatch_fetch_rebar2 if [ ! -d $(ERLANG_MK_TMP)/rebar3 ]; then \ git clone -q -n -- $(REBAR3_GIT) $(ERLANG_MK_TMP)/rebar3; \ cd $(ERLANG_MK_TMP)/rebar3; \ git checkout -q $(REBAR3_COMMIT); \ ./bootstrap; \ cd -; \ fi endef define dep_autopatch_rebar if [ -f $(DEPS_DIR)/$1/Makefile ]; then \ mv $(DEPS_DIR)/$1/Makefile $(DEPS_DIR)/$1/Makefile.orig.mk; \ fi; \ $(call erlang,$(call dep_autopatch_rebar.erl,$1)); \ rm -f $(DEPS_DIR)/$1/ebin/$1.app endef define dep_autopatch_rebar.erl application:load(rebar), application:set_env(rebar, log_level, debug), {module, rebar3} = c:l(rebar3), Conf1 = case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config)") of {ok, Conf0} -> Conf0; _ -> [] end, {Conf, OsEnv} = fun() -> case filelib:is_file("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)") of false -> {Conf1, []}; true -> Bindings0 = erl_eval:new_bindings(), Bindings1 = erl_eval:add_binding('CONFIG', Conf1, Bindings0), Bindings = erl_eval:add_binding('SCRIPT', "$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings1), Before = os:getenv(), {ok, Conf2} = file:script("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings), {Conf2, lists:foldl(fun(E, Acc) -> lists:delete(E, Acc) end, os:getenv(), Before)} end end(), Write = fun (Text) -> file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) end, Escape = fun (Text) -> re:replace(Text, "\\\\$$", "\$$$$", [global, {return, list}]) end, Write("IGNORE_DEPS += edown eper eunit_formatters meck node_package " "rebar_lock_deps_plugin rebar_vsn_plugin reltool_util\n"), Write("C_SRC_DIR = /path/do/not/exist\n"), Write("C_SRC_TYPE = rebar\n"), Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"), Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]), ToList = fun (V) when is_atom(V) -> atom_to_list(V); (V) when is_list(V) -> "'\\"" ++ V ++ "\\"'" end, fun() -> Write("ERLC_OPTS = +debug_info\n"), case lists:keyfind(erl_opts, 1, Conf) of false -> ok; {_, ErlOpts} -> lists:foreach(fun ({d, D}) -> Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n"); ({d, DKey, DVal}) -> Write("ERLC_OPTS += -D" ++ ToList(DKey) ++ "=" ++ ToList(DVal) ++ "\n"); ({i, I}) -> Write(["ERLC_OPTS += -I ", I, "\n"]); ({platform_define, Regex, D}) -> case rebar_utils:is_arch(Regex) of true -> Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n"); false -> ok end; ({parse_transform, PT}) -> Write("ERLC_OPTS += +'{parse_transform, " ++ ToList(PT) ++ "}'\n"); (_) -> ok end, ErlOpts) end, Write("\n") end(), GetHexVsn2 = fun(N, NP) -> case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.lock)") of {ok, Lock} -> LockPkgs = case lists:keyfind("1.2.0", 1, Lock) of {_, LP} -> LP; _ -> case lists:keyfind("1.1.0", 1, Lock) of {_, LP} -> LP; _ -> false end end, if is_list(LockPkgs) -> case lists:keyfind(atom_to_binary(N, latin1), 1, LockPkgs) of {_, {pkg, _, Vsn}, _} -> {N, {hex, NP, binary_to_list(Vsn)}}; _ -> false end; true -> false end; _ -> false end end, GetHexVsn3Common = fun(N, NP, S0) -> case GetHexVsn2(N, NP) of false -> S2 = case S0 of " " ++ S1 -> S1; _ -> S0 end, S = case length([ok || $$. <- S2]) of 0 -> S2 ++ ".0.0"; 1 -> S2 ++ ".0"; _ -> S2 end, {N, {hex, NP, S}}; NameSource -> NameSource end end, GetHexVsn3 = fun (N, NP, "~>" ++ S0) -> GetHexVsn3Common(N, NP, S0); (N, NP, ">=" ++ S0) -> GetHexVsn3Common(N, NP, S0); (N, NP, S) -> {N, {hex, NP, S}} end, ConvertCommit = fun ({branch, C}) -> C; ({ref, C}) -> C; ({tag, C}) -> C; (C) -> C end, fun() -> File = case lists:keyfind(deps, 1, Conf) of false -> []; {_, Deps} -> [begin case case Dep of N when is_atom(N) -> GetHexVsn2(N, N); {N, S} when is_atom(N), is_list(S) -> GetHexVsn3(N, N, S); {N, {pkg, NP}} when is_atom(N) -> GetHexVsn2(N, NP); {N, S, {pkg, NP}} -> GetHexVsn3(N, NP, S); {N, S} when is_tuple(S) -> {N, S}; {N, _, S} -> {N, S}; {N, _, S, _} -> {N, S}; _ -> false end of false -> ok; {Name, {git_subdir, Repo, Commit, SubDir}} -> Write(io_lib:format("DEPS += ~s\ndep_~s = git-subfolder ~s ~s ~s~n", [Name, Name, Repo, ConvertCommit(Commit), SubDir])); {Name, Source} -> {Method, Repo, Commit} = case Source of {hex, NPV, V} -> {hex, V, NPV}; {git, R} -> {git, R, master}; {M, R, C} -> {M, R, C} end, Write(io_lib:format("DEPS += ~s\ndep_~s = ~s ~s ~s~n", [Name, Name, Method, Repo, ConvertCommit(Commit)])) end end || Dep <- Deps] end end(), fun() -> case lists:keyfind(erl_first_files, 1, Conf) of false -> ok; {_, Files0} -> Files = [begin hd(filelib:wildcard("$(call core_native_path,$(DEPS_DIR)/$1/src/)**/" ++ filename:rootname(F) ++ ".*rl")) end || "src/" ++ F <- Files0], Names = [[" ", case lists:reverse(F) of "lre." ++ Elif -> lists:reverse(Elif); "lrx." ++ Elif -> lists:reverse(Elif); "lry." ++ Elif -> lists:reverse(Elif); Elif -> lists:reverse(Elif) end] || "$(call core_native_path,$(DEPS_DIR)/$1/src/)" ++ F <- Files], Write(io_lib:format("COMPILE_FIRST +=~s\n", [Names])) end end(), Write("\n\nrebar_dep: preprocess pre-deps deps pre-app app post-app\n"), Write("\npreprocess::\n"), Write("\npre-deps::\n"), Write("\npre-app::\n"), Write("\npost-app::\n"), PatchHook = fun(Cmd) -> Cmd2 = re:replace(Cmd, "^([g]?make)(.*)( -C.*)", "\\\\1\\\\3\\\\2", [{return, list}]), case Cmd2 of "make -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); "gmake -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); "make " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); "gmake " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); _ -> Escape(Cmd) end end, fun() -> case lists:keyfind(pre_hooks, 1, Conf) of false -> ok; {_, Hooks} -> [case H of {'get-deps', Cmd} -> Write("\npre-deps::\n\t" ++ PatchHook(Cmd) ++ "\n"); {compile, Cmd} -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); {{pc, compile}, Cmd} -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); {Regex, compile, Cmd} -> case rebar_utils:is_arch(Regex) of true -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); false -> ok end; _ -> ok end || H <- Hooks] end end(), fun() -> case lists:keyfind(post_hooks, 1, Conf) of false -> ok; {_, Hooks} -> [case H of {compile, Cmd} -> Write("\npost-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); {{pc, compile}, Cmd} -> Write("\npost-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); {Regex, compile, Cmd} -> case rebar_utils:is_arch(Regex) of true -> Write("\npost-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); false -> ok end; _ -> ok end || H <- Hooks] end end(), ShellToMk = fun(V0) -> V1 = re:replace(V0, "[$$][(]", "$$\(shell ", [global]), V = re:replace(V1, "([$$])(?![(])(\\\\w*)", "\\\\1(\\\\2)", [global]), re:replace(V, "-Werror\\\\b", "", [{return, list}, global]) end, PortSpecs = fun() -> case lists:keyfind(port_specs, 1, Conf) of false -> case filelib:is_dir("$(call core_native_path,$(DEPS_DIR)/$1/c_src)") of false -> []; true -> [{"priv/" ++ proplists:get_value(so_name, Conf, "$(1)_drv.so"), proplists:get_value(port_sources, Conf, ["c_src/*.c"]), []}] end; {_, Specs} -> lists:flatten([case S of {Output, Input} -> {ShellToMk(Output), Input, []}; {Regex, Output, Input} -> case rebar_utils:is_arch(Regex) of true -> {ShellToMk(Output), Input, []}; false -> [] end; {Regex, Output, Input, [{env, Env}]} -> case rebar_utils:is_arch(Regex) of true -> {ShellToMk(Output), Input, Env}; false -> [] end end || S <- Specs]) end end(), PortSpecWrite = fun (Text) -> file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/c_src/Makefile.erlang.mk)", Text, [append]) end, case PortSpecs of [] -> ok; _ -> Write("\npre-app::\n\t@$$\(MAKE) --no-print-directory -f c_src/Makefile.erlang.mk\n"), PortSpecWrite(io_lib:format("ERL_CFLAGS ?= -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), PortSpecWrite(io_lib:format("ERL_LDFLAGS ?= -L \\"~s\\" -lei\n", [code:lib_dir(erl_interface, lib)])), [PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv], FilterEnv = fun(Env) -> lists:flatten([case E of {_, _} -> E; {Regex, K, V} -> case rebar_utils:is_arch(Regex) of true -> {K, V}; false -> [] end end || E <- Env]) end, MergeEnv = fun(Env) -> lists:foldl(fun ({K, V}, Acc) -> case lists:keyfind(K, 1, Acc) of false -> [{K, rebar_utils:expand_env_variable(V, K, "")}|Acc]; {_, V0} -> [{K, rebar_utils:expand_env_variable(V, K, V0)}|Acc] end end, [], Env) end, PortEnv = case lists:keyfind(port_env, 1, Conf) of false -> []; {_, PortEnv0} -> FilterEnv(PortEnv0) end, PortSpec = fun ({Output, Input0, Env}) -> filelib:ensure_dir("$(call core_native_path,$(DEPS_DIR)/$1/)" ++ Output), Input = [[" ", I] || I <- Input0], PortSpecWrite([ [["\n", K, " = ", ShellToMk(V)] || {K, V} <- lists:reverse(MergeEnv(PortEnv))], case $(PLATFORM) of darwin -> "\n\nLDFLAGS += -flat_namespace -undefined suppress"; _ -> "" end, "\n\nall:: ", Output, "\n\t@:\n\n", "%.o: %.c\n\t$$\(CC) -c -o $$\@ $$\< $$\(CFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", [[Output, ": ", K, " += ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], Output, ": $$\(foreach ext,.c .C .cc .cpp,", "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(LDLIBS) $$\(EXE_LDFLAGS)", case {filename:extension(Output), $(PLATFORM)} of {[], _} -> "\n"; {".so", darwin} -> " -shared\n"; {".dylib", darwin} -> " -shared\n"; {_, darwin} -> "\n"; _ -> " -shared\n" end]) end, [PortSpec(S) || S <- PortSpecs] end, fun() -> case lists:keyfind(plugins, 1, Conf) of false -> ok; {_, Plugins0} -> Plugins = [P || P <- Plugins0, is_tuple(P)], case lists:keyfind('lfe-compile', 1, Plugins) of false -> ok; _ -> Write("\nBUILD_DEPS = lfe lfe.mk\ndep_lfe.mk = git https://github.com/ninenines/lfe.mk master\nDEP_PLUGINS = lfe.mk\n") end end end(), Write("\ninclude $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"), RunPlugin = fun(Plugin, Step) -> case erlang:function_exported(Plugin, Step, 2) of false -> ok; true -> c:cd("$(call core_native_path,$(DEPS_DIR)/$1/)"), Ret = Plugin:Step({config, "", Conf, dict:new(), dict:new(), dict:new(), dict:store(base_dir, "", dict:new())}, undefined), io:format("rebar plugin ~p step ~p ret ~p~n", [Plugin, Step, Ret]) end end, fun() -> case lists:keyfind(plugins, 1, Conf) of false -> ok; {_, Plugins0} -> Plugins = [P || P <- Plugins0, is_atom(P)], [begin case lists:keyfind(deps, 1, Conf) of false -> ok; {_, Deps} -> case lists:keyfind(P, 1, Deps) of false -> ok; _ -> Path = "$(call core_native_path,$(DEPS_DIR)/)" ++ atom_to_list(P), io:format("~s", [os:cmd("$(MAKE) -C $(call core_native_path,$(DEPS_DIR)/$1) " ++ Path)]), io:format("~s", [os:cmd("$(MAKE) -C " ++ Path ++ " IS_DEP=1")]), code:add_patha(Path ++ "/ebin") end end end || P <- Plugins], [case code:load_file(P) of {module, P} -> ok; _ -> case lists:keyfind(plugin_dir, 1, Conf) of false -> ok; {_, PluginsDir} -> ErlFile = "$(call core_native_path,$(DEPS_DIR)/$1/)" ++ PluginsDir ++ "/" ++ atom_to_list(P) ++ ".erl", {ok, P, Bin} = compile:file(ErlFile, [binary]), {module, P} = code:load_binary(P, ErlFile, Bin) end end || P <- Plugins], [RunPlugin(P, preprocess) || P <- Plugins], [RunPlugin(P, pre_compile) || P <- Plugins], [RunPlugin(P, compile) || P <- Plugins] end end(), halt() endef define dep_autopatch_appsrc_script.erl AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", AppSrcScript = AppSrc ++ ".script", Conf1 = case file:consult(AppSrc) of {ok, Conf0} -> Conf0; {error, enoent} -> [] end, Bindings0 = erl_eval:new_bindings(), Bindings1 = erl_eval:add_binding('CONFIG', Conf1, Bindings0), Bindings = erl_eval:add_binding('SCRIPT', AppSrcScript, Bindings1), Conf = case file:script(AppSrcScript, Bindings) of {ok, [C]} -> C; {ok, C} -> C end, ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])), halt() endef define dep_autopatch_appsrc.erl AppSrcOut = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", AppSrcIn = case filelib:is_regular(AppSrcOut) of false -> "$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"; true -> AppSrcOut end, case filelib:is_regular(AppSrcIn) of false -> ok; true -> {ok, [{application, $1, L0}]} = file:consult(AppSrcIn), L1 = lists:keystore(modules, 1, L0, {modules, []}), L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, lists:droplast(os:cmd("git -C $(DEPS_DIR)/$1 describe --dirty --tags --always"))}); {_, {cmd, _}} -> lists:keyreplace(vsn, 1, L1, {vsn, "cmd"}); _ -> L1 end, L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end, ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $1, L3}])), case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end end, halt() endef ifeq ($(CACHE_DEPS),1) define dep_cache_fetch_git mkdir -p $(CACHE_DIR)/git; \ if test -d "$(join $(CACHE_DIR)/git/,$(call query_name,$1))"; then \ cd $(join $(CACHE_DIR)/git/,$(call query_name,$1)); \ if ! git checkout -q $(call query_version,$1); then \ git remote set-url origin $(call query_repo_git,$1) && \ git pull --all && \ git cat-file -e $(call query_version_git,$1) 2>/dev/null; \ fi; \ else \ git clone -q -n -- $(call query_repo_git,$1) $(join $(CACHE_DIR)/git/,$(call query_name,$1)); \ fi; \ git clone -q --single-branch -- $(join $(CACHE_DIR)/git/,$(call query_name,$1)) $2; \ cd $2 && git checkout -q $(call query_version_git,$1) endef define dep_fetch_git $(call dep_cache_fetch_git,$1,$(DEPS_DIR)/$(call query_name,$1)); endef define dep_fetch_git-subfolder mkdir -p $(ERLANG_MK_TMP)/git-subfolder; \ $(call dep_cache_fetch_git,$1,$(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1)); \ ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1)/$(word 4,$(dep_$1)) \ $(DEPS_DIR)/$(call query_name,$1); endef else define dep_fetch_git git clone -q -n -- $(call query_repo_git,$1) $(DEPS_DIR)/$(call query_name,$1); \ cd $(DEPS_DIR)/$(call query_name,$1) && git checkout -q $(call query_version_git,$1); endef define dep_fetch_git-subfolder mkdir -p $(ERLANG_MK_TMP)/git-subfolder; \ git clone -q -n -- $(call query_repo_git-subfolder,$1) \ $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1); \ cd $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1) \ && git checkout -q $(call query_version_git-subfolder,$1); \ ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1)/$(word 4,$(dep_$1)) \ $(DEPS_DIR)/$(call query_name,$1); endef endif define dep_fetch_git-submodule git submodule update --init -- $(DEPS_DIR)/$1; endef define dep_fetch_hg hg clone -q -U $(call query_repo_hg,$1) $(DEPS_DIR)/$(call query_name,$1); \ cd $(DEPS_DIR)/$(call query_name,$1) && hg update -q $(call query_version_hg,$1); endef define dep_fetch_svn svn checkout -q $(call query_repo_svn,$1) $(DEPS_DIR)/$(call query_name,$1); endef define dep_fetch_cp cp -R $(call query_repo_cp,$1) $(DEPS_DIR)/$(call query_name,$1); endef define dep_fetch_ln ln -s $(call query_repo_ln,$1) $(DEPS_DIR)/$(call query_name,$1); endef define hex_get_tarball.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), case hex_repo:get_tarball(Config, <<"$1">>, <<"$(strip $2)">>) of {ok, {200, _, Tarball}} -> ok = file:write_file("$(call core_native_path,$3)", Tarball), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(79) end endef ifeq ($(CACHE_DEPS),1) # Hex only has a package version. No need to look in the Erlang.mk packages. define dep_fetch_hex mkdir -p $(CACHE_DIR)/hex $(DEPS_DIR)/$1; \ $(eval hex_pkg_name := $(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1)) \ $(eval hex_tar_name := $(hex_pkg_name)-$(strip $(word 2,$(dep_$1))).tar) \ $(if $(wildcard $(CACHE_DIR)/hex/$(hex_tar_name)),,\ $(call erlang,$(call hex_get_tarball.erl,$(hex_pkg_name),$(word 2,$(dep_$1)),$(CACHE_DIR)/hex/$(hex_tar_name)));) \ tar -xOf $(CACHE_DIR)/hex/$(hex_tar_name) contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; endef else # Hex only has a package version. No need to look in the Erlang.mk packages. define dep_fetch_hex mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \ $(call erlang,$(call hex_get_tarball.erl,$(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1),$(word 2,$(dep_$1)),$(ERLANG_MK_TMP)/hex/$1.tar)); \ tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -; endef endif define dep_fetch_fail echo "Error: Unknown or invalid dependency: $1." >&2; \ exit 78; endef define dep_target $(DEPS_DIR)/$(call query_name,$1): $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),$(if $(filter-out elixir,$1),$(DEPS_DIR)/elixir/ebin/dep_built)) $(if $(filter hex,$(call query_fetch_method,$1)),$(if $(wildcard $(DEPS_DIR)/$(call query_name,$1)),,$(DEPS_DIR)/hex_core/ebin/dep_built)) | $(ERLANG_MK_TMP) $(eval DEP_NAME := $(call query_name,$1)) $(eval DEP_STR := $(if $(filter $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)." >&2; \ exit 17; \ fi $(verbose) mkdir -p $(DEPS_DIR) $(dep_verbose) $(call dep_fetch_$(strip $(call query_fetch_method,$1)),$1) $(verbose) if [ -f $(DEPS_DIR)/$1/configure.ac -o -f $(DEPS_DIR)/$1/configure.in ] \ && [ ! -f $(DEPS_DIR)/$1/configure ]; then \ echo " AUTO " $(DEP_STR); \ cd $(DEPS_DIR)/$1 && autoreconf -Wall -vif -I m4; \ fi - $(verbose) if [ -f $(DEPS_DIR)/$(DEP_NAME)/configure ]; then \ echo " CONF " $(DEP_STR); \ cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \ fi ifeq ($(filter $1,$(NO_AUTOPATCH)),) $(verbose) AUTOPATCH_METHOD=`$(call dep_autopatch_detect,$1)`; \ if [ $$$$? -eq 99 ]; then \ echo "Elixir is currently disabled. Please set 'ELIXIR = system' in the Makefile to enable"; \ exit 99; \ fi; \ $$(MAKE) --no-print-directory autopatch-$(DEP_NAME) AUTOPATCH_METHOD=$$$$AUTOPATCH_METHOD endif .PHONY: autopatch-$(call query_name,$1) ifeq ($1,elixir) autopatch-elixir:: $$(verbose) ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/ else autopatch-$(call query_name,$1):: $$(autopatch_verbose) $$(call dep_autopatch_for_$(AUTOPATCH_METHOD),$(call query_name,$1)) endif endef # We automatically depend on hex_core when the project isn't already. $(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\ $(eval $(call dep_target,hex_core))) $(DEPS_DIR)/hex_core/ebin/dep_built: | $(ERLANG_MK_TMP) $(verbose) $(call maybe_flock,$(ERLANG_MK_TMP)/hex_core.lock,\ if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \ $(MAKE) $(DEPS_DIR)/hex_core; \ $(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \ touch $(DEPS_DIR)/hex_core/ebin/dep_built; \ fi) $(DEPS_DIR)/elixir/ebin/dep_built: | $(ERLANG_MK_TMP) $(verbose) $(call maybe_flock,$(ERLANG_MK_TMP)/elixir.lock,\ if [ ! -e $(DEPS_DIR)/elixir/ebin/dep_built ]; then \ $(MAKE) $(DEPS_DIR)/elixir; \ $(MAKE) -C $(DEPS_DIR)/elixir; \ touch $(DEPS_DIR)/elixir/ebin/dep_built; \ fi) $(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep)))) ifndef IS_APP clean:: clean-apps clean-apps: $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ $(MAKE) -C $$dep clean IS_APP=1; \ done distclean:: distclean-apps distclean-apps: $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \ $(MAKE) -C $$dep distclean IS_APP=1; \ done endif ifndef SKIP_DEPS distclean:: distclean-deps distclean-deps: $(gen_verbose) rm -rf $(DEPS_DIR) endif ifeq ($(CACHE_DEPS),1) cacheclean:: cacheclean-git cacheclean-hex cacheclean-git: $(gen_verbose) rm -rf $(CACHE_DIR)/git cacheclean-hex: $(gen_verbose) rm -rf $(CACHE_DIR)/hex endif # Forward-declare variables used in core/deps-tools.mk. This is required # in case plugins use them. ERLANG_MK_RECURSIVE_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-deps-list.log ERLANG_MK_RECURSIVE_DOC_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-doc-deps-list.log ERLANG_MK_RECURSIVE_REL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-rel-deps-list.log ERLANG_MK_RECURSIVE_TEST_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-test-deps-list.log ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-shell-deps-list.log ERLANG_MK_QUERY_DEPS_FILE = $(ERLANG_MK_TMP)/query-deps.log ERLANG_MK_QUERY_DOC_DEPS_FILE = $(ERLANG_MK_TMP)/query-doc-deps.log ERLANG_MK_QUERY_REL_DEPS_FILE = $(ERLANG_MK_TMP)/query-rel-deps.log ERLANG_MK_QUERY_TEST_DEPS_FILE = $(ERLANG_MK_TMP)/query-test-deps.log ERLANG_MK_QUERY_SHELL_DEPS_FILE = $(ERLANG_MK_TMP)/query-shell-deps.log # Copyright (c) 2024, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: beam-cache-restore-app beam-cache-restore-test clean-beam-cache distclean-beam-cache BEAM_CACHE_DIR ?= $(ERLANG_MK_TMP)/beam-cache PROJECT_BEAM_CACHE_DIR = $(BEAM_CACHE_DIR)/$(PROJECT) clean:: clean-beam-cache clean-beam-cache: $(verbose) rm -rf $(PROJECT_BEAM_CACHE_DIR) distclean:: distclean-beam-cache $(PROJECT_BEAM_CACHE_DIR): $(verbose) mkdir -p $(PROJECT_BEAM_CACHE_DIR) distclean-beam-cache: $(gen_verbose) rm -rf $(BEAM_CACHE_DIR) beam-cache-restore-app: | $(PROJECT_BEAM_CACHE_DIR) $(verbose) rm -rf $(PROJECT_BEAM_CACHE_DIR)/ebin-test ifneq ($(wildcard ebin/),) $(verbose) mv ebin/ $(PROJECT_BEAM_CACHE_DIR)/ebin-test endif ifneq ($(wildcard $(PROJECT_BEAM_CACHE_DIR)/ebin-app),) $(gen_verbose) mv $(PROJECT_BEAM_CACHE_DIR)/ebin-app ebin/ else $(verbose) $(MAKE) --no-print-directory clean-app endif beam-cache-restore-test: | $(PROJECT_BEAM_CACHE_DIR) $(verbose) rm -rf $(PROJECT_BEAM_CACHE_DIR)/ebin-app ifneq ($(wildcard ebin/),) $(verbose) mv ebin/ $(PROJECT_BEAM_CACHE_DIR)/ebin-app endif ifneq ($(wildcard $(PROJECT_BEAM_CACHE_DIR)/ebin-test),) $(gen_verbose) mv $(PROJECT_BEAM_CACHE_DIR)/ebin-test ebin/ else $(verbose) $(MAKE) --no-print-directory clean-app endif # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-app # Configuration. ERLC_OPTS ?= -Werror +debug_info +warn_export_vars +warn_shadow_vars \ +warn_obsolete_guard # +bin_opt_info +warn_export_all +warn_missing_spec COMPILE_FIRST ?= COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) ERLC_EXCLUDE ?= ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) ERLC_ASN1_OPTS ?= ERLC_MIB_OPTS ?= COMPILE_MIB_FIRST ?= COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) # Verbosity. app_verbose_0 = @echo " APP " $(PROJECT); app_verbose_2 = set -x; app_verbose = $(app_verbose_$(V)) appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; appsrc_verbose_2 = set -x; appsrc_verbose = $(appsrc_verbose_$(V)) makedep_verbose_0 = @echo " DEPEND" $(PROJECT).d; makedep_verbose_2 = set -x; makedep_verbose = $(makedep_verbose_$(V)) erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ $(filter %.erl %.core,$(?F))); erlc_verbose_2 = set -x; erlc_verbose = $(erlc_verbose_$(V)) xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); xyrl_verbose_2 = set -x; xyrl_verbose = $(xyrl_verbose_$(V)) asn1_verbose_0 = @echo " ASN1 " $(filter %.asn1,$(?F)); asn1_verbose_2 = set -x; asn1_verbose = $(asn1_verbose_$(V)) mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); mib_verbose_2 = set -x; mib_verbose = $(mib_verbose_$(V)) ifneq ($(wildcard src/)$(wildcard lib/),) # Targets. app:: $(if $(wildcard ebin/test),beam-cache-restore-app) deps $(verbose) $(MAKE) --no-print-directory $(PROJECT).d $(verbose) $(MAKE) --no-print-directory app-build PROJECT_MOD := $(if $(PROJECT_MOD),$(PROJECT_MOD),$(if $(wildcard src/$(PROJECT)_app.erl),$(PROJECT)_app)) define app_file {application, '$(PROJECT)', [ {description, "$(PROJECT_DESCRIPTION)"}, {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), {id$(comma)$(space)"$1"}$(comma)) {modules, [$(call comma_list,$2)]}, {registered, [$(if $(PROJECT_MOD),$(call comma_list,$(if $(filter $(PROJECT_MOD),$(PROJECT)_app),$(PROJECT)_sup) $(PROJECT_REGISTERED)))]}, {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]}, {optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},$(if $(PROJECT_MOD), {mod$(comma)$(space){$(patsubst %,'%',$(PROJECT_MOD))$(comma)$(space)[]}}$(comma)) {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) ]}. endef app-build: ebin/$(PROJECT).app $(verbose) : # Source files. ALL_SRC_FILES := $(sort $(call core_find,src/,*)) ERL_FILES := $(filter %.erl,$(ALL_SRC_FILES)) CORE_FILES := $(filter %.core,$(ALL_SRC_FILES)) ALL_LIB_FILES := $(sort $(call core_find,lib/,*)) EX_FILES := $(filter-out lib/mix/%,$(filter %.ex,$(ALL_SRC_FILES) $(ALL_LIB_FILES))) # ASN.1 files. ifneq ($(wildcard asn1/),) ASN1_FILES = $(sort $(call core_find,asn1/,*.asn1)) ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) define compile_asn1 $(verbose) mkdir -p include/ $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $1 $(verbose) mv asn1/*.erl src/ -$(verbose) mv asn1/*.hrl include/ $(verbose) mv asn1/*.asn1db include/ endef $(PROJECT).d:: $(ASN1_FILES) $(if $(strip $?),$(call compile_asn1,$?)) endif # SNMP MIB files. ifneq ($(wildcard mibs/),) MIB_FILES = $(sort $(call core_find,mibs/,*.mib)) $(PROJECT).d:: $(COMPILE_MIB_FIRST_PATHS) $(MIB_FILES) $(verbose) mkdir -p include/ priv/mibs/ $(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ -I priv/mibs/ $? $(mib_verbose) erlc -o include/ -- $(addprefix priv/mibs/,$(patsubst %.mib,%.bin,$(notdir $?))) endif # Leex and Yecc files. XRL_FILES := $(filter %.xrl,$(ALL_SRC_FILES)) XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES)))) ERL_FILES += $(XRL_ERL_FILES) YRL_FILES := $(filter %.yrl,$(ALL_SRC_FILES)) YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES)))) ERL_FILES += $(YRL_ERL_FILES) $(PROJECT).d:: $(XRL_FILES) $(YRL_FILES) $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $(YRL_ERLC_OPTS) $?) # Erlang and Core Erlang files. define makedep.erl E = ets:new(makedep, [bag]), G = digraph:new([acyclic]), ErlFiles = lists:usort(string:tokens("$(ERL_FILES)", " ")), DepsDir = "$(call core_native_path,$(DEPS_DIR))", AppsDir = "$(call core_native_path,$(APPS_DIR))", DepsDirsSrc = "$(if $(wildcard $(DEPS_DIR)/*/src), $(call core_native_path,$(wildcard $(DEPS_DIR)/*/src)))", DepsDirsInc = "$(if $(wildcard $(DEPS_DIR)/*/include), $(call core_native_path,$(wildcard $(DEPS_DIR)/*/include)))", AppsDirsSrc = "$(if $(wildcard $(APPS_DIR)/*/src), $(call core_native_path,$(wildcard $(APPS_DIR)/*/src)))", AppsDirsInc = "$(if $(wildcard $(APPS_DIR)/*/include), $(call core_native_path,$(wildcard $(APPS_DIR)/*/include)))", DepsDirs = lists:usort(string:tokens(DepsDirsSrc++DepsDirsInc, " ")), AppsDirs = lists:usort(string:tokens(AppsDirsSrc++AppsDirsInc, " ")), Modules = [{list_to_atom(filename:basename(F, ".erl")), F} || F <- ErlFiles], Add = fun (Mod, Dep) -> case lists:keyfind(Dep, 1, Modules) of false -> ok; {_, DepFile} -> {_, ModFile} = lists:keyfind(Mod, 1, Modules), ets:insert(E, {ModFile, DepFile}), digraph:add_vertex(G, Mod), digraph:add_vertex(G, Dep), digraph:add_edge(G, Mod, Dep) end end, AddHd = fun (F, Mod, DepFile) -> case file:open(DepFile, [read]) of {error, enoent} -> ok; {ok, Fd} -> {_, ModFile} = lists:keyfind(Mod, 1, Modules), case ets:match(E, {ModFile, DepFile}) of [] -> ets:insert(E, {ModFile, DepFile}), F(F, Fd, Mod,0); _ -> ok end end end, SearchHrl = fun F(_Hrl, []) -> {error,enoent}; F(Hrl, [Dir|Dirs]) -> HrlF = filename:join([Dir,Hrl]), case filelib:is_file(HrlF) of true -> {ok, HrlF}; false -> F(Hrl,Dirs) end end, Attr = fun (_F, Mod, behavior, Dep) -> Add(Mod, Dep); (_F, Mod, behaviour, Dep) -> Add(Mod, Dep); (_F, Mod, compile, {parse_transform, Dep}) -> Add(Mod, Dep); (_F, Mod, compile, Opts) when is_list(Opts) -> case proplists:get_value(parse_transform, Opts) of undefined -> ok; Dep -> Add(Mod, Dep) end; (F, Mod, include, Hrl) -> case SearchHrl(Hrl, ["src", "include",AppsDir,DepsDir]++AppsDirs++DepsDirs) of {ok, FoundHrl} -> AddHd(F, Mod, FoundHrl); {error, _} -> false end; (F, Mod, include_lib, Hrl) -> case SearchHrl(Hrl, ["src", "include",AppsDir,DepsDir]++AppsDirs++DepsDirs) of {ok, FoundHrl} -> AddHd(F, Mod, FoundHrl); {error, _} -> false end; (F, Mod, import, {Imp, _}) -> IsFile = case lists:keyfind(Imp, 1, Modules) of false -> false; {_, FilePath} -> filelib:is_file(FilePath) end, case IsFile of false -> ok; true -> Add(Mod, Imp) end; (_, _, _, _) -> ok end, MakeDepend = fun (F, Fd, Mod, StartLocation) -> case io:parse_erl_form(Fd, undefined, StartLocation) of {ok, AbsData, EndLocation} -> case AbsData of {attribute, _, Key, Value} -> Attr(F, Mod, Key, Value), F(F, Fd, Mod, EndLocation); _ -> F(F, Fd, Mod, EndLocation) end; {eof, _ } -> file:close(Fd); {error, ErrorDescription } -> file:close(Fd); {error, ErrorInfo, ErrorLocation} -> F(F, Fd, Mod, ErrorLocation) end, ok end, [begin Mod = list_to_atom(filename:basename(F, ".erl")), case file:open(F, [read]) of {ok, Fd} -> MakeDepend(MakeDepend, Fd, Mod,0); {error, enoent} -> ok end end || F <- ErlFiles], Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))), CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)], TargetPath = fun(Target) -> case lists:keyfind(Target, 1, Modules) of false -> ""; {_, DepFile} -> DirSubname = tl(string:tokens(filename:dirname(DepFile), "/")), string:join(DirSubname ++ [atom_to_list(Target)], "/") end end, Output0 = [ "# Generated by Erlang.mk. Edit at your own risk!\n\n", [[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend], "\nCOMPILE_FIRST +=", [[" ", TargetPath(CF)] || CF <- CompileFirst], "\n" ], Output = case "é" of [233] -> unicode:characters_to_binary(Output0); _ -> Output0 end, ok = file:write_file("$1", Output), halt() endef ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) $(PROJECT).d:: $(ERL_FILES) $(EX_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST) # Rebuild everything when the .d file does not exist. # We touch $@ to make sure the command doesn't fail in empty projects. # The file will be generated with content immediately after. $(verbose) if ! test -e $@; then \ touch $@ $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES); \ fi $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) endif ifeq ($(IS_APP)$(IS_DEP),) ifneq ($(words $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES) $(EX_FILES)),0) # Rebuild everything when the Makefile changes. $(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST) | $(ERLANG_MK_TMP) $(verbose) if test -f $@; then \ touch $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES) $(EX_FILES); \ touch -c $(PROJECT).d; \ fi $(verbose) touch $@ $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change endif endif $(PROJECT).d:: $(verbose) : include $(wildcard $(PROJECT).d) ebin/$(PROJECT).app:: ebin/ ebin/: $(verbose) mkdir -p ebin/ define compile_erl $(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \ -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $1) endef define validate_app_file case file:consult("ebin/$(PROJECT).app") of {ok, _} -> halt(); _ -> halt(1) end endef ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) $(EX_FILES) $(eval FILES_TO_COMPILE := $(filter-out $(EX_FILES) src/$(PROJECT).app.src,$?)) $(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE))) $(if $(filter $(ELIXIR),disable),,$(if $(filter $?,$(EX_FILES)),$(elixirc_verbose) $(eval MODULES := $(shell $(call erlang,$(call compile_ex.erl,$(EX_FILES))))))) $(eval ELIXIR_COMP_FAILED := $(if $(filter _ERROR_,$(firstword $(MODULES))),true,false)) # Older git versions do not have the --first-parent flag. Do without in that case. $(verbose) if $(ELIXIR_COMP_FAILED); then exit 1; fi $(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null \ || git describe --dirty --abbrev=7 --tags --always 2>/dev/null || true)) $(eval MODULES := $(MODULES) $(patsubst %,'%',$(sort $(notdir $(basename \ $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) ifeq ($(wildcard src/$(PROJECT).app.src),) $(app_verbose) printf '$(subst %,%%,$(subst $(newline),\n,$(subst ','\'',$(call app_file,$(GITDESCRIBE),$(MODULES)))))' \ > ebin/$(PROJECT).app $(verbose) if ! $(call erlang,$(call validate_app_file)); then \ echo "The .app file produced is invalid. Please verify the value of PROJECT_ENV." >&2; \ exit 1; \ fi else $(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \ echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk documentation for instructions." >&2; \ exit 1; \ fi $(appsrc_verbose) cat src/$(PROJECT).app.src \ | sed "s/{[[:space:]]*modules[[:space:]]*,[[:space:]]*\[\]}/{modules, \[$(call comma_list,$(MODULES))\]}/" \ | sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ > ebin/$(PROJECT).app endif ifneq ($(wildcard src/$(PROJECT).appup),) $(verbose) cp src/$(PROJECT).appup ebin/ endif clean:: clean-app clean-app: $(gen_verbose) rm -rf $(PROJECT).d ebin/ priv/mibs/ $(XRL_ERL_FILES) $(YRL_ERL_FILES) \ $(addprefix include/,$(patsubst %.mib,%.hrl,$(notdir $(MIB_FILES)))) \ $(addprefix include/,$(patsubst %.asn1,%.hrl,$(notdir $(ASN1_FILES)))) \ $(addprefix include/,$(patsubst %.asn1,%.asn1db,$(notdir $(ASN1_FILES)))) \ $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) endif # Copyright (c) 2024, Tyler Hughes # Copyright (c) 2024, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(ELIXIR),system) # We expect 'elixir' to be on the path. ELIXIR_BIN ?= $(shell readlink -f `which elixir`) ELIXIR_LIBS ?= $(abspath $(dir $(ELIXIR_BIN))/../lib) # Fallback in case 'elixir' is a shim. ifeq ($(wildcard $(ELIXIR_LIBS)/elixir/),) ELIXIR_LIBS = $(abspath $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))')/../) endif ELIXIR_LIBS := $(ELIXIR_LIBS) export ELIXIR_LIBS ERL_LIBS := $(ERL_LIBS):$(ELIXIR_LIBS) else ifeq ($(ELIXIR),dep) ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR)/elixir/lib/ endif endif elixirc_verbose_0 = @echo " EXC $(words $(EX_FILES)) files"; elixirc_verbose_2 = set -x; elixirc_verbose = $(elixirc_verbose_$(V)) # Unfortunately this currently requires Elixir. # https://github.com/jelly-beam/verl is a good choice # for an Erlang implementation, but we already have to # pull hex_core and Rebar3 so adding yet another pull # is annoying, especially one that would be necessary # every time we autopatch Rebar projects. Wait and see. define hex_version_resolver.erl HexVersionResolve = fun(Name, Req) -> application:ensure_all_started(ssl), application:ensure_all_started(inets), Config = $(hex_config.erl), case hex_repo:get_package(Config, atom_to_binary(Name)) of {ok, {200, _RespHeaders, Package}} -> #{releases := List} = Package, {value, #{version := Version}} = lists:search(fun(#{version := Vsn}) -> M = list_to_atom("Elixir.Version"), F = list_to_atom("match?"), M:F(Vsn, Req) end, List), {ok, Version}; {ok, {Status, _, Errors}} -> {error, Status, Errors} end end, HexVersionResolveAndPrint = fun(Name, Req) -> case HexVersionResolve(Name, Req) of {ok, Version} -> io:format("~s", [Version]), halt(0); {error, Status, Errors} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(77) end end endef define dep_autopatch_mix.erl $(call hex_version_resolver.erl), {ok, _} = application:ensure_all_started(elixir), {ok, _} = application:ensure_all_started(mix), MixFile = <<"$(call core_native_path,$(DEPS_DIR)/$1/mix.exs)">>, {Mod, Bin} = case elixir_compiler:file(MixFile, fun(_File, _LexerPid) -> ok end) of [{T = {_, _}, _CheckerPid}] -> T; [T = {_, _}] -> T end, {module, Mod} = code:load_binary(Mod, binary_to_list(MixFile), Bin), Project = Mod:project(), Application = try Mod:application() catch error:undef -> [] end, StartMod = case lists:keyfind(mod, 1, Application) of {mod, {StartMod0, _StartArgs}} -> atom_to_list(StartMod0); _ -> "" end, Write = fun (Text) -> file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) end, Write([ "PROJECT = ", atom_to_list(proplists:get_value(app, Project)), "\n" "PROJECT_DESCRIPTION = ", proplists:get_value(description, Project, ""), "\n" "PROJECT_VERSION = ", proplists:get_value(version, Project, ""), "\n" "PROJECT_MOD = ", StartMod, "\n" "define PROJECT_ENV\n", io_lib:format("~p", [proplists:get_value(env, Application, [])]), "\n" "endef\n\n"]), ExtraApps = lists:usort([eex, elixir, logger, mix] ++ proplists:get_value(extra_applications, Application, [])), Write(["LOCAL_DEPS += ", lists:join(" ", [atom_to_list(App) || App <- ExtraApps]), "\n\n"]), Deps = proplists:get_value(deps, Project, []) -- [elixir_make], IsRequiredProdDep = fun(Opts) -> (proplists:get_value(optional, Opts) =/= true) andalso case proplists:get_value(only, Opts, prod) of prod -> true; L when is_list(L) -> lists:member(prod, L); _ -> false end end, lists:foreach(fun ({Name, Req}) when is_binary(Req) -> {ok, Vsn} = HexVersionResolve(Name, Req), Write(["DEPS += ", atom_to_list(Name), "\n"]), Write(["dep_", atom_to_list(Name), " = hex ", Vsn, " ", atom_to_list(Name), "\n"]); ({Name, Opts}) when is_list(Opts) -> Path = proplists:get_value(path, Opts), case IsRequiredProdDep(Opts) of true when Path =/= undefined -> Write(["DEPS += ", atom_to_list(Name), "\n"]), Write(["dep_", atom_to_list(Name), " = ln ", Path, "\n"]); true when Path =:= undefined -> Write(["DEPS += ", atom_to_list(Name), "\n"]), io:format(standard_error, "Warning: No version given for ~p.", [Name]); false -> ok end; ({Name, Req, Opts}) -> case IsRequiredProdDep(Opts) of true -> {ok, Vsn} = HexVersionResolve(Name, Req), Write(["DEPS += ", atom_to_list(Name), "\n"]), Write(["dep_", atom_to_list(Name), " = hex ", Vsn, " ", atom_to_list(Name), "\n"]); false -> ok end; (_) -> ok end, Deps), case lists:member(elixir_make, proplists:get_value(compilers, Project, [])) of false -> ok; true -> Write("# https://hexdocs.pm/elixir_make/Mix.Tasks.Compile.ElixirMake.html\n"), MakeVal = fun(Key, Proplist, DefaultVal, DefaultReplacement) -> case proplists:get_value(Key, Proplist, DefaultVal) of DefaultVal -> DefaultReplacement; Value -> Value end end, MakeMakefile = binary_to_list(MakeVal(make_makefile, Project, default, <<"Makefile">>)), MakeExe = MakeVal(make_executable, Project, default, "$$\(MAKE)"), MakeCwd = MakeVal(make_cwd, Project, undefined, <<".">>), MakeTargets = MakeVal(make_targets, Project, [], []), MakeArgs = MakeVal(make_args, Project, undefined, []), case file:rename("$(DEPS_DIR)/$1/" ++ MakeMakefile, "$(DEPS_DIR)/$1/elixir_make.mk") of ok -> ok; Err = {error, _} -> io:format(standard_error, "Failed to copy Makefile with error ~p~n", [Err]), halt(90) end, Write(["app::\n" "\t", MakeExe, " -C ", MakeCwd, " -f $(DEPS_DIR)/$1/elixir_make.mk", lists:join(" ", MakeTargets), lists:join(" ", MakeArgs), "\n\n"]), case MakeVal(make_clean, Project, nil, undefined) of undefined -> ok; Clean -> Write(["clean::\n\t", Clean, "\n\n"]) end end, Write("ERLC_OPTS = +debug_info\n\n"), Write("include $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"), halt() endef define dep_autopatch_mix sed 's|\(defmodule.*do\)|\1\n try do\n Code.compiler_options(on_undefined_variable: :warn)\n rescue _ -> :ok\n end\n|g' $(DEPS_DIR)/$(1)/mix.exs > $(DEPS_DIR)/$(1)/mix.exs.new; \ mv $(DEPS_DIR)/$(1)/mix.exs.new $(DEPS_DIR)/$(1)/mix.exs; \ $(MAKE) $(DEPS_DIR)/hex_core/ebin/dep_built; \ MIX_ENV="$(if $(MIX_ENV),$(strip $(MIX_ENV)),prod)" \ $(call erlang,$(call dep_autopatch_mix.erl,$1)) endef # We change the group leader so the Elixir io:format output # isn't captured as we need to either print the modules on # success, or print _ERROR_ on failure. define compile_ex.erl {ok, _} = application:ensure_all_started(elixir), {ok, _} = application:ensure_all_started(mix), $(foreach dep,$(LOCAL_DEPS),_ = application:load($(dep)),) ModCode = list_to_atom("Elixir.Code"), ModCode:put_compiler_option(ignore_module_conflict, true), ModComp = list_to_atom("Elixir.Kernel.ParallelCompiler"), ModMixProject = list_to_atom("Elixir.Mix.Project"), erlang:group_leader(whereis(standard_error), self()), ModMixProject:in_project($(PROJECT), ".", [], fun(_MixFile) -> case ModComp:compile_to_path([$(call comma_list,$(patsubst %,<<"%">>,$1))], <<"ebin/">>) of {ok, Modules, _} -> lists:foreach(fun(E) -> io:format(user, "~p ", [E]) end, Modules), halt(0); {error, _ErroredModules, _WarnedModules} -> io:format(user, "_ERROR_", []), halt(1) end end) endef # Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2015, Viktor Söderqvist # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: docs-deps # Configuration. ALL_DOC_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DOC_DEPS)) # Targets. $(foreach dep,$(DOC_DEPS),$(eval $(call dep_target,$(dep)))) ifneq ($(SKIP_DEPS),) doc-deps: else doc-deps: $(ALL_DOC_DEPS_DIRS) $(verbose) set -e; for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done endif # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: rel-deps # Configuration. ALL_REL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(REL_DEPS)) # Targets. $(foreach dep,$(REL_DEPS),$(eval $(call dep_target,$(dep)))) ifneq ($(SKIP_DEPS),) rel-deps: else rel-deps: $(ALL_REL_DEPS_DIRS) $(verbose) set -e; for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done endif # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: test-deps test-dir test-build clean-test-dir # Configuration. TEST_DIR ?= $(CURDIR)/test ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard TEST_ERLC_OPTS += -DTEST=1 # Targets. $(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) ifneq ($(SKIP_DEPS),) test-deps: else test-deps: $(ALL_TEST_DEPS_DIRS) $(verbose) set -e; for dep in $(ALL_TEST_DEPS_DIRS) ; do \ if [ -z "$(strip $(FULL))" ] && [ ! -L $$dep ] && [ -f $$dep/ebin/dep_built ]; then \ :; \ else \ $(MAKE) -C $$dep IS_DEP=1; \ if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \ fi \ done endif ifneq ($(wildcard $(TEST_DIR)),) test-dir: $(ERLANG_MK_TMP)/$(PROJECT).last-testdir-build @: test_erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ $(filter %.erl %.core,$(notdir $(FILES_TO_COMPILE)))); test_erlc_verbose_2 = set -x; test_erlc_verbose = $(test_erlc_verbose_$(V)) define compile_test_erl $(test_erlc_verbose) erlc -v $(TEST_ERLC_OPTS) -o $(TEST_DIR) \ -pa ebin/ -I include/ $1 endef ERL_TEST_FILES = $(call core_find,$(TEST_DIR)/,*.erl) $(ERLANG_MK_TMP)/$(PROJECT).last-testdir-build: $(ERL_TEST_FILES) $(MAKEFILE_LIST) # When we have to recompile files in src/ the .d file always gets rebuilt. # Therefore we want to ignore it when rebuilding test files. $(eval FILES_TO_COMPILE := $(if $(filter $(filter-out $(PROJECT).d,$(MAKEFILE_LIST)),$?),$(filter $(ERL_TEST_FILES),$^),$(filter $(ERL_TEST_FILES),$?))) $(if $(strip $(FILES_TO_COMPILE)),$(call compile_test_erl,$(FILES_TO_COMPILE)) && touch $@) endif test-build:: IS_TEST=1 test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) test-build:: $(if $(wildcard src),$(if $(wildcard ebin/test),,beam-cache-restore-test)) $(if $(IS_APP),,deps test-deps) # We already compiled everything when IS_APP=1. ifndef IS_APP ifneq ($(wildcard src),) $(verbose) $(MAKE) --no-print-directory $(PROJECT).d ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))" $(verbose) $(MAKE) --no-print-directory app-build ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))" $(gen_verbose) touch ebin/test endif ifneq ($(wildcard $(TEST_DIR)),) $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))" endif endif # Roughly the same as test-build, but when IS_APP=1. # We only care about compiling the current application. ifdef IS_APP test-build-app:: ERLC_OPTS=$(TEST_ERLC_OPTS) test-build-app:: deps test-deps ifneq ($(wildcard src),) $(verbose) $(MAKE) --no-print-directory $(PROJECT).d ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))" $(verbose) $(MAKE) --no-print-directory app-build ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))" $(gen_verbose) touch ebin/test endif ifneq ($(wildcard $(TEST_DIR)),) $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))" endif endif clean:: clean-test-dir clean-test-dir: ifneq ($(wildcard $(TEST_DIR)/*.beam),) $(gen_verbose) rm -f $(TEST_DIR)/*.beam $(ERLANG_MK_TMP)/$(PROJECT).last-testdir-build endif # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: rebar.config compat_ref = {$(shell (git -C $(DEPS_DIR)/$1 show-ref -q --verify "refs/heads/$2" && echo branch) || (git -C $(DEPS_DIR)/$1 show-ref -q --verify "refs/tags/$2" && echo tag) || echo ref),"$2"} # We strip out -Werror because we don't want to fail due to # warnings when used as a dependency. compat_prepare_erlc_opts = $(shell echo "$1" | sed 's/, */,/g') define compat_convert_erlc_opts $(if $(filter-out -Werror,$1),\ $(if $(findstring +,$1),\ $(shell echo $1 | cut -b 2-))) endef define compat_erlc_opts_to_list [$(call comma_list,$(foreach o,$(call compat_prepare_erlc_opts,$1),$(call compat_convert_erlc_opts,$o)))] endef define compat_rebar_config {deps, [ $(call comma_list,$(foreach d,$(DEPS),\ $(if $(filter hex,$(call query_fetch_method,$d)),\ {$(call query_name,$d)$(comma)"$(call query_version_hex,$d)"},\ {$(call query_name,$d)$(comma)".*"$(comma){git,"$(call query_repo,$d)"$(comma)$(call compat_ref,$(call query_name,$d),$(call query_version,$d))}}))) ]}. {erl_opts, $(call compat_erlc_opts_to_list,$(ERLC_OPTS))}. endef rebar.config: deps $(gen_verbose) $(call core_render,compat_rebar_config,rebar.config) define tpl_application.app.src {application, project_name, [ {description, ""}, {vsn, "0.1.0"}, {id, "git"}, {modules, []}, {registered, []}, {applications, [ kernel, stdlib ]}, {mod, {project_name_app, []}}, {env, []} ]}. endef define tpl_application -module(project_name_app). -behaviour(application). -export([start/2]). -export([stop/1]). start(_Type, _Args) -> project_name_sup:start_link(). stop(_State) -> ok. endef define tpl_apps_Makefile PROJECT = project_name PROJECT_DESCRIPTION = New project PROJECT_VERSION = 0.1.0 template_sp # Make sure we know where the applications are located. ROOT_DIR ?= rel_root_dir APPS_DIR ?= .. DEPS_DIR ?= rel_deps_dir include rel_root_dir/erlang.mk endef define tpl_cowboy_http_h -module(template_name). -behaviour(cowboy_http_handler). -export([init/3]). -export([handle/2]). -export([terminate/3]). -record(state, { }). init(_, Req, _Opts) -> {ok, Req, #state{}}. handle(Req, State=#state{}) -> {ok, Req2} = cowboy_req:reply(200, Req), {ok, Req2, State}. terminate(_Reason, _Req, _State) -> ok. endef define tpl_cowboy_loop_h -module(template_name). -behaviour(cowboy_loop_handler). -export([init/3]). -export([info/3]). -export([terminate/3]). -record(state, { }). init(_, Req, _Opts) -> {loop, Req, #state{}, 5000, hibernate}. info(_Info, Req, State) -> {loop, Req, State, hibernate}. terminate(_Reason, _Req, _State) -> ok. endef define tpl_cowboy_rest_h -module(template_name). -export([init/3]). -export([content_types_provided/2]). -export([get_html/2]). init(_, _Req, _Opts) -> {upgrade, protocol, cowboy_rest}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"html">>, '*'}, get_html}], Req, State}. get_html(Req, State) -> {<<"This is REST!">>, Req, State}. endef define tpl_cowboy_websocket_h -module(template_name). -behaviour(cowboy_websocket_handler). -export([init/3]). -export([websocket_init/3]). -export([websocket_handle/3]). -export([websocket_info/3]). -export([websocket_terminate/3]). -record(state, { }). init(_, _, _) -> {upgrade, protocol, cowboy_websocket}. websocket_init(_, Req, _Opts) -> Req2 = cowboy_req:compact(Req), {ok, Req2, #state{}}. websocket_handle({text, Data}, Req, State) -> {reply, {text, Data}, Req, State}; websocket_handle({binary, Data}, Req, State) -> {reply, {binary, Data}, Req, State}; websocket_handle(_Frame, Req, State) -> {ok, Req, State}. websocket_info(_Info, Req, State) -> {ok, Req, State}. websocket_terminate(_Reason, _Req, _State) -> ok. endef define tpl_gen_fsm -module(template_name). -behaviour(gen_fsm). %% API. -export([start_link/0]). %% gen_fsm. -export([init/1]). -export([state_name/2]). -export([handle_event/3]). -export([state_name/3]). -export([handle_sync_event/4]). -export([handle_info/3]). -export([terminate/3]). -export([code_change/4]). -record(state, { }). %% API. -spec start_link() -> {ok, pid()}. start_link() -> gen_fsm:start_link(?MODULE, [], []). %% gen_fsm. init([]) -> {ok, state_name, #state{}}. state_name(_Event, StateData) -> {next_state, state_name, StateData}. handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. state_name(_Event, _From, StateData) -> {reply, ignored, state_name, StateData}. handle_sync_event(_Event, _From, StateName, StateData) -> {reply, ignored, StateName, StateData}. handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. terminate(_Reason, _StateName, _StateData) -> ok. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. endef define tpl_gen_server -module(template_name). -behaviour(gen_server). %% API. -export([start_link/0]). %% gen_server. -export([init/1]). -export([handle_call/3]). -export([handle_cast/2]). -export([handle_info/2]). -export([terminate/2]). -export([code_change/3]). -record(state, { }). %% API. -spec start_link() -> {ok, pid()}. start_link() -> gen_server:start_link(?MODULE, [], []). %% gen_server. init([]) -> {ok, #state{}}. handle_call(_Request, _From, State) -> {reply, ignored, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. endef define tpl_gen_statem -module(template_name). -behaviour(gen_statem). %% API. -export([start_link/0]). %% gen_statem. -export([callback_mode/0]). -export([init/1]). -export([state_name/3]). -export([handle_event/4]). -export([terminate/3]). -export([code_change/4]). -record(state, { }). %% API. -spec start_link() -> {ok, pid()}. start_link() -> gen_statem:start_link(?MODULE, [], []). %% gen_statem. callback_mode() -> state_functions. init([]) -> {ok, state_name, #state{}}. state_name(_EventType, _EventData, StateData) -> {next_state, state_name, StateData}. handle_event(_EventType, _EventData, StateName, StateData) -> {next_state, StateName, StateData}. terminate(_Reason, _StateName, _StateData) -> ok. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. endef define tpl_library.app.src {application, project_name, [ {description, ""}, {vsn, "0.1.0"}, {id, "git"}, {modules, []}, {registered, []}, {applications, [ kernel, stdlib ]} ]}. endef define tpl_module -module(template_name). -export([]). endef define tpl_ranch_protocol -module(template_name). -behaviour(ranch_protocol). -export([start_link/4]). -export([init/4]). -type opts() :: []. -export_type([opts/0]). -record(state, { socket :: inet:socket(), transport :: module() }). start_link(Ref, Socket, Transport, Opts) -> Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), {ok, Pid}. -spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok. init(Ref, Socket, Transport, _Opts) -> ok = ranch:accept_ack(Ref), loop(#state{socket=Socket, transport=Transport}). loop(State) -> loop(State). endef define tpl_relx.config {release, {project_name_release, "1"}, [project_name, sasl, runtime_tools]}. {dev_mode, false}. {include_erts, true}. {extended_start_script, true}. {sys_config, "config/sys.config"}. {vm_args, "config/vm.args"}. endef define tpl_supervisor -module(template_name). -behaviour(supervisor). -export([start_link/0]). -export([init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> Procs = [], {ok, {{one_for_one, 1, 5}, Procs}}. endef define tpl_sys.config [ ]. endef define tpl_top_Makefile PROJECT = project_name PROJECT_DESCRIPTION = New project PROJECT_VERSION = 0.1.0 template_sp include erlang.mk endef define tpl_vm.args -name project_name@127.0.0.1 -setcookie project_name -heart endef # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(filter asciideck,$(DEPS) $(DOC_DEPS)),asciideck) .PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc-guide distclean-asciidoc-manual # Core targets. docs:: asciidoc distclean:: distclean-asciidoc-guide distclean-asciidoc-manual # Plugin-specific targets. asciidoc: asciidoc-guide asciidoc-manual # User guide. ifeq ($(wildcard doc/src/guide/book.asciidoc),) asciidoc-guide: else asciidoc-guide: distclean-asciidoc-guide doc-deps a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/ distclean-asciidoc-guide: $(gen_verbose) rm -rf doc/html/ doc/guide.pdf endif # Man pages. ASCIIDOC_MANUAL_FILES := $(wildcard doc/src/manual/*.asciidoc) ifeq ($(ASCIIDOC_MANUAL_FILES),) asciidoc-manual: else # Configuration. MAN_INSTALL_PATH ?= /usr/local/share/man MAN_SECTIONS ?= 3 7 MAN_PROJECT ?= $(shell echo $(PROJECT) | sed 's/^./\U&\E/') MAN_VERSION ?= $(PROJECT_VERSION) # Plugin-specific targets. define asciidoc2man.erl try [begin io:format(" ADOC ~s~n", [F]), ok = asciideck:to_manpage(asciideck:parse_file(F), #{ compress => gzip, outdir => filename:dirname(F), extra2 => "$(MAN_PROJECT) $(MAN_VERSION)", extra3 => "$(MAN_PROJECT) Function Reference" }) end || F <- [$(shell echo $(addprefix $(comma)\",$(addsuffix \",$1)) | sed 's/^.//')]], halt(0) catch C:E$(if $V,:S) -> io:format("Exception: ~p:~p~n$(if $V,Stacktrace: ~p~n)", [C, E$(if $V,$(comma) S)]), halt(1) end. endef asciidoc-manual:: doc-deps asciidoc-manual:: $(ASCIIDOC_MANUAL_FILES) $(gen_verbose) $(call erlang,$(call asciidoc2man.erl,$?)) $(verbose) $(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;) install-docs:: install-asciidoc install-asciidoc: asciidoc-manual $(foreach s,$(MAN_SECTIONS),\ mkdir -p $(MAN_INSTALL_PATH)/man$s/ && \ install -g `id -g` -o `id -u` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;) distclean-asciidoc-manual: $(gen_verbose) rm -rf $(addprefix doc/man,$(MAN_SECTIONS)) endif endif # Copyright (c) 2014-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates # Core targets. help:: $(verbose) printf "%s\n" "" \ "Bootstrap targets:" \ " bootstrap Generate a skeleton of an OTP application" \ " bootstrap-lib Generate a skeleton of an OTP library" \ " bootstrap-rel Generate the files needed to build a release" \ " new-app in=NAME Create a new local OTP application NAME" \ " new-lib in=NAME Create a new local OTP library NAME" \ " new t=TPL n=NAME Generate a module NAME based on the template TPL" \ " new t=T n=N in=APP Generate a module NAME based on the template TPL in APP" \ " list-templates List available templates" # Plugin-specific targets. ifndef WS ifdef SP WS = $(subst a,,a $(wordlist 1,$(SP),a a a a a a a a a a a a a a a a a a a a)) else WS = $(tab) endif endif ifdef SP define template_sp # By default templates indent with a single tab per indentation # level. Set this variable to the number of spaces you prefer: SP = $(SP) endef else template_sp = endif # @todo Additional template placeholders could be added. subst_template = $(subst rel_root_dir,$(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app),$(subst rel_deps_dir,$(call core_relpath,$(DEPS_DIR),$(APPS_DIR)/app),$(subst template_sp,$(template_sp),$(subst project_name,$p,$(subst template_name,$n,$1))))) define core_render_template $(eval define _tpl_$(1)$(newline)$(call subst_template,$(tpl_$(1)))$(newline)endef) $(verbose) $(call core_render,_tpl_$(1),$2) endef bootstrap: ifneq ($(wildcard src/),) $(error Error: src/ directory already exists) endif $(eval p := $(PROJECT)) $(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\ $(error Error: Invalid characters in the application name)) $(eval n := $(PROJECT)_sup) $(verbose) $(call core_render_template,top_Makefile,Makefile) $(verbose) mkdir src/ ifdef LEGACY $(verbose) $(call core_render_template,application.app.src,src/$(PROJECT).app.src) endif $(verbose) $(call core_render_template,application,src/$(PROJECT)_app.erl) $(verbose) $(call core_render_template,supervisor,src/$(PROJECT)_sup.erl) bootstrap-lib: ifneq ($(wildcard src/),) $(error Error: src/ directory already exists) endif $(eval p := $(PROJECT)) $(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\ $(error Error: Invalid characters in the application name)) $(verbose) $(call core_render_template,top_Makefile,Makefile) $(verbose) mkdir src/ ifdef LEGACY $(verbose) $(call core_render_template,library.app.src,src/$(PROJECT).app.src) endif bootstrap-rel: ifneq ($(wildcard relx.config),) $(error Error: relx.config already exists) endif ifneq ($(wildcard config/),) $(error Error: config/ directory already exists) endif $(eval p := $(PROJECT)) $(verbose) $(call core_render_template,relx.config,relx.config) $(verbose) mkdir config/ $(verbose) $(call core_render_template,sys.config,config/sys.config) $(verbose) $(call core_render_template,vm.args,config/vm.args) $(verbose) awk '/^include erlang.mk/ && !ins {print "REL_DEPS += relx";ins=1};{print}' Makefile > Makefile.bak $(verbose) mv Makefile.bak Makefile new-app: ifndef in $(error Usage: $(MAKE) new-app in=APP) endif ifneq ($(wildcard $(APPS_DIR)/$in),) $(error Error: Application $in already exists) endif $(eval p := $(in)) $(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\ $(error Error: Invalid characters in the application name)) $(eval n := $(in)_sup) $(verbose) mkdir -p $(APPS_DIR)/$p/src/ $(verbose) $(call core_render_template,apps_Makefile,$(APPS_DIR)/$p/Makefile) ifdef LEGACY $(verbose) $(call core_render_template,application.app.src,$(APPS_DIR)/$p/src/$p.app.src) endif $(verbose) $(call core_render_template,application,$(APPS_DIR)/$p/src/$p_app.erl) $(verbose) $(call core_render_template,supervisor,$(APPS_DIR)/$p/src/$p_sup.erl) new-lib: ifndef in $(error Usage: $(MAKE) new-lib in=APP) endif ifneq ($(wildcard $(APPS_DIR)/$in),) $(error Error: Application $in already exists) endif $(eval p := $(in)) $(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\ $(error Error: Invalid characters in the application name)) $(verbose) mkdir -p $(APPS_DIR)/$p/src/ $(verbose) $(call core_render_template,apps_Makefile,$(APPS_DIR)/$p/Makefile) ifdef LEGACY $(verbose) $(call core_render_template,library.app.src,$(APPS_DIR)/$p/src/$p.app.src) endif # These are not necessary because we don't expose those as "normal" templates. BOOTSTRAP_TEMPLATES = apps_Makefile top_Makefile \ application.app.src library.app.src application \ relx.config sys.config vm.args # Templates may override the path they will be written to when using 'new'. # Only special template paths must be listed. Default is src/template_name.erl # Substitution is also applied to the paths. Examples: # #tplp_top_Makefile = Makefile #tplp_application.app.src = src/project_name.app.src #tplp_application = src/project_name_app.erl #tplp_relx.config = relx.config # Erlang.mk bundles its own templates at build time into the erlang.mk file. new: $(if $(t),,$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])) $(if $(n),,$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])) $(if $(tpl_$(t)),,$(error Error: $t template does not exist; try $(Make) list-templates)) $(eval dest := $(if $(in),$(APPS_DIR)/$(in)/)$(call subst_template,$(if $(tplp_$(t)),$(tplp_$(t)),src/template_name.erl))) $(if $(wildcard $(dir $(dest))),,$(error Error: $(dir $(dest)) directory does not exist)) $(if $(wildcard $(dest)),$(error Error: The file $(dest) already exists)) $(eval p := $(PROJECT)) $(call core_render_template,$(t),$(dest)) list-templates: $(verbose) @echo Available templates: $(verbose) printf " %s\n" $(sort $(filter-out $(BOOTSTRAP_TEMPLATES),$(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES))))) # Copyright (c) 2014-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: clean-c_src distclean-c_src-env # Configuration. C_SRC_DIR ?= $(CURDIR)/c_src C_SRC_ENV ?= $(C_SRC_DIR)/env.mk C_SRC_OUTPUT ?= $(CURDIR)/priv/$(PROJECT) C_SRC_TYPE ?= shared # System type and C compiler/flags. ifeq ($(PLATFORM),msys2) C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= .exe C_SRC_OUTPUT_SHARED_EXTENSION ?= .dll C_SRC_OUTPUT_STATIC_EXTENSION ?= .lib else C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= C_SRC_OUTPUT_SHARED_EXTENSION ?= .so C_SRC_OUTPUT_STATIC_EXTENSION ?= .a endif ifeq ($(C_SRC_TYPE),shared) C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_SHARED_EXTENSION) else ifeq ($(C_SRC_TYPE),static) C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_STATIC_EXTENSION) else C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_EXECUTABLE_EXTENSION) endif RANLIB ?= ranlib ARFLAGS ?= cr ifeq ($(PLATFORM),msys2) # We hardcode the compiler used on MSYS2. The default CC=cc does # not produce working code. The "gcc" MSYS2 package also doesn't. CC = /mingw64/bin/gcc export CC CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -finline-functions -Wall else ifeq ($(PLATFORM),darwin) CC ?= cc CFLAGS ?= -O3 -std=c99 -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -Wall LDFLAGS ?= -flat_namespace -undefined suppress else ifeq ($(PLATFORM),freebsd) CC ?= cc CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -finline-functions -Wall else ifeq ($(PLATFORM),linux) CC ?= gcc CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes CXXFLAGS ?= -O3 -finline-functions -Wall endif ifneq ($(PLATFORM),msys2) CFLAGS += -fPIC CXXFLAGS += -fPIC endif ifeq ($(C_SRC_TYPE),static) CFLAGS += -DSTATIC_ERLANG_NIF=1 CXXFLAGS += -DSTATIC_ERLANG_NIF=1 endif CFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" CXXFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" LDLIBS += -L"$(ERL_INTERFACE_LIB_DIR)" -lei # Verbosity. c_verbose_0 = @echo " C " $(filter-out $(notdir $(MAKEFILE_LIST) $(C_SRC_ENV)),$(^F)); c_verbose = $(c_verbose_$(V)) cpp_verbose_0 = @echo " CPP " $(filter-out $(notdir $(MAKEFILE_LIST) $(C_SRC_ENV)),$(^F)); cpp_verbose = $(cpp_verbose_$(V)) link_verbose_0 = @echo " LD " $(@F); link_verbose = $(link_verbose_$(V)) ar_verbose_0 = @echo " AR " $(@F); ar_verbose = $(ar_verbose_$(V)) ranlib_verbose_0 = @echo " RANLIB" $(@F); ranlib_verbose = $(ranlib_verbose_$(V)) # Targets. ifeq ($(wildcard $(C_SRC_DIR)),) else ifneq ($(wildcard $(C_SRC_DIR)/Makefile),) app:: app-c_src test-build:: app-c_src app-c_src: $(MAKE) -C $(C_SRC_DIR) clean:: $(MAKE) -C $(C_SRC_DIR) clean else ifeq ($(SOURCES),) SOURCES := $(sort $(foreach pat,*.c *.C *.cc *.cpp,$(call core_find,$(C_SRC_DIR)/,$(pat)))) endif OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c app:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) test-build:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) ifneq ($(C_SRC_TYPE),static) $(C_SRC_OUTPUT_FILE): $(OBJECTS) $(verbose) mkdir -p $(dir $@) $(link_verbose) $(CC) $(OBJECTS) \ $(LDFLAGS) $(if $(filter $(C_SRC_TYPE),shared),-shared) $(LDLIBS) \ -o $(C_SRC_OUTPUT_FILE) else $(C_SRC_OUTPUT_FILE): $(OBJECTS) $(verbose) mkdir -p $(dir $@) $(ar_verbose) $(AR) $(ARFLAGS) $(C_SRC_OUTPUT_FILE) $(OBJECTS) $(ranlib_verbose) $(RANLIB) $(C_SRC_OUTPUT_FILE) endif $(OBJECTS): $(MAKEFILE_LIST) $(C_SRC_ENV) %.o: %.c $(COMPILE_C) $(OUTPUT_OPTION) $< %.o: %.cc $(COMPILE_CPP) $(OUTPUT_OPTION) $< %.o: %.C $(COMPILE_CPP) $(OUTPUT_OPTION) $< %.o: %.cpp $(COMPILE_CPP) $(OUTPUT_OPTION) $< clean:: clean-c_src clean-c_src: $(gen_verbose) rm -f $(C_SRC_OUTPUT_FILE) $(OBJECTS) endif ifneq ($(wildcard $(C_SRC_DIR)),) ERL_ERTS_DIR = $(shell $(ERL) -eval 'io:format("~s~n", [code:lib_dir(erts)]), halt().') $(C_SRC_ENV): $(verbose) $(ERL) -eval "file:write_file(\"$(call core_native_path,$(C_SRC_ENV))\", \ io_lib:format( \ \"# Generated by Erlang.mk. Edit at your own risk!~n~n\" \ \"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \ \"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \ \"ERL_INTERFACE_LIB_DIR ?= ~s~n\" \ \"ERTS_DIR ?= $(ERL_ERTS_DIR)~n\", \ [code:root_dir(), erlang:system_info(version), \ code:lib_dir(erl_interface, include), \ code:lib_dir(erl_interface, lib)])), \ halt()." distclean:: distclean-c_src-env distclean-c_src-env: $(gen_verbose) rm -f $(C_SRC_ENV) -include $(C_SRC_ENV) ifneq ($(ERL_ERTS_DIR),$(ERTS_DIR)) $(shell rm -f $(C_SRC_ENV)) endif endif # Templates. define bs_c_nif #include "erl_nif.h" static int loads = 0; static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) { /* Initialize private data. */ *priv_data = NULL; loads++; return 0; } static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) { /* Convert the private data to the new version. */ *priv_data = *old_priv_data; loads++; return 0; } static void unload(ErlNifEnv* env, void* priv_data) { if (loads == 1) { /* Destroy the private data. */ } loads--; } static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { if (enif_is_atom(env, argv[0])) { return enif_make_tuple2(env, enif_make_atom(env, "hello"), argv[0]); } return enif_make_tuple2(env, enif_make_atom(env, "error"), enif_make_atom(env, "badarg")); } static ErlNifFunc nif_funcs[] = { {"hello", 1, hello} }; ERL_NIF_INIT($n, nif_funcs, load, NULL, upgrade, unload) endef define bs_erl_nif -module($n). -export([hello/1]). -on_load(on_load/0). on_load() -> PrivDir = case code:priv_dir(?MODULE) of {error, _} -> AppPath = filename:dirname(filename:dirname(code:which(?MODULE))), filename:join(AppPath, "priv"); Path -> Path end, erlang:load_nif(filename:join(PrivDir, atom_to_list(?MODULE)), 0). hello(_) -> erlang:nif_error({not_loaded, ?MODULE}). endef new-nif: ifneq ($(wildcard $(C_SRC_DIR)/$n.c),) $(error Error: $(C_SRC_DIR)/$n.c already exists) endif ifneq ($(wildcard src/$n.erl),) $(error Error: src/$n.erl already exists) endif ifndef n $(error Usage: $(MAKE) new-nif n=NAME [in=APP]) endif ifdef in $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new-nif n=$n in= else $(verbose) mkdir -p $(C_SRC_DIR) src/ $(verbose) $(call core_render,bs_c_nif,$(C_SRC_DIR)/$n.c) $(verbose) $(call core_render,bs_erl_nif,src/$n.erl) endif # Copyright (c) 2015-2017, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: ci ci-prepare ci-setup CI_OTP ?= ifeq ($(strip $(CI_OTP)),) ci:: else ci:: $(addprefix ci-,$(CI_OTP)) ci-prepare: $(addprefix ci-prepare-,$(CI_OTP)) ci-setup:: $(verbose) : ci-extra:: $(verbose) : ci_verbose_0 = @echo " CI " $1; ci_verbose = $(ci_verbose_$(V)) define ci_target ci-prepare-$1: $(KERL_INSTALL_DIR)/$2 $(verbose) : ci-$1: ci-prepare-$1 $(verbose) $(MAKE) --no-print-directory clean $(ci_verbose) \ PATH="$(KERL_INSTALL_DIR)/$2/bin:$(PATH)" \ CI_OTP_RELEASE="$1" \ CT_OPTS="-label $1" \ CI_VM="$3" \ $(MAKE) ci-setup tests $(verbose) $(MAKE) --no-print-directory ci-extra endef $(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp),$(otp),otp))) $(foreach otp,$(filter-out $(ERLANG_OTP),$(CI_OTP)),$(eval $(call kerl_otp_target,$(otp)))) help:: $(verbose) printf "%s\n" "" \ "Continuous Integration targets:" \ " ci Run '$(MAKE) tests' on all configured Erlang versions." \ "" \ "The CI_OTP variable must be defined with the Erlang versions" \ "that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3" endif # Copyright (c) 2020, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifdef CONCUERROR_TESTS .PHONY: concuerror distclean-concuerror # Configuration CONCUERROR_LOGS_DIR ?= $(CURDIR)/logs CONCUERROR_OPTS ?= # Core targets. check:: concuerror ifndef KEEP_LOGS distclean:: distclean-concuerror endif # Plugin-specific targets. $(ERLANG_MK_TMP)/Concuerror/bin/concuerror: | $(ERLANG_MK_TMP) $(verbose) git clone https://github.com/parapluu/Concuerror $(ERLANG_MK_TMP)/Concuerror $(verbose) $(MAKE) -C $(ERLANG_MK_TMP)/Concuerror $(CONCUERROR_LOGS_DIR): $(verbose) mkdir -p $(CONCUERROR_LOGS_DIR) define concuerror_html_report Concuerror HTML report

Concuerror HTML report

Generated on $(concuerror_date)

    $(foreach t,$(concuerror_targets),
  • $(t)
  • )
endef concuerror: $(addprefix concuerror-,$(subst :,-,$(CONCUERROR_TESTS))) $(eval concuerror_date := $(shell date)) $(eval concuerror_targets := $^) $(verbose) $(call core_render,concuerror_html_report,$(CONCUERROR_LOGS_DIR)/concuerror.html) define concuerror_target .PHONY: concuerror-$1-$2 concuerror-$1-$2: test-build | $(ERLANG_MK_TMP)/Concuerror/bin/concuerror $(CONCUERROR_LOGS_DIR) $(ERLANG_MK_TMP)/Concuerror/bin/concuerror \ --pa $(CURDIR)/ebin --pa $(TEST_DIR) \ -o $(CONCUERROR_LOGS_DIR)/concuerror-$1-$2.txt \ $$(CONCUERROR_OPTS) -m $1 -t $2 endef $(foreach test,$(CONCUERROR_TESTS),$(eval $(call concuerror_target,$(firstword $(subst :, ,$(test))),$(lastword $(subst :, ,$(test)))))) distclean-concuerror: $(gen_verbose) rm -rf $(CONCUERROR_LOGS_DIR) endif # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: ct apps-ct distclean-ct # Configuration. CT_OPTS ?= ifneq ($(wildcard $(TEST_DIR)),) ifndef CT_SUITES CT_SUITES := $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) endif endif CT_SUITES ?= CT_LOGS_DIR ?= $(CURDIR)/logs # Core targets. tests:: ct ifndef KEEP_LOGS distclean:: distclean-ct endif help:: $(verbose) printf "%s\n" "" \ "Common_test targets:" \ " ct Run all the common_test suites for this project" \ "" \ "All your common_test suites have their associated targets." \ "A suite named http_SUITE can be ran using the ct-http target." # Plugin-specific targets. CT_RUN = ct_run \ -no_auto_compile \ -noinput \ -pa $(CURDIR)/ebin $(TEST_DIR) \ -dir $(TEST_DIR) \ -logdir $(CT_LOGS_DIR) ifeq ($(CT_SUITES),) ct: $(if $(IS_APP)$(ROOT_DIR),,apps-ct) else # We do not run tests if we are in an apps/* with no test directory. ifneq ($(IS_APP)$(wildcard $(TEST_DIR)),1) ct: test-build $(if $(IS_APP)$(ROOT_DIR),,apps-ct) $(verbose) mkdir -p $(CT_LOGS_DIR) $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) endif endif ifneq ($(ALL_APPS_DIRS),) define ct_app_target apps-ct-$1: test-build $$(MAKE) -C $1 ct IS_APP=1 endef $(foreach app,$(ALL_APPS_DIRS),$(eval $(call ct_app_target,$(app)))) apps-ct: $(addprefix apps-ct-,$(ALL_APPS_DIRS)) endif ifdef t ifeq (,$(findstring :,$t)) CT_EXTRA = -group $t else t_words = $(subst :, ,$t) CT_EXTRA = -group $(firstword $(t_words)) -case $(lastword $(t_words)) endif else ifdef c CT_EXTRA = -case $c else CT_EXTRA = endif endif define ct_suite_target ct-$1: test-build $$(verbose) mkdir -p $$(CT_LOGS_DIR) $$(gen_verbose_esc) $$(CT_RUN) -sname ct_$$(PROJECT) -suite $$(addsuffix _SUITE,$1) $$(CT_EXTRA) $$(CT_OPTS) endef $(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) distclean-ct: $(gen_verbose) rm -rf $(CT_LOGS_DIR) # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: plt distclean-plt dialyze # Configuration. DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt export DIALYZER_PLT PLT_APPS ?= DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS) DIALYZER_OPTS ?= -Werror_handling -Wunmatched_returns # -Wunderspecs DIALYZER_PLT_OPTS ?= # Core targets. check:: dialyze distclean:: distclean-plt help:: $(verbose) printf "%s\n" "" \ "Dialyzer targets:" \ " plt Build a PLT file for this project" \ " dialyze Analyze the project using Dialyzer" # Plugin-specific targets. define filter_opts.erl Opts = init:get_plain_arguments(), {Filtered, _} = lists:foldl(fun (O, {Os, true}) -> {[O|Os], false}; (O = "-D", {Os, _}) -> {[O|Os], true}; (O = [\\$$-, \\$$D, _ | _], {Os, _}) -> {[O|Os], false}; (O = "-I", {Os, _}) -> {[O|Os], true}; (O = [\\$$-, \\$$I, _ | _], {Os, _}) -> {[O|Os], false}; (O = "-pa", {Os, _}) -> {[O|Os], true}; (_, Acc) -> Acc end, {[], false}, Opts), io:format("~s~n", [string:join(lists:reverse(Filtered), " ")]), halt(). endef # DIALYZER_PLT is a variable understood directly by Dialyzer. # # We append the path to erts at the end of the PLT. This works # because the PLT file is in the external term format and the # function binary_to_term/1 ignores any trailing data. $(DIALYZER_PLT): deps app $(eval DEPS_LOG := $(shell test -f $(ERLANG_MK_TMP)/deps.log && \ while read p; do test -d $$p/ebin && echo $$p/ebin; done <$(ERLANG_MK_TMP)/deps.log)) $(verbose) dialyzer --build_plt $(DIALYZER_PLT_OPTS) --apps \ erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS_LOG) || test $$? -eq 2 $(verbose) $(ERL) -eval 'io:format("~n~s~n", [code:lib_dir(erts)]), halt().' >> $@ plt: $(DIALYZER_PLT) distclean-plt: $(gen_verbose) rm -f $(DIALYZER_PLT) ifneq ($(wildcard $(DIALYZER_PLT)),) dialyze: $(if $(filter --src,$(DIALYZER_DIRS)),,deps app) $(verbose) if ! tail -n1 $(DIALYZER_PLT) | \ grep -q "^`$(ERL) -eval 'io:format("~s", [code:lib_dir(erts)]), halt().'`$$"; then \ rm $(DIALYZER_PLT); \ $(MAKE) plt; \ fi else dialyze: $(DIALYZER_PLT) endif $(verbose) dialyzer `$(ERL) \ -eval "$(subst $(newline),,$(call escape_dquotes,$(call filter_opts.erl)))" \ -extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS) $(if $(wildcard ebin/),-pa ebin/) # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-edoc edoc # Configuration. EDOC_OPTS ?= EDOC_SRC_DIRS ?= EDOC_OUTPUT ?= doc define edoc.erl SrcPaths = lists:foldl(fun(P, Acc) -> filelib:wildcard(atom_to_list(P) ++ "/{src,c_src}") ++ lists:filter(fun(D) -> filelib:is_dir(D) end, filelib:wildcard(atom_to_list(P) ++ "/{src,c_src}/**")) ++ Acc end, [], [$(call comma_list,$(patsubst %,'%',$(call core_native_path,$(EDOC_SRC_DIRS))))]), DefaultOpts = [{dir, "$(EDOC_OUTPUT)"}, {source_path, SrcPaths}, {subpackages, false}], edoc:application($(1), ".", [$(2)] ++ DefaultOpts), halt(0). endef # Core targets. ifneq ($(strip $(EDOC_SRC_DIRS)$(wildcard doc/overview.edoc)),) docs:: edoc endif distclean:: distclean-edoc # Plugin-specific targets. edoc: distclean-edoc doc-deps $(gen_verbose) $(call erlang,$(call edoc.erl,$(PROJECT),$(EDOC_OPTS))) distclean-edoc: $(gen_verbose) rm -f $(EDOC_OUTPUT)/*.css $(EDOC_OUTPUT)/*.html $(EDOC_OUTPUT)/*.png $(EDOC_OUTPUT)/edoc-info # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. # Configuration. DTL_FULL_PATH ?= DTL_PATH ?= templates/ DTL_PREFIX ?= DTL_SUFFIX ?= _dtl DTL_OPTS ?= # Verbosity. dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); dtl_verbose = $(dtl_verbose_$(V)) # Core targets. DTL_PATH := $(abspath $(DTL_PATH)) DTL_FILES := $(sort $(call core_find,$(DTL_PATH),*.dtl)) ifneq ($(DTL_FILES),) DTL_NAMES = $(addprefix $(DTL_PREFIX),$(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%))) DTL_MODULES = $(if $(DTL_FULL_PATH),$(subst /,_,$(DTL_NAMES)),$(notdir $(DTL_NAMES))) BEAM_FILES += $(addsuffix .beam,$(addprefix ebin/,$(DTL_MODULES))) ifneq ($(words $(DTL_FILES)),0) # Rebuild templates when the Makefile changes. $(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST) | $(ERLANG_MK_TMP) $(verbose) if test -f $@; then \ touch $(DTL_FILES); \ fi $(verbose) touch $@ ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change-erlydtl endif define erlydtl_compile.erl [begin Module0 = case "$(strip $(DTL_FULL_PATH))" of "" -> filename:basename(F, ".dtl"); _ -> "$(call core_native_path,$(DTL_PATH))/" ++ F2 = filename:rootname(F, ".dtl"), re:replace(F2, "/", "_", [{return, list}, global]) end, Module = list_to_atom("$(DTL_PREFIX)" ++ string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors]) of ok -> ok; {ok, _} -> ok end end || F <- string:tokens("$(1)", " ")], halt(). endef ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ $(if $(strip $?),\ $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$(call core_native_path,$?)),\ -pa ebin/)) endif # Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2014, Dave Cottlehuber # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-escript escript escript-zip # Configuration. ESCRIPT_NAME ?= $(PROJECT) ESCRIPT_FILE ?= $(ESCRIPT_NAME) ESCRIPT_SHEBANG ?= /usr/bin/env escript ESCRIPT_COMMENT ?= This is an -*- erlang -*- file ESCRIPT_EMU_ARGS ?= -escript main $(ESCRIPT_NAME) ESCRIPT_ZIP ?= 7z a -tzip -mx=9 -mtc=off $(if $(filter-out 0,$(V)),,> /dev/null) ESCRIPT_ZIP_FILE ?= $(ERLANG_MK_TMP)/escript.zip # Core targets. distclean:: distclean-escript help:: $(verbose) printf "%s\n" "" \ "Escript targets:" \ " escript Build an executable escript archive" \ # Plugin-specific targets. ALL_ESCRIPT_DEPS_DIRS = $(LOCAL_DEPS_DIRS) $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(DEPS)),$(call query_name,$(dep)))) ESCRIPT_RUNTIME_DEPS_FILE ?= $(ERLANG_MK_TMP)/escript-deps.log escript-list-runtime-deps: ifeq ($(IS_DEP),) $(verbose) rm -f $(ESCRIPT_RUNTIME_DEPS_FILE) endif $(verbose) touch $(ESCRIPT_RUNTIME_DEPS_FILE) $(verbose) set -e; for dep in $(ALL_ESCRIPT_DEPS_DIRS) ; do \ if ! grep -qs ^$$dep$$ $(ESCRIPT_RUNTIME_DEPS_FILE); then \ echo $$dep >> $(ESCRIPT_RUNTIME_DEPS_FILE); \ if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \ $$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \ $(MAKE) -C $$dep escript-list-runtime-deps \ IS_DEP=1 \ ESCRIPT_RUNTIME_DEPS_FILE=$(ESCRIPT_RUNTIME_DEPS_FILE); \ fi \ fi \ done ifeq ($(IS_DEP),) $(verbose) sort < $(ESCRIPT_RUNTIME_DEPS_FILE) | uniq > $(ESCRIPT_RUNTIME_DEPS_FILE).sorted $(verbose) mv $(ESCRIPT_RUNTIME_DEPS_FILE).sorted $(ESCRIPT_RUNTIME_DEPS_FILE) endif escript-prepare: deps app $(MAKE) escript-list-runtime-deps escript-zip:: escript-prepare $(verbose) mkdir -p $(dir $(abspath $(ESCRIPT_ZIP_FILE))) $(verbose) rm -f $(abspath $(ESCRIPT_ZIP_FILE)) $(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) $(notdir $(CURDIR))/ebin/* ifneq ($(DEPS),) $(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) \ $(subst $(DEPS_DIR)/,,$(addsuffix /*,$(wildcard \ $(addsuffix /ebin,$(shell cat $(ESCRIPT_RUNTIME_DEPS_FILE)))))) endif # @todo Only generate the zip file if there were changes. escript:: escript-zip $(gen_verbose) printf "%s\n" \ "#!$(ESCRIPT_SHEBANG)" \ "%% $(ESCRIPT_COMMENT)" \ "%%! $(ESCRIPT_EMU_ARGS)" > $(ESCRIPT_FILE) $(verbose) cat $(abspath $(ESCRIPT_ZIP_FILE)) >> $(ESCRIPT_FILE) $(verbose) chmod +x $(ESCRIPT_FILE) distclean-escript: $(gen_verbose) rm -f $(ESCRIPT_FILE) $(abspath $(ESCRIPT_ZIP_FILE)) # Copyright (c) 2015-2016, Loïc Hoguin # Copyright (c) 2014, Enrique Fernandez # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: eunit apps-eunit # Eunit can be disabled by setting this to any other value. EUNIT ?= system ifeq ($(EUNIT),system) # Configuration EUNIT_OPTS ?= EUNIT_ERL_OPTS ?= EUNIT_TEST_SPEC ?= $1 # Core targets. tests:: eunit help:: $(verbose) printf "%s\n" "" \ "EUnit targets:" \ " eunit Run all the EUnit tests for this project" # Plugin-specific targets. define eunit.erl $(call cover.erl) CoverSetup(), case eunit:test($(call EUNIT_TEST_SPEC,$1), [$(EUNIT_OPTS)]) of ok -> ok; error -> halt(2) end, CoverExport("$(call core_native_path,$(COVER_DATA_DIR))/eunit.coverdata"), halt() endef EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(CURDIR)/ebin ifdef t ifeq (,$(findstring :,$(t))) eunit: test-build cover-data-dir $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS)) else eunit: test-build cover-data-dir $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS)) endif else EUNIT_EBIN_MODS = $(notdir $(basename $(ERL_FILES) $(BEAM_FILES))) EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl))) EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') eunit: test-build $(if $(IS_APP)$(ROOT_DIR),,apps-eunit) cover-data-dir ifneq ($(wildcard src/ $(TEST_DIR)),) $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS)) endif ifneq ($(ALL_APPS_DIRS),) apps-eunit: test-build $(verbose) eunit_retcode=0 ; for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; \ [ $$? -ne 0 ] && eunit_retcode=1 ; done ; \ exit $$eunit_retcode endif endif endif # Copyright (c) 2020, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. define hex_user_create.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), case hex_api_user:create(Config, <<"$(strip $1)">>, <<"$(strip $2)">>, <<"$(strip $3)">>) of {ok, {201, _, #{<<"email">> := Email, <<"url">> := URL, <<"username">> := Username}}} -> io:format("User ~s (~s) created at ~s~n" "Please check your inbox for a confirmation email.~n" "You must confirm before you are allowed to publish packages.~n", [Username, Email, URL]), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(80) end endef # The $(info ) call inserts a new line after the password prompt. hex-user-create: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username))) $(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info ))) $(if $(HEX_EMAIL),,$(eval HEX_EMAIL := $(shell read -p "Email: " email; echo $$email))) $(gen_verbose) $(call erlang,$(call hex_user_create.erl,$(HEX_USERNAME),$(HEX_PASSWORD),$(HEX_EMAIL))) define hex_key_add.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => iolist_to_binary([<<"Basic ">>, base64:encode(<<"$(strip $1):$(strip $2)">>)])}, Permissions = [ case string:split(P, <<":">>) of [D] -> #{domain => D}; [D, R] -> #{domain => D, resource => R} end || P <- string:split(<<"$(strip $4)">>, <<",">>, all)], case hex_api_key:add(ConfigF, <<"$(strip $3)">>, Permissions) of {ok, {201, _, #{<<"secret">> := Secret}}} -> io:format("Key ~s created for user ~s~nSecret: ~s~n" "Please store the secret in a secure location, such as a password store.~n" "The secret will be requested for most Hex-related operations.~n", [<<"$(strip $3)">>, <<"$(strip $1)">>, Secret]), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(81) end endef hex-key-add: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username))) $(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info ))) $(gen_verbose) $(call erlang,$(call hex_key_add.erl,$(HEX_USERNAME),$(HEX_PASSWORD),\ $(if $(name),$(name),$(shell hostname)-erlang-mk),\ $(if $(perm),$(perm),api))) HEX_TARBALL_EXTRA_METADATA ?= # @todo Check that we can += files HEX_TARBALL_FILES ?= \ $(wildcard early-plugins.mk) \ $(wildcard ebin/$(PROJECT).app) \ $(wildcard ebin/$(PROJECT).appup) \ $(wildcard $(notdir $(ERLANG_MK_FILENAME))) \ $(sort $(call core_find,include/,*.hrl)) \ $(wildcard LICENSE*) \ $(wildcard Makefile) \ $(wildcard plugins.mk) \ $(sort $(call core_find,priv/,*)) \ $(wildcard README*) \ $(wildcard rebar.config) \ $(sort $(if $(LEGACY),$(filter-out src/$(PROJECT).app.src,$(call core_find,src/,*)),$(call core_find,src/,*))) HEX_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT).tar # @todo Need to check for rebar.config and/or the absence of DEPS to know # whether a project will work with Rebar. # # @todo contributors licenses links in HEX_TARBALL_EXTRA_METADATA # In order to build the requirements metadata we look into DEPS. # We do not require that the project use Hex dependencies, however # Hex.pm does require that the package name and version numbers # correspond to a real Hex package. define hex_tarball_create.erl Files0 = [$(call comma_list,$(patsubst %,"%",$(HEX_TARBALL_FILES)))], Requirements0 = #{ $(foreach d,$(DEPS), <<"$(if $(subst hex,,$(call query_fetch_method,$d)),$d,$(if $(word 3,$(dep_$d)),$(word 3,$(dep_$d)),$d))">> => #{ <<"app">> => <<"$d">>, <<"optional">> => false, <<"requirement">> => <<"$(if $(hex_req_$d),$(strip $(hex_req_$d)),$(call query_version,$d))">> },) $(if $(DEPS),dummy => dummy) }, Requirements = maps:remove(dummy, Requirements0), Metadata0 = #{ app => <<"$(strip $(PROJECT))">>, build_tools => [<<"make">>, <<"rebar3">>], description => <<"$(strip $(PROJECT_DESCRIPTION))">>, files => [unicode:characters_to_binary(F) || F <- Files0], name => <<"$(strip $(PROJECT))">>, requirements => Requirements, version => <<"$(strip $(PROJECT_VERSION))">> }, Metadata = Metadata0$(HEX_TARBALL_EXTRA_METADATA), Files = [case file:read_file(F) of {ok, Bin} -> {F, Bin}; {error, Reason} -> io:format("Error trying to open file ~0p: ~0p~n", [F, Reason]), halt(82) end || F <- Files0], case hex_tarball:create(Metadata, Files) of {ok, #{tarball := Tarball}} -> ok = file:write_file("$(strip $(HEX_TARBALL_OUTPUT_FILE))", Tarball), halt(0); {error, Reason} -> io:format("Error ~0p~n", [Reason]), halt(83) end endef hex_tar_verbose_0 = @echo " TAR $(notdir $(ERLANG_MK_TMP))/$(@F)"; hex_tar_verbose_2 = set -x; hex_tar_verbose = $(hex_tar_verbose_$(V)) $(HEX_TARBALL_OUTPUT_FILE): $(DEPS_DIR)/hex_core/ebin/dep_built app $(hex_tar_verbose) $(call erlang,$(call hex_tarball_create.erl)) hex-tarball-create: $(HEX_TARBALL_OUTPUT_FILE) define hex_release_publish_summary.erl {ok, Tarball} = erl_tar:open("$(strip $(HEX_TARBALL_OUTPUT_FILE))", [read]), ok = erl_tar:extract(Tarball, [{cwd, "$(ERLANG_MK_TMP)"}, {files, ["metadata.config"]}]), {ok, Metadata} = file:consult("$(ERLANG_MK_TMP)/metadata.config"), #{ <<"name">> := Name, <<"version">> := Version, <<"files">> := Files, <<"requirements">> := Deps } = maps:from_list(Metadata), io:format("Publishing ~s ~s~n Dependencies:~n", [Name, Version]), case Deps of [] -> io:format(" (none)~n"); _ -> [begin #{<<"app">> := DA, <<"requirement">> := DR} = maps:from_list(D), io:format(" ~s ~s~n", [DA, DR]) end || {_, D} <- Deps] end, io:format(" Included files:~n"), [io:format(" ~s~n", [F]) || F <- Files], io:format("You may also review the contents of the tarball file.~n" "Please enter your secret key to proceed.~n"), halt(0) endef define hex_release_publish.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => <<"$(strip $1)">>}, {ok, Tarball} = file:read_file("$(strip $(HEX_TARBALL_OUTPUT_FILE))"), case hex_api_release:publish(ConfigF, Tarball, [{replace, $2}]) of {ok, {200, _, #{}}} -> io:format("Release replaced~n"), halt(0); {ok, {201, _, #{}}} -> io:format("Release published~n"), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(84) end endef hex-release-tarball: $(DEPS_DIR)/hex_core/ebin/dep_built $(HEX_TARBALL_OUTPUT_FILE) $(verbose) $(call erlang,$(call hex_release_publish_summary.erl)) hex-release-publish: $(DEPS_DIR)/hex_core/ebin/dep_built hex-release-tarball $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),false)) hex-release-replace: $(DEPS_DIR)/hex_core/ebin/dep_built hex-release-tarball $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),true)) define hex_release_delete.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => <<"$(strip $1)">>}, case hex_api_release:delete(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $(PROJECT_VERSION))">>) of {ok, {204, _, _}} -> io:format("Release $(strip $(PROJECT_VERSION)) deleted~n"), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(85) end endef hex-release-delete: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_delete.erl,$(HEX_SECRET))) define hex_release_retire.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => <<"$(strip $1)">>}, Params = #{<<"reason">> => <<"$(strip $3)">>, <<"message">> => <<"$(strip $4)">>}, case hex_api_release:retire(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $2)">>, Params) of {ok, {204, _, _}} -> io:format("Release $(strip $2) has been retired~n"), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(86) end endef hex-release-retire: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_retire.erl,$(HEX_SECRET),\ $(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)),\ $(if $(HEX_REASON),$(HEX_REASON),invalid),\ $(HEX_MESSAGE))) define hex_release_unretire.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => <<"$(strip $1)">>}, case hex_api_release:unretire(ConfigF, <<"$(strip $(PROJECT))">>, <<"$(strip $2)">>) of {ok, {204, _, _}} -> io:format("Release $(strip $2) is not retired anymore~n"), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(87) end endef hex-release-unretire: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_unretire.erl,$(HEX_SECRET),\ $(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)))) HEX_DOCS_DOC_DIR ?= doc/ HEX_DOCS_TARBALL_FILES ?= $(sort $(call core_find,$(HEX_DOCS_DOC_DIR),*)) HEX_DOCS_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT)-docs.tar.gz $(HEX_DOCS_TARBALL_OUTPUT_FILE): $(DEPS_DIR)/hex_core/ebin/dep_built app docs $(hex_tar_verbose) tar czf $(HEX_DOCS_TARBALL_OUTPUT_FILE) -C $(HEX_DOCS_DOC_DIR) \ $(HEX_DOCS_TARBALL_FILES:$(HEX_DOCS_DOC_DIR)%=%) hex-docs-tarball-create: $(HEX_DOCS_TARBALL_OUTPUT_FILE) define hex_docs_publish.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => <<"$(strip $1)">>}, {ok, Tarball} = file:read_file("$(strip $(HEX_DOCS_TARBALL_OUTPUT_FILE))"), case hex_api:post(ConfigF, ["packages", "$(strip $(PROJECT))", "releases", "$(strip $(PROJECT_VERSION))", "docs"], {"application/octet-stream", Tarball}) of {ok, {Status, _, _}} when Status >= 200, Status < 300 -> io:format("Docs published~n"), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(88) end endef hex-docs-publish: $(DEPS_DIR)/hex_core/ebin/dep_built hex-docs-tarball-create $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_docs_publish.erl,$(HEX_SECRET))) define hex_docs_delete.erl {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(inets), Config = $(hex_config.erl), ConfigF = Config#{api_key => <<"$(strip $1)">>}, case hex_api:delete(ConfigF, ["packages", "$(strip $(PROJECT))", "releases", "$(strip $2)", "docs"]) of {ok, {Status, _, _}} when Status >= 200, Status < 300 -> io:format("Docs removed~n"), halt(0); {ok, {Status, _, Errors}} -> io:format("Error ~b: ~0p~n", [Status, Errors]), halt(89) end endef hex-docs-delete: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_docs_delete.erl,$(HEX_SECRET),\ $(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)))) # Copyright (c) 2015-2017, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(filter proper,$(DEPS) $(TEST_DEPS)),proper) .PHONY: proper # Targets. tests:: proper define proper_check.erl $(call cover.erl) code:add_pathsa([ "$(call core_native_path,$(CURDIR)/ebin)", "$(call core_native_path,$(DEPS_DIR)/*/ebin)", "$(call core_native_path,$(TEST_DIR))"]), Module = fun(M) -> [true] =:= lists:usort([ case atom_to_list(F) of "prop_" ++ _ -> io:format("Testing ~p:~p/0~n", [M, F]), proper:quickcheck(M:F(), nocolors); _ -> true end || {F, 0} <- M:module_info(exports)]) end, try begin CoverSetup(), Res = case $(1) of all -> [true] =:= lists:usort([Module(M) || M <- [$(call comma_list,$(3))]]); module -> Module($(2)); function -> proper:quickcheck($(2), nocolors) end, CoverExport("$(COVER_DATA_DIR)/proper.coverdata"), Res end of true -> halt(0); _ -> halt(1) catch error:undef$(if $V,:Stacktrace) -> io:format("Undefined property or module?~n$(if $V,~p~n)", [$(if $V,Stacktrace)]), halt(0) end. endef ifdef t ifeq (,$(findstring :,$(t))) proper: test-build cover-data-dir $(verbose) $(call erlang,$(call proper_check.erl,module,$(t))) else proper: test-build cover-data-dir $(verbose) echo Testing $(t)/0 $(verbose) $(call erlang,$(call proper_check.erl,function,$(t)())) endif else proper: test-build cover-data-dir $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ $(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam)))))) $(gen_verbose) $(call erlang,$(call proper_check.erl,all,undefined,$(MODULES))) endif endif # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. # Verbosity. proto_verbose_0 = @echo " PROTO " $(filter %.proto,$(?F)); proto_verbose = $(proto_verbose_$(V)) # Core targets. ifneq ($(wildcard src/),) ifneq ($(filter gpb protobuffs,$(BUILD_DEPS) $(DEPS)),) PROTO_FILES := $(filter %.proto,$(ALL_SRC_FILES)) ERL_FILES += $(addprefix src/,$(patsubst %.proto,%_pb.erl,$(notdir $(PROTO_FILES)))) ifeq ($(PROTO_FILES),) $(ERLANG_MK_TMP)/last-makefile-change-protobuffs: $(verbose) : else # Rebuild proto files when the Makefile changes. # We exclude $(PROJECT).d to avoid a circular dependency. $(ERLANG_MK_TMP)/last-makefile-change-protobuffs: $(filter-out $(PROJECT).d,$(MAKEFILE_LIST)) | $(ERLANG_MK_TMP) $(verbose) if test -f $@; then \ touch $(PROTO_FILES); \ fi $(verbose) touch $@ $(PROJECT).d:: $(ERLANG_MK_TMP)/last-makefile-change-protobuffs endif ifeq ($(filter gpb,$(BUILD_DEPS) $(DEPS)),) define compile_proto.erl [begin protobuffs_compile:generate_source(F, [ {output_include_dir, "./include"}, {output_src_dir, "./src"}]) end || F <- string:tokens("$1", " ")], halt(). endef else define compile_proto.erl [begin gpb_compile:file(F, [ $(foreach i,$(sort $(dir $(PROTO_FILES))),{i$(comma) "$i"}$(comma)) {include_as_lib, true}, {module_name_suffix, "_pb"}, {o_hrl, "./include"}, {o_erl, "./src"}, {use_packages, true} ]) end || F <- string:tokens("$1", " ")], halt(). endef endif ifneq ($(PROTO_FILES),) $(PROJECT).d:: $(PROTO_FILES) $(verbose) mkdir -p ebin/ include/ $(if $(strip $?),$(proto_verbose) $(call erlang,$(call compile_proto.erl,$?))) endif endif endif # Copyright (c) 2013-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(filter relx,$(BUILD_DEPS) $(DEPS) $(REL_DEPS)),relx) .PHONY: relx-rel relx-relup distclean-relx-rel run # Configuration. RELX_CONFIG ?= $(CURDIR)/relx.config RELX_CONFIG_SCRIPT ?= $(CURDIR)/relx.config.script RELX_OUTPUT_DIR ?= _rel RELX_REL_EXT ?= RELX_TAR ?= 1 ifdef SFX RELX_TAR = 1 endif # Core targets. ifeq ($(IS_DEP),) ifneq ($(wildcard $(RELX_CONFIG))$(wildcard $(RELX_CONFIG_SCRIPT)),) rel:: relx-rel relup:: relx-relup endif endif distclean:: distclean-relx-rel # Plugin-specific targets. define relx_get_config.erl (fun() -> Config0 = case file:consult("$(call core_native_path,$(RELX_CONFIG))") of {ok, Terms} -> Terms; {error, _} -> [] end, case filelib:is_file("$(call core_native_path,$(RELX_CONFIG_SCRIPT))") of true -> Bindings = erl_eval:add_binding('CONFIG', Config0, erl_eval:new_bindings()), {ok, Config1} = file:script("$(call core_native_path,$(RELX_CONFIG_SCRIPT))", Bindings), Config1; false -> Config0 end end)() endef define relx_release.erl Config = $(call relx_get_config.erl), {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config), Vsn = case Vsn0 of {cmd, Cmd} -> os:cmd(Cmd); semver -> ""; {semver, _} -> ""; {git, short} -> string:trim(os:cmd("git rev-parse --short HEAD"), both, "\n"); {git, long} -> string:trim(os:cmd("git rev-parse HEAD"), both, "\n"); VsnStr -> Vsn0 end, {ok, _} = relx:build_release(#{name => Name, vsn => Vsn}, Config ++ [{output_dir, "$(RELX_OUTPUT_DIR)"}]), halt(0). endef define relx_tar.erl Config = $(call relx_get_config.erl), {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config), Vsn = case Vsn0 of {cmd, Cmd} -> os:cmd(Cmd); semver -> ""; {semver, _} -> ""; {git, short} -> string:trim(os:cmd("git rev-parse --short HEAD"), both, "\n"); {git, long} -> string:trim(os:cmd("git rev-parse HEAD"), both, "\n"); VsnStr -> Vsn0 end, {ok, _} = relx:build_tar(#{name => Name, vsn => Vsn}, Config ++ [{output_dir, "$(RELX_OUTPUT_DIR)"}]), halt(0). endef define relx_relup.erl Config = $(call relx_get_config.erl), {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config), Vsn = case Vsn0 of {cmd, Cmd} -> os:cmd(Cmd); semver -> ""; {semver, _} -> ""; {git, short} -> string:trim(os:cmd("git rev-parse --short HEAD"), both, "\n"); {git, long} -> string:trim(os:cmd("git rev-parse HEAD"), both, "\n"); VsnStr -> Vsn0 end, {ok, _} = relx:build_relup(Name, Vsn, undefined, Config ++ [{output_dir, "$(RELX_OUTPUT_DIR)"}]), halt(0). endef relx-rel: rel-deps app $(call erlang,$(call relx_release.erl),-pa ebin/) $(verbose) $(MAKE) relx-post-rel $(if $(filter-out 0,$(RELX_TAR)),$(call erlang,$(call relx_tar.erl),-pa ebin/)) relx-relup: rel-deps app $(call erlang,$(call relx_release.erl),-pa ebin/) $(MAKE) relx-post-rel $(call erlang,$(call relx_relup.erl),-pa ebin/) $(if $(filter-out 0,$(RELX_TAR)),$(call erlang,$(call relx_tar.erl),-pa ebin/)) distclean-relx-rel: $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) # Default hooks. relx-post-rel:: $(verbose) : # Run target. ifeq ($(wildcard $(RELX_CONFIG))$(wildcard $(RELX_CONFIG_SCRIPT)),) run:: else define get_relx_release.erl Config = $(call relx_get_config.erl), {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config), Vsn = case Vsn0 of {cmd, Cmd} -> os:cmd(Cmd); semver -> ""; {semver, _} -> ""; {git, short} -> string:trim(os:cmd("git rev-parse --short HEAD"), both, "\n"); {git, long} -> string:trim(os:cmd("git rev-parse HEAD"), both, "\n"); VsnStr -> Vsn0 end, Extended = case lists:keyfind(extended_start_script, 1, Config) of {_, true} -> "1"; _ -> "" end, io:format("~s ~s ~s", [Name, Vsn, Extended]), halt(0). endef RELX_REL := $(shell $(call erlang,$(get_relx_release.erl))) RELX_REL_NAME := $(word 1,$(RELX_REL)) RELX_REL_VSN := $(word 2,$(RELX_REL)) RELX_REL_CMD := $(if $(word 3,$(RELX_REL)),console) ifeq ($(PLATFORM),msys2) RELX_REL_EXT := .cmd endif run:: RELX_TAR := 0 run:: all $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) $(RELX_REL_CMD) ifdef RELOAD rel:: $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) ping $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) \ eval "io:format(\"~p~n\", [c:lm()])." endif help:: $(verbose) printf "%s\n" "" \ "Relx targets:" \ " run Compile the project, build the release and run it" endif endif # Copyright (c) 2015-2016, Loïc Hoguin # Copyright (c) 2014, M Robert Martin # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: shell # Configuration. SHELL_ERL ?= erl SHELL_PATHS ?= $(CURDIR)/ebin $(TEST_DIR) SHELL_OPTS ?= ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS)) # Core targets help:: $(verbose) printf "%s\n" "" \ "Shell targets:" \ " shell Run an erlang shell with SHELL_OPTS or reasonable default" # Plugin-specific targets. $(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep)))) ifneq ($(SKIP_DEPS),) build-shell-deps: else build-shell-deps: $(ALL_SHELL_DEPS_DIRS) $(verbose) set -e; for dep in $(ALL_SHELL_DEPS_DIRS) ; do \ if [ -z "$(strip $(FULL))" ] && [ ! -L $$dep ] && [ -f $$dep/ebin/dep_built ]; then \ :; \ else \ $(MAKE) -C $$dep IS_DEP=1; \ if [ ! -L $$dep ] && [ -d $$dep/ebin ]; then touch $$dep/ebin/dep_built; fi; \ fi \ done endif shell:: build-shell-deps $(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS) # Copyright 2017, Stanislaw Klekot # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: distclean-sphinx sphinx # Configuration. SPHINX_BUILD ?= sphinx-build SPHINX_SOURCE ?= doc SPHINX_CONFDIR ?= SPHINX_FORMATS ?= html SPHINX_DOCTREES ?= $(ERLANG_MK_TMP)/sphinx.doctrees SPHINX_OPTS ?= #sphinx_html_opts = #sphinx_html_output = html #sphinx_man_opts = #sphinx_man_output = man #sphinx_latex_opts = #sphinx_latex_output = latex # Helpers. sphinx_build_0 = @echo " SPHINX" $1; $(SPHINX_BUILD) -N -q sphinx_build_1 = $(SPHINX_BUILD) -N sphinx_build_2 = set -x; $(SPHINX_BUILD) sphinx_build = $(sphinx_build_$(V)) define sphinx.build $(call sphinx_build,$1) -b $1 -d $(SPHINX_DOCTREES) $(if $(SPHINX_CONFDIR),-c $(SPHINX_CONFDIR)) $(SPHINX_OPTS) $(sphinx_$1_opts) -- $(SPHINX_SOURCE) $(call sphinx.output,$1) endef define sphinx.output $(if $(sphinx_$1_output),$(sphinx_$1_output),$1) endef # Targets. ifneq ($(wildcard $(if $(SPHINX_CONFDIR),$(SPHINX_CONFDIR),$(SPHINX_SOURCE))/conf.py),) docs:: sphinx distclean:: distclean-sphinx endif help:: $(verbose) printf "%s\n" "" \ "Sphinx targets:" \ " sphinx Generate Sphinx documentation." \ "" \ "ReST sources and 'conf.py' file are expected in directory pointed by" \ "SPHINX_SOURCE ('doc' by default). SPHINX_FORMATS lists formats to build (only" \ "'html' format is generated by default); target directory can be specified by" \ 'setting sphinx_$${format}_output, for example: sphinx_html_output = output/html' \ "Additional Sphinx options can be set in SPHINX_OPTS." # Plugin-specific targets. sphinx: $(foreach F,$(SPHINX_FORMATS),$(call sphinx.build,$F)) distclean-sphinx: $(gen_verbose) rm -rf $(filter-out $(SPHINX_SOURCE),$(foreach F,$(SPHINX_FORMATS),$(call sphinx.output,$F))) # Copyright (c) 2017, Jean-Sébastien Pédron # This file is contributed to erlang.mk and subject to the terms of the ISC License. .PHONY: show-ERL_LIBS show-ERLC_OPTS show-TEST_ERLC_OPTS show-ERL_LIBS: @echo $(ERL_LIBS) show-ERLC_OPTS: @$(foreach opt,$(ERLC_OPTS) -pa ebin -I include,echo "$(opt)";) show-TEST_ERLC_OPTS: @$(foreach opt,$(TEST_ERLC_OPTS) -pa ebin -I include,echo "$(opt)";) # Copyright (c) 2015-2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) .PHONY: triq # Targets. tests:: triq define triq_check.erl $(call cover.erl) code:add_pathsa([ "$(call core_native_path,$(CURDIR)/ebin)", "$(call core_native_path,$(DEPS_DIR)/*/ebin)", "$(call core_native_path,$(TEST_DIR))"]), try begin CoverSetup(), Res = case $(1) of all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]); module -> triq:check($(2)); function -> triq:check($(2)) end, CoverExport("$(COVER_DATA_DIR)/triq.coverdata"), Res end of true -> halt(0); _ -> halt(1) catch error:undef$(if $V,:Stacktrace) -> io:format("Undefined property or module?~n$(if $V,~p~n)", [$(if $V,Stacktrace)]), halt(0) end. endef ifdef t ifeq (,$(findstring :,$(t))) triq: test-build cover-data-dir $(verbose) $(call erlang,$(call triq_check.erl,module,$(t))) else triq: test-build cover-data-dir $(verbose) echo Testing $(t)/0 $(verbose) $(call erlang,$(call triq_check.erl,function,$(t)())) endif else triq: test-build cover-data-dir $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ $(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam)))))) $(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES))) endif endif # Copyright (c) 2022, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: xref # Configuration. # We do not use locals_not_used or deprecated_function_calls # because the compiler will error out by default in those # cases with Erlang.mk. Deprecated functions may make sense # in some cases but few libraries define them. We do not # use exports_not_used by default because it hinders more # than it helps library projects such as Cowboy. Finally, # undefined_functions provides little that undefined_function_calls # doesn't already provide, so it's not enabled by default. XREF_CHECKS ?= [undefined_function_calls] # Instead of predefined checks a query can be evaluated # using the Xref DSL. The $q variable is used in that case. # The scope is a list of keywords that correspond to # application directories, being essentially an easy way # to configure which applications to analyze. With: # # - app: . # - apps: $(ALL_APPS_DIRS) # - deps: $(ALL_DEPS_DIRS) # - otp: Built-in Erlang/OTP applications. # # The default is conservative (app) and will not be # appropriate for all types of queries (for example # application_call requires adding all applications # that might be called or they will not be found). XREF_SCOPE ?= app # apps deps otp # If the above is not enough, additional application # directories can be configured. XREF_EXTRA_APP_DIRS ?= # As well as additional non-application directories. XREF_EXTRA_DIRS ?= # Erlang.mk supports -ignore_xref([...]) with forms # {M, F, A} | {F, A} | M, the latter ignoring whole # modules. Ignores can also be provided project-wide. XREF_IGNORE ?= [] # All callbacks may be ignored. Erlang.mk will ignore # them automatically for exports_not_used (unless it # is explicitly disabled by the user). XREF_IGNORE_CALLBACKS ?= # Core targets. help:: $(verbose) printf '%s\n' '' \ 'Xref targets:' \ ' xref Analyze the project using Xref' \ ' xref q=QUERY Evaluate an Xref query' # Plugin-specific targets. define xref.erl {ok, Xref} = xref:start([]), Scope = [$(call comma_list,$(XREF_SCOPE))], AppDirs0 = [$(call comma_list,$(foreach d,$(XREF_EXTRA_APP_DIRS),"$d"))], AppDirs1 = case lists:member(otp, Scope) of false -> AppDirs0; true -> RootDir = code:root_dir(), AppDirs0 ++ [filename:dirname(P) || P <- code:get_path(), lists:prefix(RootDir, P)] end, AppDirs2 = case lists:member(deps, Scope) of false -> AppDirs1; true -> [$(call comma_list,$(foreach d,$(ALL_DEPS_DIRS),"$d"))] ++ AppDirs1 end, AppDirs3 = case lists:member(apps, Scope) of false -> AppDirs2; true -> [$(call comma_list,$(foreach d,$(ALL_APPS_DIRS),"$d"))] ++ AppDirs2 end, AppDirs = case lists:member(app, Scope) of false -> AppDirs3; true -> ["../$(notdir $(CURDIR))"|AppDirs3] end, [{ok, _} = xref:add_application(Xref, AppDir, [{builtins, true}]) || AppDir <- AppDirs], ExtraDirs = [$(call comma_list,$(foreach d,$(XREF_EXTRA_DIRS),"$d"))], [{ok, _} = xref:add_directory(Xref, ExtraDir, [{builtins, true}]) || ExtraDir <- ExtraDirs], ok = xref:set_library_path(Xref, code:get_path() -- (["ebin", "."] ++ AppDirs ++ ExtraDirs)), Checks = case {$1, is_list($2)} of {check, true} -> $2; {check, false} -> [$2]; {query, _} -> [$2] end, FinalRes = [begin IsInformational = case $1 of query -> true; check -> is_tuple(Check) andalso lists:member(element(1, Check), [call, use, module_call, module_use, application_call, application_use]) end, {ok, Res0} = case $1 of check -> xref:analyze(Xref, Check); query -> xref:q(Xref, Check) end, Res = case IsInformational of true -> Res0; false -> lists:filter(fun(R) -> {Mod, InMFA, MFA} = case R of {InMFA0 = {M, _, _}, MFA0} -> {M, InMFA0, MFA0}; {M, _, _} -> {M, R, R} end, Attrs = try Mod:module_info(attributes) catch error:undef -> [] end, InlineIgnores = lists:flatten([ [case V of M when is_atom(M) -> {M, '_', '_'}; {F, A} -> {Mod, F, A}; _ -> V end || V <- Values] || {ignore_xref, Values} <- Attrs]), BuiltinIgnores = [ {eunit_test, wrapper_test_exported_, 0} ], DoCallbackIgnores = case {Check, "$(strip $(XREF_IGNORE_CALLBACKS))"} of {exports_not_used, ""} -> true; {_, "0"} -> false; _ -> true end, CallbackIgnores = case DoCallbackIgnores of false -> []; true -> Behaviors = lists:flatten([ [BL || {behavior, BL} <- Attrs], [BL || {behaviour, BL} <- Attrs] ]), [{Mod, CF, CA} || B <- Behaviors, {CF, CA} <- B:behaviour_info(callbacks)] end, WideIgnores = if is_list($(XREF_IGNORE)) -> [if is_atom(I) -> {I, '_', '_'}; true -> I end || I <- $(XREF_IGNORE)]; true -> [$(XREF_IGNORE)] end, Ignores = InlineIgnores ++ BuiltinIgnores ++ CallbackIgnores ++ WideIgnores, not (lists:member(InMFA, Ignores) orelse lists:member(MFA, Ignores) orelse lists:member({Mod, '_', '_'}, Ignores)) end, Res0) end, case Res of [] -> ok; _ when IsInformational -> case Check of {call, {CM, CF, CA}} -> io:format("Functions that ~s:~s/~b calls:~n", [CM, CF, CA]); {use, {CM, CF, CA}} -> io:format("Function ~s:~s/~b is called by:~n", [CM, CF, CA]); {module_call, CMod} -> io:format("Modules that ~s calls:~n", [CMod]); {module_use, CMod} -> io:format("Module ~s is used by:~n", [CMod]); {application_call, CApp} -> io:format("Applications that ~s calls:~n", [CApp]); {application_use, CApp} -> io:format("Application ~s is used by:~n", [CApp]); _ when $1 =:= query -> io:format("Query ~s returned:~n", [Check]) end, [case R of {{InM, InF, InA}, {M, F, A}} -> io:format("- ~s:~s/~b called by ~s:~s/~b~n", [M, F, A, InM, InF, InA]); {M, F, A} -> io:format("- ~s:~s/~b~n", [M, F, A]); ModOrApp -> io:format("- ~s~n", [ModOrApp]) end || R <- Res], ok; _ -> [case {Check, R} of {undefined_function_calls, {{InM, InF, InA}, {M, F, A}}} -> io:format("Undefined function ~s:~s/~b called by ~s:~s/~b~n", [M, F, A, InM, InF, InA]); {undefined_functions, {M, F, A}} -> io:format("Undefined function ~s:~s/~b~n", [M, F, A]); {locals_not_used, {M, F, A}} -> io:format("Unused local function ~s:~s/~b~n", [M, F, A]); {exports_not_used, {M, F, A}} -> io:format("Unused exported function ~s:~s/~b~n", [M, F, A]); {deprecated_function_calls, {{InM, InF, InA}, {M, F, A}}} -> io:format("Deprecated function ~s:~s/~b called by ~s:~s/~b~n", [M, F, A, InM, InF, InA]); {deprecated_functions, {M, F, A}} -> io:format("Deprecated function ~s:~s/~b~n", [M, F, A]); _ -> io:format("~p: ~p~n", [Check, R]) end || R <- Res], error end end || Check <- Checks], stopped = xref:stop(Xref), case lists:usort(FinalRes) of [ok] -> halt(0); _ -> halt(1) end endef xref: deps app ifdef q $(verbose) $(call erlang,$(call xref.erl,query,"$q"),-pa ebin/) else $(verbose) $(call erlang,$(call xref.erl,check,$(XREF_CHECKS)),-pa ebin/) endif # Copyright (c) 2016, Loïc Hoguin # Copyright (c) 2015, Viktor Söderqvist # This file is part of erlang.mk and subject to the terms of the ISC License. COVER_REPORT_DIR ?= cover COVER_DATA_DIR ?= $(COVER_REPORT_DIR) ifdef COVER COVER_APPS ?= $(notdir $(ALL_APPS_DIRS)) COVER_DEPS ?= COVER_EXCLUDE_MODS ?= endif # Code coverage for Common Test. ifdef COVER ifdef CT_RUN ifneq ($(wildcard $(TEST_DIR)),) test-build:: $(TEST_DIR)/ct.cover.spec $(TEST_DIR)/ct.cover.spec: cover-data-dir $(gen_verbose) printf "%s\n" \ "{incl_app, '$(PROJECT)', details}." \ "{incl_dirs, '$(PROJECT)', [\"$(call core_native_path,$(CURDIR)/ebin)\" \ $(foreach a,$(COVER_APPS),$(comma) \"$(call core_native_path,$(APPS_DIR)/$a/ebin)\") \ $(foreach d,$(COVER_DEPS),$(comma) \"$(call core_native_path,$(DEPS_DIR)/$d/ebin)\")]}." \ '{export,"$(call core_native_path,$(abspath $(COVER_DATA_DIR))/ct.coverdata)"}.' \ "{excl_mods, '$(PROJECT)', [$(call comma_list,$(COVER_EXCLUDE_MODS))]}." > $@ CT_RUN += -cover $(TEST_DIR)/ct.cover.spec endif endif endif # Code coverage for other tools. ifdef COVER define cover.erl CoverSetup = fun() -> Dirs = ["$(call core_native_path,$(CURDIR)/ebin)" $(foreach a,$(COVER_APPS),$(comma) "$(call core_native_path,$(APPS_DIR)/$a/ebin)") $(foreach d,$(COVER_DEPS),$(comma) "$(call core_native_path,$(DEPS_DIR)/$d/ebin)")], Excludes = [$(call comma_list,$(foreach e,$(COVER_EXCLUDE_MODS),"$e"))], [case file:list_dir(Dir) of {error, enotdir} -> false; {error, _} -> halt(2); {ok, Files} -> BeamFiles = [filename:join(Dir, File) || File <- Files, not lists:member(filename:basename(File, ".beam"), Excludes), filename:extension(File) =:= ".beam"], case cover:compile_beam(BeamFiles) of {error, _} -> halt(1); _ -> true end end || Dir <- Dirs] end, CoverExport = fun(Filename) -> cover:export(Filename) end, endef else define cover.erl CoverSetup = fun() -> ok end, CoverExport = fun(_) -> ok end, endef endif # Core targets ifdef COVER ifneq ($(COVER_REPORT_DIR),) tests:: $(verbose) $(MAKE) --no-print-directory cover-report endif cover-data-dir: | $(COVER_DATA_DIR) $(COVER_DATA_DIR): $(verbose) mkdir -p $(COVER_DATA_DIR) else cover-data-dir: endif clean:: coverdata-clean ifneq ($(COVER_REPORT_DIR),) distclean:: cover-report-clean endif help:: $(verbose) printf "%s\n" "" \ "Cover targets:" \ " cover-report Generate a HTML coverage report from previously collected" \ " cover data." \ " all.coverdata Merge all coverdata files into all.coverdata." \ "" \ "If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \ "target tests additionally generates a HTML coverage report from the combined" \ "coverdata files from each of these testing tools. HTML reports can be disabled" \ "by setting COVER_REPORT_DIR to empty." # Plugin specific targets COVERDATA = $(filter-out $(COVER_DATA_DIR)/all.coverdata,$(wildcard $(COVER_DATA_DIR)/*.coverdata)) .PHONY: coverdata-clean coverdata-clean: $(gen_verbose) rm -f $(COVER_DATA_DIR)/*.coverdata $(TEST_DIR)/ct.cover.spec # Merge all coverdata files into one. define cover_export.erl $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) cover:export("$(COVER_DATA_DIR)/$@"), halt(0). endef all.coverdata: $(COVERDATA) cover-data-dir $(gen_verbose) $(call erlang,$(cover_export.erl)) # These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to # empty if you want the coverdata files but not the HTML report. ifneq ($(COVER_REPORT_DIR),) .PHONY: cover-report-clean cover-report cover-report-clean: $(gen_verbose) rm -rf $(COVER_REPORT_DIR) ifneq ($(COVER_REPORT_DIR),$(COVER_DATA_DIR)) $(if $(shell ls -A $(COVER_DATA_DIR)/),,$(verbose) rmdir $(COVER_DATA_DIR)) endif ifeq ($(COVERDATA),) cover-report: else # Modules which include eunit.hrl always contain one line without coverage # because eunit defines test/0 which is never called. We compensate for this. EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \ grep -H -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ | sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq)) define cover_report.erl $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) Ms = cover:imported_modules(), [cover:analyse_to_file(M, "$(COVER_REPORT_DIR)/" ++ atom_to_list(M) ++ ".COVER.html", [html]) || M <- Ms], Report = [begin {ok, R} = cover:analyse(M, module), R end || M <- Ms], EunitHrlMods = [$(EUNIT_HRL_MODS)], Report1 = [{M, {Y, case lists:member(M, EunitHrlMods) of true -> N - 1; false -> N end}} || {M, {Y, N}} <- Report], TotalY = lists:sum([Y || {_, {Y, _}} <- Report1]), TotalN = lists:sum([N || {_, {_, N}} <- Report1]), Perc = fun(Y, N) -> case Y + N of 0 -> 100; S -> round(100 * Y / S) end end, TotalPerc = Perc(TotalY, TotalN), {ok, F} = file:open("$(COVER_REPORT_DIR)/index.html", [write]), io:format(F, "~n" "~n" "Coverage report~n" "~n", []), io:format(F, "

Coverage

~n

Total: ~p%

~n", [TotalPerc]), io:format(F, "~n", []), [io:format(F, "" "~n", [M, M, Perc(Y, N)]) || {M, {Y, N}} <- Report1], How = "$(subst $(space),$(comma)$(space),$(basename $(COVERDATA)))", Date = "$(shell date -u "+%Y-%m-%dT%H:%M:%SZ")", io:format(F, "
ModuleCoverage
~p~p%
~n" "

Generated using ~s and erlang.mk on ~s.

~n" "", [How, Date]), halt(). endef cover-report: $(verbose) mkdir -p $(COVER_REPORT_DIR) $(gen_verbose) $(call erlang,$(cover_report.erl)) endif endif # ifneq ($(COVER_REPORT_DIR),) # Copyright (c) 2016, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. .PHONY: sfx ifdef RELX_REL ifdef SFX # Configuration. SFX_ARCHIVE ?= $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/$(RELX_REL_NAME)-$(RELX_REL_VSN).tar.gz SFX_OUTPUT_FILE ?= $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME).run # Core targets. rel:: sfx # Plugin-specific targets. define sfx_stub #!/bin/sh TMPDIR=`mktemp -d` ARCHIVE=`awk '/^__ARCHIVE_BELOW__$$/ {print NR + 1; exit 0;}' $$0` FILENAME=$$(basename $$0) REL=$${FILENAME%.*} tail -n+$$ARCHIVE $$0 | tar -xzf - -C $$TMPDIR $$TMPDIR/bin/$$REL console RET=$$? rm -rf $$TMPDIR exit $$RET __ARCHIVE_BELOW__ endef sfx: $(verbose) $(call core_render,sfx_stub,$(SFX_OUTPUT_FILE)) $(gen_verbose) cat $(SFX_ARCHIVE) >> $(SFX_OUTPUT_FILE) $(verbose) chmod +x $(SFX_OUTPUT_FILE) endif endif # Copyright (c) 2013-2017, Loïc Hoguin # This file is part of erlang.mk and subject to the terms of the ISC License. # External plugins. DEP_PLUGINS ?= $(foreach p,$(DEP_PLUGINS),\ $(eval $(if $(findstring /,$p),\ $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ $(call core_dep_plugin,$p/plugins.mk,$p)))) help:: help-plugins help-plugins:: $(verbose) : # Copyright (c) 2013-2015, Loïc Hoguin # Copyright (c) 2015-2016, Jean-Sébastien Pédron # This file is part of erlang.mk and subject to the terms of the ISC License. # Fetch dependencies recursively (without building them). .PHONY: fetch-deps fetch-doc-deps fetch-rel-deps fetch-test-deps \ fetch-shell-deps .PHONY: $(ERLANG_MK_RECURSIVE_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) fetch-deps: $(ERLANG_MK_RECURSIVE_DEPS_LIST) fetch-doc-deps: $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) fetch-rel-deps: $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) fetch-test-deps: $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) fetch-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) ifneq ($(SKIP_DEPS),) $(ERLANG_MK_RECURSIVE_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): $(verbose) :> $@ else # By default, we fetch "normal" dependencies. They are also included no # matter the type of requested dependencies. # # $(ALL_DEPS_DIRS) includes $(BUILD_DEPS). $(ERLANG_MK_RECURSIVE_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_DOC_DEPS_DIRS) $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_REL_DEPS_DIRS) $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_TEST_DEPS_DIRS) $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): $(LOCAL_DEPS_DIRS) $(ALL_DEPS_DIRS) $(ALL_SHELL_DEPS_DIRS) # Allow to use fetch-deps and $(DEP_TYPES) to fetch multiple types of # dependencies with a single target. ifneq ($(filter doc,$(DEP_TYPES)),) $(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_DOC_DEPS_DIRS) endif ifneq ($(filter rel,$(DEP_TYPES)),) $(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_REL_DEPS_DIRS) endif ifneq ($(filter test,$(DEP_TYPES)),) $(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_TEST_DEPS_DIRS) endif ifneq ($(filter shell,$(DEP_TYPES)),) $(ERLANG_MK_RECURSIVE_DEPS_LIST): $(ALL_SHELL_DEPS_DIRS) endif ERLANG_MK_RECURSIVE_TMP_LIST := $(abspath $(ERLANG_MK_TMP)/recursive-tmp-deps-$(shell echo $$PPID).log) $(ERLANG_MK_RECURSIVE_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) \ $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST): | $(ERLANG_MK_TMP) ifeq ($(IS_APP)$(IS_DEP),) $(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST) endif $(verbose) touch $(ERLANG_MK_RECURSIVE_TMP_LIST) $(verbose) set -e; for dep in $^ ; do \ if ! grep -qs ^$$dep$$ $(ERLANG_MK_RECURSIVE_TMP_LIST); then \ echo $$dep >> $(ERLANG_MK_RECURSIVE_TMP_LIST); \ if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \ $$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \ $(MAKE) -C $$dep fetch-deps \ IS_DEP=1 \ ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \ fi \ fi \ done ifeq ($(IS_APP)$(IS_DEP),) $(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | \ uniq > $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $(verbose) mv $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@ $(verbose) rm $(ERLANG_MK_RECURSIVE_TMP_LIST) endif endif # ifneq ($(SKIP_DEPS),) # List dependencies recursively. .PHONY: list-deps list-doc-deps list-rel-deps list-test-deps \ list-shell-deps list-deps: $(ERLANG_MK_RECURSIVE_DEPS_LIST) list-doc-deps: $(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST) list-rel-deps: $(ERLANG_MK_RECURSIVE_REL_DEPS_LIST) list-test-deps: $(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST) list-shell-deps: $(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST) list-deps list-doc-deps list-rel-deps list-test-deps list-shell-deps: $(verbose) cat $^ # Query dependencies recursively. .PHONY: query-deps query-doc-deps query-rel-deps query-test-deps \ query-shell-deps QUERY ?= name fetch_method repo version define query_target $1: $2 clean-tmp-query.log ifeq ($(IS_APP)$(IS_DEP),) $(verbose) rm -f $4 endif $(verbose) $(foreach dep,$3,\ echo $(PROJECT): $(foreach q,$(QUERY),$(call query_$(q),$(dep))) >> $4 ;) $(if $(filter-out query-deps,$1),,\ $(verbose) set -e; for dep in $3 ; do \ if grep -qs ^$$$$dep$$$$ $(ERLANG_MK_TMP)/query.log; then \ :; \ else \ echo $$$$dep >> $(ERLANG_MK_TMP)/query.log; \ $(MAKE) -C $(DEPS_DIR)/$$$$dep $$@ QUERY="$(QUERY)" IS_DEP=1 || true; \ fi \ done) ifeq ($(IS_APP)$(IS_DEP),) $(verbose) touch $4 $(verbose) cat $4 endif endef clean-tmp-query.log: ifeq ($(IS_DEP),) $(verbose) rm -f $(ERLANG_MK_TMP)/query.log endif $(eval $(call query_target,query-deps,$(ERLANG_MK_RECURSIVE_DEPS_LIST),$(BUILD_DEPS) $(DEPS),$(ERLANG_MK_QUERY_DEPS_FILE))) $(eval $(call query_target,query-doc-deps,$(ERLANG_MK_RECURSIVE_DOC_DEPS_LIST),$(DOC_DEPS),$(ERLANG_MK_QUERY_DOC_DEPS_FILE))) $(eval $(call query_target,query-rel-deps,$(ERLANG_MK_RECURSIVE_REL_DEPS_LIST),$(REL_DEPS),$(ERLANG_MK_QUERY_REL_DEPS_FILE))) $(eval $(call query_target,query-test-deps,$(ERLANG_MK_RECURSIVE_TEST_DEPS_LIST),$(TEST_DEPS),$(ERLANG_MK_QUERY_TEST_DEPS_FILE))) $(eval $(call query_target,query-shell-deps,$(ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST),$(SHELL_DEPS),$(ERLANG_MK_QUERY_SHELL_DEPS_FILE))) ================================================ FILE: examples/README.asciidoc ================================================ = Cowboy examples * link:chunked_hello_world[]: demonstrate chunked data transfer with two one-second delays * link:compress_response[]: send a response body compressed if the client supports it * link:cookie[]: set cookies from server and client side * link:echo_get[]: parse and echo a GET query string * link:echo_post[]: parse and echo a POST parameter * link:eventsource[]: eventsource emitter and consumer * link:file_server[]: file server with directory listing * link:hello_world[]: simplest example application * link:markdown_middleware[]: static file handler with markdown preprocessor * link:rest_basic_auth[]: basic HTTP authorization with REST * link:rest_hello_world[]: return the data type that matches the request type (ex: html, text, json) * link:rest_pastebin[]: create text objects and return the data type that matches the request type (html, text) * link:ssl_hello_world[]: simplest SSL application * link:upload[]: multipart/form-data upload * link:websocket[]: websocket example == Other languages * https://github.com/joshrotenberg/elixir_cowboy_examples[Elixir] * https://github.com/quasiquoting/lfe-cowboy-examples[LFE] ================================================ FILE: examples/chunked_hello_world/Makefile ================================================ PROJECT = chunked_hello_world PROJECT_DESCRIPTION = Cowboy chunked Hello World example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/chunked_hello_world/README.asciidoc ================================================ = Chunked hello world example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 or use `curl` to see the chunks arriving one at a time every second. == HTTP/1.1 example output [source,bash] ---- $ time curl -i http://localhost:8080 HTTP/1.1 200 OK transfer-encoding: chunked connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:24:16 GMT Hello World Chunked! curl -i http://localhost:8080 0.01s user 0.00s system 0% cpu 2.015 total ---- == HTTP/2 example output [source,bash] ---- $ nghttp -v http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.006] recv SETTINGS frame (niv=0) [ 0.006] recv SETTINGS frame ; ACK (niv=0) [ 0.006] send SETTINGS frame ; ACK (niv=0) [ 0.010] recv (stream_id=13) :status: 200 [ 0.010] recv (stream_id=13) date: Mon, 13 Jun 2016 14:16:26 GMT [ 0.010] recv (stream_id=13) server: Cowboy [ 0.010] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header Hello [ 0.010] recv DATA frame World [ 1.012] recv DATA frame Chunked! [ 2.013] recv DATA frame [ 2.013] recv DATA frame ; END_STREAM [ 2.013] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/chunked_hello_world/relx.config ================================================ {release, {chunked_hello_world_example, "1"}, [chunked_hello_world]}. {extended_start_script, true}. ================================================ FILE: examples/chunked_hello_world/src/chunked_hello_world_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(chunked_hello_world_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), chunked_hello_world_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/chunked_hello_world/src/chunked_hello_world_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(chunked_hello_world_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/chunked_hello_world/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Chunked hello world handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body("Hello\r\n", nofin, Req), timer:sleep(1000), cowboy_req:stream_body("World\r\n", nofin, Req), timer:sleep(1000), cowboy_req:stream_body("Chunked!\r\n", fin, Req), {ok, Req, Opts}. ================================================ FILE: examples/compress_response/Makefile ================================================ PROJECT = compress_response PROJECT_DESCRIPTION = Cowboy compressed response example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/compress_response/README.asciidoc ================================================ = Compressed response example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 == HTTP/1.1 example output Without compression: [source,bash] ---- $ curl -i http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Mon, 07 Jan 2013 18:42:29 GMT content-length: 909 A cowboy is an animal herder who tends cattle on ranches in North America, traditionally on horseback, and often performs a multitude of other ranch- related tasks. The historic American cowboy of the late 19th century arose from the vaquero traditions of northern Mexico and became a figure of special significance and legend. A subtype, called a wrangler, specifically tends the horses used to work cattle. In addition to ranch work, some cowboys work for or participate in rodeos. Cowgirls, first defined as such in the late 19th century, had a less-well documented historical role, but in the modern world have established the ability to work at virtually identical tasks and obtained considerable respect for their achievements. There are also cattle handlers in many other parts of the world, particularly South America and Australia, who perform work similar to the cowboy in their respective nations. ---- With compression: [source,bash] ---- $ curl -i --compressed http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Mon, 07 Jan 2013 18:42:30 GMT content-encoding: gzip content-length: 510 A cowboy is an animal herder who tends cattle on ranches in North America, traditionally on horseback, and often performs a multitude of other ranch- related tasks. The historic American cowboy of the late 19th century arose from the vaquero traditions of northern Mexico and became a figure of special significance and legend. A subtype, called a wrangler, specifically tends the horses used to work cattle. In addition to ranch work, some cowboys work for or participate in rodeos. Cowgirls, first defined as such in the late 19th century, had a less-well documented historical role, but in the modern world have established the ability to work at virtually identical tasks and obtained considerable respect for their achievements. There are also cattle handlers in many other parts of the world, particularly South America and Australia, who perform work similar to the cowboy in their respective nations. ---- == HTTP/2 example output Without compression: [source,bash] ---- $ nghttp -v -H 'accept-encoding: compress' http://localhost:8080 [ 0.001] Connected [ 0.001] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.001] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.002] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: compress user-agent: nghttp2/1.18.1 [ 0.002] recv SETTINGS frame (niv=0) [ 0.002] recv SETTINGS frame ; ACK (niv=0) [ 0.002] send SETTINGS frame ; ACK (niv=0) [ 0.003] recv (stream_id=13) :status: 200 [ 0.003] recv (stream_id=13) content-length: 909 [ 0.003] recv (stream_id=13) date: Sun, 22 Jan 2017 19:13:47 GMT [ 0.003] recv (stream_id=13) server: Cowboy [ 0.003] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header A cowboy is an animal herder who tends cattle on ranches in North America, traditionally on horseback, and often performs a multitude of other ranch- related tasks. The historic American cowboy of the late 19th century arose from the vaquero traditions of northern Mexico and became a figure of special significance and legend. A subtype, called a wrangler, specifically tends the horses used to work cattle. In addition to ranch work, some cowboys work for or participate in rodeos. Cowgirls, first defined as such in the late 19th century, had a less-well documented historical role, but in the modern world have established the ability to work at virtually identical tasks and obtained considerable respect for their achievements. There are also cattle handlers in many other parts of the world, particularly South America and Australia, who perform work similar to the cowboy in their respective nations. [ 0.003] recv DATA frame ; END_STREAM [ 0.003] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- With compression: [source,bash] ---- $ nghttp -v http://localhost:8080 [ERROR] Could not connect to the address ::1 Trying next address 127.0.0.1 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.18.1 [ 0.000] recv SETTINGS frame (niv=0) [ 0.000] send SETTINGS frame ; ACK (niv=0) [ 0.000] recv SETTINGS frame ; ACK (niv=0) [ 0.000] recv (stream_id=13) :status: 200 [ 0.000] recv (stream_id=13) content-encoding: gzip [ 0.000] recv (stream_id=13) content-length: 510 [ 0.000] recv (stream_id=13) date: Sun, 22 Jan 2017 19:15:16 GMT [ 0.000] recv (stream_id=13) server: Cowboy [ 0.000] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header A cowboy is an animal herder who tends cattle on ranches in North America, traditionally on horseback, and often performs a multitude of other ranch- related tasks. The historic American cowboy of the late 19th century arose from the vaquero traditions of northern Mexico and became a figure of special significance and legend. A subtype, called a wrangler, specifically tends the horses used to work cattle. In addition to ranch work, some cowboys work for or participate in rodeos. Cowgirls, first defined as such in the late 19th century, had a less-well documented historical role, but in the modern world have established the ability to work at virtually identical tasks and obtained considerable respect for their achievements. There are also cattle handlers in many other parts of the world, particularly South America and Australia, who perform work similar to the cowboy in their respective nations. [ 0.000] recv DATA frame ; END_STREAM [ 0.000] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/compress_response/relx.config ================================================ {release, {compress_response_example, "1"}, [compress_response]}. {extended_start_script, true}. ================================================ FILE: examples/compress_response/src/compress_response_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(compress_response_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }), compress_response_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/compress_response/src/compress_response_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(compress_response_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/compress_response/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Compress response handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> BigBody = <<"A cowboy is an animal herder who tends cattle on ranches in North America, traditionally on horseback, and often performs a multitude of other ranch- related tasks. The historic American cowboy of the late 19th century arose from the vaquero traditions of northern Mexico and became a figure of special significance and legend. A subtype, called a wrangler, specifically tends the horses used to work cattle. In addition to ranch work, some cowboys work for or participate in rodeos. Cowgirls, first defined as such in the late 19th century, had a less-well documented historical role, but in the modern world have established the ability to work at virtually identical tasks and obtained considerable respect for their achievements. There are also cattle handlers in many other parts of the world, particularly South America and Australia, who perform work similar to the cowboy in their respective nations.\n">>, Req = cowboy_req:reply(200, #{}, BigBody, Req0), {ok, Req, Opts}. ================================================ FILE: examples/cookie/Makefile ================================================ PROJECT = cookie PROJECT_DESCRIPTION = Cowboy Cookie example PROJECT_VERSION = 1 DEPS = cowboy erlydtl dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/cookie/README.asciidoc ================================================ = Cookie example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 This example allows you to use any path to show that the cookies are defined site-wide. Try it! ================================================ FILE: examples/cookie/relx.config ================================================ {release, {cookie_example, "1"}, [cookie]}. {extended_start_script, true}. ================================================ FILE: examples/cookie/src/cookie_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(cookie_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {'_', toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), cookie_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/cookie/src/cookie_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(cookie_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/cookie/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Cookie handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> NewValue = integer_to_list(rand:uniform(1000000)), Req1 = cowboy_req:set_resp_cookie(<<"server">>, NewValue, Req0, #{path => <<"/">>}), #{client := ClientCookie, server := ServerCookie} = cowboy_req:match_cookies([{client, [], <<>>}, {server, [], <<>>}], Req1), {ok, Body} = toppage_dtl:render([ {client, ClientCookie}, {server, ServerCookie} ]), Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/html">> }, Body, Req1), {ok, Req, Opts}. ================================================ FILE: examples/cookie/templates/toppage.dtl ================================================ Cowboy Cookie Example

Cowboy Cookie Example

Refresh the page to see the next cookie.

Cookie Set Server-Side

{{ server }}

Cookie Set Client-Side

{{ client }}

================================================ FILE: examples/echo_get/Makefile ================================================ PROJECT = echo_get PROJECT_DESCRIPTION = Cowboy GET echo example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/echo_get/README.asciidoc ================================================ = GET parameter echo example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080/?echo=hello You can replace the `echo` parameter with another to check that the handler is echoing it back properly. == HTTP/1.1 example output [source,bash] ---- $ curl -i "http://localhost:8080/?echo=saymyname" HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:09:04 GMT content-length: 9 content-type: text/plain; charset=utf-8 saymyname ---- == HTTP/2 example output [source,bash] ---- $ nghttp -v "http://localhost:8080/?echo=saymyname" [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: /?echo=saymyname :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.000] recv SETTINGS frame (niv=0) [ 0.000] send SETTINGS frame ; ACK (niv=0) [ 0.000] recv SETTINGS frame ; ACK (niv=0) [ 0.001] recv (stream_id=13) :status: 200 [ 0.001] recv (stream_id=13) content-length: 9 [ 0.001] recv (stream_id=13) content-type: text/plain; charset=utf-8 [ 0.001] recv (stream_id=13) date: Thu, 09 Jun 2016 09:06:05 GMT [ 0.001] recv (stream_id=13) server: Cowboy [ 0.001] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header saymyname[ 0.001] recv DATA frame ; END_STREAM [ 0.001] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/echo_get/relx.config ================================================ {release, {echo_get_example, "1"}, [echo_get]}. {extended_start_script, true}. ================================================ FILE: examples/echo_get/src/echo_get_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(echo_get_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), echo_get_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/echo_get/src/echo_get_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(echo_get_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/echo_get/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc GET echo handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> Method = cowboy_req:method(Req0), #{echo := Echo} = cowboy_req:match_qs([{echo, [], undefined}], Req0), Req = echo(Method, Echo, Req0), {ok, Req, Opts}. echo(<<"GET">>, undefined, Req) -> cowboy_req:reply(400, #{}, <<"Missing echo parameter.">>, Req); echo(<<"GET">>, Echo, Req) -> cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain; charset=utf-8">> }, Echo, Req); echo(_, _, Req) -> %% Method not allowed. cowboy_req:reply(405, Req). ================================================ FILE: examples/echo_post/Makefile ================================================ PROJECT = echo_post PROJECT_DESCRIPTION = Cowboy POST echo example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/echo_post/README.asciidoc ================================================ = POST parameter echo example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run As this example echoes a POST parameter, it is a little more complex to test. Some browsers feature tools that allow you to perform one such request, or you can use the command line tool `curl` as we will demonstrate. == HTTP/1.1 example output [source,bash] ---- $ curl -i -d echo=echomeplz http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:12:36 GMT content-length: 9 content-type: text/plain; charset=utf-8 echomeplz ---- == HTTP/2 example output [source,bash] ---- $ echo echo=echomeplz | nghttp -v -d - http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.001] send HEADERS frame ; END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: POST :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 content-length: 15 [ 0.001] send DATA frame [ 0.001] send DATA frame ; END_STREAM [ 0.012] recv SETTINGS frame (niv=0) [ 0.012] recv SETTINGS frame ; ACK (niv=0) [ 0.012] send SETTINGS frame ; ACK (niv=0) [ 0.020] recv (stream_id=13) :status: 200 [ 0.020] recv (stream_id=13) content-length: 10 [ 0.020] recv (stream_id=13) content-type: text/plain; charset=utf-8 [ 0.020] recv (stream_id=13) date: Thu, 09 Jun 2016 09:19:35 GMT [ 0.020] recv (stream_id=13) server: Cowboy [ 0.020] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header echomeplz [ 0.020] recv DATA frame ; END_STREAM [ 0.020] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/echo_post/relx.config ================================================ {release, {echo_post_example, "1"}, [echo_post]}. {extended_start_script, true}. ================================================ FILE: examples/echo_post/src/echo_post_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(echo_post_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), echo_post_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/echo_post/src/echo_post_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(echo_post_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/echo_post/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc POST echo handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> Method = cowboy_req:method(Req0), HasBody = cowboy_req:has_body(Req0), Req = maybe_echo(Method, HasBody, Req0), {ok, Req, Opts}. maybe_echo(<<"POST">>, true, Req0) -> {ok, PostVals, Req} = cowboy_req:read_urlencoded_body(Req0), Echo = proplists:get_value(<<"echo">>, PostVals), echo(Echo, Req); maybe_echo(<<"POST">>, false, Req) -> cowboy_req:reply(400, #{}, <<"Missing body.">>, Req); maybe_echo(_, _, Req) -> %% Method not allowed. cowboy_req:reply(405, Req). echo(undefined, Req) -> cowboy_req:reply(400, #{}, <<"Missing echo parameter.">>, Req); echo(Echo, Req) -> cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain; charset=utf-8">> }, Echo, Req). ================================================ FILE: examples/eventsource/Makefile ================================================ PROJECT = eventsource PROJECT_DESCRIPTION = Cowboy EventSource example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/eventsource/README.asciidoc ================================================ = EventSource example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 ================================================ FILE: examples/eventsource/priv/index.html ================================================ Hi!
================================================ FILE: examples/eventsource/relx.config ================================================ {release, {eventsource_example, "1"}, [eventsource]}. {extended_start_script, true}. ================================================ FILE: examples/eventsource/src/eventsource_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(eventsource_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/eventsource", eventsource_h, []}, {"/", cowboy_static, {priv_file, eventsource, "index.html"}} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), eventsource_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/eventsource/src/eventsource_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc EventSource emitter. -module(eventsource_h). -export([init/2]). -export([info/3]). init(Req0, Opts) -> Req = cowboy_req:stream_reply(200, #{ <<"content-type">> => <<"text/event-stream">> }, Req0), erlang:send_after(1000, self(), {message, "Tick"}), {cowboy_loop, Req, Opts}. info({message, Msg}, Req, State) -> cowboy_req:stream_events(#{ id => id(), data => Msg }, nofin, Req), erlang:send_after(1000, self(), {message, "Tick"}), {ok, Req, State}. id() -> integer_to_list(erlang:unique_integer([positive, monotonic]), 16). ================================================ FILE: examples/eventsource/src/eventsource_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(eventsource_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/file_server/Makefile ================================================ PROJECT = file_server PROJECT_DESCRIPTION = Cowboy file server example with directory listing PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/file_server/README.asciidoc ================================================ = File server example with directory listing To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 to browse the contents of the `priv` directory. Interesting examples include: * http://localhost:8080/test.txt[Plain text file] * http://localhost:8080/video.html[HTML5 video demo] == HTTP/1.1 example output [source,bash] ---- $ curl -i http://localhost:8080/test.txt HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Mon, 09 Sep 2013 13:49:50 GMT content-length: 52 content-type: text/plain last-modified: Fri, 18 Jan 2013 16:33:31 GMT If you read this then the static file server works! ---- == HTTP/2 example output [source,bash] ---- $ nghttp -v http://localhost:8080/test.txt [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: /test.txt :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.001] recv SETTINGS frame (niv=0) [ 0.001] recv SETTINGS frame ; ACK (niv=0) [ 0.001] send SETTINGS frame ; ACK (niv=0) [ 0.007] recv (stream_id=13) :status: 200 [ 0.007] recv (stream_id=13) content-length: 52 [ 0.007] recv (stream_id=13) content-type: text/plain [ 0.007] recv (stream_id=13) date: Mon, 13 Jun 2016 11:25:20 GMT [ 0.007] recv (stream_id=13) etag: "1371478245" [ 0.007] recv (stream_id=13) last-modified: Tue, 12 Aug 2014 17:00:17 GMT [ 0.007] recv (stream_id=13) server: Cowboy [ 0.007] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header If you read this then the static file server works! [ 0.007] recv DATA frame ; END_STREAM [ 0.007] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/file_server/priv/test.txt ================================================ If you read this then the static file server works! ================================================ FILE: examples/file_server/priv/video.html ================================================

HTML5 Video Example

Videos taken from TechSlides

================================================ FILE: examples/file_server/priv/中文/中文.html ================================================ 中文! ================================================ FILE: examples/file_server/relx.config ================================================ {release, {file_server_example, "1"}, [file_server]}. {extended_start_script, true}. ================================================ FILE: examples/file_server/src/directory_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Directory handler. -module(directory_h). %% REST Callbacks -export([init/2]). -export([allowed_methods/2]). -export([resource_exists/2]). -export([content_types_provided/2]). -export([charsets_provided/2]). %% Callback Callbacks -export([list_json/2]). -export([list_html/2]). init(Req, Paths) -> {cowboy_rest, Req, Paths}. allowed_methods(Req, State) -> {[<<"GET">>], Req, State}. resource_exists(Req, {ReqPath, FilePath}) -> case file:list_dir(FilePath) of {ok, Fs} -> {true, Req, {ReqPath, lists:sort(Fs)}}; _Err -> {false, Req, {ReqPath, FilePath}} end. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"html">>, []}, list_html}, {{<<"application">>, <<"json">>, []}, list_json} ], Req, State}. charsets_provided(Req, State) -> {[<<"utf-8">>], Req, State}. list_json(Req, {Path, Fs}) -> Files = [unicode:characters_to_binary(F) || F <- Fs], {json:encode(Files), Req, Path}. list_html(Req, {Path, Fs}) -> Body = [[links(Path, unicode:characters_to_binary(F)) || F <- [".."|Fs]]], HTML = [<<"Index", "">>, Body, <<"\n">>], {HTML, Req, Path}. links(<<>>, "..") -> "..
\n"; links(Prefix, "..") -> Tokens = string:tokens(binary_to_list(Prefix), "/"), Back = lists:join("/", lists:reverse(tl(lists:reverse(Tokens)))), ["..
\n"]; links(<<>>, File) -> ["", File, "
\n"]; links(Prefix, File) -> ["", File, "
\n"]. ================================================ FILE: examples/file_server/src/directory_lister.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(directory_lister). -behaviour(cowboy_middleware). -export([execute/2]). execute(Req, Env=#{handler := cowboy_static}) -> redirect_directory(Req, Env); execute(Req, Env) -> {ok, Req, Env}. redirect_directory(Req, Env=#{handler_opts := {_, _, _, Extra}}) -> Path = cowboy_req:path_info(Req), Path1 = << <> || S <- Path >>, {dir_handler, DirHandler} = lists:keyfind(dir_handler, 1, Extra), FullPath = resource_path(Path1), case valid_path(Path) and filelib:is_dir(FullPath) of true -> handle_directory(Req, Env, Path1, FullPath, DirHandler); false -> {ok, Req, Env} end. handle_directory(Req, Env, Prefix, Path, DirHandler) -> {ok, Req, Env#{handler => DirHandler, handler_opts => {Prefix, Path}}}. valid_path([]) -> true; valid_path([<<"..">> | _T]) -> false; valid_path([<<"/", _/binary>> | _T]) -> false; valid_path([_H | Rest]) -> valid_path(Rest). resource_path(Path) -> filename:join([code:priv_dir(file_server), Path]). ================================================ FILE: examples/file_server/src/file_server_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(file_server_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/[...]", cowboy_static, {priv_dir, file_server, "", [ {mimetypes, cow_mimetypes, all}, {dir_handler, directory_h}, {charset, <<"utf-8">>} ]}} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch}, middlewares => [cowboy_router, directory_lister, cowboy_handler] }), file_server_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/file_server/src/file_server_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(file_server_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/hello_world/Makefile ================================================ PROJECT = hello_world PROJECT_DESCRIPTION = Cowboy Hello World example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/hello_world/README.asciidoc ================================================ = Hello world example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 == HTTP/1.1 example output [source,bash] ---- $ curl -i http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:10:25 GMT content-length: 12 content-type: text/plain Hello world! ---- == HTTP/2 example output [source,bash] ---- $ nghttp -v http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.008] recv SETTINGS frame (niv=0) [ 0.008] recv SETTINGS frame ; ACK (niv=0) [ 0.008] send SETTINGS frame ; ACK (niv=0) [ 0.013] recv (stream_id=13) :status: 200 [ 0.013] recv (stream_id=13) content-length: 12 [ 0.013] recv (stream_id=13) content-type: text/plain [ 0.013] recv (stream_id=13) date: Thu, 09 Jun 2016 08:56:56 GMT [ 0.013] recv (stream_id=13) server: Cowboy [ 0.013] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header Hello world![ 0.013] recv DATA frame ; END_STREAM [ 0.013] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/hello_world/relx.config ================================================ {release, {hello_world_example, "1"}, [hello_world]}. {extended_start_script, true}. ================================================ FILE: examples/hello_world/src/hello_world_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(hello_world_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), hello_world_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/hello_world/src/hello_world_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(hello_world_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/hello_world/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Hello world handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain">> }, <<"Hello world!">>, Req0), {ok, Req, Opts}. ================================================ FILE: examples/markdown_middleware/Makefile ================================================ PROJECT = markdown_middleware PROJECT_DESCRIPTION = Cowboy static file handler example with middleware component PROJECT_VERSION = 1 DEPS = cowboy markdown dep_cowboy_commit = master dep_markdown = git https://github.com/hypernumbers/erlmarkdown master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/markdown_middleware/README.asciidoc ================================================ = Middleware example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080/video.html Cowboy will serve all the files you put in the `priv` directory. If you request a `.html` file that has a corresponding `.md` file that has been modified more recently than the `.html` file, the Markdown file will be converted to HTML and served by Cowboy. ================================================ FILE: examples/markdown_middleware/priv/video.md ================================================ HTML5 Video With Markdown ========================= Videos taken from [TechSlides](http://techslides.com/sample-webm-ogg-and-mp4-video-files-for-html5/) ================================================ FILE: examples/markdown_middleware/relx.config ================================================ {release, {markdown_middleware_example, "1"}, [markdown_middleware]}. {extended_start_script, true}. ================================================ FILE: examples/markdown_middleware/src/markdown_converter.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(markdown_converter). -behaviour(cowboy_middleware). -export([execute/2]). execute(Req, Env) -> [Path] = cowboy_req:path_info(Req), case filename:extension(Path) of <<".html">> -> maybe_generate_markdown(resource_path(Path)); _Ext -> ok end, {ok, Req, Env}. maybe_generate_markdown(Path) -> ModifiedAt = filelib:last_modified(source_path(Path)), GeneratedAt = filelib:last_modified(Path), case ModifiedAt > GeneratedAt of true -> markdown:conv_file(source_path(Path), Path); false -> ok end. resource_path(Path) -> PrivDir = code:priv_dir(markdown_middleware), filename:join([PrivDir, Path]). source_path(Path) -> << (filename:rootname(Path))/binary, ".md" >>. ================================================ FILE: examples/markdown_middleware/src/markdown_middleware_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(markdown_middleware_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/[...]", cowboy_static, {priv_dir, markdown_middleware, ""}} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch}, middlewares => [cowboy_router, markdown_converter, cowboy_handler] }), markdown_middleware_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/markdown_middleware/src/markdown_middleware_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(markdown_middleware_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/rest_basic_auth/Makefile ================================================ PROJECT = rest_basic_auth PROJECT_DESCRIPTION = Cowboy Basic HTTP Authorization example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/rest_basic_auth/README.asciidoc ================================================ = Basic authorization example using REST To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 == HTTP/1.1 example output Request with no authentication: [source,bash] ---- $ curl -i http://localhost:8080 HTTP/1.1 401 Unauthorized connection: keep-alive server: Cowboy date: Sun, 20 Jan 2013 14:10:27 GMT content-length: 0 www-authenticate: Basic realm="cowboy" ---- Request with authentication: [source,bash] ---- $ curl -i -u "Alladin:open sesame" http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Sun, 20 Jan 2013 14:11:12 GMT content-length: 16 content-type: text/plain Hello, Alladin! ---- == HTTP/2 example output Request with no authentication: [source,bash] ---- $ nghttp -v http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.004] recv SETTINGS frame (niv=0) [ 0.004] recv SETTINGS frame ; ACK (niv=0) [ 0.004] send SETTINGS frame ; ACK (niv=0) [ 0.004] recv (stream_id=13) :status: 401 [ 0.004] recv (stream_id=13) content-length: 0 [ 0.004] recv (stream_id=13) date: Tue, 14 Jun 2016 09:15:56 GMT [ 0.004] recv (stream_id=13) server: Cowboy [ 0.004] recv (stream_id=13) www-authenticate: Basic realm="cowboy" [ 0.004] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header [ 0.004] recv DATA frame ; END_STREAM [ 0.004] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- Request with authentication: [source,bash] ---- $ nghttp -v -H "Authorization: Basic `echo -n Alladin:open sesame | base64`" http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.001] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 authorization: Basic QWxsYWRpbjpvcGVuIHNlc2FtZQ== [ 0.002] recv SETTINGS frame (niv=0) [ 0.002] recv SETTINGS frame ; ACK (niv=0) [ 0.002] send SETTINGS frame ; ACK (niv=0) [ 0.004] recv (stream_id=13) :status: 200 [ 0.004] recv (stream_id=13) content-length: 16 [ 0.004] recv (stream_id=13) content-type: text/plain [ 0.004] recv (stream_id=13) date: Tue, 14 Jun 2016 09:15:48 GMT [ 0.004] recv (stream_id=13) server: Cowboy [ 0.004] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header Hello, Alladin! [ 0.004] recv DATA frame ; END_STREAM [ 0.004] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/rest_basic_auth/relx.config ================================================ {release, {rest_basic_auth_example, "1"}, [rest_basic_auth]}. {extended_start_script, true}. ================================================ FILE: examples/rest_basic_auth/src/rest_basic_auth_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(rest_basic_auth_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), rest_basic_auth_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/rest_basic_auth/src/rest_basic_auth_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(rest_basic_auth_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/rest_basic_auth/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Handler with basic HTTP authorization. -module(toppage_h). -export([init/2]). -export([content_types_provided/2]). -export([is_authorized/2]). -export([to_text/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. is_authorized(Req, State) -> case cowboy_req:parse_header(<<"authorization">>, Req) of {basic, User = <<"Alladin">>, <<"open sesame">>} -> {true, Req, User}; _ -> {{false, <<"Basic realm=\"cowboy\"">>}, Req, State} end. content_types_provided(Req, State) -> {[ {<<"text/plain">>, to_text} ], Req, State}. to_text(Req, User) -> {<< "Hello, ", User/binary, "!\n" >>, Req, User}. ================================================ FILE: examples/rest_hello_world/Makefile ================================================ PROJECT = rest_hello_world PROJECT_DESCRIPTION = Cowboy REST Hello World example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/rest_hello_world/README.asciidoc ================================================ = REST hello world example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 == HTTP/1.1 example output Request HTML: [source,bash] ---- $ curl -i http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:15:52 GMT content-length: 136 content-type: text/html vary: Accept REST Hello World!

REST Hello World as HTML!

---- Request JSON: [source,bash] ---- $ curl -i -H "Accept: application/json" http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:16:46 GMT content-length: 24 content-type: application/json vary: Accept {"rest": "Hello World!"} ---- Request plain text: [source,bash] ---- $ curl -i -H "Accept: text/plain" http://localhost:8080 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:18:35 GMT content-length: 25 content-type: text/plain vary: Accept REST Hello World as text! ---- Request a non acceptable content-type: [source,bash] ---- $ curl -i -H "Accept: text/css" http://localhost:8080 HTTP/1.1 406 Not Acceptable connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:18:51 GMT content-length: 0 ---- == HTTP/2 example output Request HTML: [source,bash] ---- $ nghttp -v http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.000] recv SETTINGS frame (niv=0) [ 0.000] send SETTINGS frame ; ACK (niv=0) [ 0.000] recv SETTINGS frame ; ACK (niv=0) [ 0.001] recv (stream_id=13) :status: 200 [ 0.001] recv (stream_id=13) content-length: 136 [ 0.001] recv (stream_id=13) content-type: text/html [ 0.001] recv (stream_id=13) date: Thu, 09 Jun 2016 14:28:50 GMT [ 0.001] recv (stream_id=13) server: Cowboy [ 0.001] recv (stream_id=13) vary: accept [ 0.001] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header REST Hello World!

REST Hello World as HTML!

[ 0.001] recv DATA frame ; END_STREAM [ 0.001] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- Request JSON: [source,bash] ---- $ nghttp -v -H "accept: application/json" http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.001] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.001] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: application/json accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.001] recv SETTINGS frame (niv=0) [ 0.001] send SETTINGS frame ; ACK (niv=0) [ 0.001] recv SETTINGS frame ; ACK (niv=0) [ 0.001] recv (stream_id=13) :status: 200 [ 0.001] recv (stream_id=13) content-length: 24 [ 0.001] recv (stream_id=13) content-type: application/json [ 0.001] recv (stream_id=13) date: Thu, 09 Jun 2016 14:29:00 GMT [ 0.001] recv (stream_id=13) server: Cowboy [ 0.001] recv (stream_id=13) vary: accept [ 0.001] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header {"rest": "Hello World!"}[ 0.002] recv DATA frame ; END_STREAM [ 0.002] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- Request plain text: [source,bash] ---- $ nghttp -v -H "accept: text/plain" http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: text/plain accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.000] recv SETTINGS frame (niv=0) [ 0.000] send SETTINGS frame ; ACK (niv=0) [ 0.000] recv SETTINGS frame ; ACK (niv=0) [ 0.000] recv (stream_id=13) :status: 200 [ 0.000] recv (stream_id=13) content-length: 25 [ 0.000] recv (stream_id=13) content-type: text/plain [ 0.000] recv (stream_id=13) date: Thu, 09 Jun 2016 14:28:25 GMT [ 0.000] recv (stream_id=13) server: Cowboy [ 0.000] recv (stream_id=13) vary: accept [ 0.000] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header REST Hello World as text![ 0.000] recv DATA frame ; END_STREAM [ 0.000] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- Request a non acceptable content-type: [source,bash] ---- $ nghttp -v -H "accept: text/css" http://localhost:8080 [ 0.000] Connected [ 0.000] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.000] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.000] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: http :authority: localhost:8080 accept: text/css accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.007] recv SETTINGS frame (niv=0) [ 0.007] recv SETTINGS frame ; ACK (niv=0) [ 0.007] send SETTINGS frame ; ACK (niv=0) [ 0.021] recv (stream_id=13) :status: 406 [ 0.021] recv (stream_id=13) content-length: 0 [ 0.021] recv (stream_id=13) date: Thu, 09 Jun 2016 14:29:15 GMT [ 0.021] recv (stream_id=13) server: Cowboy [ 0.021] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header [ 0.021] recv DATA frame ; END_STREAM [ 0.021] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/rest_hello_world/relx.config ================================================ {release, {rest_hello_world_example, "1"}, [rest_hello_world]}. {extended_start_script, true}. ================================================ FILE: examples/rest_hello_world/src/rest_hello_world_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(rest_hello_world_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), rest_hello_world_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/rest_hello_world/src/rest_hello_world_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(rest_hello_world_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/rest_hello_world/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Hello world handler. -module(toppage_h). -export([init/2]). -export([content_types_provided/2]). -export([hello_to_html/2]). -export([hello_to_json/2]). -export([hello_to_text/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[ {<<"text/html">>, hello_to_html}, {<<"application/json">>, hello_to_json}, {<<"text/plain">>, hello_to_text} ], Req, State}. hello_to_html(Req, State) -> Body = <<" REST Hello World!

REST Hello World as HTML!

">>, {Body, Req, State}. hello_to_json(Req, State) -> Body = <<"{\"rest\": \"Hello World!\"}">>, {Body, Req, State}. hello_to_text(Req, State) -> {<<"REST Hello World as text!">>, Req, State}. ================================================ FILE: examples/rest_pastebin/Makefile ================================================ PROJECT = rest_pastebin PROJECT_DESCRIPTION = Cowboy REST Pastebin example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/rest_pastebin/README.asciidoc ================================================ = REST pastebin example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 == Usage To upload something to the paste application, you can use `curl`: [source,bash] $ | curl -i --data-urlencode paste@- localhost:8080 Or, to upload the file `my_file`: [source,bash] curl -i --data-urlencode paste@my_file localhost:8080 The URL of your data will be in the location header. Alternately, you can visit http://localhost:8080 with your favorite web browser and submit your paste via the form. Code that has been pasted can be highlighted with ?lang= option if you have http://www.andre-simon.de/doku/highlight/en/highlight.html[highlight] installed (although `pygments` or any other should work just fine). This will show the contents of the HTML file: [source,bash] curl -i --data-urlencode paste@priv/index.html localhost:8080 curl If your terminal supports color sequences and `highlight` is installed, the following command will show the same contents but with HTML syntax highlighting. [source,bash] curl ?lang=html If you open the same URL in your web browser and your web browser tells Cowboy that it prefers HTML files, you will see the file highlighted with special HTML markup and CSS. Firefox is known to work. ================================================ FILE: examples/rest_pastebin/priv/index.html ================================================ Simple Pastebin

Simple Pastebin

You can paste your text into the text field to submit, or you can capture the output of a command with:

command | curl -i --data-urlencode paste@- localhost:8080
================================================ FILE: examples/rest_pastebin/priv/index.txt ================================================ Simple Pastebin --------------- You can paste your text into the text field to submit, or you can capture the output of a command with: | curl -i --data-urlencode paste@- localhost:8080 ================================================ FILE: examples/rest_pastebin/relx.config ================================================ {release, {rest_pastebin_example, "1"}, [rest_pastebin]}. {extended_start_script, true}. ================================================ FILE: examples/rest_pastebin/src/rest_pastebin_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(rest_pastebin_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/[:paste_id]", toppage_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), rest_pastebin_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/rest_pastebin/src/rest_pastebin_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(rest_pastebin_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/rest_pastebin/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Pastebin handler. -module(toppage_h). %% Standard callbacks. -export([init/2]). -export([allowed_methods/2]). -export([content_types_provided/2]). -export([content_types_accepted/2]). -export([resource_exists/2]). %% Custom callbacks. -export([create_paste/2]). -export([paste_html/2]). -export([paste_text/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. allowed_methods(Req, State) -> {[<<"GET">>, <<"POST">>], Req, State}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, []}, paste_text}, {{<<"text">>, <<"html">>, []}, paste_html} ], Req, State}. content_types_accepted(Req, State) -> {[{{<<"application">>, <<"x-www-form-urlencoded">>, '*'}, create_paste}], Req, State}. resource_exists(Req, _State) -> case cowboy_req:binding(paste_id, Req) of undefined -> {true, Req, index}; PasteID -> case valid_path(PasteID) and file_exists(PasteID) of true -> {true, Req, PasteID}; false -> {false, Req, PasteID} end end. create_paste(Req, State) -> PasteID = new_paste_id(), {ok, [{<<"paste">>, Paste}], Req2} = cowboy_req:read_urlencoded_body(Req), ok = file:write_file(full_path(PasteID), Paste), case cowboy_req:method(Req2) of <<"POST">> -> {{true, <<$/, PasteID/binary>>}, Req2, State}; _ -> {true, Req2, State} end. paste_html(Req, index) -> {read_file("index.html"), Req, index}; paste_html(Req, Paste) -> #{lang := Lang} = cowboy_req:match_qs([{lang, [fun lang_constraint/2], plain}], Req), {format_html(Paste, Lang), Req, Paste}. paste_text(Req, index) -> {read_file("index.txt"), Req, index}; paste_text(Req, Paste) -> #{lang := Lang} = cowboy_req:match_qs([{lang, [fun lang_constraint/2], plain}], Req), {format_text(Paste, Lang), Req, Paste}. % Private lang_constraint(forward, Bin) -> case re:run(Bin, "^[a-z0-9_]+$", [{capture, none}]) of match -> {ok, Bin}; nomatch -> {error, bad_lang} end; lang_constraint(format_error, {bad_lang, _}) -> "Invalid lang parameter.". read_file(Name) -> {ok, Binary} = file:read_file(full_path(Name)), Binary. full_path(Name) -> filename:join([code:priv_dir(rest_pastebin), Name]). file_exists(Name) -> case file:read_file_info(full_path(Name)) of {ok, _Info} -> true; {error, _Reason} -> false end. valid_path(<<>>) -> true; valid_path(<<$., _T/binary>>) -> false; valid_path(<<$/, _T/binary>>) -> false; valid_path(<<_Char, T/binary>>) -> valid_path(T). new_paste_id() -> Initial = rand:uniform(62) - 1, new_paste_id(<>, 7). new_paste_id(Bin, 0) -> Chars = <<"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890">>, << <<(binary_part(Chars, B, 1))/binary>> || <> <= Bin >>; new_paste_id(Bin, Rem) -> Next = rand:uniform(62) - 1, new_paste_id(<>, Rem - 1). format_html(Paste, plain) -> Text = escape_html_chars(read_file(Paste)), <<"", "paste", "
", Text/binary, "
\n">>; format_html(Paste, Lang) -> highlight(full_path(Paste), Lang, "html"). format_text(Paste, plain) -> read_file(Paste); format_text(Paste, Lang) -> highlight(full_path(Paste), Lang, "ansi"). highlight(Path, Lang, Type) -> Path1 = binary_to_list(Path), Lang1 = binary_to_list(Lang), os:cmd(["highlight --syntax=", Lang1, " --doc-title=paste ", " --out-format=", Type, " --include-style ", Path1]). % Escape some HTML characters that might make a fuss escape_html_chars(Bin) -> << <<(escape_html_char(B))/binary>> || <> <= Bin >>. escape_html_char($<) -> <<"<">>; escape_html_char($>) -> <<">">>; escape_html_char($&) -> <<"&">>; escape_html_char(C) -> <>. ================================================ FILE: examples/ssl_hello_world/Makefile ================================================ PROJECT = ssl_hello_world PROJECT_DESCRIPTION = Cowboy SSL Hello World example PROJECT_VERSION = 1 DEPS = cowboy LOCAL_DEPS = ssl dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/ssl_hello_world/README.asciidoc ================================================ = Secure hello world example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to https://localhost:8443 You will be greeted by a security message. You can ask for more information and ultimately accept to access localhost. This is due to the example using a self-signed certificate. Recent browsers will communicate using HTTP/2. Older browsers will use HTTP/1.1. == HTTP/1.1 example output [source,bash] ---- $ curl -k -i https://localhost:8443 HTTP/1.1 200 OK connection: keep-alive server: Cowboy date: Fri, 28 Sep 2012 04:10:25 GMT content-length: 12 Hello world! ---- == HTTP/2 example output [source,bash] ---- $ nghttp -v https://localhost:8443 [ 0.001] Connected The negotiated protocol: h2 [ 0.009] recv SETTINGS frame (niv=0) [ 0.009] send SETTINGS frame (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.009] send SETTINGS frame ; ACK (niv=0) [ 0.009] send PRIORITY frame (dep_stream_id=0, weight=201, exclusive=0) [ 0.009] send PRIORITY frame (dep_stream_id=0, weight=101, exclusive=0) [ 0.009] send PRIORITY frame (dep_stream_id=0, weight=1, exclusive=0) [ 0.009] send PRIORITY frame (dep_stream_id=7, weight=1, exclusive=0) [ 0.009] send PRIORITY frame (dep_stream_id=3, weight=1, exclusive=0) [ 0.009] send HEADERS frame ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: https :authority: localhost:8443 accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.7.1 [ 0.010] recv SETTINGS frame ; ACK (niv=0) [ 0.010] recv (stream_id=13) :status: 200 [ 0.010] recv (stream_id=13) content-length: 12 [ 0.010] recv (stream_id=13) content-type: text/plain [ 0.010] recv (stream_id=13) date: Sat, 30 Apr 2016 12:54:32 GMT [ 0.010] recv (stream_id=13) server: Cowboy [ 0.010] recv HEADERS frame ; END_HEADERS (padlen=0) ; First response header Hello world![ 0.010] recv DATA frame ; END_STREAM [ 0.010] send GOAWAY frame (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) ---- ================================================ FILE: examples/ssl_hello_world/priv/ssl/cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIDTzCCAjegAwIBAgIUD7jNyCgABo8GlnEojOSTFWZzkJswDQYJKoZIhvcNAQEL BQAwNzELMAkGA1UEBhMCRlIxEzARBgNVBAgMClNvbWUtU3RhdGUxEzARBgNVBAoM Ck5pbmUgTmluZXMwHhcNMjQwMTI2MTQyODExWhcNMzcxMDA0MTQyODExWjA3MQsw CQYDVQQGEwJGUjETMBEGA1UECAwKU29tZS1TdGF0ZTETMBEGA1UECgwKTmluZSBO aW5lczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKfNEwF0v1Gm2e6a M4hqI3JhmerZSNYWw8NiaUybR5hVUS9X4Chk+/y8kBLX2OYbGGlAxgbOZJa5D+kf H1iakoUQaILinxPx3yxtIOePS3q/Xi5/EBVTdwLOoI26oSdzY2RTKKAPO1PCcAjq 6gDpw2u7q26sSU1kul6dD4Wle6+yNtnJdNKo9zLCLXr6TtuHdvbAU1oblLCKZ1Db /uLkhGaUI/EUNeU1ZJrPmnoneYkTcG5mC5PMFVhqJ3bNYez5Hgr2Ra1Fz0dVgmRM FpJ8NF6UQgA9dAs2Oh1uWbTjJiX0tO92RslXlhpLHS2VKZWsxiN2bniNXsNKzQ9M ty0qnxkCAwEAAaNTMFEwHQYDVR0OBBYEFKuBPzB9rBCJNAnUyQMXjkVKIMJlMB8G A1UdIwQYMBaAFKuBPzB9rBCJNAnUyQMXjkVKIMJlMA8GA1UdEwEB/wQFMAMBAf8w DQYJKoZIhvcNAQELBQADggEBAHWXDKlY39csROTQ2Dm3CnTj14tj3cW4onsOYTKW FSlVdMOk3+ionB4vZA/Ino8OjrjiZ2dB3Tvl2J+AxEea3ltDbdh6qVuqSwvQZCeV 8gWp05wzyTfIpQRD10ZwOU6dzR89T+o7oG/7D8Ydk3nzecthF1aU0YBW8OtuZFog lC/PIIoVEyUiTEnFJrkQge1OmZWiAuImIed+cEmkw9ZAN2/9i/OxWZKAGoKrmfPq kzdOoxxFRLnqHo2OYdA0IPpSuGK5ayjYrLgXW0Wa4FKzmDh7Gy+JSrvLuFur9PEi D0Encva2uX1hAcFQDrzICTsD6ANuIbw0cmlrCJYH6E21PrM= -----END CERTIFICATE----- ================================================ FILE: examples/ssl_hello_world/priv/ssl/key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCnzRMBdL9Rptnu mjOIaiNyYZnq2UjWFsPDYmlMm0eYVVEvV+AoZPv8vJAS19jmGxhpQMYGzmSWuQ/p Hx9YmpKFEGiC4p8T8d8sbSDnj0t6v14ufxAVU3cCzqCNuqEnc2NkUyigDztTwnAI 6uoA6cNru6turElNZLpenQ+FpXuvsjbZyXTSqPcywi16+k7bh3b2wFNaG5SwimdQ 2/7i5IRmlCPxFDXlNWSaz5p6J3mJE3BuZguTzBVYaid2zWHs+R4K9kWtRc9HVYJk TBaSfDRelEIAPXQLNjodblm04yYl9LTvdkbJV5YaSx0tlSmVrMYjdm54jV7DSs0P TLctKp8ZAgMBAAECggEAR5e6D6l5hUNcgS4+ZWnvhLo6utYI+vrMfFzNE3e+5LIm CL6D74gicRMcn0WDj62ozSNrOfUuOpZrwOlb7OhKMkataIZ7G73bG6/V1aYwLIdg jhL9UDQDt2lkXAPwBQ54rhHC6AOHqvVu6ocb3tbd32W7P2V3gvNChuKZAEr6Chwc 1JE5e1k7uZK4rjqZhd86pV2hks/jNknAZpEROTw80qpo3MzlMDMhXyKmyGa84t91 1bijJ2DMPKsaxSYkWa06Zx3ymiX+qtKFRnSqZo2aEqpeTgQ0hRBSA429d7uCKO0o kwqOyT85qMFRA+4jfkcAwUi4DELVCFlN/QNWCMH09wKBgQDVuw/sGnjVxCQ/s7pH FuGA55S1qUtrcYsMHV5uZNtxLOqeAURomgiTpDVNNhLBuJwVjZrBv8Msl1/99EZ7 8Hws+ERcjlbmyBiq6/VdRW6bJsrFnOS4qUbwWQp0Yztdeu6sTwIEI0KO/oFypf9G L9mwjXwTvWEFg5etW1BPq+XmMwKBgQDI/KXNul1zCnrOY6sYrbPShYLZgPQRjNi5 Ho6N5NxRc3xhyzExbjNtA/N/30d+/p7H8ND+TgpsYdjvEqqgpQQmCeg3/n6eSzb2 hotCVBt8dU2TjD5v68DLzGv61s7PV81e4grkU5nCe+y7zJMwKGQ8BbmYTBBYEO0P nTHwuwHhgwKBgQCx2B8OopRro/NZwm69Wq+3+HtIkh98vxUptoJuL6RdzzdG1N0c gRej6t6jadw/sCLI2HSuxaddQnSQt6Oy29AoB0mzDooHLPdBumgH/Y9ksOnHd57m fYzWz/CgGjY6ueFCJdgSo1ht7h6+zJvWxlhIzeIx9sJ1uSMMEFCKiwoY+wKBgGb+ kTjLt/er9yKskJEk8nF/WX58RpZ3xteWgRbVoNFcjPDQX3UlM9U5oR52HP1HHbb4 ASFQfKbtvW1F84o/BdE4YnfPQrN7d779U3+5+hvdQNPLmnNgLHxDVVJFodU++U8W Jt66uKChQL88JnEXQcZAaMtSr01x3wmRVHY4Xs5hAoGBAMPfa+rcGukjbMF+MZ0P ZV1Pq7AxVJ/C0XINnpZrsN+e6dO52Y2VXbnQkML7PKZXzSY88QwunBp88VoPlDux llmLZc54zUFlsC1iHrEzt+hoxFG0tfL83vic5kSx6u5oZdxjZ2InqTzE8TmORU3v 6/ik7Q4VeDQ5uLnR4GiLW+qj -----END PRIVATE KEY----- ================================================ FILE: examples/ssl_hello_world/relx.config ================================================ {release, {ssl_hello_world_example, "1"}, [ssl_hello_world]}. {extended_start_script, true}. ================================================ FILE: examples/ssl_hello_world/src/ssl_hello_world_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(ssl_hello_world_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", toppage_h, []} ]} ]), PrivDir = code:priv_dir(ssl_hello_world), {ok, _} = cowboy:start_tls(https, [ {port, 8443}, {certfile, PrivDir ++ "/ssl/cert.pem"}, {keyfile, PrivDir ++ "/ssl/key.pem"} ], #{env => #{dispatch => Dispatch}}), ssl_hello_world_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(https). ================================================ FILE: examples/ssl_hello_world/src/ssl_hello_world_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(ssl_hello_world_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/ssl_hello_world/src/toppage_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Hello world handler. -module(toppage_h). -export([init/2]). init(Req0, Opts) -> Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/plain">> }, <<"Hello world!">>, Req0), {ok, Req, Opts}. ================================================ FILE: examples/upload/Makefile ================================================ PROJECT = upload PROJECT_DESCRIPTION = Cowboy multipart upload example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/upload/README.asciidoc ================================================ = Multipart upload example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 The uploaded file will be displayed in the shell directly. ================================================ FILE: examples/upload/priv/index.html ================================================ Multipart upload example
================================================ FILE: examples/upload/relx.config ================================================ {release, {upload_example, "1"}, [upload]}. {extended_start_script, true}. ================================================ FILE: examples/upload/src/upload_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(upload_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", cowboy_static, {priv_file, upload, "index.html"}}, {"/upload", upload_h, []} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), upload_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/upload/src/upload_h.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @doc Upload handler. -module(upload_h). -export([init/2]). init(Req, Opts) -> {ok, Headers, Req2} = cowboy_req:read_part(Req), {ok, Data, Req3} = cowboy_req:read_part_body(Req2), {file, <<"inputfile">>, Filename, ContentType} = cow_multipart:form_data(Headers), io:format("Received file ~p of content-type ~p as follow:~n~p~n~n", [Filename, ContentType, Data]), {ok, Req3, Opts}. ================================================ FILE: examples/upload/src/upload_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(upload_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/websocket/Makefile ================================================ PROJECT = websocket PROJECT_DESCRIPTION = Cowboy Websocket example PROJECT_VERSION = 1 DEPS = cowboy dep_cowboy_commit = master REL_DEPS = relx include ../../erlang.mk ================================================ FILE: examples/websocket/README.asciidoc ================================================ = Websocket example To try this example, you need GNU `make` and `git` in your PATH. To build and run the example, use the following command: [source,bash] $ make run Then point your browser to http://localhost:8080 ================================================ FILE: examples/websocket/priv/index.html ================================================ Websocket client

Websocket client

================================================ FILE: examples/websocket/relx.config ================================================ {release, {websocket_example, "1"}, [websocket]}. {extended_start_script, true}. ================================================ FILE: examples/websocket/src/websocket_app.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(websocket_app). -behaviour(application). %% API. -export([start/2]). -export([stop/1]). %% API. start(_Type, _Args) -> Dispatch = cowboy_router:compile([ {'_', [ {"/", cowboy_static, {priv_file, websocket, "index.html"}}, {"/websocket", ws_h, []}, {"/static/[...]", cowboy_static, {priv_dir, websocket, "static"}} ]} ]), {ok, _} = cowboy:start_clear(http, [{port, 8080}], #{ env => #{dispatch => Dispatch} }), websocket_sup:start_link(). stop(_State) -> ok = cowboy:stop_listener(http). ================================================ FILE: examples/websocket/src/websocket_sup.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. %% @private -module(websocket_sup). -behaviour(supervisor). %% API. -export([start_link/0]). %% supervisor. -export([init/1]). %% API. -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% supervisor. init([]) -> Procs = [], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: examples/websocket/src/ws_h.erl ================================================ -module(ws_h). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> {cowboy_websocket, Req, Opts}. websocket_init(State) -> erlang:start_timer(1000, self(), <<"Hello!">>), {[], State}. websocket_handle({text, Msg}, State) -> {[{text, << "That's what she said! ", Msg/binary >>}], State}; websocket_handle(_Data, State) -> {[], State}. websocket_info({timeout, _Ref, Msg}, State) -> erlang:start_timer(1000, self(), <<"How' you doin'?">>), {[{text, Msg}], State}; websocket_info(_Info, State) -> {[], State}. ================================================ FILE: plugins.mk ================================================ # See LICENSE for licensing information. # Plain HTTP handlers. define tpl_cowboy.http -module($(n)). -behavior(cowboy_handler). -export([init/2]). init(Req, State) -> {ok, Req, State}. endef # Loop handlers. define tpl_cowboy.loop -module($(n)). -behavior(cowboy_loop). -export([init/2]). -export([info/3]). init(Req, State) -> {cowboy_loop, Req, State, hibernate}. info(_Info, Req, State) -> {ok, Req, State, hibernate}. endef # REST handlers. define tpl_cowboy.rest -module($(n)). -behavior(cowboy_rest). -export([init/2]). -export([content_types_provided/2]). -export([to_html/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"html">>, '*'}, to_html} ], Req, State}. to_html(Req, State) -> {<<"This is REST!">>, Req, State}. endef # Websocket handlers. define tpl_cowboy.ws -module($(n)). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, State) -> {cowboy_websocket, Req, State}. websocket_init(State) -> {[], State}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}; websocket_handle(_Frame, State) -> {[], State}. websocket_info(_Info, State) -> {[], State}. endef ================================================ FILE: rebar.config ================================================ {deps, [ {cowlib,".*",{git,"https://github.com/ninenines/cowlib",{tag,"2.16.0"}}},{ranch,".*",{git,"https://github.com/ninenines/ranch",{tag,"1.8.1"}}} ]}. {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}. ================================================ FILE: src/cowboy.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy). -export([start_clear/3]). -export([start_tls/3]). -export([stop_listener/1]). -export([get_env/2]). -export([get_env/3]). -export([set_env/3]). -ifdef(COWBOY_QUICER). -export([start_quic/3]). %% Don't warn about the bad quicer specs. -dialyzer([{nowarn_function, start_quic/3}]). -endif. %% Internal. -export([log/2]). -export([log/4]). -type opts() :: cowboy_http:opts() | cowboy_http2:opts(). -export_type([opts/0]). -type fields() :: [atom() | {atom(), cowboy_constraints:constraint() | [cowboy_constraints:constraint()]} | {atom(), cowboy_constraints:constraint() | [cowboy_constraints:constraint()], any()}]. -export_type([fields/0]). -type http_headers() :: #{binary() => iodata()}. -export_type([http_headers/0]). -type http_status() :: non_neg_integer() | binary(). -export_type([http_status/0]). -type http_version() :: 'HTTP/2' | 'HTTP/1.1' | 'HTTP/1.0'. -export_type([http_version/0]). -spec start_clear(ranch:ref(), ranch:opts(), opts()) -> {ok, pid()} | {error, any()}. start_clear(Ref, TransOpts0, ProtoOpts0) -> TransOpts1 = ranch:normalize_opts(TransOpts0), {TransOpts2, DynamicBuffer} = ensure_dynamic_buffer(TransOpts1, ProtoOpts0), {TransOpts, ConnectionType} = ensure_connection_type(TransOpts2), ProtoOpts = ProtoOpts0#{ connection_type => ConnectionType, dynamic_buffer => DynamicBuffer }, ranch:start_listener(Ref, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts). -spec start_tls(ranch:ref(), ranch:opts(), opts()) -> {ok, pid()} | {error, any()}. start_tls(Ref, TransOpts0, ProtoOpts0) -> TransOpts1 = ranch:normalize_opts(TransOpts0), {TransOpts2, DynamicBuffer} = ensure_dynamic_buffer(TransOpts1, ProtoOpts0), TransOpts3 = ensure_alpn(TransOpts2), {TransOpts, ConnectionType} = ensure_connection_type(TransOpts3), ProtoOpts = ProtoOpts0#{ connection_type => ConnectionType, dynamic_buffer => DynamicBuffer }, ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts). -ifdef(COWBOY_QUICER). %% @todo Experimental function to start a barebone QUIC listener. %% This will need to be reworked to be closer to Ranch %% listeners and provide equivalent features. %% %% @todo Better type for transport options. Might require fixing quicer types. -spec start_quic(ranch:ref(), #{socket_opts => [{atom(), _}]}, cowboy_http3:opts()) -> {ok, pid()}. %% @todo Implement dynamic_buffer for HTTP/3 if/when it applies. start_quic(Ref, TransOpts, ProtoOpts) -> {ok, _} = application:ensure_all_started(quicer), Parent = self(), SocketOpts0 = maps:get(socket_opts, TransOpts, []), {Port, SocketOpts2} = case lists:keytake(port, 1, SocketOpts0) of {value, {port, Port0}, SocketOpts1} -> {Port0, SocketOpts1}; false -> {port_0(), SocketOpts0} end, SocketOpts = [ {alpn, ["h3"]}, %% @todo Why not binary? %% We only need 3 for control and QPACK enc/dec, %% but we need more for WebTransport. %% @todo Use 3 if WT is disabled. {peer_unidi_stream_count, 100}, {peer_bidi_stream_count, 100}, %% For WebTransport. %% @todo We probably don't want it enabled if WT isn't used. {datagram_send_enabled, 1}, {datagram_receive_enabled, 1} |SocketOpts2], _ListenerPid = spawn(fun() -> {ok, Listener} = quicer:listen(Port, SocketOpts), Parent ! {ok, Listener}, _AcceptorPid = [spawn(fun AcceptLoop() -> {ok, Conn} = quicer:accept(Listener, []), Pid = spawn(fun() -> receive go -> ok end, %% We have to do the handshake after handing control of %% the connection otherwise streams may come in before %% the controlling process is changed and messages will %% not be sent to the correct process. {ok, Conn} = quicer:handshake(Conn), process_flag(trap_exit, true), %% @todo Only if supervisor though. try cowboy_http3:init(Parent, Ref, Conn, ProtoOpts) catch exit:{shutdown,_} -> ok; C:E:S -> log(error, "CRASH ~p:~p:~p", [C,E,S], ProtoOpts) end end), ok = quicer:controlling_process(Conn, Pid), Pid ! go, AcceptLoop() end) || _ <- lists:seq(1, 20)], %% Listener process must not terminate. receive after infinity -> ok end end), receive {ok, Listener} -> {ok, Listener} end. %% Select a random UDP port using gen_udp because quicer %% does not provide equivalent functionality. Taken from %% quicer test suites. port_0() -> {ok, Socket} = gen_udp:open(0, [{reuseaddr, true}]), {ok, {_, Port}} = inet:sockname(Socket), gen_udp:close(Socket), case os:type() of {unix, darwin} -> %% Apparently macOS doesn't free the port immediately. timer:sleep(500); _ -> ok end, Port. -endif. ensure_alpn(TransOpts) -> SocketOpts = maps:get(socket_opts, TransOpts, []), TransOpts#{socket_opts => [ {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]} |SocketOpts]}. ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) -> {TransOpts, ConnectionType}; ensure_connection_type(TransOpts) -> {TransOpts#{connection_type => supervisor}, supervisor}. %% Dynamic buffer was set; accept transport options as-is. %% Note that initial 'buffer' size may be lower than dynamic buffer allows. ensure_dynamic_buffer(TransOpts, #{dynamic_buffer := DynamicBuffer}) -> {TransOpts, DynamicBuffer}; %% Dynamic buffer was not set; define default dynamic buffer %% only if 'buffer' size was not configured. In that case we %% set the 'buffer' size to the lowest value. ensure_dynamic_buffer(TransOpts=#{socket_opts := SocketOpts}, _) -> case proplists:get_value(buffer, SocketOpts, undefined) of undefined -> {TransOpts#{socket_opts => [{buffer, 512}|SocketOpts]}, {512, 131072}}; _ -> {TransOpts, false} end. -spec stop_listener(ranch:ref()) -> ok | {error, not_found}. stop_listener(Ref) -> ranch:stop_listener(Ref). -spec get_env(ranch:ref(), atom()) -> ok. get_env(Ref, Name) -> Opts = ranch:get_protocol_options(Ref), Env = maps:get(env, Opts, #{}), maps:get(Name, Env). -spec get_env(ranch:ref(), atom(), any()) -> ok. get_env(Ref, Name, Default) -> Opts = ranch:get_protocol_options(Ref), Env = maps:get(env, Opts, #{}), maps:get(Name, Env, Default). -spec set_env(ranch:ref(), atom(), any()) -> ok. set_env(Ref, Name, Value) -> Opts = ranch:get_protocol_options(Ref), Env = maps:get(env, Opts, #{}), Opts2 = maps:put(env, maps:put(Name, Value, Env), Opts), ok = ranch:set_protocol_options(Ref, Opts2). %% Internal. -spec log({log, logger:level(), io:format(), list()}, opts()) -> ok. log({log, Level, Format, Args}, Opts) -> log(Level, Format, Args, Opts). -spec log(logger:level(), io:format(), list(), opts()) -> ok. log(Level, Format, Args, #{logger := Logger}) when Logger =/= error_logger -> _ = Logger:Level(Format, Args), ok; %% We use error_logger by default. Because error_logger does %% not have all the levels we accept we have to do some %% mapping to error_logger functions. log(Level, Format, Args, _) -> Function = case Level of emergency -> error_msg; alert -> error_msg; critical -> error_msg; error -> error_msg; warning -> warning_msg; notice -> warning_msg; info -> info_msg; debug -> info_msg end, error_logger:Function(Format, Args). ================================================ FILE: src/cowboy_app.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_app). -behaviour(application). -export([start/2]). -export([stop/1]). -spec start(_, _) -> {ok, pid()}. start(_, _) -> cowboy_sup:start_link(). -spec stop(_) -> ok. stop(_) -> ok. ================================================ FILE: src/cowboy_bstr.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_bstr). %% Binary strings. -export([capitalize_token/1]). -export([to_lower/1]). -export([to_upper/1]). %% Characters. -export([char_to_lower/1]). -export([char_to_upper/1]). %% The first letter and all letters after a dash are capitalized. %% This is the form seen for header names in the HTTP/1.1 RFC and %% others. Note that using this form isn't required, as header names %% are case insensitive, and it is only provided for use with eventual %% badly implemented clients. -spec capitalize_token(B) -> B when B::binary(). capitalize_token(B) -> capitalize_token(B, true, <<>>). capitalize_token(<<>>, _, Acc) -> Acc; capitalize_token(<< $-, Rest/bits >>, _, Acc) -> capitalize_token(Rest, true, << Acc/binary, $- >>); capitalize_token(<< C, Rest/bits >>, true, Acc) -> capitalize_token(Rest, false, << Acc/binary, (char_to_upper(C)) >>); capitalize_token(<< C, Rest/bits >>, false, Acc) -> capitalize_token(Rest, false, << Acc/binary, (char_to_lower(C)) >>). -spec to_lower(B) -> B when B::binary(). to_lower(B) -> << << (char_to_lower(C)) >> || << C >> <= B >>. -spec to_upper(B) -> B when B::binary(). to_upper(B) -> << << (char_to_upper(C)) >> || << C >> <= B >>. -spec char_to_lower(char()) -> char(). char_to_lower($A) -> $a; char_to_lower($B) -> $b; char_to_lower($C) -> $c; char_to_lower($D) -> $d; char_to_lower($E) -> $e; char_to_lower($F) -> $f; char_to_lower($G) -> $g; char_to_lower($H) -> $h; char_to_lower($I) -> $i; char_to_lower($J) -> $j; char_to_lower($K) -> $k; char_to_lower($L) -> $l; char_to_lower($M) -> $m; char_to_lower($N) -> $n; char_to_lower($O) -> $o; char_to_lower($P) -> $p; char_to_lower($Q) -> $q; char_to_lower($R) -> $r; char_to_lower($S) -> $s; char_to_lower($T) -> $t; char_to_lower($U) -> $u; char_to_lower($V) -> $v; char_to_lower($W) -> $w; char_to_lower($X) -> $x; char_to_lower($Y) -> $y; char_to_lower($Z) -> $z; char_to_lower(Ch) -> Ch. -spec char_to_upper(char()) -> char(). char_to_upper($a) -> $A; char_to_upper($b) -> $B; char_to_upper($c) -> $C; char_to_upper($d) -> $D; char_to_upper($e) -> $E; char_to_upper($f) -> $F; char_to_upper($g) -> $G; char_to_upper($h) -> $H; char_to_upper($i) -> $I; char_to_upper($j) -> $J; char_to_upper($k) -> $K; char_to_upper($l) -> $L; char_to_upper($m) -> $M; char_to_upper($n) -> $N; char_to_upper($o) -> $O; char_to_upper($p) -> $P; char_to_upper($q) -> $Q; char_to_upper($r) -> $R; char_to_upper($s) -> $S; char_to_upper($t) -> $T; char_to_upper($u) -> $U; char_to_upper($v) -> $V; char_to_upper($w) -> $W; char_to_upper($x) -> $X; char_to_upper($y) -> $Y; char_to_upper($z) -> $Z; char_to_upper(Ch) -> Ch. %% Tests. -ifdef(TEST). capitalize_token_test_() -> Tests = [ {<<"heLLo-woRld">>, <<"Hello-World">>}, {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>}, {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>}, {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>}, {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>}, {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>}, {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>} ], [{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests]. -endif. ================================================ FILE: src/cowboy_children.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_children). -export([init/0]). -export([up/4]). -export([down/2]). -export([shutdown/2]). -export([shutdown_timeout/3]). -export([terminate/1]). -export([handle_supervisor_call/4]). -record(child, { pid :: pid(), streamid :: cowboy_stream:streamid() | undefined, shutdown :: timeout(), timer = undefined :: undefined | reference() }). -type children() :: [#child{}]. -export_type([children/0]). -spec init() -> []. init() -> []. -spec up(Children, pid(), cowboy_stream:streamid(), timeout()) -> Children when Children::children(). up(Children, Pid, StreamID, Shutdown) -> [#child{ pid=Pid, streamid=StreamID, shutdown=Shutdown }|Children]. -spec down(Children, pid()) -> {ok, cowboy_stream:streamid() | undefined, Children} | error when Children::children(). down(Children0, Pid) -> case lists:keytake(Pid, #child.pid, Children0) of {value, #child{streamid=StreamID, timer=Ref}, Children} -> _ = case Ref of undefined -> ok; _ -> erlang:cancel_timer(Ref, [{async, true}, {info, false}]) end, {ok, StreamID, Children}; false -> error end. %% We ask the processes to shutdown first. This gives %% a chance to processes that are trapping exits to %% shut down gracefully. Others will exit immediately. %% %% @todo We currently fire one timer per process being %% shut down. This is probably not the most efficient. %% A more efficient solution could be to maintain a %% single timer and decrease the shutdown time of all %% processes when it fires. This is however much more %% complex, and there aren't that many processes that %% will need to be shutdown through this function, so %% this is left for later. -spec shutdown(Children, cowboy_stream:streamid()) -> Children when Children::children(). shutdown(Children0, StreamID) -> [ case Child of #child{pid=Pid, streamid=StreamID, shutdown=Shutdown} -> exit(Pid, shutdown), Ref = erlang:start_timer(Shutdown, self(), {shutdown, Pid}), Child#child{streamid=undefined, timer=Ref}; _ -> Child end || Child <- Children0]. -spec shutdown_timeout(children(), reference(), pid()) -> ok. shutdown_timeout(Children, Ref, Pid) -> case lists:keyfind(Pid, #child.pid, Children) of #child{timer=Ref} -> exit(Pid, kill), ok; _ -> ok end. -spec terminate(children()) -> ok. terminate(Children) -> %% For each child, either ask for it to shut down, %% or cancel its shutdown timer if it already is. %% %% We do not need to flush stray timeout messages out because %% we are either terminating or switching protocols, %% and in the latter case we flush all messages. _ = [case TRef of undefined -> exit(Pid, shutdown); _ -> erlang:cancel_timer(TRef, [{async, true}, {info, false}]) end || #child{pid=Pid, timer=TRef} <- Children], before_terminate_loop(Children). before_terminate_loop([]) -> ok; before_terminate_loop(Children) -> %% Find the longest shutdown time. Time = longest_shutdown_time(Children, 0), %% We delay the creation of the timer if one of the %% processes has an infinity shutdown value. TRef = case Time of infinity -> undefined; _ -> erlang:start_timer(Time, self(), terminate) end, %% Loop until that time or until all children are dead. terminate_loop(Children, TRef). terminate_loop([], TRef) -> %% Don't forget to cancel the timer, if any! case TRef of undefined -> ok; _ -> _ = erlang:cancel_timer(TRef, [{async, true}, {info, false}]), ok end; terminate_loop(Children, TRef) -> receive {'EXIT', Pid, _} when TRef =:= undefined -> {value, #child{shutdown=Shutdown}, Children1} = lists:keytake(Pid, #child.pid, Children), %% We delayed the creation of the timer. If a process with %% infinity shutdown just ended, we might have to start that timer. case Shutdown of infinity -> before_terminate_loop(Children1); _ -> terminate_loop(Children1, TRef) end; {'EXIT', Pid, _} -> terminate_loop(lists:keydelete(Pid, #child.pid, Children), TRef); {timeout, TRef, terminate} -> %% Brutally kill any remaining children. _ = [exit(Pid, kill) || #child{pid=Pid} <- Children], ok end. longest_shutdown_time([], Time) -> Time; longest_shutdown_time([#child{shutdown=ChildTime}|Tail], Time) when ChildTime > Time -> longest_shutdown_time(Tail, ChildTime); longest_shutdown_time([_|Tail], Time) -> longest_shutdown_time(Tail, Time). -spec handle_supervisor_call(any(), {pid(), any()}, children(), module()) -> ok. handle_supervisor_call(which_children, {From, Tag}, Children, Module) -> From ! {Tag, which_children(Children, Module)}, ok; handle_supervisor_call(count_children, {From, Tag}, Children, _) -> From ! {Tag, count_children(Children)}, ok; %% We disable start_child since only incoming requests %% end up creating a new process. handle_supervisor_call({start_child, _}, {From, Tag}, _, _) -> From ! {Tag, {error, start_child_disabled}}, ok; %% All other calls refer to children. We act in a similar way %% to a simple_one_for_one so we never find those. handle_supervisor_call(_, {From, Tag}, _, _) -> From ! {Tag, {error, not_found}}, ok. -spec which_children(children(), module()) -> [{module(), pid(), worker, [module()]}]. which_children(Children, Module) -> [{Module, Pid, worker, [Module]} || #child{pid=Pid} <- Children]. -spec count_children(children()) -> [{atom(), non_neg_integer()}]. count_children(Children) -> Count = length(Children), [ {specs, 1}, {active, Count}, {supervisors, 0}, {workers, Count} ]. ================================================ FILE: src/cowboy_clear.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_clear). -behavior(ranch_protocol). -export([start_link/3]). -export([start_link/4]). -export([connection_process/4]). %% Ranch 1. -spec start_link(ranch:ref(), inet:socket(), module(), cowboy:opts()) -> {ok, pid()}. start_link(Ref, _Socket, Transport, Opts) -> start_link(Ref, Transport, Opts). %% Ranch 2. -spec start_link(ranch:ref(), module(), cowboy:opts()) -> {ok, pid()}. start_link(Ref, Transport, Opts) -> Pid = proc_lib:spawn_link(?MODULE, connection_process, [self(), Ref, Transport, Opts]), {ok, Pid}. -spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok. connection_process(Parent, Ref, Transport, Opts) -> ProxyInfo = get_proxy_info(Ref, Opts), {ok, Socket} = ranch:handshake(Ref), %% Use cowboy_http2 directly only when 'http' is missing. Protocol = case maps:get(protocols, Opts, [http2, http]) of [http2] -> cowboy_http2; [_|_] -> cowboy_http end, init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol). init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) -> _ = case maps:get(connection_type, Opts, supervisor) of worker -> ok; supervisor -> process_flag(trap_exit, true) end, Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts). get_proxy_info(Ref, #{proxy_header := true}) -> case ranch:recv_proxy_header(Ref, 1000) of {ok, ProxyInfo} -> ProxyInfo; {error, closed} -> exit({shutdown, closed}) end; get_proxy_info(_, _) -> undefined. ================================================ FILE: src/cowboy_clock.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% While a gen_server process runs in the background to update %% the cache of formatted dates every second, all API calls are %% local and directly read from the ETS cache table, providing %% fast time and date computations. -module(cowboy_clock). -behaviour(gen_server). %% API. -export([start_link/0]). -export([stop/0]). -export([rfc1123/0]). -export([rfc1123/1]). %% gen_server. -export([init/1]). -export([handle_call/3]). -export([handle_cast/2]). -export([handle_info/2]). -export([terminate/2]). -export([code_change/3]). -record(state, { universaltime = undefined :: undefined | calendar:datetime(), rfc1123 = <<>> :: binary(), tref = undefined :: undefined | reference() }). %% API. -spec start_link() -> {ok, pid()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec stop() -> stopped. stop() -> gen_server:call(?MODULE, stop). %% When the ets table doesn't exist, either because of a bug %% or because Cowboy is being restarted, we perform in a %% slightly degraded state and build a new timestamp for %% every request. -spec rfc1123() -> binary(). rfc1123() -> try ets:lookup_element(?MODULE, rfc1123, 2) catch error:badarg -> rfc1123(erlang:universaltime()) end. -spec rfc1123(calendar:datetime()) -> binary(). rfc1123(DateTime) -> update_rfc1123(<<>>, undefined, DateTime). %% gen_server. -spec init([]) -> {ok, #state{}}. init([]) -> ?MODULE = ets:new(?MODULE, [set, protected, named_table, {read_concurrency, true}]), T = erlang:universaltime(), B = update_rfc1123(<<>>, undefined, T), TRef = erlang:send_after(1000, self(), update), ets:insert(?MODULE, {rfc1123, B}), {ok, #state{universaltime=T, rfc1123=B, tref=TRef}}. -type from() :: {pid(), term()}. -spec handle_call (stop, from(), State) -> {stop, normal, stopped, State} when State::#state{}. handle_call(stop, _From, State) -> {stop, normal, stopped, State}; handle_call(_Request, _From, State) -> {reply, ignored, State}. -spec handle_cast(_, State) -> {noreply, State} when State::#state{}. handle_cast(_Msg, State) -> {noreply, State}. -spec handle_info(any(), State) -> {noreply, State} when State::#state{}. handle_info(update, #state{universaltime=Prev, rfc1123=B1, tref=TRef0}) -> %% Cancel the timer in case an external process sent an update message. _ = erlang:cancel_timer(TRef0, [{async, true}, {info, false}]), T = erlang:universaltime(), B2 = update_rfc1123(B1, Prev, T), ets:insert(?MODULE, {rfc1123, B2}), TRef = erlang:send_after(1000, self(), update), {noreply, #state{universaltime=T, rfc1123=B2, tref=TRef}}; handle_info(_Info, State) -> {noreply, State}. -spec terminate(_, _) -> ok. terminate(_Reason, _State) -> ok. -spec code_change(_, State, _) -> {ok, State} when State::#state{}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %% Internal. -spec update_rfc1123(binary(), undefined | calendar:datetime(), calendar:datetime()) -> binary(). update_rfc1123(Bin, Now, Now) -> Bin; update_rfc1123(<< Keep:23/binary, _/bits >>, {Date, {H, M, _}}, {Date, {H, M, S}}) -> << Keep/binary, (pad_int(S))/binary, " GMT" >>; update_rfc1123(<< Keep:20/binary, _/bits >>, {Date, {H, _, _}}, {Date, {H, M, S}}) -> << Keep/binary, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>; update_rfc1123(<< Keep:17/binary, _/bits >>, {Date, _}, {Date, {H, M, S}}) -> << Keep/binary, (pad_int(H))/binary, $:, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>; update_rfc1123(<< _:7/binary, Keep:10/binary, _/bits >>, {{Y, Mo, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) -> Wday = calendar:day_of_the_week(Date), << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, Keep/binary, (pad_int(H))/binary, $:, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>; update_rfc1123(<< _:11/binary, Keep:6/binary, _/bits >>, {{Y, _, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) -> Wday = calendar:day_of_the_week(Date), << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ", (month(Mo))/binary, Keep/binary, (pad_int(H))/binary, $:, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>; update_rfc1123(_, _, {Date = {Y, Mo, D}, {H, M, S}}) -> Wday = calendar:day_of_the_week(Date), << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ", (month(Mo))/binary, " ", (integer_to_binary(Y))/binary, " ", (pad_int(H))/binary, $:, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>. %% Following suggestion by MononcQc on #erlounge. -spec pad_int(0..59) -> binary(). pad_int(X) when X < 10 -> << $0, ($0 + X) >>; pad_int(X) -> integer_to_binary(X). -spec weekday(1..7) -> <<_:24>>. weekday(1) -> <<"Mon">>; weekday(2) -> <<"Tue">>; weekday(3) -> <<"Wed">>; weekday(4) -> <<"Thu">>; weekday(5) -> <<"Fri">>; weekday(6) -> <<"Sat">>; weekday(7) -> <<"Sun">>. -spec month(1..12) -> <<_:24>>. month( 1) -> <<"Jan">>; month( 2) -> <<"Feb">>; month( 3) -> <<"Mar">>; month( 4) -> <<"Apr">>; month( 5) -> <<"May">>; month( 6) -> <<"Jun">>; month( 7) -> <<"Jul">>; month( 8) -> <<"Aug">>; month( 9) -> <<"Sep">>; month(10) -> <<"Oct">>; month(11) -> <<"Nov">>; month(12) -> <<"Dec">>. %% Tests. -ifdef(TEST). update_rfc1123_test_() -> Tests = [ {<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined, {{2011, 5, 14}, {14, 25, 33}}, <<>>}, {<<"Sat, 14 May 2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}, {{2011, 5, 14}, {14, 25, 33}}, <<"Sat, 14 May 2011 14:25:33 GMT">>}, {<<"Sat, 14 May 2011 14:25:34 GMT">>, {{2011, 5, 14}, {14, 25, 33}}, {{2011, 5, 14}, {14, 25, 34}}, <<"Sat, 14 May 2011 14:25:33 GMT">>}, {<<"Sat, 14 May 2011 14:26:00 GMT">>, {{2011, 5, 14}, {14, 25, 59}}, {{2011, 5, 14}, {14, 26, 0}}, <<"Sat, 14 May 2011 14:25:59 GMT">>}, {<<"Sat, 14 May 2011 15:00:00 GMT">>, {{2011, 5, 14}, {14, 59, 59}}, {{2011, 5, 14}, {15, 0, 0}}, <<"Sat, 14 May 2011 14:59:59 GMT">>}, {<<"Sun, 15 May 2011 00:00:00 GMT">>, {{2011, 5, 14}, {23, 59, 59}}, {{2011, 5, 15}, { 0, 0, 0}}, <<"Sat, 14 May 2011 23:59:59 GMT">>}, {<<"Wed, 01 Jun 2011 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}}, {{2011, 6, 1}, { 0, 0, 0}}, <<"Tue, 31 May 2011 23:59:59 GMT">>}, {<<"Sun, 01 Jan 2012 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}}, {{2012, 1, 1}, { 0, 0, 0}}, <<"Sat, 31 Dec 2011 23:59:59 GMT">>} ], [{R, fun() -> R = update_rfc1123(B, P, N) end} || {R, P, N, B} <- Tests]. pad_int_test_() -> Tests = [ { 0, <<"00">>}, { 1, <<"01">>}, { 2, <<"02">>}, { 3, <<"03">>}, { 4, <<"04">>}, { 5, <<"05">>}, { 6, <<"06">>}, { 7, <<"07">>}, { 8, <<"08">>}, { 9, <<"09">>}, {10, <<"10">>}, {11, <<"11">>}, {12, <<"12">>}, {13, <<"13">>}, {14, <<"14">>}, {15, <<"15">>}, {16, <<"16">>}, {17, <<"17">>}, {18, <<"18">>}, {19, <<"19">>}, {20, <<"20">>}, {21, <<"21">>}, {22, <<"22">>}, {23, <<"23">>}, {24, <<"24">>}, {25, <<"25">>}, {26, <<"26">>}, {27, <<"27">>}, {28, <<"28">>}, {29, <<"29">>}, {30, <<"30">>}, {31, <<"31">>}, {32, <<"32">>}, {33, <<"33">>}, {34, <<"34">>}, {35, <<"35">>}, {36, <<"36">>}, {37, <<"37">>}, {38, <<"38">>}, {39, <<"39">>}, {40, <<"40">>}, {41, <<"41">>}, {42, <<"42">>}, {43, <<"43">>}, {44, <<"44">>}, {45, <<"45">>}, {46, <<"46">>}, {47, <<"47">>}, {48, <<"48">>}, {49, <<"49">>}, {50, <<"50">>}, {51, <<"51">>}, {52, <<"52">>}, {53, <<"53">>}, {54, <<"54">>}, {55, <<"55">>}, {56, <<"56">>}, {57, <<"57">>}, {58, <<"58">>}, {59, <<"59">>} ], [{I, fun() -> O = pad_int(I) end} || {I, O} <- Tests]. -endif. ================================================ FILE: src/cowboy_compress_h.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_compress_h). -behavior(cowboy_stream). -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). -record(state, { next :: any(), threshold :: non_neg_integer() | undefined, compress = undefined :: undefined | gzip, deflate = undefined :: undefined | zlib:zstream(), deflate_flush = sync :: none | sync }). -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> {cowboy_stream:commands(), #state{}}. init(StreamID, Req, Opts) -> State0 = check_req(Req), CompressThreshold = maps:get(compress_threshold, Opts, 300), DeflateFlush = buffering_to_zflush(maps:get(compress_buffering, Opts, false)), {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts), fold(Commands0, State0#state{next=Next, threshold=CompressThreshold, deflate_flush=DeflateFlush}). -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) -> {cowboy_stream:commands(), State} when State::#state{}. data(StreamID, IsFin, Data, State0=#state{next=Next0}) -> {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), fold(Commands0, State0#state{next=Next}). -spec info(cowboy_stream:streamid(), any(), State) -> {cowboy_stream:commands(), State} when State::#state{}. info(StreamID, Info, State0=#state{next=Next0}) -> {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), fold(Commands0, State0#state{next=Next}). -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). terminate(StreamID, Reason, #state{next=Next, deflate=Z}) -> %% Clean the zlib:stream() in case something went wrong. %% In the normal scenario the stream is already closed. case Z of undefined -> ok; _ -> zlib:close(Z) end, cowboy_stream:terminate(StreamID, Reason, Next). -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp when Resp::cowboy_stream:resp_command(). early_error(StreamID, Reason, PartialReq, Resp, Opts) -> cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). %% Internal. %% Check if the client supports decoding of gzip responses. %% %% A malformed accept-encoding header is ignored (no compression). check_req(Req) -> try cowboy_req:parse_header(<<"accept-encoding">>, Req) of %% Client doesn't support any compression algorithm. undefined -> #state{compress=undefined}; Encodings -> %% We only support gzip so look for it specifically. %% @todo A recipient SHOULD consider "x-gzip" to be %% equivalent to "gzip". (RFC7230 4.2.3) case [E || E={<<"gzip">>, Q} <- Encodings, Q =/= 0] of [] -> #state{compress=undefined}; _ -> #state{compress=gzip} end catch _:_ -> #state{compress=undefined} end. %% Do not compress responses that contain the content-encoding header. check_resp_headers(#{<<"content-encoding">> := _}, State) -> State#state{compress=undefined}; %% Do not compress responses that contain the etag header. check_resp_headers(#{<<"etag">> := _}, State) -> State#state{compress=undefined}; check_resp_headers(_, State) -> State. fold(Commands, State=#state{compress=undefined}) -> fold_vary_only(Commands, State, []); fold(Commands, State) -> fold(Commands, State, []). fold([], State, Acc) -> {lists:reverse(Acc), State}; %% We do not compress full sendfile bodies. fold([Response={response, _, _, {sendfile, _, _, _}}|Tail], State, Acc) -> fold(Tail, State, [vary_response(Response)|Acc]); %% We compress full responses directly, unless they are lower than %% the configured threshold or we find we are not able to by looking at the headers. fold([Response0={response, _, Headers, Body}|Tail], State0=#state{threshold=CompressThreshold}, Acc) -> case check_resp_headers(Headers, State0) of State=#state{compress=undefined} -> fold(Tail, State, [vary_response(Response0)|Acc]); State1 -> BodyLength = iolist_size(Body), if BodyLength =< CompressThreshold -> fold(Tail, State1, [vary_response(Response0)|Acc]); true -> {Response, State} = gzip_response(Response0, State1), fold(Tail, State, [vary_response(Response)|Acc]) end end; %% Check headers and initiate compression... fold([Response0={headers, _, Headers}|Tail], State0, Acc) -> case check_resp_headers(Headers, State0) of State=#state{compress=undefined} -> fold(Tail, State, [vary_headers(Response0)|Acc]); State1 -> {Response, State} = gzip_headers(Response0, State1), fold(Tail, State, [vary_headers(Response)|Acc]) end; %% then compress each data commands individually. fold([Data0={data, _, _}|Tail], State0=#state{compress=gzip}, Acc) -> {Data, State} = gzip_data(Data0, State0), fold(Tail, State, [Data|Acc]); %% When trailers are sent we need to end the compression. %% This results in an extra data command being sent. fold([Trailers={trailers, _}|Tail], State0=#state{compress=gzip}, Acc) -> {{data, fin, Data}, State} = gzip_data({data, fin, <<>>}, State0), fold(Tail, State, [Trailers, {data, nofin, Data}|Acc]); %% All the options from this handler can be updated for the current stream. %% The set_options command must be propagated as-is regardless. fold([SetOptions={set_options, Opts}|Tail], State=#state{ threshold=CompressThreshold0, deflate_flush=DeflateFlush0}, Acc) -> CompressThreshold = maps:get(compress_threshold, Opts, CompressThreshold0), DeflateFlush = case Opts of #{compress_buffering := CompressBuffering} -> buffering_to_zflush(CompressBuffering); _ -> DeflateFlush0 end, fold(Tail, State#state{threshold=CompressThreshold, deflate_flush=DeflateFlush}, [SetOptions|Acc]); %% Otherwise, we have an unrelated command or compression is disabled. fold([Command|Tail], State, Acc) -> fold(Tail, State, [Command|Acc]). fold_vary_only([], State, Acc) -> {lists:reverse(Acc), State}; fold_vary_only([Response={response, _, _, _}|Tail], State, Acc) -> fold_vary_only(Tail, State, [vary_response(Response)|Acc]); fold_vary_only([Response={headers, _, _}|Tail], State, Acc) -> fold_vary_only(Tail, State, [vary_headers(Response)|Acc]); fold_vary_only([Command|Tail], State, Acc) -> fold_vary_only(Tail, State, [Command|Acc]). buffering_to_zflush(true) -> none; buffering_to_zflush(false) -> sync. gzip_response({response, Status, Headers, Body}, State) -> %% We can't call zlib:gzip/1 because it does an %% iolist_to_binary(GzBody) at the end to return %% a binary(). Therefore the code here is largely %% a duplicate of the code of that function. Z = zlib:open(), GzBody = try %% 31 = 16+?MAX_WBITS from zlib.erl %% @todo It might be good to allow them to be configured? zlib:deflateInit(Z, default, deflated, 31, 8, default), Gz = zlib:deflate(Z, Body, finish), zlib:deflateEnd(Z), Gz after zlib:close(Z) end, {{response, Status, Headers#{ <<"content-length">> => integer_to_binary(iolist_size(GzBody)), <<"content-encoding">> => <<"gzip">> }, GzBody}, State}. gzip_headers({headers, Status, Headers0}, State) -> Z = zlib:open(), %% We use the same arguments as when compressing the body fully. %% @todo It might be good to allow them to be configured? zlib:deflateInit(Z, default, deflated, 31, 8, default), Headers = maps:remove(<<"content-length">>, Headers0), {{headers, Status, Headers#{ <<"content-encoding">> => <<"gzip">> }}, State#state{deflate=Z}}. vary_response({response, Status, Headers, Body}) -> {response, Status, vary(Headers), Body}. vary_headers({headers, Status, Headers}) -> {headers, Status, vary(Headers)}. %% We must add content-encoding to vary if it's not already there. vary(Headers=#{<<"vary">> := Vary}) -> try cow_http_hd:parse_vary(iolist_to_binary(Vary)) of '*' -> Headers; List -> case lists:member(<<"accept-encoding">>, List) of true -> Headers; false -> Headers#{<<"vary">> => [Vary, <<", accept-encoding">>]} end catch _:_ -> %% The vary header is invalid. Probably empty. We replace it with ours. Headers#{<<"vary">> => <<"accept-encoding">>} end; vary(Headers) -> Headers#{<<"vary">> => <<"accept-encoding">>}. %% It is not possible to combine zlib and the sendfile %% syscall as far as I can tell, because the zlib format %% includes a checksum at the end of the stream. We have %% to read the file in memory, making this not suitable for %% large files. gzip_data({data, nofin, Sendfile={sendfile, _, _, _}}, State=#state{deflate=Z, deflate_flush=Flush}) -> {ok, Data0} = read_file(Sendfile), Data = zlib:deflate(Z, Data0, Flush), {{data, nofin, Data}, State}; gzip_data({data, fin, Sendfile={sendfile, _, _, _}}, State=#state{deflate=Z}) -> {ok, Data0} = read_file(Sendfile), Data = zlib:deflate(Z, Data0, finish), zlib:deflateEnd(Z), zlib:close(Z), {{data, fin, Data}, State#state{deflate=undefined}}; gzip_data({data, nofin, Data0}, State=#state{deflate=Z, deflate_flush=Flush}) -> Data = zlib:deflate(Z, Data0, Flush), {{data, nofin, Data}, State}; gzip_data({data, fin, Data0}, State=#state{deflate=Z}) -> Data = zlib:deflate(Z, Data0, finish), zlib:deflateEnd(Z), zlib:close(Z), {{data, fin, Data}, State#state{deflate=undefined}}. read_file({sendfile, Offset, Bytes, Path}) -> {ok, IoDevice} = file:open(Path, [read, raw, binary]), try _ = case Offset of 0 -> ok; _ -> file:position(IoDevice, {bof, Offset}) end, file:read(IoDevice, Bytes) after file:close(IoDevice) end. ================================================ FILE: src/cowboy_constraints.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_constraints). -export([validate/2]). -export([reverse/2]). -export([format_error/1]). -type constraint() :: int | nonempty | fun(). -export_type([constraint/0]). -type reason() :: {constraint(), any(), any()}. -export_type([reason/0]). -spec validate(binary(), constraint() | [constraint()]) -> {ok, any()} | {error, reason()}. validate(Value, Constraints) when is_list(Constraints) -> apply_list(forward, Value, Constraints); validate(Value, Constraint) -> apply_list(forward, Value, [Constraint]). -spec reverse(any(), constraint() | [constraint()]) -> {ok, binary()} | {error, reason()}. reverse(Value, Constraints) when is_list(Constraints) -> apply_list(reverse, Value, Constraints); reverse(Value, Constraint) -> apply_list(reverse, Value, [Constraint]). -spec format_error(reason()) -> iodata(). format_error({Constraint, Reason, Value}) -> apply_constraint(format_error, {Reason, Value}, Constraint). apply_list(_, Value, []) -> {ok, Value}; apply_list(Type, Value0, [Constraint|Tail]) -> case apply_constraint(Type, Value0, Constraint) of {ok, Value} -> apply_list(Type, Value, Tail); {error, Reason} -> {error, {Constraint, Reason, Value0}} end. %% @todo {int, From, To}, etc. apply_constraint(Type, Value, int) -> int(Type, Value); apply_constraint(Type, Value, nonempty) -> nonempty(Type, Value); apply_constraint(Type, Value, F) when is_function(F) -> F(Type, Value). %% Constraint functions. int(forward, Value) -> try {ok, binary_to_integer(Value)} catch _:_ -> {error, not_an_integer} end; int(reverse, Value) -> try {ok, integer_to_binary(Value)} catch _:_ -> {error, not_an_integer} end; int(format_error, {not_an_integer, Value}) -> io_lib:format("The value ~p is not an integer.", [Value]). nonempty(Type, <<>>) when Type =/= format_error -> {error, empty}; nonempty(Type, Value) when Type =/= format_error, is_binary(Value) -> {ok, Value}; nonempty(format_error, {empty, Value}) -> io_lib:format("The value ~p is empty.", [Value]). -ifdef(TEST). validate_test() -> F = fun(_, Value) -> try {ok, binary_to_atom(Value, latin1)} catch _:_ -> {error, not_a_binary} end end, %% Value, Constraints, Result. Tests = [ {<<>>, [], <<>>}, {<<"123">>, int, 123}, {<<"123">>, [int], 123}, {<<"123">>, [nonempty, int], 123}, {<<"123">>, [int, nonempty], 123}, {<<>>, nonempty, error}, {<<>>, [nonempty], error}, {<<"hello">>, F, hello}, {<<"hello">>, [F], hello}, {<<"123">>, [F, int], error}, {<<"123">>, [int, F], error}, {<<"hello">>, [nonempty, F], hello}, {<<"hello">>, [F, nonempty], hello} ], [{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() -> case R of error -> {error, _} = validate(V, C); _ -> {ok, R} = validate(V, C) end end} || {V, C, R} <- Tests]. reverse_test() -> F = fun(_, Value) -> try {ok, atom_to_binary(Value, latin1)} catch _:_ -> {error, not_an_atom} end end, %% Value, Constraints, Result. Tests = [ {<<>>, [], <<>>}, {123, int, <<"123">>}, {123, [int], <<"123">>}, {123, [nonempty, int], <<"123">>}, {123, [int, nonempty], <<"123">>}, {<<>>, nonempty, error}, {<<>>, [nonempty], error}, {hello, F, <<"hello">>}, {hello, [F], <<"hello">>}, {123, [F, int], error}, {123, [int, F], error}, {hello, [nonempty, F], <<"hello">>}, {hello, [F, nonempty], <<"hello">>} ], [{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() -> case R of error -> {error, _} = reverse(V, C); _ -> {ok, R} = reverse(V, C) end end} || {V, C, R} <- Tests]. int_format_error_test() -> {error, Reason} = validate(<<"string">>, int), Bin = iolist_to_binary(format_error(Reason)), true = is_binary(Bin), ok. nonempty_format_error_test() -> {error, Reason} = validate(<<>>, nonempty), Bin = iolist_to_binary(format_error(Reason)), true = is_binary(Bin), ok. fun_format_error_test() -> F = fun (format_error, {test, <<"value">>}) -> formatted; (_, _) -> {error, test} end, {error, Reason} = validate(<<"value">>, F), formatted = format_error(Reason), ok. -endif. ================================================ FILE: src/cowboy_decompress_h.erl ================================================ %% Copyright (c) jdamanalo %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_decompress_h). -behavior(cowboy_stream). -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). -record(state, { next :: any(), enabled = true :: boolean(), ratio_limit :: non_neg_integer() | undefined, compress = undefined :: undefined | gzip, inflate = undefined :: undefined | zlib:zstream(), is_reading = false :: boolean(), %% We use a list of binaries to avoid doing unnecessary %% memory allocations when inflating. We convert to binary %% when we propagate the data. The data must be reversed %% before converting to binary or inflating: this is done %% via the buffer_to_binary/buffer_to_iovec functions. read_body_buffer = [] :: [binary()], read_body_is_fin = nofin :: nofin | {fin, non_neg_integer()} }). -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> {cowboy_stream:commands(), #state{}}. init(StreamID, Req0, Opts) -> Enabled = maps:get(decompress_enabled, Opts, true), RatioLimit = maps:get(decompress_ratio_limit, Opts, 20), {Req, State} = check_and_update_req(Req0), Inflate = case State#state.compress of undefined -> undefined; gzip -> Z = zlib:open(), zlib:inflateInit(Z, 31), Z end, {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), fold(Commands, State#state{next=Next, enabled=Enabled, ratio_limit=RatioLimit, inflate=Inflate}). -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) -> {cowboy_stream:commands(), State} when State::#state{}. data(StreamID, IsFin, Data, State=#state{next=Next0, inflate=undefined}) -> {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), fold(Commands, State#state{next=Next, read_body_is_fin=IsFin}); data(StreamID, IsFin, Data, State=#state{next=Next0, enabled=false, read_body_buffer=Buffer}) -> {Commands, Next} = cowboy_stream:data(StreamID, IsFin, buffer_to_binary([Data|Buffer]), Next0), fold(Commands, State#state{next=Next, read_body_is_fin=IsFin}); data(StreamID, IsFin, Data0, State0=#state{next=Next0, ratio_limit=RatioLimit, inflate=Z, is_reading=true, read_body_buffer=Buffer}) -> Data = buffer_to_iovec([Data0|Buffer]), Limit = iolist_size(Data) * RatioLimit, case cow_deflate:inflate(Z, Data, Limit) of {error, ErrorType} -> zlib:close(Z), Status = case ErrorType of data_error -> 400; size_error -> 413 end, Commands = [ {error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>}, stop ], fold(Commands, State0#state{inflate=undefined, read_body_buffer=[]}); {ok, Inflated} -> State = case IsFin of nofin -> State0; fin -> zlib:close(Z), State0#state{inflate=undefined} end, {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Inflated, Next0), fold(Commands, State#state{next=Next, read_body_buffer=[], read_body_is_fin=IsFin}) end; data(_, IsFin, Data, State=#state{read_body_buffer=Buffer}) -> {[], State#state{read_body_buffer=[Data|Buffer], read_body_is_fin=IsFin}}. -spec info(cowboy_stream:streamid(), any(), State) -> {cowboy_stream:commands(), State} when State::#state{}. info(StreamID, Info, State=#state{next=Next0, inflate=undefined}) -> {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), fold(Commands, State#state{next=Next}); info(StreamID, Info={CommandTag, _, _, _, _}, State=#state{next=Next0, read_body_is_fin=IsFin}) when CommandTag =:= read_body; CommandTag =:= read_body_timeout -> {Commands0, Next1} = cowboy_stream:info(StreamID, Info, Next0), {Commands, Next} = data(StreamID, IsFin, <<>>, State#state{next=Next1, is_reading=true}), fold(Commands ++ Commands0, Next); info(StreamID, Info={set_options, Opts}, State0=#state{next=Next0, enabled=Enabled0, ratio_limit=RatioLimit0, is_reading=IsReading}) -> Enabled = maps:get(decompress_enabled, Opts, Enabled0), RatioLimit = maps:get(decompress_ratio_limit, Opts, RatioLimit0), {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), %% We can't change the enabled setting after we start reading, %% otherwise the data becomes garbage. Changing the setting %% is not treated as an error, it is just ignored. State = case IsReading of true -> State0; false -> State0#state{enabled=Enabled} end, fold(Commands, State#state{next=Next, ratio_limit=RatioLimit}); info(StreamID, Info, State=#state{next=Next0}) -> {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), fold(Commands, State#state{next=Next}). -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). terminate(StreamID, Reason, #state{next=Next, inflate=Z}) -> case Z of undefined -> ok; _ -> zlib:close(Z) end, cowboy_stream:terminate(StreamID, Reason, Next). -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp when Resp::cowboy_stream:resp_command(). early_error(StreamID, Reason, PartialReq, Resp, Opts) -> cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). %% Internal. %% Check whether the request needs content decoding, and if it does %% whether it fits our criteria for decoding. We also update the %% Req to indicate whether content was decoded. %% %% We always set the content_decoded value in the Req because it %% indicates whether content decoding was attempted. %% %% A malformed content-encoding header results in no decoding. check_and_update_req(Req=#{headers := Headers}) -> ContentDecoded = maps:get(content_decoded, Req, []), try cowboy_req:parse_header(<<"content-encoding">>, Req) of %% We only automatically decompress when gzip is the only %% encoding used. Since it's the only encoding used, we %% can remove the header entirely before passing the Req %% forward. [<<"gzip">>] -> {Req#{ headers => maps:remove(<<"content-encoding">>, Headers), content_decoded => [<<"gzip">>|ContentDecoded] }, #state{compress=gzip}}; _ -> {Req#{content_decoded => ContentDecoded}, #state{compress=undefined}} catch _:_ -> {Req#{content_decoded => ContentDecoded}, #state{compress=undefined}} end. buffer_to_iovec(Buffer) -> lists:reverse(Buffer). buffer_to_binary(Buffer) -> iolist_to_binary(lists:reverse(Buffer)). fold(Commands, State) -> fold(Commands, State, []). fold([], State, Acc) -> {lists:reverse(Acc), State}; fold([{response, Status, Headers0, Body}|Tail], State=#state{enabled=true}, Acc) -> Headers = add_accept_encoding(Headers0), fold(Tail, State, [{response, Status, Headers, Body}|Acc]); fold([{headers, Status, Headers0} | Tail], State=#state{enabled=true}, Acc) -> Headers = add_accept_encoding(Headers0), fold(Tail, State, [{headers, Status, Headers}|Acc]); fold([Command|Tail], State, Acc) -> fold(Tail, State, [Command|Acc]). add_accept_encoding(Headers=#{<<"accept-encoding">> := AcceptEncoding}) -> try cow_http_hd:parse_accept_encoding(iolist_to_binary(AcceptEncoding)) of List -> case lists:keyfind(<<"gzip">>, 1, List) of %% gzip is excluded but this handler is enabled; we replace. {_, 0} -> Replaced = lists:keyreplace(<<"gzip">>, 1, List, {<<"gzip">>, 1000}), Codings = build_accept_encoding(Replaced), Headers#{<<"accept-encoding">> => Codings}; {_, _} -> Headers; false -> case lists:keyfind(<<"*">>, 1, List) of %% Others are excluded along with gzip; we add. {_, 0} -> WithGzip = [{<<"gzip">>, 1000} | List], Codings = build_accept_encoding(WithGzip), Headers#{<<"accept-encoding">> => Codings}; {_, _} -> Headers; false -> Headers#{<<"accept-encoding">> => [AcceptEncoding, <<", gzip">>]} end end catch _:_ -> %% The accept-encoding header is invalid. Probably empty. We replace it with ours. Headers#{<<"accept-encoding">> => <<"gzip">>} end; add_accept_encoding(Headers) -> Headers#{<<"accept-encoding">> => <<"gzip">>}. %% @todo From cowlib, maybe expose? qvalue_to_iodata(0) -> <<"0">>; qvalue_to_iodata(Q) when Q < 10 -> [<<"0.00">>, integer_to_binary(Q)]; qvalue_to_iodata(Q) when Q < 100 -> [<<"0.0">>, integer_to_binary(Q)]; qvalue_to_iodata(Q) when Q < 1000 -> [<<"0.">>, integer_to_binary(Q)]; qvalue_to_iodata(1000) -> <<"1">>. %% @todo Should be added to Cowlib. build_accept_encoding([{ContentCoding, Q}|Tail]) -> Weight = iolist_to_binary(qvalue_to_iodata(Q)), Acc = < DynamicBuffer; init_dynamic_buffer_size(#{dynamic_buffer := {LowDynamicBuffer, _}}) -> LowDynamicBuffer; init_dynamic_buffer_size(_) -> false. maybe_resize_buffer(State=#state{dynamic_buffer_size=false}, _) -> State; maybe_resize_buffer(State=#state{transport=Transport, socket=Socket, opts=#{dynamic_buffer := {LowDynamicBuffer, HighDynamicBuffer}}, dynamic_buffer_size=BufferSize0, dynamic_buffer_moving_average=MovingAvg0}, Data) -> DataLen = byte_size(Data), MovingAvg = (MovingAvg0 * 7 + DataLen) / 8, if BufferSize0 < HighDynamicBuffer andalso MovingAvg > BufferSize0 * 0.9 -> BufferSize = min(BufferSize0 * 2, HighDynamicBuffer), ok = maybe_socket_error(State, Transport:setopts(Socket, [{buffer, BufferSize}])), State#state{dynamic_buffer_moving_average=MovingAvg, dynamic_buffer_size=BufferSize}; BufferSize0 > LowDynamicBuffer andalso MovingAvg < BufferSize0 * 0.4 -> BufferSize = max(BufferSize0 div 2, LowDynamicBuffer), ok = maybe_socket_error(State, Transport:setopts(Socket, [{buffer, BufferSize}])), State#state{dynamic_buffer_moving_average=MovingAvg, dynamic_buffer_size=BufferSize}; true -> State#state{dynamic_buffer_moving_average=MovingAvg} end. ================================================ FILE: src/cowboy_handler.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% Handler middleware. %% %% Execute the handler given by the handler and handler_opts %% environment values. The result of this execution is added to the %% environment under the result value. -module(cowboy_handler). -behaviour(cowboy_middleware). -export([execute/2]). -export([terminate/4]). -callback init(Req, any()) -> {ok | module(), Req, any()} | {module(), Req, any(), any()} when Req::cowboy_req:req(). -callback terminate(any(), map(), any()) -> ok. -optional_callbacks([terminate/3]). -spec execute(Req, Env) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) -> try Handler:init(Req, HandlerOpts) of {ok, Req2, State} -> Result = terminate(normal, Req2, State, Handler), {ok, Req2, Env#{result => Result}}; {Mod, Req2, State} -> Mod:upgrade(Req2, Env, Handler, State); {Mod, Req2, State, Opts} -> Mod:upgrade(Req2, Env, Handler, State, Opts) catch Class:Reason:Stacktrace -> terminate({crash, Class, Reason}, Req, HandlerOpts, Handler), erlang:raise(Class, Reason, Stacktrace) end. -spec terminate(any(), Req | undefined, any(), module()) -> ok when Req::cowboy_req:req(). terminate(Reason, Req, State, Handler) -> case erlang:function_exported(Handler, terminate, 3) of true -> Handler:terminate(Reason, Req, State); false -> ok end. ================================================ FILE: src/cowboy_http.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% @todo Worth renaming to cowboy_http1. %% @todo Change use of cow_http to cow_http1 where appropriate. -module(cowboy_http). -export([init/6]). -export([loop/1]). -export([system_continue/3]). -export([system_terminate/4]). -export([system_code_change/4]). -type opts() :: #{ active_n => pos_integer(), alpn_default_protocol => http | http2, chunked => boolean(), compress_buffering => boolean(), compress_threshold => non_neg_integer(), connection_type => worker | supervisor, dynamic_buffer => false | {pos_integer(), pos_integer()}, dynamic_buffer_initial_average => non_neg_integer(), dynamic_buffer_initial_size => pos_integer(), env => cowboy_middleware:env(), hibernate => boolean(), http10_keepalive => boolean(), idle_timeout => timeout(), inactivity_timeout => timeout(), initial_stream_flow_size => non_neg_integer(), linger_timeout => timeout(), logger => module(), max_authority_length => non_neg_integer(), max_empty_lines => non_neg_integer(), max_header_name_length => non_neg_integer(), max_header_value_length => non_neg_integer(), max_headers => non_neg_integer(), max_keepalive => non_neg_integer(), max_method_length => non_neg_integer(), max_request_line_length => non_neg_integer(), metrics_callback => cowboy_metrics_h:metrics_callback(), metrics_req_filter => fun((cowboy_req:req()) -> map()), metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()), middlewares => [module()], protocols => [http | http2], proxy_header => boolean(), request_timeout => timeout(), reset_idle_timeout_on_send => boolean(), sendfile => boolean(), shutdown_timeout => timeout(), stream_handlers => [module()], tracer_callback => cowboy_tracer_h:tracer_callback(), tracer_flags => [atom()], tracer_match_specs => cowboy_tracer_h:tracer_match_specs(), %% Open ended because configured stream handlers might add options. _ => _ }. -export_type([opts/0]). -record(ps_request_line, { empty_lines = 0 :: non_neg_integer() }). -record(ps_header, { method = undefined :: binary(), authority = undefined :: binary() | undefined, path = undefined :: binary(), qs = undefined :: binary(), version = undefined :: cowboy:http_version(), headers = undefined :: cowboy:http_headers() | undefined, name = undefined :: binary() | undefined }). -record(ps_body, { length :: non_neg_integer() | undefined, received = 0 :: non_neg_integer(), transfer_decode_fun :: fun((binary(), cow_http_te:state()) -> cow_http_te:decode_ret()), transfer_decode_state :: cow_http_te:state() }). -record(stream, { id = undefined :: cowboy_stream:streamid(), %% Stream handlers and their state. state = undefined :: {module(), any()}, %% Request method. method = undefined :: binary(), %% Client HTTP version for this stream. version = undefined :: cowboy:http_version(), %% Unparsed te header. Used to know if we can send trailers. te :: undefined | binary(), %% Expected body size. local_expected_size = undefined :: undefined | non_neg_integer(), %% Sent body size. local_sent_size = 0 :: non_neg_integer(), %% Commands queued. queue = [] :: cowboy_stream:commands() }). -type stream() :: #stream{}. -record(state, { parent :: pid(), ref :: ranch:ref(), socket :: inet:socket(), transport :: module(), proxy_header :: undefined | ranch_proxy_header:proxy_info(), opts = #{} :: cowboy:opts(), buffer = <<>> :: binary(), %% Some options may be overriden for the current stream. overriden_opts = #{} :: cowboy:opts(), %% Remote address and port for the connection. peer = undefined :: {inet:ip_address(), inet:port_number()}, %% Local address and port for the connection. sock = undefined :: {inet:ip_address(), inet:port_number()}, %% Client certificate (TLS only). cert :: undefined | binary(), timer = undefined :: undefined | reference(), %% Whether we are currently receiving data from the socket. active = true :: boolean(), %% Identifier for the stream currently being read (or waiting to be received). in_streamid = 1 :: pos_integer(), %% Parsing state for the current stream or stream-to-be. in_state = #ps_request_line{} :: #ps_request_line{} | #ps_header{} | #ps_body{}, %% Flow requested for the current stream. flow = infinity :: non_neg_integer() | infinity, %% Dynamic buffer moving average and current buffer size. dynamic_buffer_size :: pos_integer() | false, dynamic_buffer_moving_average :: float(), %% Identifier for the stream currently being written. %% Note that out_streamid =< in_streamid. out_streamid = 1 :: pos_integer(), %% Whether we finished writing data for the current stream. out_state = wait :: wait | chunked | streaming | done, %% The connection will be closed after this stream. last_streamid = undefined :: pos_integer(), %% Currently active HTTP/1.1 streams. streams = [] :: [stream()], %% Children processes created by streams. children = cowboy_children:init() :: cowboy_children:children() }). -include_lib("cowlib/include/cow_inline.hrl"). -include_lib("cowlib/include/cow_parse.hrl"). -spec init(pid(), ranch:ref(), inet:socket(), module(), ranch_proxy_header:proxy_info(), cowboy:opts()) -> ok. init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) -> {ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket), 'A socket error occurred when retrieving the peer name.'), {ok, Sock} = maybe_socket_error(undefined, Transport:sockname(Socket), 'A socket error occurred when retrieving the sock name.'), CertResult = case Transport:name() of ssl -> case ssl:peercert(Socket) of {error, no_peercert} -> {ok, undefined}; Cert0 -> Cert0 end; _ -> {ok, undefined} end, {ok, Cert} = maybe_socket_error(undefined, CertResult, 'A socket error occurred when retrieving the client TLS certificate.'), State = #state{ parent=Parent, ref=Ref, socket=Socket, transport=Transport, proxy_header=ProxyHeader, opts=Opts, peer=Peer, sock=Sock, cert=Cert, dynamic_buffer_size=init_dynamic_buffer_size(Opts), dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0.0), last_streamid=maps:get(max_keepalive, Opts, 1000)}, safe_setopts_active(State), before_loop(set_timeout(State, request_timeout)). -include("cowboy_dynamic_buffer.hrl"). setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) -> N = maps:get(active_n, Opts, 1), Transport:setopts(Socket, [{active, N}]). safe_setopts_active(State) -> ok = maybe_socket_error(State, setopts_active(State)). active(State) -> safe_setopts_active(State), State#state{active=true}. passive(State=#state{socket=Socket, transport=Transport}) -> ok = maybe_socket_error(State, Transport:setopts(Socket, [{active, false}])), Messages = Transport:messages(), flush_passive(Socket, Messages), State#state{active=false}. flush_passive(Socket, Messages) -> receive {Passive, Socket} when Passive =:= element(4, Messages); %% Hardcoded for compatibility with Ranch 1.x. Passive =:= tcp_passive; Passive =:= ssl_passive -> flush_passive(Socket, Messages) after 0 -> ok end. before_loop(State=#state{opts=#{hibernate := true}}) -> proc_lib:hibernate(?MODULE, loop, [State]); before_loop(State) -> loop(State). -spec loop(#state{}) -> ok. loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts, buffer=Buffer, timer=TimerRef, children=Children, in_streamid=InStreamID, last_streamid=LastStreamID}) -> Messages = Transport:messages(), InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000), receive %% Discard data coming in after the last request %% we want to process was received fully. {OK, Socket, Data} when OK =:= element(1, Messages), InStreamID > LastStreamID -> State1 = maybe_resize_buffer(State, Data), before_loop(State1); %% Socket messages. {OK, Socket, Data} when OK =:= element(1, Messages) -> State1 = maybe_resize_buffer(State, Data), parse(<< Buffer/binary, Data/binary >>, State1); {Closed, Socket} when Closed =:= element(2, Messages) -> terminate(State, {socket_error, closed, 'The socket has been closed.'}); {Error, Socket, Reason} when Error =:= element(3, Messages) -> terminate(State, {socket_error, Reason, 'An error has occurred on the socket.'}); {Passive, Socket} when Passive =:= element(4, Messages); %% Hardcoded for compatibility with Ranch 1.x. Passive =:= tcp_passive; Passive =:= ssl_passive -> safe_setopts_active(State), before_loop(State); %% Timeouts. {timeout, Ref, {shutdown, Pid}} -> cowboy_children:shutdown_timeout(Children, Ref, Pid), before_loop(State); {timeout, TimerRef, Reason} -> timeout(State, Reason); {timeout, _, _} -> before_loop(State); %% System messages. {'EXIT', Parent, shutdown} -> Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'}, before_loop(initiate_closing(State, Reason)); {'EXIT', Parent, Reason} -> terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'}); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State); %% Messages pertaining to a stream. {{Pid, StreamID}, Msg} when Pid =:= self() -> before_loop(info(State, StreamID, Msg)); %% Exit signal from children. Msg = {'EXIT', Pid, _} -> before_loop(down(State, Pid, Msg)); %% Calls from supervisor module. {'$gen_call', From, Call} -> cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE), before_loop(State); %% Unknown messages. Msg -> cowboy:log(warning, "Received stray message ~p.~n", [Msg], Opts), before_loop(State) after InactivityTimeout -> terminate(State, {internal_error, timeout, 'No message or data received before timeout.'}) end. %% For HTTP/1.1 we have two types of timeouts: the request_timeout %% is used when there is no currently ongoing request. This means %% that we are not currently sending or receiving data and that %% the next data to be received will be a new request. The %% request_timeout is set once when we no longer have ongoing %% requests, and runs until the full set of request headers %% is received. It is not reset. %% %% After that point we use the idle_timeout. We continue using %% the idle_timeout if pipelined requests come in: we are doing %% work and just want to ensure the socket is not half-closed. %% We continue using the idle_timeout up until there is no %% ongoing request. This includes requests that were processed %% and for which we only want to skip the body. Once the body %% has been read fully we can go back to request_timeout. The %% idle_timeout is reset every time we receive data and, %% optionally, every time we send data. %% We do not set request_timeout if we are skipping a body. set_timeout(State=#state{in_state=#ps_body{}}, request_timeout) -> State; %% We do not set idle_timeout if there are no active streams, %% unless when we are skipping a body. set_timeout(State=#state{streams=[], in_state=InState}, idle_timeout) when element(1, InState) =/= ps_body -> State; %% Otherwise we can set the timeout. %% @todo Don't do this so often, use a strategy similar to Websocket/H2 if possible. set_timeout(State0=#state{opts=Opts, overriden_opts=Override}, Name) -> State = cancel_timeout(State0), Default = case Name of request_timeout -> 5000; idle_timeout -> 60000 end, Timeout = case Override of %% The timeout may have been overriden for the current stream. #{Name := Timeout0} -> Timeout0; _ -> maps:get(Name, Opts, Default) end, TimerRef = case Timeout of infinity -> undefined; Timeout -> erlang:start_timer(Timeout, self(), Name) end, State#state{timer=TimerRef}. maybe_reset_idle_timeout(State=#state{opts=Opts}) -> case maps:get(reset_idle_timeout_on_send, Opts, false) of true -> set_timeout(State, idle_timeout); false -> State end. cancel_timeout(State=#state{timer=TimerRef}) -> ok = case TimerRef of undefined -> ok; _ -> %% Do a synchronous cancel and remove the message if any %% to avoid receiving stray messages. _ = erlang:cancel_timer(TimerRef, [{async, false}, {info, false}]), receive {timeout, TimerRef, _} -> ok after 0 -> ok end end, State#state{timer=undefined}. -spec timeout(_, _) -> no_return(). timeout(State=#state{in_state=#ps_request_line{}}, request_timeout) -> terminate(State, {connection_error, timeout, 'No request-line received before timeout.'}); timeout(State=#state{in_state=#ps_header{}}, request_timeout) -> error_terminate(408, State, {connection_error, timeout, 'Request headers not received before timeout.'}); timeout(State, idle_timeout) -> terminate(State, {connection_error, timeout, 'Connection idle longer than configuration allows.'}). parse(<<>>, State) -> before_loop(State#state{buffer= <<>>}); %% Do not process requests that come in after the last request %% and discard the buffer if any to save memory. parse(_, State=#state{in_streamid=InStreamID, in_state=#ps_request_line{}, last_streamid=LastStreamID}) when InStreamID > LastStreamID -> before_loop(State#state{buffer= <<>>}); parse(Buffer, State=#state{in_state=#ps_request_line{empty_lines=EmptyLines}}) -> after_parse(parse_request(Buffer, State, EmptyLines)); parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=undefined}}) -> after_parse(parse_header(Buffer, State#state{in_state=PS#ps_header{headers=undefined}}, Headers)); parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=Name}}) -> after_parse(parse_hd_before_value(Buffer, State#state{in_state=PS#ps_header{headers=undefined, name=undefined}}, Headers, Name)); parse(Buffer, State=#state{in_state=#ps_body{}}) -> after_parse(parse_body(Buffer, State)). after_parse({request, Req=#{streamid := StreamID, method := Method, headers := Headers, version := Version}, State0=#state{opts=Opts, buffer=Buffer, streams=Streams0}}) -> try cowboy_stream:init(StreamID, Req, Opts) of {Commands, StreamState} -> Flow = maps:get(initial_stream_flow_size, Opts, 65535), TE = maps:get(<<"te">>, Headers, undefined), Streams = [#stream{id=StreamID, state=StreamState, method=Method, version=Version, te=TE}|Streams0], State1 = State0#state{streams=Streams, flow=Flow}, State2 = case maybe_req_close(State1, Headers, Version) of close -> State1#state{last_streamid=StreamID}; keepalive -> State1; bad_connection_header -> error_terminate(400, State1, {connection_error, protocol_error, 'The Connection header is invalid. (RFC7230 6.1)'}) end, State = set_timeout(State2, idle_timeout), parse(Buffer, commands(State, StreamID, Commands)) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(init, [StreamID, Req, Opts], Class, Exception, Stacktrace), Opts), %% We do not reset the idle timeout on send here %% because an error occurred in the application. While we %% are keeping the connection open for further requests we %% do not want to keep the connection up too long if no %% additional requests come in. early_error(500, State0, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:init/3.'}, Req), parse(Buffer, State0) end; %% Streams are sequential so the body is always about the last stream created %% unless that stream has terminated. after_parse({data, StreamID, IsFin, Data, State0=#state{opts=Opts, buffer=Buffer, streams=Streams0=[Stream=#stream{id=StreamID, state=StreamState0}|_]}}) -> try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of {Commands, StreamState} -> Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream#stream{state=StreamState}), State1 = set_timeout(State0, idle_timeout), State = update_flow(IsFin, Data, State1#state{streams=Streams}), parse(Buffer, commands(State, StreamID, Commands)) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(data, [StreamID, IsFin, Data, StreamState0], Class, Exception, Stacktrace), Opts), %% @todo Should call parse after this. stream_terminate(State0, StreamID, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:data/4.'}) end; %% No corresponding stream. We must skip the body of the previous request %% in order to process the next one. after_parse({data, _, IsFin, _, State=#state{buffer=Buffer}}) -> parse(Buffer, set_timeout(State, case IsFin of fin -> request_timeout; nofin -> idle_timeout end)); after_parse({more, State}) -> before_loop(set_timeout(State, idle_timeout)). update_flow(fin, _, State) -> %% This function is only called after parsing, therefore we %% are expecting to be in active mode already. State#state{flow=infinity}; update_flow(nofin, Data, State0=#state{flow=Flow0}) -> Flow = Flow0 - byte_size(Data), State = State0#state{flow=Flow}, if Flow0 > 0, Flow =< 0 -> passive(State); true -> State end. %% Request-line. -spec parse_request(Buffer, State, non_neg_integer()) -> {request, cowboy_req:req(), State} | {data, cowboy_stream:streamid(), cowboy_stream:fin(), binary(), State} | {more, State} when Buffer::binary(), State::#state{}. %% Empty lines must be using \r\n. parse_request(<< $\n, _/bits >>, State, _) -> error_terminate(400, State, {connection_error, protocol_error, 'Empty lines between requests must use the CRLF line terminator. (RFC7230 3.5)'}); parse_request(<< $\s, _/bits >>, State, _) -> error_terminate(400, State, {connection_error, protocol_error, 'The request-line must not begin with a space. (RFC7230 3.1.1, RFC7230 3.5)'}); %% We limit the length of the Request-line to MaxLength to avoid endlessly %% reading from the socket and eventually crashing. parse_request(Buffer, State=#state{opts=Opts, in_streamid=InStreamID}, EmptyLines) -> MaxLength = maps:get(max_request_line_length, Opts, 8000), MaxEmptyLines = maps:get(max_empty_lines, Opts, 5), case match_eol(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> error_terminate(414, State, {connection_error, limit_reached, 'The request-line length is larger than configuration allows. (RFC7230 3.1.1)'}); nomatch -> {more, State#state{buffer=Buffer, in_state=#ps_request_line{empty_lines=EmptyLines}}}; 1 when EmptyLines =:= MaxEmptyLines -> error_terminate(400, State, {connection_error, limit_reached, 'More empty lines were received than configuration allows. (RFC7230 3.5)'}); 1 -> << _:16, Rest/bits >> = Buffer, parse_request(Rest, State, EmptyLines + 1); _ -> case Buffer of %% @todo * is only for server-wide OPTIONS request (RFC7230 5.3.4); tests << "OPTIONS * ", Rest/bits >> -> parse_version(Rest, State, <<"OPTIONS">>, undefined, <<"*">>, <<>>); <<"CONNECT ", _/bits>> -> error_terminate(501, State, {connection_error, no_error, 'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'}); <<"TRACE ", _/bits>> -> error_terminate(501, State, {connection_error, no_error, 'The TRACE method is currently not implemented. (RFC7231 4.3.8)'}); %% Accept direct HTTP/2 only at the beginning of the connection. << "PRI * HTTP/2.0\r\n", _/bits >> when InStreamID =:= 1 -> case lists:member(http2, maps:get(protocols, Opts, [http2, http])) of true -> http2_upgrade(State, Buffer); false -> error_terminate(501, State, {connection_error, no_error, 'Prior knowledge upgrade to HTTP/2 is disabled by configuration.'}) end; _ -> parse_method(Buffer, State, <<>>, maps:get(max_method_length, Opts, 32)) end end. match_eol(<< $\n, _/bits >>, N) -> N; match_eol(<< _, Rest/bits >>, N) -> match_eol(Rest, N + 1); match_eol(_, _) -> nomatch. parse_method(_, State, _, 0) -> error_terminate(501, State, {connection_error, limit_reached, 'The method name is longer than configuration allows. (RFC7230 3.1.1)'}); parse_method(<< C, Rest/bits >>, State, SoFar, Remaining) -> case C of $\r -> error_terminate(400, State, {connection_error, protocol_error, 'The method name must not be followed with a line break. (RFC7230 3.1.1)'}); $\s -> parse_uri(Rest, State, SoFar); _ when ?IS_TOKEN(C) -> parse_method(Rest, State, << SoFar/binary, C >>, Remaining - 1); _ -> error_terminate(400, State, {connection_error, protocol_error, 'The method name must contain only valid token characters. (RFC7230 3.1.1)'}) end. parse_uri(<< H, T, T, P, "://", Rest/bits >>, State, Method) when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T; P =:= $p orelse P =:= $P -> parse_uri_authority(Rest, State, Method); parse_uri(<< H, T, T, P, S, "://", Rest/bits >>, State, Method) when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T; P =:= $p orelse P =:= $P; S =:= $s orelse S =:= $S -> parse_uri_authority(Rest, State, Method); parse_uri(<< $/, Rest/bits >>, State, Method) -> parse_uri_path(Rest, State, Method, undefined, <<$/>>); parse_uri(_, State, _) -> error_terminate(400, State, {connection_error, protocol_error, 'Invalid request-line or request-target. (RFC7230 3.1.1, RFC7230 5.3)'}). %% @todo We probably want to apply max_authority_length also %% to the host header and to document this option. It might %% also be useful for HTTP/2 requests. parse_uri_authority(Rest, State=#state{opts=Opts}, Method) -> parse_uri_authority(Rest, State, Method, <<>>, maps:get(max_authority_length, Opts, 255)). parse_uri_authority(_, State, _, _, 0) -> error_terminate(414, State, {connection_error, limit_reached, 'The authority component of the absolute URI is longer than configuration allows. (RFC7230 2.7.1)'}); parse_uri_authority(<>, State, Method, SoFar, Remaining) -> case C of $\r -> error_terminate(400, State, {connection_error, protocol_error, 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); $@ -> error_terminate(400, State, {connection_error, protocol_error, 'Absolute URIs must not include a userinfo component. (RFC7230 2.7.1)'}); C when SoFar =:= <<>> andalso ((C =:= $/) orelse (C =:= $\s) orelse (C =:= $?) orelse (C =:= $#)) -> error_terminate(400, State, {connection_error, protocol_error, 'Absolute URIs must include a non-empty host component. (RFC7230 2.7.1)'}); $: when SoFar =:= <<>> -> error_terminate(400, State, {connection_error, protocol_error, 'Absolute URIs must include a non-empty host component. (RFC7230 2.7.1)'}); $/ -> parse_uri_path(Rest, State, Method, SoFar, <<"/">>); $\s -> parse_version(Rest, State, Method, SoFar, <<"/">>, <<>>); $? -> parse_uri_query(Rest, State, Method, SoFar, <<"/">>, <<>>); $# -> skip_uri_fragment(Rest, State, Method, SoFar, <<"/">>, <<>>); C -> parse_uri_authority(Rest, State, Method, <>, Remaining - 1) end. parse_uri_path(<>, State, Method, Authority, SoFar) -> case C of $\r -> error_terminate(400, State, {connection_error, protocol_error, 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); $\s -> parse_version(Rest, State, Method, Authority, SoFar, <<>>); $? -> parse_uri_query(Rest, State, Method, Authority, SoFar, <<>>); $# -> skip_uri_fragment(Rest, State, Method, Authority, SoFar, <<>>); _ -> parse_uri_path(Rest, State, Method, Authority, <>) end. parse_uri_query(<>, State, M, A, P, SoFar) -> case C of $\r -> error_terminate(400, State, {connection_error, protocol_error, 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); $\s -> parse_version(Rest, State, M, A, P, SoFar); $# -> skip_uri_fragment(Rest, State, M, A, P, SoFar); _ -> parse_uri_query(Rest, State, M, A, P, <>) end. skip_uri_fragment(<>, State, M, A, P, Q) -> case C of $\r -> error_terminate(400, State, {connection_error, protocol_error, 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); $\s -> parse_version(Rest, State, M, A, P, Q); _ -> skip_uri_fragment(Rest, State, M, A, P, Q) end. parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, State, M, A, P, Q) -> before_parse_headers(Rest, State, M, A, P, Q, 'HTTP/1.1'); parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, State, M, A, P, Q) -> before_parse_headers(Rest, State, M, A, P, Q, 'HTTP/1.0'); parse_version(<< "HTTP/1.", _, C, _/bits >>, State, _, _, _, _) when C =:= $\s; C =:= $\t -> error_terminate(400, State, {connection_error, protocol_error, 'Whitespace is not allowed after the HTTP version. (RFC7230 3.1.1)'}); parse_version(<< C, _/bits >>, State, _, _, _, _) when C =:= $\s; C =:= $\t -> error_terminate(400, State, {connection_error, protocol_error, 'The separator between request target and version must be a single SP. (RFC7230 3.1.1)'}); parse_version(_, State, _, _, _, _) -> error_terminate(505, State, {connection_error, protocol_error, 'Unsupported HTTP version. (RFC7230 2.6)'}). before_parse_headers(Rest, State, M, A, P, Q, V) -> parse_header(Rest, State#state{in_state=#ps_header{ method=M, authority=A, path=P, qs=Q, version=V}}, #{}). %% Headers. %% We need two or more bytes in the buffer to continue. parse_header(Rest, State=#state{in_state=PS}, Headers) when byte_size(Rest) < 2 -> {more, State#state{buffer=Rest, in_state=PS#ps_header{headers=Headers}}}; parse_header(<< $\r, $\n, Rest/bits >>, S, Headers) -> request(Rest, S, Headers); parse_header(Buffer, State=#state{opts=Opts, in_state=PS}, Headers) -> MaxHeaders = maps:get(max_headers, Opts, 100), NumHeaders = maps:size(Headers), if NumHeaders >= MaxHeaders -> error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}}, {connection_error, limit_reached, 'The number of headers is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); true -> parse_header_colon(Buffer, State, Headers) end. parse_header_colon(Buffer, State=#state{opts=Opts, in_state=PS}, Headers) -> MaxLength = maps:get(max_header_name_length, Opts, 64), case match_colon(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}}, {connection_error, limit_reached, 'A header name is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); nomatch -> %% We don't have a colon but we might have an invalid header line, %% so check if we have an LF and abort with an error if we do. case match_eol(Buffer, 0) of nomatch -> {more, State#state{buffer=Buffer, in_state=PS#ps_header{headers=Headers}}}; _ -> error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, {connection_error, protocol_error, 'A header line is missing a colon separator. (RFC7230 3.2.4)'}) end; _ -> parse_hd_name(Buffer, State, Headers, <<>>) end. match_colon(<< $:, _/bits >>, N) -> N; match_colon(<< _, Rest/bits >>, N) -> match_colon(Rest, N + 1); match_colon(_, _) -> nomatch. parse_hd_name(<< $:, Rest/bits >>, State, H, SoFar) -> parse_hd_before_value(Rest, State, H, SoFar); parse_hd_name(<< C, _/bits >>, State=#state{in_state=PS}, H, <<>>) when ?IS_WS(C) -> error_terminate(400, State#state{in_state=PS#ps_header{headers=H}}, {connection_error, protocol_error, 'Whitespace is not allowed before the header name. (RFC7230 3.2)'}); parse_hd_name(<< C, _/bits >>, State=#state{in_state=PS}, H, _) when ?IS_WS(C) -> error_terminate(400, State#state{in_state=PS#ps_header{headers=H}}, {connection_error, protocol_error, 'Whitespace is not allowed between the header name and the colon. (RFC7230 3.2.4)'}); parse_hd_name(<< C, Rest/bits >>, State, H, SoFar) -> ?LOWER(parse_hd_name, Rest, State, H, SoFar). parse_hd_before_value(<< $\s, Rest/bits >>, S, H, N) -> parse_hd_before_value(Rest, S, H, N); parse_hd_before_value(<< $\t, Rest/bits >>, S, H, N) -> parse_hd_before_value(Rest, S, H, N); parse_hd_before_value(Buffer, State=#state{opts=Opts, in_state=PS}, H, N) -> MaxLength = max_header_value_length(N, Opts), case match_eol(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> error_terminate(431, State#state{in_state=PS#ps_header{headers=H}}, {connection_error, limit_reached, 'A header value is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); nomatch -> {more, State#state{buffer=Buffer, in_state=PS#ps_header{headers=H, name=N}}}; _ -> parse_hd_value(Buffer, State, H, N, <<>>) end. max_header_value_length(<<"authorization">>, #{max_authorization_header_value_length := Max}) -> Max; max_header_value_length(<<"cookie">>, #{max_cookie_header_value_length := Max}) -> Max; max_header_value_length(_, #{max_header_value_length := Max}) -> Max; max_header_value_length(_, _) -> 4096. parse_hd_value(<< $\r, $\n, Rest/bits >>, S, Headers0, Name, SoFar) -> Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1), Headers = case maps:get(Name, Headers0, undefined) of undefined -> Headers0#{Name => Value}; %% The cookie header does not use proper HTTP header lists. Value0 when Name =:= <<"cookie">> -> Headers0#{Name => << Value0/binary, "; ", Value/binary >>}; Value0 -> Headers0#{Name => << Value0/binary, ", ", Value/binary >>} end, parse_header(Rest, S, Headers); parse_hd_value(<< C, Rest/bits >>, S, H, N, SoFar) -> parse_hd_value(Rest, S, H, N, << SoFar/binary, C >>). clean_value_ws_end(_, -1) -> <<>>; clean_value_ws_end(Value, N) -> case binary:at(Value, N) of $\s -> clean_value_ws_end(Value, N - 1); $\t -> clean_value_ws_end(Value, N - 1); _ -> S = N + 1, << Value2:S/binary, _/bits >> = Value, Value2 end. -ifdef(TEST). clean_value_ws_end_test_() -> Tests = [ {<<>>, <<>>}, {<<" ">>, <<>>}, {<<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " "text/html;level=2;q=0.4, */*;q=0.5 \t \t ">>, <<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " "text/html;level=2;q=0.4, */*;q=0.5">>} ], [{V, fun() -> R = clean_value_ws_end(V, byte_size(V) - 1) end} || {V, R} <- Tests]. horse_clean_value_ws_end() -> horse:repeat(200000, clean_value_ws_end( <<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " "text/html;level=2;q=0.4, */*;q=0.5 ">>, byte_size(<<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " "text/html;level=2;q=0.4, */*;q=0.5 ">>) - 1) ). -endif. request(Buffer, State=#state{transport=Transport, in_state=PS=#ps_header{authority=Authority, version=Version}}, Headers) -> case maps:get(<<"host">>, Headers, undefined) of undefined when Version =:= 'HTTP/1.1' -> %% @todo Might want to not close the connection on this and next one. error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'}); undefined -> request(Buffer, State, Headers, <<>>, default_port(Transport:secure())); %% @todo When CONNECT requests come in we need to ignore the RawHost %% and instead use the Authority as the source of host. RawHost when Authority =:= undefined; Authority =:= RawHost -> request_parse_host(Buffer, State, Headers, RawHost); %% RFC7230 does not explicitly ask us to reject requests %% that have a different authority component and host header. %% However it DOES ask clients to set them to the same value, %% so we enforce that. _ -> error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'The host header is different than the absolute-form authority component. (RFC7230 5.4)'}) end. request_parse_host(Buffer, State=#state{transport=Transport, in_state=PS}, Headers, RawHost) -> try cow_http_hd:parse_host(RawHost) of {Host, undefined} -> request(Buffer, State, Headers, Host, default_port(Transport:secure())); {Host, Port} when Port > 0, Port =< 65535 -> request(Buffer, State, Headers, Host, Port); _ -> error_terminate(400, State, {stream_error, protocol_error, 'The port component of the absolute-form is not in the range 0..65535. (RFC7230 2.7.1)'}) catch _:_ -> error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'The host header is invalid. (RFC7230 5.4)'}) end. -spec default_port(boolean()) -> 80 | 443. default_port(true) -> 443; default_port(_) -> 80. %% End of request parsing. request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock, cert=Cert, opts=Opts, proxy_header=ProxyHeader, in_streamid=StreamID, in_state= PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}}, Headers, Host, Port) -> Scheme = case Transport:secure() of true -> <<"https">>; false -> <<"http">> end, {HasBody, BodyLength, TDecodeFun, TDecodeState} = case Headers of #{<<"transfer-encoding">> := _, <<"content-length">> := _} -> error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'The request had both transfer-encoding and content-length headers. (RFC7230 3.3.3)'}); #{<<"transfer-encoding">> := TransferEncoding0} -> try cow_http_hd:parse_transfer_encoding(TransferEncoding0) of [<<"chunked">>] -> {true, undefined, fun cow_http_te:stream_chunked/2, {0, 0}}; _ -> error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'Cowboy only supports transfer-encoding: chunked. (RFC7230 3.3.1)'}) catch _:_ -> error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'The transfer-encoding header is invalid. (RFC7230 3.3.1)'}) end; #{<<"content-length">> := <<"0">>} -> {false, 0, undefined, undefined}; #{<<"content-length">> := BinLength} -> Length = try cow_http_hd:parse_content_length(BinLength) catch _:_ -> error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, {stream_error, protocol_error, 'The content-length header is invalid. (RFC7230 3.3.2)'}) end, {true, Length, fun cow_http_te:stream_identity/2, {0, Length}}; _ -> {false, 0, undefined, undefined} end, Req0 = #{ ref => Ref, pid => self(), streamid => StreamID, peer => Peer, sock => Sock, cert => Cert, method => Method, scheme => Scheme, host => Host, port => Port, path => Path, qs => Qs, version => Version, %% We are transparently taking care of transfer-encodings so %% the user code has no need to know about it. headers => maps:remove(<<"transfer-encoding">>, Headers), has_body => HasBody, body_length => BodyLength }, %% We add the PROXY header information if any. Req = case ProxyHeader of undefined -> Req0; _ -> Req0#{proxy_header => ProxyHeader} end, case is_http2_upgrade(Headers, Version, Opts) of false -> State = case HasBody of true -> State0#state{in_state=#ps_body{ length = BodyLength, transfer_decode_fun = TDecodeFun, transfer_decode_state = TDecodeState }}; false -> State0#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}} end, {request, Req, State#state{buffer=Buffer}}; {true, HTTP2Settings} -> %% We save the headers in case the upgrade will fail %% and we need to pass them to cowboy_stream:early_error. http2_upgrade(State0#state{in_state=PS#ps_header{headers=Headers}}, Buffer, HTTP2Settings, Req) end. %% HTTP/2 upgrade. is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade, <<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1', Opts) -> Conns = cow_http_hd:parse_connection(Conn), case lists:member(<<"upgrade">>, Conns) andalso lists:member(<<"http2-settings">>, Conns) andalso lists:member(http2, maps:get(protocols, Opts, [http2, http])) of true -> Protocols = cow_http_hd:parse_upgrade(Upgrade), case lists:member(<<"h2c">>, Protocols) of true -> {true, HTTP2Settings}; false -> false end; _ -> false end; is_http2_upgrade(_, _, _) -> false. %% Prior knowledge upgrade, without an HTTP/1.1 request. http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, proxy_header=ProxyHeader, peer=Peer, sock=Sock, cert=Cert}, Buffer) -> case Transport:secure() of false -> _ = cancel_timeout(State), cowboy_http2:init(Parent, Ref, Socket, Transport, ProxyHeader, opts_for_upgrade(State), Peer, Sock, Cert, Buffer); true -> error_terminate(400, State, {connection_error, protocol_error, 'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'}) end. %% Upgrade via an HTTP/1.1 request. http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, proxy_header=ProxyHeader, peer=Peer, sock=Sock, cert=Cert}, Buffer, HTTP2Settings, Req) -> case Transport:secure() of false -> %% @todo %% However if the client sent a body, we need to read the body in full %% and if we can't do that, return a 413 response. Some options are in order. %% Always half-closed stream coming from this side. try cow_http_hd:parse_http2_settings(HTTP2Settings) of Settings -> _ = cancel_timeout(State), cowboy_http2:init(Parent, Ref, Socket, Transport, ProxyHeader, opts_for_upgrade(State), Peer, Sock, Cert, Buffer, Settings, Req) catch _:_ -> error_terminate(400, State, {connection_error, protocol_error, 'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'}) end; true -> error_terminate(400, State, {connection_error, protocol_error, 'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'}) end. opts_for_upgrade(#state{opts=Opts, dynamic_buffer_size=false}) -> Opts; opts_for_upgrade(#state{opts=Opts, dynamic_buffer_size=Size, dynamic_buffer_moving_average=MovingAvg}) -> Opts#{ dynamic_buffer_initial_average => MovingAvg, dynamic_buffer_initial_size => Size }. %% Request body parsing. parse_body(Buffer, State=#state{in_streamid=StreamID, in_state= PS=#ps_body{received=Received, transfer_decode_fun=TDecode, transfer_decode_state=TState0}}) -> %% @todo Proper trailers. try TDecode(Buffer, TState0) of more -> {more, State#state{buffer=Buffer}}; {more, Data, TState} -> {data, StreamID, nofin, Data, State#state{buffer= <<>>, in_state=PS#ps_body{received=Received + byte_size(Data), transfer_decode_state=TState}}}; {more, Data, _Length, TState} when is_integer(_Length) -> {data, StreamID, nofin, Data, State#state{buffer= <<>>, in_state=PS#ps_body{received=Received + byte_size(Data), transfer_decode_state=TState}}}; {more, Data, Rest, TState} -> {data, StreamID, nofin, Data, State#state{buffer=Rest, in_state=PS#ps_body{received=Received + byte_size(Data), transfer_decode_state=TState}}}; {done, _HasTrailers, Rest} -> {data, StreamID, fin, <<>>, State#state{buffer=Rest, in_streamid=StreamID + 1, in_state=#ps_request_line{}}}; {done, Data, _HasTrailers, Rest} -> {data, StreamID, fin, Data, State#state{buffer=Rest, in_streamid=StreamID + 1, in_state=#ps_request_line{}}} catch _:_ -> Reason = {connection_error, protocol_error, 'Failure to decode the content. (RFC7230 4)'}, terminate(stream_terminate(State, StreamID, Reason), Reason) end. %% Message handling. down(State=#state{opts=Opts, children=Children0}, Pid, Msg) -> case cowboy_children:down(Children0, Pid) of %% The stream was terminated already. {ok, undefined, Children} -> State#state{children=Children}; %% The stream is still running. {ok, StreamID, Children} -> info(State#state{children=Children}, StreamID, Msg); %% The process was unknown. error -> cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n", [Msg, Pid], Opts), State end. info(State=#state{opts=Opts, streams=Streams0}, StreamID, Msg) -> case lists:keyfind(StreamID, #stream.id, Streams0) of Stream = #stream{state=StreamState0} -> try cowboy_stream:info(StreamID, Msg, StreamState0) of {Commands, StreamState} -> Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream#stream{state=StreamState}), commands(State#state{streams=Streams}, StreamID, Commands) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(info, [StreamID, Msg, StreamState0], Class, Exception, Stacktrace), Opts), stream_terminate(State, StreamID, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:info/3.'}) end; false -> cowboy:log(warning, "Received message ~p for unknown stream ~p.~n", [Msg, StreamID], Opts), State end. %% Commands. %% %% The order in which the commands are given matters. Cowboy may %% stop processing commands after the 'stop' command or when an %% error occurred, such as a socket error. Critical commands such %% as 'spawn' should always be given first. commands(State, _, []) -> State; %% Supervise a child process. commands(State=#state{children=Children}, StreamID, [{spawn, Pid, Shutdown}|Tail]) -> commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)}, StreamID, Tail); %% Error handling. commands(State, StreamID, [Error = {internal_error, _, _}|Tail]) -> commands(stream_terminate(State, StreamID, Error), StreamID, Tail); %% Commands for a stream currently inactive. commands(State=#state{out_streamid=Current, streams=Streams0}, StreamID, Commands) when Current =/= StreamID -> %% @todo We still want to handle some commands... Stream = #stream{queue=Queue} = lists:keyfind(StreamID, #stream.id, Streams0), Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream#stream{queue=Queue ++ Commands}), State#state{streams=Streams}; %% When we have finished reading the request body, do nothing. commands(State=#state{flow=infinity}, StreamID, [{flow, _}|Tail]) -> commands(State, StreamID, Tail); %% Read the request body. commands(State0=#state{flow=Flow0}, StreamID, [{flow, Size}|Tail]) -> %% We must read *at least* Size of data otherwise functions %% like cowboy_req:read_body/1,2 will wait indefinitely. Flow = if Flow0 < 0 -> Size; true -> Flow0 + Size end, %% Reenable active mode if necessary. State = if Flow0 =< 0, Flow > 0 -> active(State0); true -> State0 end, commands(State#state{flow=Flow}, StreamID, Tail); %% Error responses are sent only if a response wasn't sent already. commands(State=#state{out_state=wait, out_streamid=StreamID}, StreamID, [{error_response, Status, Headers0, Body}|Tail]) -> %% We close the connection when the error response is 408, as it %% indicates a timeout and the RFC recommends that we stop here. (RFC7231 6.5.7) Headers = case Status of 408 -> Headers0#{<<"connection">> => <<"close">>}; <<"408", _/bits>> -> Headers0#{<<"connection">> => <<"close">>}; _ -> Headers0 end, commands(State, StreamID, [{response, Status, Headers, Body}|Tail]); commands(State, StreamID, [{error_response, _, _, _}|Tail]) -> commands(State, StreamID, Tail); %% Send an informational response. commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams}, StreamID, [{inform, StatusCode, Headers}|Tail]) -> %% @todo I'm pretty sure the last stream in the list is the one we want %% considering all others are queued. #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams), _ = case Version of 'HTTP/1.1' -> ok = maybe_socket_error(State0, Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)))); %% Do not send informational responses to HTTP/1.0 clients. (RFC7231 6.2) 'HTTP/1.0' -> ok end, State = maybe_reset_idle_timeout(State0), commands(State, StreamID, Tail); %% Send a full response. %% %% @todo Kill the stream if it sent a response when one has already been sent. %% @todo Keep IsFin in the state. %% @todo Same two things above apply to DATA, possibly promise too. commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams}, StreamID, [{response, StatusCode, Headers0, Body}|Tail]) -> %% @todo I'm pretty sure the last stream in the list is the one we want %% considering all others are queued. #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams), {State1, Headers} = connection(State0, Headers0, StreamID, Version), State2 = State1#state{out_state=done}, %% @todo Ensure content-length is set. 204 must never have content-length set. Response = cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)), %% @todo 204 and 304 responses must not include a response body. (RFC7230 3.3.1, RFC7230 3.3.2) case Body of {sendfile, _, _, _} -> ok = maybe_socket_error(State2, Transport:send(Socket, Response)), sendfile(State2, Body); _ -> ok = maybe_socket_error(State2, Transport:send(Socket, [Response, Body])) end, State = maybe_reset_idle_timeout(State2), commands(State, StreamID, Tail); %% Send response headers and initiate chunked encoding or streaming. commands(State0=#state{socket=Socket, transport=Transport, opts=Opts, overriden_opts=Override, streams=Streams0, out_state=OutState}, StreamID, [{headers, StatusCode, Headers0}|Tail]) -> %% @todo Same as above (about the last stream in the list). Stream = #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams0), Status = cow_http:status_to_integer(StatusCode), ContentLength = maps:get(<<"content-length">>, Headers0, undefined), %% Chunked transfer-encoding can be disabled on a per-request basis. Chunked = case Override of #{chunked := Chunked0} -> Chunked0; _ -> maps:get(chunked, Opts, true) end, {State1, Headers1} = case {Status, ContentLength, Version} of {204, _, 'HTTP/1.1'} -> {State0#state{out_state=done}, Headers0}; {304, _, 'HTTP/1.1'} -> {State0#state{out_state=done}, Headers0}; {_, undefined, 'HTTP/1.1'} when Chunked -> {State0#state{out_state=chunked}, Headers0#{<<"transfer-encoding">> => <<"chunked">>}}; %% Close the connection after streaming without content-length %% to all HTTP/1.0 clients and to HTTP/1.1 clients when chunked is disabled. {_, undefined, _} -> {State0#state{out_state=streaming, last_streamid=StreamID}, Headers0}; %% Stream the response body without chunked transfer-encoding. _ -> ExpectedSize = cow_http_hd:parse_content_length(ContentLength), Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream#stream{local_expected_size=ExpectedSize}), {State0#state{out_state=streaming, streams=Streams}, Headers0} end, Headers2 = case stream_te(OutState, Stream) of trailers -> Headers1; _ -> maps:remove(<<"trailer">>, Headers1) end, {State2, Headers} = connection(State1, Headers2, StreamID, Version), ok = maybe_socket_error(State2, Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)))), State = maybe_reset_idle_timeout(State2), commands(State, StreamID, Tail); %% Send a response body chunk. %% @todo We need to kill the stream if it tries to send data before headers. commands(State0=#state{socket=Socket, transport=Transport, streams=Streams0, out_state=OutState}, StreamID, [{data, IsFin, Data}|Tail]) -> %% Do not send anything when the user asks to send an empty %% data frame, as that would break the protocol. Size = case Data of {sendfile, _, B, _} -> B; _ -> iolist_size(Data) end, %% Depending on the current state we may need to send nothing, %% the last chunk, chunked data with/without the last chunk, %% or just the data as-is. Stream = case lists:keyfind(StreamID, #stream.id, Streams0) of Stream0=#stream{method= <<"HEAD">>} -> Stream0; Stream0 when Size =:= 0, IsFin =:= fin, OutState =:= chunked -> ok = maybe_socket_error(State0, Transport:send(Socket, <<"0\r\n\r\n">>)), Stream0; Stream0 when Size =:= 0 -> Stream0; Stream0 when is_tuple(Data), OutState =:= chunked -> ok = maybe_socket_error(State0, Transport:send(Socket, [integer_to_binary(Size, 16), <<"\r\n">>])), sendfile(State0, Data), ok = maybe_socket_error(State0, Transport:send(Socket, case IsFin of fin -> <<"\r\n0\r\n\r\n">>; nofin -> <<"\r\n">> end) ), Stream0; Stream0 when OutState =:= chunked -> ok = maybe_socket_error(State0, Transport:send(Socket, [ integer_to_binary(Size, 16), <<"\r\n">>, Data, case IsFin of fin -> <<"\r\n0\r\n\r\n">>; nofin -> <<"\r\n">> end ]) ), Stream0; Stream0 when OutState =:= streaming -> #stream{local_sent_size=SentSize0, local_expected_size=ExpectedSize} = Stream0, SentSize = SentSize0 + Size, if %% ExpectedSize may be undefined, which is > any integer value. SentSize > ExpectedSize -> terminate(State0, response_body_too_large); is_tuple(Data) -> sendfile(State0, Data); true -> ok = maybe_socket_error(State0, Transport:send(Socket, Data)) end, Stream0#stream{local_sent_size=SentSize} end, State1 = case IsFin of fin -> State0#state{out_state=done}; nofin -> State0 end, State = maybe_reset_idle_timeout(State1), Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream), commands(State#state{streams=Streams}, StreamID, Tail); commands(State0=#state{socket=Socket, transport=Transport, streams=Streams, out_state=OutState}, StreamID, [{trailers, Trailers}|Tail]) -> case stream_te(OutState, lists:keyfind(StreamID, #stream.id, Streams)) of trailers -> ok = maybe_socket_error(State0, Transport:send(Socket, [ <<"0\r\n">>, cow_http:headers(maps:to_list(Trailers)), <<"\r\n">> ]) ); no_trailers -> ok = maybe_socket_error(State0, Transport:send(Socket, <<"0\r\n\r\n">>)); not_chunked -> ok end, State = maybe_reset_idle_timeout(State0#state{out_state=done}), commands(State, StreamID, Tail); %% Protocol takeover. commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transport, out_state=OutState, buffer=Buffer, children=Children}, StreamID, [{switch_protocol, Headers, Protocol, InitialState}|_Tail]) -> %% @todo If there's streams opened after this one, fail instead of 101. State1 = cancel_timeout(State0), %% Before we send the 101 response we need to stop receiving data %% from the socket, otherwise the data might be receive before the %% call to flush/0 and we end up inadvertently dropping a packet. %% %% @todo Handle cases where the request came with a body. We need %% to process or skip the body before the upgrade can be completed. State = passive(State1), %% Send a 101 response if necessary, then terminate the stream. #state{streams=Streams} = case OutState of wait -> info(State, StreamID, {inform, 101, Headers}); _ -> State end, #stream{state=StreamState} = lists:keyfind(StreamID, #stream.id, Streams), stream_call_terminate(StreamID, switch_protocol, StreamState, State), %% Terminate children processes and flush any remaining messages from the mailbox. cowboy_children:terminate(Children), flush(Parent), %% Turn off the trap_exit process flag %% since this process will no longer be a supervisor. process_flag(trap_exit, false), Protocol:takeover(Parent, Ref, Socket, Transport, opts_for_upgrade(State), Buffer, InitialState); %% Set options dynamically. commands(State0, StreamID, [{set_options, SetOpts}|Tail]) -> State = maps:fold(fun (chunked, Chunked, StateF=#state{overriden_opts=Opts}) -> StateF#state{overriden_opts=Opts#{chunked => Chunked}}; (idle_timeout, IdleTimeout, StateF=#state{overriden_opts=Opts}) -> set_timeout(StateF#state{overriden_opts=Opts#{idle_timeout => IdleTimeout}}, idle_timeout); (_, _, StateF) -> StateF end, State0, SetOpts), commands(State, StreamID, Tail); %% Stream shutdown. commands(State, StreamID, [stop|Tail]) -> %% @todo Do we want to run the commands after a stop? %% @todo We currently wait for the stop command before we %% continue with the next request/response. In theory, if %% the request body was read fully and the response body %% was sent fully we should be able to start working on %% the next request concurrently. This can be done as a %% future optimization. maybe_terminate(State, StreamID, Tail); %% Log event. commands(State=#state{opts=Opts}, StreamID, [Log={log, _, _, _}|Tail]) -> cowboy:log(Log, Opts), commands(State, StreamID, Tail); %% HTTP/1.1 does not support push; ignore. commands(State, StreamID, [{push, _, _, _, _, _, _, _}|Tail]) -> commands(State, StreamID, Tail). %% The set-cookie header is special; we can only send one cookie per header. headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) -> Headers1 = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)), Headers1 ++ [{<<"set-cookie">>, Value} || Value <- SetCookies]; headers_to_list(Headers) -> maps:to_list(Headers). %% We wrap the sendfile call into a try/catch because on OTP-20 %% and earlier a few different crashes could occur for sockets %% that were closing or closed. For example a badarg in %% erlang:port_get_data(#Port<...>) or a badmatch like %% {{badmatch,{error,einval}},[{prim_file,sendfile,8,[]}... %% %% OTP-21 uses a NIF instead of a port so the implementation %% and behavior has dramatically changed and it is unclear %% whether it will be necessary in the future. %% %% This try/catch prevents some noisy logs to be written %% when these errors occur. sendfile(State=#state{socket=Socket, transport=Transport, opts=Opts}, {sendfile, Offset, Bytes, Path}) -> try %% When sendfile is disabled we explicitly use the fallback. {ok, _} = maybe_socket_error(State, case maps:get(sendfile, Opts, true) of true -> Transport:sendfile(Socket, Path, Offset, Bytes); false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, []) end ), ok catch _:_ -> terminate(State, {socket_error, sendfile_crash, 'An error occurred when using the sendfile function.'}) end. %% Flush messages specific to cowboy_http before handing over the %% connection to another protocol. flush(Parent) -> receive {timeout, _, _} -> flush(Parent); {{Pid, _}, _} when Pid =:= self() -> flush(Parent); {'EXIT', Pid, _} when Pid =/= Parent -> flush(Parent) after 0 -> ok end. %% @todo In these cases I'm not sure if we should continue processing commands. maybe_terminate(State=#state{last_streamid=StreamID}, StreamID, _Tail) -> terminate(stream_terminate(State, StreamID, normal), normal); %% @todo Reason ok? maybe_terminate(State, StreamID, _Tail) -> stream_terminate(State, StreamID, normal). stream_terminate(State0=#state{opts=Opts, in_streamid=InStreamID, in_state=InState, out_streamid=OutStreamID, out_state=OutState, streams=Streams0, children=Children0}, StreamID, Reason) -> #stream{version=Version, local_expected_size=ExpectedSize, local_sent_size=SentSize} = lists:keyfind(StreamID, #stream.id, Streams0), %% Send a response or terminate chunks depending on the current output state. State1 = #state{streams=Streams1} = case OutState of wait when element(1, Reason) =:= internal_error -> info(State0, StreamID, {response, 500, #{<<"content-length">> => <<"0">>}, <<>>}); wait when element(1, Reason) =:= connection_error -> info(State0, StreamID, {response, 400, #{<<"content-length">> => <<"0">>}, <<>>}); wait -> info(State0, StreamID, {response, 204, #{}, <<>>}); chunked when Version =:= 'HTTP/1.1' -> info(State0, StreamID, {data, fin, <<>>}); streaming when SentSize < ExpectedSize -> terminate(State0, response_body_too_small); _ -> %% done or Version =:= 'HTTP/1.0' State0 end, %% Stop the stream, shutdown children and reset overriden options. {value, #stream{state=StreamState}, Streams} = lists:keytake(StreamID, #stream.id, Streams1), stream_call_terminate(StreamID, Reason, StreamState, State1), Children = cowboy_children:shutdown(Children0, StreamID), State = State1#state{overriden_opts=#{}, streams=Streams, children=Children}, %% We want to drop the connection if the body was not read fully %% and we don't know its length or more remains to be read than %% configuration allows. MaxSkipBodyLength = maps:get(max_skip_body_length, Opts, 1000000), case InState of #ps_body{length=undefined} when InStreamID =:= OutStreamID -> terminate(State, skip_body_unknown_length); #ps_body{length=Len, received=Received} when InStreamID =:= OutStreamID, Received + MaxSkipBodyLength < Len -> terminate(State, skip_body_too_large); #ps_body{} when InStreamID =:= OutStreamID -> stream_next(State#state{flow=infinity}); _ -> stream_next(State) end. stream_next(State0=#state{opts=Opts, active=Active, out_streamid=OutStreamID, streams=Streams}) -> %% Enable active mode again if it was disabled. State1 = case Active of true -> State0; false -> active(State0) end, NextOutStreamID = OutStreamID + 1, case lists:keyfind(NextOutStreamID, #stream.id, Streams) of false -> State = State1#state{out_streamid=NextOutStreamID, out_state=wait}, %% There are no streams remaining. We therefore can %% and want to switch back to the request_timeout. set_timeout(State, request_timeout); #stream{queue=Commands} -> %% @todo Remove queue from the stream. %% We set the flow to the initial flow size even though %% we might have sent some data through already due to pipelining. Flow = maps:get(initial_stream_flow_size, Opts, 65535), commands(State1#state{flow=Flow, out_streamid=NextOutStreamID, out_state=wait}, NextOutStreamID, Commands) end. stream_call_terminate(StreamID, Reason, StreamState, #state{opts=Opts}) -> try cowboy_stream:terminate(StreamID, Reason, StreamState) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(terminate, [StreamID, Reason, StreamState], Class, Exception, Stacktrace), Opts) end. maybe_req_close(#state{opts=#{http10_keepalive := false}}, _, 'HTTP/1.0') -> close; maybe_req_close(_, #{<<"connection">> := Conn}, 'HTTP/1.0') -> try cow_http_hd:parse_connection(Conn) of Conns -> case lists:member(<<"keep-alive">>, Conns) of true -> keepalive; false -> close end catch _:_ -> bad_connection_header end; maybe_req_close(_, _, 'HTTP/1.0') -> close; maybe_req_close(_, #{<<"connection">> := Conn}, 'HTTP/1.1') -> try connection_hd_is_close(Conn) of true -> close; false -> keepalive catch _:_ -> bad_connection_header end; maybe_req_close(_, _, _) -> keepalive. connection(State=#state{last_streamid=StreamID}, Headers=#{<<"connection">> := Conn}, StreamID, _) -> case connection_hd_is_close(Conn) of true -> {State, Headers}; %% @todo Here we need to remove keep-alive and add close, not just add close. false -> {State, Headers#{<<"connection">> => [<<"close, ">>, Conn]}} end; connection(State=#state{last_streamid=StreamID}, Headers, StreamID, _) -> {State, Headers#{<<"connection">> => <<"close">>}}; connection(State, Headers=#{<<"connection">> := Conn}, StreamID, _) -> case connection_hd_is_close(Conn) of true -> {State#state{last_streamid=StreamID}, Headers}; %% @todo Here we need to set keep-alive only if it wasn't set before. false -> {State, Headers} end; connection(State, Headers, _, 'HTTP/1.0') -> {State, Headers#{<<"connection">> => <<"keep-alive">>}}; connection(State, Headers, _, _) -> {State, Headers}. connection_hd_is_close(Conn) -> Conns = cow_http_hd:parse_connection(iolist_to_binary(Conn)), lists:member(<<"close">>, Conns). stream_te(streaming, _) -> not_chunked; %% No TE header was sent. stream_te(_, #stream{te=undefined}) -> no_trailers; stream_te(_, #stream{te=TE0}) -> try cow_http_hd:parse_te(TE0) of {TE1, _} -> TE1 catch _:_ -> %% If we can't parse the TE header, assume we can't send trailers. no_trailers end. %% This function is only called when an error occurs on a new stream. -spec error_terminate(cowboy:http_status(), #state{}, _) -> no_return(). error_terminate(StatusCode, State=#state{ref=Ref, peer=Peer, in_state=StreamState}, Reason) -> PartialReq = case StreamState of #ps_request_line{} -> #{ ref => Ref, peer => Peer }; #ps_header{method=Method, path=Path, qs=Qs, version=Version, headers=ReqHeaders} -> #{ ref => Ref, peer => Peer, method => Method, path => Path, qs => Qs, version => Version, headers => case ReqHeaders of undefined -> #{}; _ -> ReqHeaders end } end, early_error(StatusCode, State, Reason, PartialReq, #{<<"connection">> => <<"close">>}), terminate(State, Reason). early_error(StatusCode, State, Reason, PartialReq) -> early_error(StatusCode, State, Reason, PartialReq, #{}). early_error(StatusCode0, State=#state{socket=Socket, transport=Transport, opts=Opts, in_streamid=StreamID}, Reason, PartialReq, RespHeaders0) -> RespHeaders1 = RespHeaders0#{<<"content-length">> => <<"0">>}, Resp = {response, StatusCode0, RespHeaders1, <<>>}, try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of {response, StatusCode, RespHeaders, RespBody} -> ok = maybe_socket_error(State, Transport:send(Socket, [ cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(RespHeaders)), %% @todo We shouldn't send the body when the method is HEAD. %% @todo Technically we allow the sendfile tuple. RespBody ]) ) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(early_error, [StreamID, Reason, PartialReq, Resp, Opts], Class, Exception, Stacktrace), Opts), %% We still need to send an error response, so send what we initially %% wanted to send. It's better than nothing. ok = maybe_socket_error(State, Transport:send(Socket, cow_http:response(StatusCode0, 'HTTP/1.1', maps:to_list(RespHeaders1))) ) end. initiate_closing(State=#state{streams=[]}, Reason) -> terminate(State, Reason); initiate_closing(State=#state{streams=Streams, out_streamid=OutStreamID}, Reason) -> {value, LastStream, TerminatedStreams} = lists:keytake(OutStreamID, #stream.id, Streams), terminate_all_streams(State, TerminatedStreams, Reason), State#state{streams=[LastStream], last_streamid=OutStreamID}. %% Function replicated in cowboy_http2. maybe_socket_error(State, {error, closed}) -> terminate(State, {socket_error, closed, 'The socket has been closed.'}); maybe_socket_error(State, Reason) -> maybe_socket_error(State, Reason, 'An error has occurred on the socket.'). maybe_socket_error(_, Result = ok, _) -> Result; maybe_socket_error(_, Result = {ok, _}, _) -> Result; maybe_socket_error(State, {error, Reason}, Human) -> terminate(State, {socket_error, Reason, Human}). -spec terminate(#state{} | undefined, _) -> no_return(). terminate(undefined, Reason) -> exit({shutdown, Reason}); terminate(State=#state{streams=Streams, children=Children}, Reason) -> terminate_all_streams(State, Streams, Reason), cowboy_children:terminate(Children), terminate_linger(State), exit({shutdown, Reason}). terminate_all_streams(_, [], _) -> ok; terminate_all_streams(State, [#stream{id=StreamID, state=StreamState}|Tail], Reason) -> stream_call_terminate(StreamID, Reason, StreamState, State), terminate_all_streams(State, Tail, Reason). terminate_linger(State=#state{socket=Socket, transport=Transport, opts=Opts}) -> case Transport:shutdown(Socket, write) of ok -> case maps:get(linger_timeout, Opts, 1000) of 0 -> ok; infinity -> terminate_linger_before_loop(State, undefined, Transport:messages()); Timeout -> TimerRef = erlang:start_timer(Timeout, self(), linger_timeout), terminate_linger_before_loop(State, TimerRef, Transport:messages()) end; {error, _} -> ok end. terminate_linger_before_loop(State, TimerRef, Messages) -> %% We may already be in active mode when we do this %% but it's OK because we are shutting down anyway. %% %% We specially handle the socket error to terminate %% when an error occurs. case setopts_active(State) of ok -> terminate_linger_loop(State, TimerRef, Messages); {error, _} -> ok end. terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) -> receive {OK, Socket, _} when OK =:= element(1, Messages) -> terminate_linger_loop(State, TimerRef, Messages); {Closed, Socket} when Closed =:= element(2, Messages) -> ok; {Error, Socket, _} when Error =:= element(3, Messages) -> ok; {Passive, Socket} when Passive =:= tcp_passive; Passive =:= ssl_passive -> terminate_linger_before_loop(State, TimerRef, Messages); {timeout, TimerRef, linger_timeout} -> ok; _ -> terminate_linger_loop(State, TimerRef, Messages) end. %% System callbacks. -spec system_continue(_, _, #state{}) -> ok. system_continue(_, _, State) -> before_loop(State). -spec system_terminate(any(), _, _, #state{}) -> no_return(). system_terminate(Reason0, _, _, State) -> Reason = {stop, {exit, Reason0}, 'sys:terminate/2,3 was called.'}, before_loop(initiate_closing(State, Reason)). -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}. system_code_change(Misc, _, _, _) -> {ok, Misc}. ================================================ FILE: src/cowboy_http2.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_http2). -export([init/6]). -export([init/10]). -export([init/12]). -export([loop/2]). -export([system_continue/3]). -export([system_terminate/4]). -export([system_code_change/4]). -type opts() :: #{ active_n => pos_integer(), alpn_default_protocol => http | http2, compress_buffering => boolean(), compress_threshold => non_neg_integer(), connection_type => worker | supervisor, connection_window_margin_size => 0..16#7fffffff, connection_window_update_threshold => 0..16#7fffffff, dynamic_buffer => false | {pos_integer(), pos_integer()}, dynamic_buffer_initial_average => non_neg_integer(), dynamic_buffer_initial_size => pos_integer(), enable_connect_protocol => boolean(), env => cowboy_middleware:env(), goaway_initial_timeout => timeout(), goaway_complete_timeout => timeout(), hibernate => boolean(), idle_timeout => timeout(), inactivity_timeout => timeout(), initial_connection_window_size => 65535..16#7fffffff, initial_stream_window_size => 0..16#7fffffff, linger_timeout => timeout(), logger => module(), max_concurrent_streams => non_neg_integer() | infinity, max_connection_buffer_size => non_neg_integer(), max_connection_window_size => 0..16#7fffffff, max_decode_table_size => non_neg_integer(), max_encode_table_size => non_neg_integer(), max_fragmented_header_block_size => 16384..16#7fffffff, max_frame_size_received => 16384..16777215, max_frame_size_sent => 16384..16777215 | infinity, max_received_frame_rate => {pos_integer(), timeout()}, max_reset_stream_rate => {pos_integer(), timeout()}, max_cancel_stream_rate => {pos_integer(), timeout()}, max_stream_buffer_size => non_neg_integer(), max_stream_window_size => 0..16#7fffffff, metrics_callback => cowboy_metrics_h:metrics_callback(), metrics_req_filter => fun((cowboy_req:req()) -> map()), metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()), middlewares => [module()], preface_timeout => timeout(), protocols => [http | http2], proxy_header => boolean(), reset_idle_timeout_on_send => boolean(), sendfile => boolean(), settings_timeout => timeout(), shutdown_timeout => timeout(), stream_handlers => [module()], stream_window_data_threshold => 0..16#7fffffff, stream_window_margin_size => 0..16#7fffffff, stream_window_update_threshold => 0..16#7fffffff, tracer_callback => cowboy_tracer_h:tracer_callback(), tracer_flags => [atom()], tracer_match_specs => cowboy_tracer_h:tracer_match_specs(), %% Open ended because configured stream handlers might add options. _ => _ }. -export_type([opts/0]). -record(stream, { %% Whether the stream is currently in a special state. %% %% - The running state is the normal state of a stream. %% - The relaying state is used by extended CONNECT protocols to %% use a 'relay' data_delivery method. %% - The stopping state indicates the stream used the 'stop' command. status = running :: running | {relaying, non_neg_integer(), pid()} | stopping, %% Flow requested for this stream. flow = 0 :: non_neg_integer(), %% Stream state. state :: {module, any()} }). %% We don't want to reset the idle timeout too often, %% so we don't reset it on data. Instead we reset the %% number of ticks we have observed. We divide the %% timeout value by a value and that value becomes %% the number of ticks at which point we can drop %% the connection. This value is the number of ticks. -define(IDLE_TIMEOUT_TICKS, 10). -record(state, { parent = undefined :: pid(), ref :: ranch:ref(), socket = undefined :: inet:socket(), transport :: module(), proxy_header :: undefined | ranch_proxy_header:proxy_info(), opts = #{} :: opts(), %% Timer for idle_timeout; also used for goaway timers. timer = undefined :: undefined | reference(), idle_timeout_num = 0 :: 0..?IDLE_TIMEOUT_TICKS, %% Remote address and port for the connection. peer = undefined :: {inet:ip_address(), inet:port_number()}, %% Local address and port for the connection. sock = undefined :: {inet:ip_address(), inet:port_number()}, %% Client certificate (TLS only). cert :: undefined | binary(), %% HTTP/2 state machine. http2_status :: sequence | settings | upgrade | connected | closing_initiated | closing, http2_machine :: cow_http2_machine:http2_machine(), %% HTTP/2 frame rate flood protection. frame_rate_num :: undefined | pos_integer(), frame_rate_time :: undefined | integer(), %% HTTP/2 reset stream flood protection. reset_rate_num :: undefined | pos_integer(), reset_rate_time :: undefined | integer(), %% HTTP/2 rapid reset attack protection. cancel_rate_num :: undefined | pos_integer(), cancel_rate_time :: undefined | integer(), %% Flow requested for all streams. flow = 0 :: non_neg_integer(), %% Dynamic buffer moving average and current buffer size. dynamic_buffer_size :: pos_integer() | false, dynamic_buffer_moving_average :: float(), %% Currently active HTTP/2 streams. Streams may be initiated either %% by the client or by the server through PUSH_PROMISE frames. streams = #{} :: #{cow_http2:streamid() => #stream{}}, %% Streams can spawn zero or more children which are then managed %% by this module if operating as a supervisor. children = cowboy_children:init() :: cowboy_children:children() }). -spec init(pid(), ranch:ref(), inet:socket(), module(), ranch_proxy_header:proxy_info() | undefined, cowboy:opts()) -> no_return(). init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) -> {ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket), 'A socket error occurred when retrieving the peer name.'), {ok, Sock} = maybe_socket_error(undefined, Transport:sockname(Socket), 'A socket error occurred when retrieving the sock name.'), CertResult = case Transport:name() of ssl -> case ssl:peercert(Socket) of {error, no_peercert} -> {ok, undefined}; Cert0 -> Cert0 end; _ -> {ok, undefined} end, {ok, Cert} = maybe_socket_error(undefined, CertResult, 'A socket error occurred when retrieving the client TLS certificate.'), init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, <<>>). -spec init(pid(), ranch:ref(), inet:socket(), module(), ranch_proxy_header:proxy_info() | undefined, cowboy:opts(), {inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()}, binary() | undefined, binary()) -> no_return(). init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) -> DynamicBuffer = init_dynamic_buffer_size(Opts), {ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts), %% Send the preface before doing all the init in case we get a socket error. ok = maybe_socket_error(undefined, Transport:send(Socket, Preface)), State = set_idle_timeout(init_rate_limiting(#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, proxy_header=ProxyHeader, opts=Opts, peer=Peer, sock=Sock, cert=Cert, dynamic_buffer_size=DynamicBuffer, dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0.0), http2_status=sequence, http2_machine=HTTP2Machine}), 0), safe_setopts_active(State), case Buffer of <<>> -> before_loop(State, Buffer); _ -> parse(State, Buffer) end. init_rate_limiting(State0) -> CurrentTime = erlang:monotonic_time(millisecond), State1 = init_frame_rate_limiting(State0, CurrentTime), State2 = init_reset_rate_limiting(State1, CurrentTime), init_cancel_rate_limiting(State2, CurrentTime). init_frame_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> {FrameRateNum, FrameRatePeriod} = maps:get(max_received_frame_rate, Opts, {10000, 10000}), State#state{ frame_rate_num=FrameRateNum, frame_rate_time=add_period(CurrentTime, FrameRatePeriod) }. init_reset_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> {ResetRateNum, ResetRatePeriod} = maps:get(max_reset_stream_rate, Opts, {10, 10000}), State#state{ reset_rate_num=ResetRateNum, reset_rate_time=add_period(CurrentTime, ResetRatePeriod) }. init_cancel_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> {CancelRateNum, CancelRatePeriod} = maps:get(max_cancel_stream_rate, Opts, {500, 10000}), State#state{ cancel_rate_num=CancelRateNum, cancel_rate_time=add_period(CurrentTime, CancelRatePeriod) }. add_period(_, infinity) -> infinity; add_period(Time, Period) -> Time + Period. %% @todo Add an argument for the request body. -spec init(pid(), ranch:ref(), inet:socket(), module(), ranch_proxy_header:proxy_info() | undefined, cowboy:opts(), {inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()}, binary() | undefined, binary(), map() | undefined, cowboy_req:req()) -> no_return(). init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer, _Settings, Req=#{method := Method}) -> DynamicBuffer = init_dynamic_buffer_size(Opts), {ok, Preface, HTTP2Machine0} = cow_http2_machine:init(server, Opts), {ok, StreamID, HTTP2Machine} = cow_http2_machine:init_upgrade_stream(Method, HTTP2Machine0), State0 = #state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, proxy_header=ProxyHeader, opts=Opts, peer=Peer, sock=Sock, cert=Cert, dynamic_buffer_size=DynamicBuffer, dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0.0), http2_status=upgrade, http2_machine=HTTP2Machine}, State1 = headers_frame(State0#state{ http2_machine=HTTP2Machine}, StreamID, Req), %% We assume that the upgrade will be applied. A stream handler %% must not prevent the normal operations of the server. State2 = info(State1, 1, {switch_protocol, #{ <<"connection">> => <<"Upgrade">>, <<"upgrade">> => <<"h2c">> }, ?MODULE, undefined}), %% @todo undefined or #{}? State = set_idle_timeout(init_rate_limiting(State2#state{http2_status=sequence}), 0), %% In the case of HTTP/1.1 Upgrade we cannot send the Preface %% until we send the 101 response. ok = maybe_socket_error(State, Transport:send(Socket, Preface)), safe_setopts_active(State), case Buffer of <<>> -> before_loop(State, Buffer); _ -> parse(State, Buffer) end. -include("cowboy_dynamic_buffer.hrl"). %% Because HTTP/2 has flow control and Cowboy has other rate limiting %% mechanisms implemented, a very large active_n value should be fine, %% as long as the stream handlers do their work in a timely manner. %% However large active_n values reduce the impact of dynamic_buffer. setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) -> N = maps:get(active_n, Opts, 1), Transport:setopts(Socket, [{active, N}]). safe_setopts_active(State) -> ok = maybe_socket_error(State, setopts_active(State)). before_loop(State=#state{opts=#{hibernate := true}}, Buffer) -> proc_lib:hibernate(?MODULE, loop, [State, Buffer]); before_loop(State, Buffer) -> loop(State, Buffer). -spec loop(#state{}, binary()) -> no_return(). loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts, timer=TimerRef, children=Children}, Buffer) -> Messages = Transport:messages(), InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000), receive %% Socket messages. {OK, Socket, Data} when OK =:= element(1, Messages) -> State1 = maybe_resize_buffer(State, Data), parse(State1#state{idle_timeout_num=0}, << Buffer/binary, Data/binary >>); {Closed, Socket} when Closed =:= element(2, Messages) -> Reason = case State#state.http2_status of closing -> {stop, closed, 'The client is going away.'}; _ -> {socket_error, closed, 'The socket has been closed.'} end, terminate(State, Reason); {Error, Socket, Reason} when Error =:= element(3, Messages) -> terminate(State, {socket_error, Reason, 'An error has occurred on the socket.'}); {Passive, Socket} when Passive =:= element(4, Messages); %% Hardcoded for compatibility with Ranch 1.x. Passive =:= tcp_passive; Passive =:= ssl_passive -> safe_setopts_active(State), before_loop(State, Buffer); %% System messages. {'EXIT', Parent, shutdown} -> Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'}, before_loop(initiate_closing(State, Reason), Buffer); {'EXIT', Parent, Reason} -> terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'}); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, Buffer}); %% Timeouts. {timeout, TimerRef, idle_timeout} -> tick_idle_timeout(State, Buffer); {timeout, Ref, {shutdown, Pid}} -> cowboy_children:shutdown_timeout(Children, Ref, Pid), before_loop(State, Buffer); {timeout, TRef, {cow_http2_machine, Name}} -> before_loop(timeout(State, Name, TRef), Buffer); {timeout, TimerRef, {goaway_initial_timeout, Reason}} -> before_loop(closing(State, Reason), Buffer); {timeout, TimerRef, {goaway_complete_timeout, Reason}} -> terminate(State, {stop, stop_reason(Reason), 'Graceful shutdown timed out.'}); %% Messages pertaining to a stream. {{Pid, StreamID}, Msg} when Pid =:= self() -> before_loop(info(State, StreamID, Msg), Buffer); {'$cowboy_relay_command', {Pid, StreamID}, RelayCommand} when Pid =:= self() -> before_loop(relay_command(State, StreamID, RelayCommand), Buffer); %% Exit signal from children. Msg = {'EXIT', Pid, _} -> before_loop(down(State, Pid, Msg), Buffer); %% Calls from supervisor module. {'$gen_call', From, Call} -> cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE), before_loop(State, Buffer); Msg -> cowboy:log(warning, "Received stray message ~p.", [Msg], Opts), before_loop(State, Buffer) after InactivityTimeout -> terminate(State, {internal_error, timeout, 'No message or data received before timeout.'}) end. tick_idle_timeout(State=#state{idle_timeout_num=?IDLE_TIMEOUT_TICKS}, _) -> terminate(State, {stop, timeout, 'Connection idle longer than configuration allows.'}); tick_idle_timeout(State=#state{idle_timeout_num=TimeoutNum}, Buffer) -> before_loop(set_idle_timeout(State, TimeoutNum + 1), Buffer). set_idle_timeout(State=#state{http2_status=Status, timer=TimerRef}, _) when Status =:= closing_initiated orelse Status =:= closing, TimerRef =/= undefined -> State; set_idle_timeout(State=#state{opts=Opts}, TimeoutNum) -> case maps:get(idle_timeout, Opts, 60000) of infinity -> State#state{timer=undefined}; Timeout -> set_timeout(State#state{idle_timeout_num=TimeoutNum}, Timeout div ?IDLE_TIMEOUT_TICKS, idle_timeout) end. set_timeout(State=#state{timer=TimerRef0}, Timeout, Message) -> ok = case TimerRef0 of undefined -> ok; _ -> erlang:cancel_timer(TimerRef0, [{async, true}, {info, false}]) end, TimerRef = case Timeout of infinity -> undefined; Timeout -> erlang:start_timer(Timeout, self(), Message) end, State#state{timer=TimerRef}. maybe_reset_idle_timeout(State=#state{opts=Opts}) -> case maps:get(reset_idle_timeout_on_send, Opts, false) of true -> State#state{idle_timeout_num=0}; false -> State end. %% HTTP/2 protocol parsing. parse(State=#state{http2_status=sequence}, Data) -> case cow_http2:parse_sequence(Data) of {ok, Rest} -> parse(State#state{http2_status=settings}, Rest); more -> before_loop(State, Data); Error = {connection_error, _, _} -> terminate(State, Error) end; parse(State=#state{http2_status=Status, http2_machine=HTTP2Machine, streams=Streams}, Data) -> MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), case cow_http2:parse(Data, MaxFrameSize) of {ok, Frame, Rest} -> parse(frame_rate(State, Frame), Rest); {ignore, Rest} -> parse(frame_rate(State, ignore), Rest); {stream_error, StreamID, Reason, Human, Rest} -> parse(reset_stream(State, StreamID, {stream_error, Reason, Human}), Rest); Error = {connection_error, _, _} -> terminate(State, Error); %% Terminate the connection if we are closing and all streams have completed. more when Status =:= closing, Streams =:= #{} -> terminate(State, {stop, normal, 'The connection is going away.'}); more -> before_loop(State, Data) end. %% Frame rate flood protection. frame_rate(State0=#state{frame_rate_num=Num0, frame_rate_time=Time}, Frame) -> {Result, State} = case Num0 - 1 of 0 -> CurrentTime = erlang:monotonic_time(millisecond), if CurrentTime < Time -> {error, State0}; true -> %% When the option has a period of infinity we cannot reach this clause. {ok, init_frame_rate_limiting(State0, CurrentTime)} end; Num -> {ok, State0#state{frame_rate_num=Num}} end, case {Result, Frame} of {ok, ignore} -> ignored_frame(State); {ok, _} -> frame(State, Frame); {error, _} -> terminate(State, {connection_error, enhance_your_calm, 'Frame rate larger than configuration allows. Flood? (CVE-2019-9512, CVE-2019-9515, CVE-2019-9518)'}) end. %% Frames received. %% We do nothing when receiving a lingering DATA frame. %% We already removed the stream flow from the connection %% flow and are therefore already accounting for the window %% being reduced by these frames. frame(State=#state{http2_machine=HTTP2Machine0}, Frame) -> case cow_http2_machine:frame(Frame, HTTP2Machine0) of {ok, HTTP2Machine} -> maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame); {ok, {data, StreamID, IsFin, Data}, HTTP2Machine} -> data_frame(State#state{http2_machine=HTTP2Machine}, StreamID, IsFin, Data); {ok, {headers, StreamID, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP2Machine} -> headers_frame(State#state{http2_machine=HTTP2Machine}, StreamID, IsFin, Headers, PseudoHeaders, BodyLen); {ok, {trailers, _StreamID, _Trailers}, HTTP2Machine} -> %% @todo Propagate trailers. State#state{http2_machine=HTTP2Machine}; {ok, {rst_stream, StreamID, Reason}, HTTP2Machine} -> rst_stream_frame(State#state{http2_machine=HTTP2Machine}, StreamID, Reason); {ok, GoAway={goaway, _, _, _}, HTTP2Machine} -> goaway(State#state{http2_machine=HTTP2Machine}, GoAway); {send, SendData, HTTP2Machine} -> %% We may need to send an alarm for each of the streams sending data. State1 = lists:foldl( fun({StreamID, _, _}, S) -> maybe_send_data_alarm(S, HTTP2Machine0, StreamID) end, send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData, []), SendData), maybe_reset_idle_timeout(State1); {error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} -> reset_stream(State#state{http2_machine=HTTP2Machine}, StreamID, {stream_error, Reason, Human}); {error, Error={connection_error, _, _}, HTTP2Machine} -> terminate(State#state{http2_machine=HTTP2Machine}, Error) end. %% We use this opportunity to mark the HTTP/2 status as connected %% if we were still waiting for a SETTINGS frame. maybe_ack(State=#state{http2_status=settings}, Frame) -> maybe_ack(State#state{http2_status=connected}, Frame); %% We do not reset the idle timeout on send here because we are %% sending data as a consequence of receiving data, which means %% we already resetted the idle timeout. maybe_ack(State=#state{socket=Socket, transport=Transport}, Frame) -> case Frame of {settings, _} -> ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:settings_ack())); {ping, Opaque} -> ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:ping_ack(Opaque))); _ -> ok end, State. data_frame(State0=#state{opts=Opts, flow=Flow0, streams=Streams}, StreamID, IsFin, Data) -> case Streams of #{StreamID := Stream=#stream{status=running, flow=StreamFlow, state=StreamState0}} -> try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of {Commands, StreamState} -> %% Remove the amount of data received from the flow. %% We may receive more data than we requested. We ensure %% that the flow value doesn't go lower than 0. Size = byte_size(Data), Flow = max(0, Flow0 - Size), %% We would normally update the window when changing the flow %% value. But because we are running commands, which themselves %% may update the window, and we want to avoid updating the %% window twice in a row, we first run the commands and then %% only update the window a flow command was executed. We know %% that it was because the flow value changed in the state. State1 = State0#state{flow=Flow, streams=Streams#{StreamID => Stream#stream{ flow=max(0, StreamFlow - Size), state=StreamState}}}, State = commands(State1, StreamID, Commands), case State of %% No flow command was executed. We must update the window %% because we changed the flow value earlier. #state{flow=Flow} -> update_window(State, StreamID); %% Otherwise the window was updated already. _ -> State end catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(data, [StreamID, IsFin, Data, StreamState0], Class, Exception, Stacktrace), Opts), reset_stream(State0, StreamID, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:data/4.'}) end; %% Stream handlers are not used for the data when relaying. #{StreamID := #stream{status={relaying, _, RelayPid}}} -> RelayPid ! {'$cowboy_relay_data', {self(), StreamID}, IsFin, Data}, %% We keep a steady flow using the configured flow value. %% Because we do not change the 'flow' value the update_window/2 %% function will always maintain this value (of course with %% thresholds applying). update_window(State0, StreamID); %% We ignore DATA frames for streams that are stopping. #{} -> State0 end. headers_frame(State, StreamID, IsFin, Headers, PseudoHeaders=#{method := <<"CONNECT">>}, _) when map_size(PseudoHeaders) =:= 2 -> early_error(State, StreamID, IsFin, Headers, PseudoHeaders, 501, 'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'); headers_frame(State, StreamID, IsFin, Headers, PseudoHeaders=#{method := <<"TRACE">>}, _) -> early_error(State, StreamID, IsFin, Headers, PseudoHeaders, 501, 'The TRACE method is currently not implemented. (RFC7231 4.3.8)'); headers_frame(State, StreamID, IsFin, Headers, PseudoHeaders=#{authority := Authority}, BodyLen) -> headers_frame_parse_host(State, StreamID, IsFin, Headers, PseudoHeaders, BodyLen, Authority); headers_frame(State, StreamID, IsFin, Headers, PseudoHeaders, BodyLen) -> case lists:keyfind(<<"host">>, 1, Headers) of {_, Authority} -> headers_frame_parse_host(State, StreamID, IsFin, Headers, PseudoHeaders, BodyLen, Authority); _ -> reset_stream(State, StreamID, {stream_error, protocol_error, 'Requests translated from HTTP/1.1 must include a host header. (RFC7540 8.1.2.3, RFC7230 5.4)'}) end. headers_frame_parse_host(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert, proxy_header=ProxyHeader}, StreamID, IsFin, Headers, PseudoHeaders=#{method := Method, scheme := Scheme, path := PathWithQs}, BodyLen, Authority) -> try cow_http_hd:parse_host(Authority) of {Host, Port0} -> Port = ensure_port(Scheme, Port0), try cow_http:parse_fullpath(PathWithQs) of {<<>>, _} -> reset_stream(State, StreamID, {stream_error, protocol_error, 'The path component must not be empty. (RFC7540 8.1.2.3)'}); {Path, Qs} -> Req0 = #{ ref => Ref, pid => self(), streamid => StreamID, peer => Peer, sock => Sock, cert => Cert, method => Method, scheme => Scheme, host => Host, port => Port, path => Path, qs => Qs, version => 'HTTP/2', headers => headers_to_map(Headers, #{}), has_body => IsFin =:= nofin, body_length => BodyLen }, %% We add the PROXY header information if any. Req1 = case ProxyHeader of undefined -> Req0; _ -> Req0#{proxy_header => ProxyHeader} end, %% We add the protocol information for extended CONNECTs. Req = case PseudoHeaders of #{protocol := Protocol} -> Req1#{protocol => Protocol}; _ -> Req1 end, headers_frame(State, StreamID, Req) catch _:_ -> reset_stream(State, StreamID, {stream_error, protocol_error, 'The :path pseudo-header is invalid. (RFC7540 8.1.2.3)'}) end catch _:_ -> reset_stream(State, StreamID, {stream_error, protocol_error, 'The :authority pseudo-header is invalid. (RFC7540 8.1.2.3)'}) end. ensure_port(<<"http">>, undefined) -> 80; ensure_port(<<"https">>, undefined) -> 443; ensure_port(_, Port) -> Port. %% This function is necessary to properly handle duplicate headers %% and the special-case cookie header. headers_to_map([], Acc) -> Acc; headers_to_map([{Name, Value}|Tail], Acc0) -> Acc = case Acc0 of %% The cookie header does not use proper HTTP header lists. #{Name := Value0} when Name =:= <<"cookie">> -> Acc0#{Name => << Value0/binary, "; ", Value/binary >>}; #{Name := Value0} -> Acc0#{Name => << Value0/binary, ", ", Value/binary >>}; _ -> Acc0#{Name => Value} end, headers_to_map(Tail, Acc). headers_frame(State=#state{opts=Opts, streams=Streams}, StreamID, Req) -> try cowboy_stream:init(StreamID, Req, Opts) of {Commands, StreamState} -> commands(State#state{ streams=Streams#{StreamID => #stream{state=StreamState}}}, StreamID, Commands) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(init, [StreamID, Req, Opts], Class, Exception, Stacktrace), Opts), reset_stream(State, StreamID, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:init/3.'}) end. early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer}, StreamID, _IsFin, Headers, #{method := Method}, StatusCode0, HumanReadable) -> %% We automatically terminate the stream but it is not an error %% per se (at least not in the first implementation). Reason = {stream_error, no_error, HumanReadable}, %% The partial Req is minimal for now. We only have one case %% where it can be called (when a method is completely disabled). %% @todo Fill in the other elements. PartialReq = #{ ref => Ref, peer => Peer, method => Method, headers => headers_to_map(Headers, #{}) }, Resp = {response, StatusCode0, RespHeaders0=#{<<"content-length">> => <<"0">>}, <<>>}, try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of {response, StatusCode, RespHeaders, RespBody} -> send_response(State0, StreamID, StatusCode, RespHeaders, RespBody) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(early_error, [StreamID, Reason, PartialReq, Resp, Opts], Class, Exception, Stacktrace), Opts), %% We still need to send an error response, so send what we initially %% wanted to send. It's better than nothing. send_headers(State0, StreamID, fin, StatusCode0, RespHeaders0) end. rst_stream_frame(State=#state{streams=Streams0, children=Children0}, StreamID, Reason) -> case maps:take(StreamID, Streams0) of {#stream{state=StreamState}, Streams} -> terminate_stream_handler(State, StreamID, Reason, StreamState), Children = cowboy_children:shutdown(Children0, StreamID), cancel_rate_limit(State#state{streams=Streams, children=Children}); error -> State end. cancel_rate_limit(State0=#state{cancel_rate_num=Num0, cancel_rate_time=Time}) -> case Num0 - 1 of 0 -> CurrentTime = erlang:monotonic_time(millisecond), if CurrentTime < Time -> terminate(State0, {connection_error, enhance_your_calm, 'Stream cancel rate larger than configuration allows. Flood? (CVE-2023-44487)'}); true -> %% When the option has a period of infinity we cannot reach this clause. init_cancel_rate_limiting(State0, CurrentTime) end; Num -> State0#state{cancel_rate_num=Num} end. ignored_frame(State=#state{http2_machine=HTTP2Machine0}) -> case cow_http2_machine:ignored_frame(HTTP2Machine0) of {ok, HTTP2Machine} -> State#state{http2_machine=HTTP2Machine}; {error, Error={connection_error, _, _}, HTTP2Machine} -> terminate(State#state{http2_machine=HTTP2Machine}, Error) end. %% HTTP/2 timeouts. timeout(State=#state{http2_machine=HTTP2Machine0}, Name, TRef) -> case cow_http2_machine:timeout(Name, TRef, HTTP2Machine0) of {ok, HTTP2Machine} -> State#state{http2_machine=HTTP2Machine}; {error, Error={connection_error, _, _}, HTTP2Machine} -> terminate(State#state{http2_machine=HTTP2Machine}, Error) end. %% Erlang messages. down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) -> State = case cowboy_children:down(Children0, Pid) of %% The stream was terminated already. {ok, undefined, Children} -> State0#state{children=Children}; %% The stream is still running. {ok, StreamID, Children} -> info(State0#state{children=Children}, StreamID, Msg); %% The process was unknown. error -> cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n", [Msg, Pid], Opts), State0 end, if State#state.http2_status =:= closing, State#state.streams =:= #{} -> terminate(State, {stop, normal, 'The connection is going away.'}); true -> State end. info(State=#state{opts=Opts, http2_machine=HTTP2Machine, streams=Streams}, StreamID, Msg) -> case Streams of #{StreamID := Stream=#stream{state=StreamState0}} -> try cowboy_stream:info(StreamID, Msg, StreamState0) of {Commands, StreamState} -> commands(State#state{streams=Streams#{StreamID => Stream#stream{state=StreamState}}}, StreamID, Commands) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(info, [StreamID, Msg, StreamState0], Class, Exception, Stacktrace), Opts), reset_stream(State, StreamID, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:info/3.'}) end; _ -> case cow_http2_machine:is_lingering_stream(StreamID, HTTP2Machine) of true -> ok; false -> cowboy:log(warning, "Received message ~p for unknown stream ~p.", [Msg, StreamID], Opts) end, State end. %% Stream handler commands. %% %% @todo Kill the stream if it tries to send a response, headers, %% data or push promise when the stream is closed or half-closed. commands(State, _, []) -> State; %% Error responses are sent only if a response wasn't sent already. commands(State=#state{http2_machine=HTTP2Machine}, StreamID, [{error_response, StatusCode, Headers, Body}|Tail]) -> case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of {ok, idle, _} -> commands(State, StreamID, [{response, StatusCode, Headers, Body}|Tail]); _ -> commands(State, StreamID, Tail) end; %% Send an informational response. commands(State0, StreamID, [{inform, StatusCode, Headers}|Tail]) -> State1 = send_headers(State0, StreamID, idle, StatusCode, Headers), State = maybe_reset_idle_timeout(State1), commands(State, StreamID, Tail); %% Send response headers. commands(State0, StreamID, [{response, StatusCode, Headers, Body}|Tail]) -> State1 = send_response(State0, StreamID, StatusCode, Headers, Body), State = maybe_reset_idle_timeout(State1), commands(State, StreamID, Tail); %% Send response headers. commands(State0, StreamID, [{headers, StatusCode, Headers}|Tail]) -> State1 = send_headers(State0, StreamID, nofin, StatusCode, Headers), State = maybe_reset_idle_timeout(State1), commands(State, StreamID, Tail); %% Send a response body chunk. commands(State0, StreamID, [{data, IsFin, Data}|Tail]) -> State = case maybe_send_data(State0, StreamID, IsFin, Data, []) of {data_sent, State1} -> maybe_reset_idle_timeout(State1); {no_data_sent, State1} -> State1 end, commands(State, StreamID, Tail); %% Send trailers. commands(State0, StreamID, [{trailers, Trailers}|Tail]) -> State = case maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}, []) of {data_sent, State1} -> maybe_reset_idle_timeout(State1); {no_data_sent, State1} -> State1 end, commands(State, StreamID, Tail); %% Send a push promise. %% %% @todo Responses sent as a result of a push_promise request %% must not send push_promise frames themselves. %% %% @todo We should not send push_promise frames when we are %% in the closing http2_status. commands(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, StreamID, [{push, Method, Scheme, Host, Port, Path, Qs, Headers0}|Tail]) -> Authority = case {Scheme, Port} of {<<"http">>, 80} -> Host; {<<"https">>, 443} -> Host; _ -> iolist_to_binary([Host, $:, integer_to_binary(Port)]) end, PathWithQs = iolist_to_binary(case Qs of <<>> -> Path; _ -> [Path, $?, Qs] end), PseudoHeaders = #{ method => Method, scheme => Scheme, authority => Authority, path => PathWithQs }, %% We need to make sure the header value is binary before we can %% create the Req object, as it expects them to be flat. Headers = maps:to_list(maps:map(fun(_, V) -> iolist_to_binary(V) end, Headers0)), State = case cow_http2_machine:prepare_push_promise(StreamID, HTTP2Machine0, PseudoHeaders, Headers) of {ok, PromisedStreamID, HeaderBlock, HTTP2Machine} -> State1 = State0#state{http2_machine=HTTP2Machine}, ok = maybe_socket_error(State1, Transport:send(Socket, cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock))), State2 = maybe_reset_idle_timeout(State1), headers_frame(State2, PromisedStreamID, fin, Headers, PseudoHeaders, 0); {error, no_push} -> State0 end, commands(State, StreamID, Tail); %% Read the request body. commands(State0=#state{flow=Flow, streams=Streams}, StreamID, [{flow, Size}|Tail]) -> #{StreamID := Stream=#stream{flow=StreamFlow}} = Streams, State = update_window(State0#state{flow=Flow + Size, streams=Streams#{StreamID => Stream#stream{flow=StreamFlow + Size}}}, StreamID), commands(State, StreamID, Tail); %% Supervise a child process. commands(State=#state{children=Children}, StreamID, [{spawn, Pid, Shutdown}|Tail]) -> commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)}, StreamID, Tail); %% Error handling. commands(State, StreamID, [Error = {internal_error, _, _}|_Tail]) -> %% @todo Do we want to run the commands after an internal_error? %% @todo Do we even allow commands after? %% @todo Only reset when the stream still exists. reset_stream(State, StreamID, Error); %% Upgrade to HTTP/2. This is triggered by cowboy_http2 itself. %% %% We do not need to reset the idle timeout on send because it %% hasn't been set yet. This is called from init/12. commands(State=#state{socket=Socket, transport=Transport, http2_status=upgrade}, StreamID, [{switch_protocol, Headers, ?MODULE, _}|Tail]) -> %% @todo This 101 response needs to be passed through stream handlers. ok = maybe_socket_error(State, Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(Headers)))), commands(State, StreamID, Tail); %% Use a different protocol within the stream (CONNECT :protocol). %% @todo Make sure we error out when the feature is disabled. %% There are two data_delivery: stream_handlers and relay. %% The former just has the data go through stream handlers %% like normal requests. The latter relays data directly. %% %% @todo When relaying there might be some data that is %% in stream handlers and that need to be received, %% depending on whether the protocol sends data %% before processing the response. commands(State0=#state{flow=Flow, streams=Streams}, StreamID, [{switch_protocol, Headers, _Mod, ModState=#{data_delivery := relay}}|Tail]) -> State1 = info(State0, StreamID, {headers, 200, Headers}), #{StreamID := Stream} = Streams, #{data_delivery_pid := RelayPid} = ModState, %% WINDOW_UPDATE frames updating the window will be sent after %% the first DATA frame has been received. RelayFlow = maps:get(data_delivery_flow, ModState, 131072), State = State1#state{flow=Flow + RelayFlow, streams=Streams#{StreamID => Stream#stream{ status={relaying, RelayFlow, RelayPid}, flow=RelayFlow}}}, commands(State, StreamID, Tail); commands(State0, StreamID, [{switch_protocol, Headers, _Mod, _ModState}|Tail]) -> State = info(State0, StreamID, {headers, 200, Headers}), commands(State, StreamID, Tail); %% Set options dynamically. commands(State, StreamID, [{set_options, _Opts}|Tail]) -> commands(State, StreamID, Tail); commands(State, StreamID, [stop|_Tail]) -> %% @todo Do we want to run the commands after a stop? %% @todo Do we even allow commands after? stop_stream(State, StreamID); %% Log event. commands(State=#state{opts=Opts}, StreamID, [Log={log, _, _, _}|Tail]) -> cowboy:log(Log, Opts), commands(State, StreamID, Tail). %% Relay data delivery commands. relay_command(State, StreamID, DataCmd = {data, _, _}) -> commands(State, StreamID, [DataCmd]); %% When going active mode again we set the RelayFlow again %% and update the window if necessary. relay_command(State=#state{flow=Flow, streams=Streams}, StreamID, active) -> #{StreamID := Stream} = Streams, #stream{status={relaying, RelayFlow, _}} = Stream, update_window(State#state{flow=Flow + RelayFlow, streams=Streams#{StreamID => Stream#stream{flow=RelayFlow}}}, StreamID); %% When going passive mode we don't update the window %% since we have not incremented it. relay_command(State=#state{flow=Flow, streams=Streams}, StreamID, passive) -> #{StreamID := Stream} = Streams, #stream{flow=StreamFlow} = Stream, State#state{flow=Flow - StreamFlow, streams=Streams#{StreamID => Stream#stream{flow=0}}}. %% Tentatively update the window after the flow was updated. update_window(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0, flow=Flow, streams=Streams}, StreamID) -> {Data1, HTTP2Machine2} = case cow_http2_machine:ensure_window(Flow, HTTP2Machine0) of ok -> {<<>>, HTTP2Machine0}; {ok, Increment1, HTTP2Machine1} -> {cow_http2:window_update(Increment1), HTTP2Machine1} end, {Data2, HTTP2Machine} = case Streams of #{StreamID := #stream{flow=StreamFlow}} -> case cow_http2_machine:ensure_window(StreamID, StreamFlow, HTTP2Machine2) of ok -> {<<>>, HTTP2Machine2}; {ok, Increment2, HTTP2Machine3} -> {cow_http2:window_update(StreamID, Increment2), HTTP2Machine3} end; _ -> %% Don't update the stream's window if it stopped. {<<>>, HTTP2Machine2} end, State = State0#state{http2_machine=HTTP2Machine}, case {Data1, Data2} of {<<>>, <<>>} -> State; _ -> ok = maybe_socket_error(State, Transport:send(Socket, [Data1, Data2])), maybe_reset_idle_timeout(State) end. %% Send the response, trailers or data. send_response(State0=#state{http2_machine=HTTP2Machine0}, StreamID, StatusCode, Headers, Body) -> Size = case Body of {sendfile, _, Bytes, _} -> Bytes; _ -> iolist_size(Body) end, case Size of 0 -> State = send_headers(State0, StreamID, fin, StatusCode, Headers), maybe_terminate_stream(State, StreamID, fin); _ -> %% @todo Add a test for HEAD to make sure we don't send the body when %% returning {response...} from a stream handler (or {headers...} then {data...}). {ok, _IsFin, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, nofin, #{status => cow_http:status_to_integer(StatusCode)}, headers_to_list(Headers)), {_, State} = maybe_send_data(State0#state{http2_machine=HTTP2Machine}, StreamID, fin, Body, [cow_http2:headers(StreamID, nofin, HeaderBlock)]), State end. send_headers(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, StreamID, IsFin0, StatusCode, Headers) -> {ok, IsFin, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, IsFin0, #{status => cow_http:status_to_integer(StatusCode)}, headers_to_list(Headers)), State = State0#state{http2_machine=HTTP2Machine}, ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock))), State. %% The set-cookie header is special; we can only send one cookie per header. headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) -> Headers = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)), Headers ++ [{<<"set-cookie">>, Value} || Value <- SetCookies]; headers_to_list(Headers) -> maps:to_list(Headers). maybe_send_data(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0, Prefix) -> Data = case is_tuple(Data0) of false -> {data, Data0}; true -> Data0 end, case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of {ok, HTTP2Machine} -> State1 = State0#state{http2_machine=HTTP2Machine}, %% If we have prefix data (like a HEADERS frame) we need to send it %% even if we do not send any DATA frames. WasDataSent = case Prefix of [] -> no_data_sent; _ -> ok = maybe_socket_error(State1, Transport:send(Socket, Prefix)), data_sent end, State = maybe_send_data_alarm(State1, HTTP2Machine0, StreamID), {WasDataSent, State}; {send, SendData, HTTP2Machine} -> State = #state{http2_status=Status, streams=Streams} = send_data(State0#state{http2_machine=HTTP2Machine}, SendData, Prefix), %% Terminate the connection if we are closing and all streams have completed. if Status =:= closing, Streams =:= #{} -> terminate(State, {stop, normal, 'The connection is going away.'}); true -> {data_sent, maybe_send_data_alarm(State, HTTP2Machine0, StreamID)} end end. send_data(State0=#state{socket=Socket, transport=Transport, opts=Opts}, SendData, Prefix) -> {Acc, State} = prepare_data(State0, SendData, [], Prefix), _ = [case Data of {sendfile, Offset, Bytes, Path} -> %% When sendfile is disabled we explicitly use the fallback. {ok, _} = maybe_socket_error(State, case maps:get(sendfile, Opts, true) of true -> Transport:sendfile(Socket, Path, Offset, Bytes); false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, []) end ), ok; _ -> ok = maybe_socket_error(State, Transport:send(Socket, Data)) end || Data <- Acc], send_data_terminate(State, SendData). send_data_terminate(State, []) -> State; send_data_terminate(State0, [{StreamID, IsFin, _}|Tail]) -> State = maybe_terminate_stream(State0, StreamID, IsFin), send_data_terminate(State, Tail). prepare_data(State, [], Acc, []) -> {lists:reverse(Acc), State}; prepare_data(State, [], Acc, Buffer) -> {lists:reverse([lists:reverse(Buffer)|Acc]), State}; prepare_data(State0, [{StreamID, IsFin, SendData}|Tail], Acc0, Buffer0) -> {Acc, Buffer, State} = prepare_data(State0, StreamID, IsFin, SendData, Acc0, Buffer0), prepare_data(State, Tail, Acc, Buffer). prepare_data(State, _, _, [], Acc, Buffer) -> {Acc, Buffer, State}; prepare_data(State0, StreamID, IsFin, [FrameData|Tail], Acc, Buffer) -> FrameIsFin = case Tail of [] -> IsFin; _ -> nofin end, case prepare_data_frame(State0, StreamID, FrameIsFin, FrameData) of {{MoreData, Sendfile}, State} when is_tuple(Sendfile) -> case Buffer of [] -> prepare_data(State, StreamID, IsFin, Tail, [Sendfile, MoreData|Acc], []); _ -> prepare_data(State, StreamID, IsFin, Tail, [Sendfile, lists:reverse([MoreData|Buffer])|Acc], []) end; {MoreData, State} -> prepare_data(State, StreamID, IsFin, Tail, Acc, [MoreData|Buffer]) end. prepare_data_frame(State, StreamID, IsFin, {data, Data}) -> {cow_http2:data(StreamID, IsFin, Data), State}; prepare_data_frame(State, StreamID, IsFin, Sendfile={sendfile, _, Bytes, _}) -> {{cow_http2:data_header(StreamID, IsFin, Bytes), Sendfile}, State}; %% The stream is terminated in cow_http2_machine:prepare_trailers. prepare_data_frame(State=#state{http2_machine=HTTP2Machine0}, StreamID, nofin, {trailers, Trailers}) -> {ok, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers), {cow_http2:headers(StreamID, fin, HeaderBlock), State#state{http2_machine=HTTP2Machine}}. %% After we have sent or queued data we may need to set or clear an alarm. %% We do this by comparing the HTTP2Machine buffer state before/after for %% the relevant streams. maybe_send_data_alarm(State=#state{opts=Opts, http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID) -> ConnBufferSizeBefore = cow_http2_machine:get_connection_local_buffer_size(HTTP2Machine0), ConnBufferSizeAfter = cow_http2_machine:get_connection_local_buffer_size(HTTP2Machine), {ok, StreamBufferSizeBefore} = cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine0), %% When the stream ends up closed after it finished sending data, %% we do not want to trigger an alarm. We act as if the buffer %% size did not change. StreamBufferSizeAfter = case cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine) of {ok, BSA} -> BSA; {error, closed} -> StreamBufferSizeBefore end, MaxConnBufferSize = maps:get(max_connection_buffer_size, Opts, 16000000), MaxStreamBufferSize = maps:get(max_stream_buffer_size, Opts, 8000000), %% I do not want to document these internal events yet. I am not yet %% convinced it should be {alarm, Name, on|off} and not {internal_event, E} %% or something else entirely. Though alarms are probably right. if ConnBufferSizeBefore >= MaxConnBufferSize, ConnBufferSizeAfter < MaxConnBufferSize -> connection_alarm(State, connection_buffer_full, off); ConnBufferSizeBefore < MaxConnBufferSize, ConnBufferSizeAfter >= MaxConnBufferSize -> connection_alarm(State, connection_buffer_full, on); StreamBufferSizeBefore >= MaxStreamBufferSize, StreamBufferSizeAfter < MaxStreamBufferSize -> stream_alarm(State, StreamID, stream_buffer_full, off); StreamBufferSizeBefore < MaxStreamBufferSize, StreamBufferSizeAfter >= MaxStreamBufferSize -> stream_alarm(State, StreamID, stream_buffer_full, on); true -> State end. connection_alarm(State0=#state{streams=Streams}, Name, Value) -> lists:foldl(fun(StreamID, State) -> stream_alarm(State, StreamID, Name, Value) end, State0, maps:keys(Streams)). stream_alarm(State, StreamID, Name, Value) -> info(State, StreamID, {alarm, Name, Value}). %% Terminate a stream or the connection. %% We may have to cancel streams even if we receive multiple %% GOAWAY frames as the LastStreamID value may be lower than %% the one previously received. %% %% We do not reset the idle timeout on send here. We already %% disabled it if we initiated shutdown; and we already reset %% it if the client sent a GOAWAY frame. goaway(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0, http2_status=Status, streams=Streams0}, {goaway, LastStreamID, Reason, _}) when Status =:= connected; Status =:= closing_initiated; Status =:= closing -> Streams = goaway_streams(State0, maps:to_list(Streams0), LastStreamID, {stop, {goaway, Reason}, 'The connection is going away.'}, []), State1 = State0#state{streams=maps:from_list(Streams)}, if Status =:= connected; Status =:= closing_initiated -> {OurLastStreamID, HTTP2Machine} = cow_http2_machine:set_last_streamid(HTTP2Machine0), State = State1#state{http2_status=closing, http2_machine=HTTP2Machine}, ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:goaway(OurLastStreamID, no_error, <<>>))), State; true -> State1 end; %% We terminate the connection immediately if it hasn't fully been initialized. goaway(State, {goaway, _, Reason, _}) -> terminate(State, {stop, {goaway, Reason}, 'The connection is going away.'}). %% Cancel client-initiated streams that are above LastStreamID. goaway_streams(_, [], _, _, Acc) -> Acc; goaway_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], LastStreamID, Reason, Acc) when StreamID > LastStreamID, (StreamID rem 2) =:= 0 -> terminate_stream_handler(State, StreamID, Reason, StreamState), goaway_streams(State, Tail, LastStreamID, Reason, Acc); goaway_streams(State, [Stream|Tail], LastStreamID, Reason, Acc) -> goaway_streams(State, Tail, LastStreamID, Reason, [Stream|Acc]). %% A server that is attempting to gracefully shut down a connection SHOULD send %% an initial GOAWAY frame with the last stream identifier set to 2^31-1 and a %% NO_ERROR code. This signals to the client that a shutdown is imminent and %% that initiating further requests is prohibited. After allowing time for any %% in-flight stream creation (at least one round-trip time), the server can send %% another GOAWAY frame with an updated last stream identifier. This ensures %% that a connection can be cleanly shut down without losing requests. -spec initiate_closing(#state{}, _) -> #state{}. initiate_closing(State=#state{http2_status=connected, socket=Socket, transport=Transport, opts=Opts}, Reason) -> ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:goaway(16#7fffffff, no_error, <<>>))), Timeout = maps:get(goaway_initial_timeout, Opts, 1000), Message = {goaway_initial_timeout, Reason}, set_timeout(State#state{http2_status=closing_initiated}, Timeout, Message); initiate_closing(State=#state{http2_status=Status}, _Reason) when Status =:= closing_initiated; Status =:= closing -> %% This happens if sys:terminate/2,3 is called twice or if the supervisor %% tells us to shutdown after sys:terminate/2,3 is called or vice versa. State; initiate_closing(State, Reason) -> terminate(State, {stop, stop_reason(Reason), 'The connection is going away.'}). %% Switch to 'closing' state and stop accepting new streams. -spec closing(#state{}, Reason :: term()) -> #state{}. closing(State=#state{streams=Streams}, Reason) when Streams =:= #{} -> terminate(State, Reason); closing(State0=#state{http2_status=closing_initiated, http2_machine=HTTP2Machine0, socket=Socket, transport=Transport}, Reason) -> %% Stop accepting new streams. {LastStreamID, HTTP2Machine} = cow_http2_machine:set_last_streamid(HTTP2Machine0), State = State0#state{http2_status=closing, http2_machine=HTTP2Machine}, ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:goaway(LastStreamID, no_error, <<>>))), closing(State, Reason); closing(State=#state{http2_status=closing, opts=Opts}, Reason) -> %% If client sent GOAWAY, we may already be in 'closing' but without the %% goaway complete timeout set. Timeout = maps:get(goaway_complete_timeout, Opts, 3000), Message = {goaway_complete_timeout, Reason}, set_timeout(State, Timeout, Message). stop_reason({stop, Reason, _}) -> Reason; stop_reason(Reason) -> Reason. %% Function copied from cowboy_http. maybe_socket_error(State, {error, closed}) -> terminate(State, {socket_error, closed, 'The socket has been closed.'}); maybe_socket_error(State, Reason) -> maybe_socket_error(State, Reason, 'An error has occurred on the socket.'). maybe_socket_error(_, Result = ok, _) -> Result; maybe_socket_error(_, Result = {ok, _}, _) -> Result; maybe_socket_error(State, {error, Reason}, Human) -> terminate(State, {socket_error, Reason, Human}). -spec terminate(#state{} | undefined, _) -> no_return(). terminate(undefined, Reason) -> exit({shutdown, Reason}); terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status, http2_machine=HTTP2Machine, streams=Streams, children=Children}, Reason) when Status =:= connected; Status =:= closing_initiated; Status =:= closing -> %% @todo We might want to optionally send the Reason value %% as debug data in the GOAWAY frame here. Perhaps more. if Status =:= connected; Status =:= closing_initiated -> %% We are terminating so it's OK if we can't send the GOAWAY anymore. _ = Transport:send(Socket, cow_http2:goaway( cow_http2_machine:get_last_streamid(HTTP2Machine), terminate_reason(Reason), <<>>)); %% We already sent the GOAWAY frame. Status =:= closing -> ok end, terminate_all_streams(State, maps:to_list(Streams), Reason), cowboy_children:terminate(Children), %% @todo Don't linger on connection errors. terminate_linger(State), exit({shutdown, Reason}); %% We are not fully connected so we can just terminate the connection. terminate(_State, Reason) -> exit({shutdown, Reason}). terminate_reason({connection_error, Reason, _}) -> Reason; terminate_reason({stop, _, _}) -> no_error; terminate_reason({socket_error, _, _}) -> internal_error; terminate_reason({internal_error, _, _}) -> internal_error. terminate_all_streams(_, [], _) -> ok; terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reason) -> terminate_stream_handler(State, StreamID, Reason, StreamState), terminate_all_streams(State, Tail, Reason). %% This code is copied from cowboy_http. terminate_linger(State=#state{socket=Socket, transport=Transport, opts=Opts}) -> case Transport:shutdown(Socket, write) of ok -> case maps:get(linger_timeout, Opts, 1000) of 0 -> ok; infinity -> terminate_linger_before_loop(State, undefined, Transport:messages()); Timeout -> TimerRef = erlang:start_timer(Timeout, self(), linger_timeout), terminate_linger_before_loop(State, TimerRef, Transport:messages()) end; {error, _} -> ok end. terminate_linger_before_loop(State, TimerRef, Messages) -> %% We may already be in active mode when we do this %% but it's OK because we are shutting down anyway. %% %% We specially handle the socket error to terminate %% when an error occurs. case setopts_active(State) of ok -> terminate_linger_loop(State, TimerRef, Messages); {error, _} -> ok end. terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) -> receive {OK, Socket, _} when OK =:= element(1, Messages) -> terminate_linger_loop(State, TimerRef, Messages); {Closed, Socket} when Closed =:= element(2, Messages) -> ok; {Error, Socket, _} when Error =:= element(3, Messages) -> ok; {Passive, Socket} when Passive =:= tcp_passive; Passive =:= ssl_passive -> terminate_linger_before_loop(State, TimerRef, Messages); {timeout, TimerRef, linger_timeout} -> ok; _ -> terminate_linger_loop(State, TimerRef, Messages) end. %% @todo Don't send an RST_STREAM if one was already sent. %% %% When resetting the stream we are technically sending data %% on the socket. However due to implementation complexities %% we do not attempt to reset the idle timeout on send. reset_stream(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, StreamID, Error) -> Reason = case Error of {internal_error, _, _} -> internal_error; {stream_error, Reason0, _} -> Reason0 end, ok = maybe_socket_error(State0, Transport:send(Socket, cow_http2:rst_stream(StreamID, Reason))), State1 = case cow_http2_machine:reset_stream(StreamID, HTTP2Machine0) of {ok, HTTP2Machine} -> terminate_stream(State0#state{http2_machine=HTTP2Machine}, StreamID, Error); {error, not_found} -> terminate_stream(State0, StreamID, Error) end, case reset_rate(State1) of {ok, State} -> State; error -> terminate(State1, {connection_error, enhance_your_calm, 'Stream reset rate larger than configuration allows. Flood? (CVE-2019-9514)'}) end. reset_rate(State0=#state{reset_rate_num=Num0, reset_rate_time=Time}) -> case Num0 - 1 of 0 -> CurrentTime = erlang:monotonic_time(millisecond), if CurrentTime < Time -> error; true -> %% When the option has a period of infinity we cannot reach this clause. {ok, init_reset_rate_limiting(State0, CurrentTime)} end; Num -> {ok, State0#state{reset_rate_num=Num}} end. stop_stream(State=#state{http2_machine=HTTP2Machine}, StreamID) -> case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of %% When the stream terminates normally (without sending RST_STREAM) %% and no response was sent, we need to send a proper response back to the client. %% We delay the termination of the stream until the response is fully sent. {ok, idle, _} -> info(stopping(State, StreamID), StreamID, {response, 204, #{}, <<>>}); %% When a response was sent but not terminated, we need to close the stream. %% We delay the termination of the stream until the response is fully sent. {ok, nofin, fin} -> stopping(State, StreamID); %% We only send a final DATA frame if there isn't one queued yet. {ok, nofin, _} -> info(stopping(State, StreamID), StreamID, {data, fin, <<>>}); %% When a response was sent fully we can terminate the stream, %% regardless of the stream being in half-closed or closed state. _ -> terminate_stream(State, StreamID) end. stopping(State=#state{streams=Streams}, StreamID) -> #{StreamID := Stream} = Streams, State#state{streams=Streams#{StreamID => Stream#stream{status=stopping}}}. %% If we finished sending data and the stream is stopping, terminate it. maybe_terminate_stream(State=#state{streams=Streams}, StreamID, fin) -> case Streams of #{StreamID := #stream{status=stopping}} -> terminate_stream(State, StreamID); _ -> State end; maybe_terminate_stream(State, _, _) -> State. %% When the stream stops normally without reading the request %% body fully we need to tell the client to stop sending it. %% We do this by sending an RST_STREAM with reason NO_ERROR. (RFC7540 8.1.0) terminate_stream(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, StreamID) -> State = case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine0) of {ok, fin, _} -> ok = maybe_socket_error(State0, Transport:send(Socket, cow_http2:rst_stream(StreamID, no_error))), {ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0), State0#state{http2_machine=HTTP2Machine}; {error, closed} -> State0 end, terminate_stream(State, StreamID, normal). %% We remove the stream flow from the connection flow. Any further %% data received for this stream is therefore fully contained within %% the extra window we allocated for this stream. terminate_stream(State=#state{flow=Flow, streams=Streams0, children=Children0}, StreamID, Reason) -> case maps:take(StreamID, Streams0) of {#stream{flow=StreamFlow, state=StreamState}, Streams} -> terminate_stream_handler(State, StreamID, Reason, StreamState), Children = cowboy_children:shutdown(Children0, StreamID), State#state{flow=Flow - StreamFlow, streams=Streams, children=Children}; error -> State end. terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) -> try cowboy_stream:terminate(StreamID, Reason, StreamState) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(terminate, [StreamID, Reason, StreamState], Class, Exception, Stacktrace), Opts) end. %% System callbacks. -spec system_continue(_, _, {#state{}, binary()}) -> no_return(). system_continue(_, _, {State, Buffer}) -> before_loop(State, Buffer). -spec system_terminate(any(), _, _, {#state{}, binary()}) -> no_return(). system_terminate(Reason0, _, _, {State, Buffer}) -> Reason = {stop, {exit, Reason0}, 'sys:terminate/2,3 was called.'}, before_loop(initiate_closing(State, Reason), Buffer). -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}. system_code_change(Misc, _, _, _) -> {ok, Misc}. ================================================ FILE: src/cowboy_http3.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% A key difference between cowboy_http2 and cowboy_http3 %% is that HTTP/3 streams are QUIC streams and therefore %% much of the connection state is handled outside of %% Cowboy. -module(cowboy_http3). -ifdef(COWBOY_QUICER). -export([init/4]). %% Temporary callback to do sendfile over QUIC. -export([send/2]). %% @todo Graceful shutdown? Linger? Timeouts? Frame rates? PROXY header? -type opts() :: #{ compress_buffering => boolean(), compress_threshold => non_neg_integer(), connection_type => worker | supervisor, enable_connect_protocol => boolean(), env => cowboy_middleware:env(), logger => module(), max_decode_blocked_streams => 0..16#3fffffffffffffff, max_decode_table_size => 0..16#3fffffffffffffff, max_encode_blocked_streams => 0..16#3fffffffffffffff, max_encode_table_size => 0..16#3fffffffffffffff, max_ignored_frame_size_received => non_neg_integer() | infinity, metrics_callback => cowboy_metrics_h:metrics_callback(), metrics_req_filter => fun((cowboy_req:req()) -> map()), metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()), middlewares => [module()], shutdown_timeout => timeout(), stream_handlers => [module()], tracer_callback => cowboy_tracer_h:tracer_callback(), tracer_flags => [atom()], tracer_match_specs => cowboy_tracer_h:tracer_match_specs(), %% Open ended because configured stream handlers might add options. _ => _ }. -export_type([opts/0]). %% HTTP/3 or WebTransport stream. %% %% WebTransport sessions involve one bidirectional CONNECT stream %% that must stay open (and can be used for signaling using the %% Capsule Protocol) and an application-defined number of %% unidirectional and bidirectional streams, as well as datagrams. %% %% WebTransport sessions run in the CONNECT request process and %% all events related to the session is sent there as a message. %% The pid of the process is kept in the state. -record(stream, { id :: cow_http3:stream_id(), %% Whether the stream is currently in a special state. status :: header | {unidi, control | encoder | decoder} | normal | {data | ignore, non_neg_integer()} | stopping | {relaying, normal | {data, non_neg_integer()}, pid()} | {webtransport_session, normal | {ignore, non_neg_integer()}} | {webtransport_stream, cow_http3:stream_id()}, %% Stream buffer. buffer = <<>> :: binary(), %% Stream state. state = undefined :: undefined | {module(), any()} }). -record(state, { parent :: pid(), ref :: ranch:ref(), conn :: cowboy_quicer:quicer_connection_handle(), opts = #{} :: opts(), %% Remote address and port for the connection. peer = undefined :: {inet:ip_address(), inet:port_number()}, %% Local address and port for the connection. sock = undefined :: {inet:ip_address(), inet:port_number()}, %% Client certificate. cert :: undefined | binary(), %% HTTP/3 state machine. http3_machine :: cow_http3_machine:http3_machine(), %% Specially handled local unidi streams. local_control_id = undefined :: undefined | cow_http3:stream_id(), local_encoder_id = undefined :: undefined | cow_http3:stream_id(), local_decoder_id = undefined :: undefined | cow_http3:stream_id(), %% Bidirectional streams used for requests and responses, %% as well as unidirectional streams initiated by the client. streams = #{} :: #{cow_http3:stream_id() => #stream{}}, %% Lingering streams that were recently reset. We may receive %% pending data or messages for these streams a short while %% after they have been reset. lingering_streams = [] :: [non_neg_integer()], %% Streams can spawn zero or more children which are then managed %% by this module if operating as a supervisor. children = cowboy_children:init() :: cowboy_children:children() }). -spec init(pid(), ranch:ref(), cowboy_quicer:quicer_connection_handle(), opts()) -> no_return(). init(Parent, Ref, Conn, Opts) -> {ok, SettingsBin, HTTP3Machine0} = cow_http3_machine:init(server, Opts), %% Immediately open a control, encoder and decoder stream. %% @todo An endpoint MAY avoid creating an encoder stream if it will not be used (for example, if its encoder does not wish to use the dynamic table or if the maximum size of the dynamic table permitted by the peer is zero). %% @todo An endpoint MAY avoid creating a decoder stream if its decoder sets the maximum capacity of the dynamic table to zero. {ok, ControlID} = maybe_socket_error(undefined, cowboy_quicer:start_unidi_stream(Conn, [<<0>>, SettingsBin]), 'A socket error occurred when opening the control stream.'), {ok, EncoderID} = maybe_socket_error(undefined, cowboy_quicer:start_unidi_stream(Conn, <<2>>), 'A socket error occurred when opening the encoder stream.'), {ok, DecoderID} = maybe_socket_error(undefined, cowboy_quicer:start_unidi_stream(Conn, <<3>>), 'A socket error occurred when opening the encoder stream.'), %% Set the control, encoder and decoder streams in the machine. HTTP3Machine = cow_http3_machine:init_unidi_local_streams( ControlID, EncoderID, DecoderID, HTTP3Machine0), %% Get the peername/sockname/cert. {ok, Peer} = maybe_socket_error(undefined, cowboy_quicer:peername(Conn), 'A socket error occurred when retrieving the peer name.'), {ok, Sock} = maybe_socket_error(undefined, cowboy_quicer:sockname(Conn), 'A socket error occurred when retrieving the sock name.'), CertResult = case cowboy_quicer:peercert(Conn) of {error, no_peercert} -> {ok, undefined}; Cert0 -> Cert0 end, {ok, Cert} = maybe_socket_error(undefined, CertResult, 'A socket error occurred when retrieving the client TLS certificate.'), %% Quick! Let's go! loop(#state{parent=Parent, ref=Ref, conn=Conn, opts=Opts, peer=Peer, sock=Sock, cert=Cert, http3_machine=HTTP3Machine, local_control_id=ControlID, local_encoder_id=EncoderID, local_decoder_id=DecoderID}). loop(State0=#state{opts=Opts, children=Children}) -> receive Msg when element(1, Msg) =:= quic -> handle_quic_msg(State0, Msg); %% Timeouts. {timeout, Ref, {shutdown, Pid}} -> cowboy_children:shutdown_timeout(Children, Ref, Pid), loop(State0); %% Messages pertaining to a stream. {{Pid, StreamID}, Msg} when Pid =:= self() -> loop(info(State0, StreamID, Msg)); {'$cowboy_relay_command', {Pid, StreamID}, RelayCommand} when Pid =:= self() -> loop(relay_command(State0, StreamID, RelayCommand)); %% WebTransport commands. {'$webtransport_commands', SessionID, Commands} -> loop(webtransport_commands(State0, SessionID, Commands)); %% Exit signal from children. Msg = {'EXIT', Pid, _} -> loop(down(State0, Pid, Msg)); Msg -> cowboy:log(warning, "Received stray message ~p.", [Msg], Opts), loop(State0) end. handle_quic_msg(State0=#state{opts=Opts}, Msg) -> case cowboy_quicer:handle(Msg) of {data, StreamID, IsFin, Data} -> parse(State0, StreamID, Data, IsFin); {datagram, Data} -> parse_datagram(State0, Data); {stream_started, StreamID, StreamType} -> State = stream_new_remote(State0, StreamID, StreamType), loop(State); {stream_closed, StreamID, ErrorCode} -> State = stream_closed(State0, StreamID, ErrorCode), loop(State); {peer_send_shutdown, StreamID} -> State = stream_peer_send_shutdown(State0, StreamID), loop(State); closed -> %% @todo Different error reason if graceful? Reason = {socket_error, closed, 'The socket has been closed.'}, terminate(State0, Reason); ok -> loop(State0); unknown -> cowboy:log(warning, "Received unknown QUIC message ~p.", [Msg], Opts), loop(State0); {socket_error, Reason} -> terminate(State0, {socket_error, Reason, 'An error has occurred on the socket.'}) end. parse(State=#state{opts=Opts}, StreamID, Data, IsFin) -> case stream_get(State, StreamID) of Stream=#stream{buffer= <<>>} -> parse1(State, Stream, Data, IsFin); Stream=#stream{buffer=Buffer} -> Stream1 = Stream#stream{buffer= <<>>}, parse1(stream_store(State, Stream1), Stream1, <>, IsFin); %% Pending data for a stream that has been reset. Ignore. error -> case is_lingering_stream(State, StreamID) of true -> ok; false -> %% We avoid logging the data as it could be quite large. cowboy:log(warning, "Received data for unknown stream ~p.", [StreamID], Opts) end, loop(State) end. parse1(State, Stream=#stream{status=header}, Data, IsFin) -> parse_unidirectional_stream_header(State, Stream, Data, IsFin); parse1(State=#state{http3_machine=HTTP3Machine0}, #stream{status={unidi, Type}, id=StreamID}, Data, IsFin) when Type =:= encoder; Type =:= decoder -> case cow_http3_machine:unidi_data(Data, IsFin, StreamID, HTTP3Machine0) of {ok, Instrs, HTTP3Machine} -> loop(send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs)); {error, Error={connection_error, _, _}, HTTP3Machine} -> terminate(State#state{http3_machine=HTTP3Machine}, Error) end; %% @todo Handle when IsFin = fin which must terminate the WT session. parse1(State=#state{conn=Conn}, Stream=#stream{id=SessionID, status= {webtransport_session, normal}}, Data, IsFin) -> case cow_capsule:parse(Data) of {ok, wt_drain_session, Rest} -> webtransport_event(State, SessionID, close_initiated), parse1(State, Stream, Rest, IsFin); {ok, {wt_close_session, AppCode, AppMsg}, Rest} -> %% This event will be handled specially and lead %% to the termination of the session process. webtransport_event(State, SessionID, {closed, AppCode, AppMsg}), %% Shutdown the CONNECT stream immediately. cowboy_quicer:shutdown_stream(Conn, SessionID), %% @todo Will we receive a {stream_closed,...} after that? %% If any data is received past that point this is an error. %% @todo Don't crash, error out properly. <<>> = Rest, loop(webtransport_terminate_session(State, Stream)); more -> loop(stream_store(State, Stream#stream{buffer=Data})); %% Ignore unhandled/unknown capsules. %% @todo Do this when cow_capsule includes some. % {ok, _, Rest} -> % parse1(State, Stream, Rest, IsFin); % {ok, Rest} -> % parse1(State, Stream, Rest, IsFin); %% @todo Make the max length configurable? {skip, Len} when Len =< 8192 -> loop(stream_store(State, Stream#stream{ status={webtransport_session, {ignore, Len}}})); {skip, Len} -> %% @todo What should be done on capsule error? error({todo, capsule_too_long, Len}); error -> %% @todo What should be done on capsule error? error({todo, capsule_error, Data}) end; parse1(State, Stream=#stream{status= {webtransport_session, {ignore, Len}}}, Data, IsFin) -> case Data of <<_:Len/unit:8, Rest/bits>> -> parse1(State, Stream#stream{status={webtransport_session, normal}}, Rest, IsFin); _ -> loop(stream_store(State, Stream#stream{ status={webtransport_session, {ignore, Len - byte_size(Data)}}})) end; parse1(State, #stream{id=StreamID, status={webtransport_stream, SessionID}}, Data, IsFin) -> webtransport_event(State, SessionID, {stream_data, StreamID, IsFin, Data}), %% No need to store the stream again, WT streams don't get changed here. loop(State); parse1(State, Stream=#stream{status={data, Len}, id=StreamID}, Data, IsFin) -> DataLen = byte_size(Data), if DataLen < Len -> %% We don't have the full frame but this is the end of the %% data we have. So FrameIsFin is equivalent to IsFin here. loop(frame(State, Stream#stream{status={data, Len - DataLen}}, {data, Data}, IsFin)); true -> <> = Data, FrameIsFin = is_fin(IsFin, Rest), parse(frame(State, Stream#stream{status=normal}, {data, Data1}, FrameIsFin), StreamID, Rest, IsFin) end; %% This clause mirrors the {data, Len} clause. parse1(State, Stream=#stream{status={relaying, {data, Len}, RelayPid}, id=StreamID}, Data, IsFin) -> DataLen = byte_size(Data), if DataLen < Len -> %% We don't have the full frame but this is the end of the %% data we have. So FrameIsFin is equivalent to IsFin here. loop(frame(State, Stream#stream{status={relaying, {data, Len - DataLen}, RelayPid}}, {data, Data}, IsFin)); true -> <> = Data, FrameIsFin = is_fin(IsFin, Rest), parse(frame(State, Stream#stream{status={relaying, normal, RelayPid}}, {data, Data1}, FrameIsFin), StreamID, Rest, IsFin) end; parse1(State, Stream=#stream{status={ignore, Len}, id=StreamID}, Data, IsFin) -> DataLen = byte_size(Data), if DataLen < Len -> loop(stream_store(State, Stream#stream{status={ignore, Len - DataLen}})); true -> <<_:Len/binary, Rest/bits>> = Data, parse(stream_store(State, Stream#stream{status=normal}), StreamID, Rest, IsFin) end; %% @todo Clause that discards receiving data for stopping streams. %% We may receive a few more frames after we abort receiving. parse1(State=#state{opts=Opts}, Stream=#stream{status=Status0, id=StreamID}, Data, IsFin) -> case cow_http3:parse(Data) of {ok, Frame, Rest} -> FrameIsFin = is_fin(IsFin, Rest), parse(frame(State, Stream, Frame, FrameIsFin), StreamID, Rest, IsFin); %% The WebTransport stream header is not a real frame. {webtransport_stream_header, SessionID, Rest} -> become_webtransport_stream(State, Stream, bidi, SessionID, Rest, IsFin); {more, Frame = {data, _}, Len} -> %% We're at the end of the data so FrameIsFin is equivalent to IsFin. case IsFin of nofin when element(1, Status0) =:= relaying -> %% The stream will be stored at the end of processing commands. Status = setelement(2, Status0, {data, Len}), loop(frame(State, Stream#stream{status=Status}, Frame, nofin)); nofin -> %% The stream will be stored at the end of processing commands. loop(frame(State, Stream#stream{status={data, Len}}, Frame, nofin)); fin -> terminate(State, {connection_error, h3_frame_error, 'Last frame on stream was truncated. (RFC9114 7.1)'}) end; {more, ignore, Len} -> %% @todo This setting should be tested. %% %% While the default value doesn't warrant doing a streaming ignore %% (and could work just fine with the 'more' clause), this value %% is configurable and users may want to set it large. MaxIgnoredLen = maps:get(max_ignored_frame_size_received, Opts, 16384), %% We're at the end of the data so FrameIsFin is equivalent to IsFin. case IsFin of nofin when Len < MaxIgnoredLen -> %% We are not processing commands so we must store the stream. %% We also call ignored_frame here; we will not need to call %% it again when ignoring the rest of the data. Stream1 = Stream#stream{status={ignore, Len}}, State1 = ignored_frame(State, Stream1), loop(stream_store(State1, Stream1)); nofin -> terminate(State, {connection_error, h3_excessive_load, 'Ignored frame larger than limit. (RFC9114 10.5)'}); fin -> terminate(State, {connection_error, h3_frame_error, 'Last frame on stream was truncated. (RFC9114 7.1)'}) end; {ignore, Rest} -> parse(ignored_frame(State, Stream), StreamID, Rest, IsFin); Error = {connection_error, _, _} -> terminate(State, Error); more when Data =:= <<>> -> %% The buffer was already reset to <<>>. loop(stream_store(State, Stream)); more -> %% We're at the end of the data so FrameIsFin is equivalent to IsFin. case IsFin of nofin -> loop(stream_store(State, Stream#stream{buffer=Data})); fin -> terminate(State, {connection_error, h3_frame_error, 'Last frame on stream was truncated. (RFC9114 7.1)'}) end end. %% We may receive multiple frames in a single QUIC packet. %% The FIN flag applies to the QUIC packet, not to the frame. %% We must therefore only consider the frame to have a FIN %% flag if there's no data remaining to be read. is_fin(fin, <<>>) -> fin; is_fin(_, _) -> nofin. parse_unidirectional_stream_header(State0=#state{http3_machine=HTTP3Machine0}, Stream0=#stream{id=StreamID}, Data, IsFin) -> case cow_http3:parse_unidi_stream_header(Data) of {ok, Type, Rest} when Type =:= control; Type =:= encoder; Type =:= decoder -> case cow_http3_machine:set_unidi_remote_stream_type( StreamID, Type, HTTP3Machine0) of {ok, HTTP3Machine} -> State = State0#state{http3_machine=HTTP3Machine}, Stream = Stream0#stream{status={unidi, Type}}, parse(stream_store(State, Stream), StreamID, Rest, IsFin); {error, Error={connection_error, _, _}, HTTP3Machine} -> terminate(State0#state{http3_machine=HTTP3Machine}, Error) end; %% @todo Perhaps do this in cow_http3_machine directly. {ok, push, _} -> terminate(State0, {connection_error, h3_stream_creation_error, 'Only servers can push. (RFC9114 6.2.2)'}); {ok, {webtransport, SessionID}, Rest} -> become_webtransport_stream(State0, Stream0, unidi, SessionID, Rest, IsFin); %% Unknown stream types must be ignored. We choose to abort the %% stream instead of reading and discarding the incoming data. {undefined, _} -> loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error)); %% Very unlikely to happen but WebTransport headers may be fragmented %% as they are more than one byte. The fin flag in this case is an error, %% but because it happens in WebTransport application data (the Session ID) %% we only reset the impacted stream and not the entire connection. more when IsFin =:= fin -> loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error)); more -> loop(stream_store(State0, Stream0#stream{buffer=Data})) end. frame(State=#state{http3_machine=HTTP3Machine0}, Stream=#stream{id=StreamID}, Frame, IsFin) -> case cow_http3_machine:frame(Frame, IsFin, StreamID, HTTP3Machine0) of {ok, HTTP3Machine} -> State#state{http3_machine=HTTP3Machine}; {ok, {data, Data}, HTTP3Machine} -> data_frame(State#state{http3_machine=HTTP3Machine}, Stream, IsFin, Data); {ok, {headers, Headers, PseudoHeaders, BodyLen}, Instrs, HTTP3Machine} -> headers_frame(send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs), Stream, IsFin, Headers, PseudoHeaders, BodyLen); {ok, {trailers, _Trailers}, Instrs, HTTP3Machine} -> %% @todo Propagate trailers. send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs); {ok, GoAway={goaway, _}, HTTP3Machine} -> goaway(State#state{http3_machine=HTTP3Machine}, GoAway); {error, Error={stream_error, _Reason, _Human}, Instrs, HTTP3Machine} -> State1 = send_instructions(State#state{http3_machine=HTTP3Machine}, Instrs), reset_stream(State1, Stream, Error); {error, Error={connection_error, _, _}, HTTP3Machine} -> terminate(State#state{http3_machine=HTTP3Machine}, Error) end. data_frame(State, Stream=#stream{status={relaying, _, RelayPid}, id=StreamID}, IsFin, Data) -> RelayPid ! {'$cowboy_relay_data', {self(), StreamID}, IsFin, Data}, stream_store(State, Stream); data_frame(State=#state{opts=Opts}, Stream=#stream{id=StreamID, state=StreamState0}, IsFin, Data) -> try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of {Commands, StreamState} -> commands(State, Stream#stream{state=StreamState}, Commands) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(data, [StreamID, IsFin, Data, StreamState0], Class, Exception, Stacktrace), Opts), reset_stream(State, Stream, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:data/4.'}) end. headers_frame(State, Stream, IsFin, Headers, PseudoHeaders=#{method := <<"CONNECT">>}, _) when map_size(PseudoHeaders) =:= 2 -> early_error(State, Stream, IsFin, Headers, PseudoHeaders, 501, 'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'); headers_frame(State, Stream, IsFin, Headers, PseudoHeaders=#{method := <<"TRACE">>}, _) -> early_error(State, Stream, IsFin, Headers, PseudoHeaders, 501, 'The TRACE method is currently not implemented. (RFC9114 4.4, RFC7231 4.3.8)'); headers_frame(State, Stream, IsFin, Headers, PseudoHeaders=#{authority := Authority}, BodyLen) -> headers_frame_parse_host(State, Stream, IsFin, Headers, PseudoHeaders, BodyLen, Authority); headers_frame(State, Stream, IsFin, Headers, PseudoHeaders, BodyLen) -> case lists:keyfind(<<"host">>, 1, Headers) of {_, Authority} -> headers_frame_parse_host(State, Stream, IsFin, Headers, PseudoHeaders, BodyLen, Authority); _ -> reset_stream(State, Stream, {stream_error, h3_message_error, 'Requests translated from HTTP/1.1 must include a host header. (RFC7540 8.1.2.3, RFC7230 5.4)'}) end. headers_frame_parse_host(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert}, Stream=#stream{id=StreamID}, IsFin, Headers, PseudoHeaders=#{method := Method, scheme := Scheme, path := PathWithQs}, BodyLen, Authority) -> try cow_http_hd:parse_host(Authority) of {Host, Port0} -> Port = ensure_port(Scheme, Port0), try cow_http:parse_fullpath(PathWithQs) of {<<>>, _} -> reset_stream(State, Stream, {stream_error, h3_message_error, 'The path component must not be empty. (RFC7540 8.1.2.3)'}); {Path, Qs} -> Req0 = #{ ref => Ref, pid => self(), streamid => StreamID, peer => Peer, sock => Sock, cert => Cert, method => Method, scheme => Scheme, host => Host, port => Port, path => Path, qs => Qs, version => 'HTTP/3', headers => headers_to_map(Headers, #{}), has_body => IsFin =:= nofin, body_length => BodyLen }, %% We add the protocol information for extended CONNECTs. Req = case PseudoHeaders of #{protocol := Protocol} -> Req0#{protocol => Protocol}; _ -> Req0 end, headers_frame(State, Stream, Req) catch _:_ -> reset_stream(State, Stream, {stream_error, h3_message_error, 'The :path pseudo-header is invalid. (RFC7540 8.1.2.3)'}) end catch _:_ -> reset_stream(State, Stream, {stream_error, h3_message_error, 'The :authority pseudo-header is invalid. (RFC7540 8.1.2.3)'}) end. %% @todo Copied from cowboy_http2. %% @todo How to handle "http"? ensure_port(<<"http">>, undefined) -> 80; ensure_port(<<"https">>, undefined) -> 443; ensure_port(_, Port) -> Port. %% @todo Copied from cowboy_http2. %% This function is necessary to properly handle duplicate headers %% and the special-case cookie header. headers_to_map([], Acc) -> Acc; headers_to_map([{Name, Value}|Tail], Acc0) -> Acc = case Acc0 of %% The cookie header does not use proper HTTP header lists. #{Name := Value0} when Name =:= <<"cookie">> -> Acc0#{Name => << Value0/binary, "; ", Value/binary >>}; #{Name := Value0} -> Acc0#{Name => << Value0/binary, ", ", Value/binary >>}; _ -> Acc0#{Name => Value} end, headers_to_map(Tail, Acc). %% @todo WebTransport CONNECT requests must have extra checks on settings. %% @todo We may also need to defer them if we didn't get settings. headers_frame(State=#state{opts=Opts}, Stream=#stream{id=StreamID}, Req) -> try cowboy_stream:init(StreamID, Req, Opts) of {Commands, StreamState} -> commands(State, Stream#stream{state=StreamState}, Commands) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(init, [StreamID, Req, Opts], Class, Exception, Stacktrace), Opts), reset_stream(State, Stream, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:init/3.'}) end. early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer}, Stream=#stream{id=StreamID}, _IsFin, Headers, #{method := Method}, StatusCode0, HumanReadable) -> %% We automatically terminate the stream but it is not an error %% per se (at least not in the first implementation). Reason = {stream_error, h3_no_error, HumanReadable}, %% The partial Req is minimal for now. We only have one case %% where it can be called (when a method is completely disabled). PartialReq = #{ ref => Ref, peer => Peer, method => Method, headers => headers_to_map(Headers, #{}) }, Resp = {response, StatusCode0, RespHeaders0=#{<<"content-length">> => <<"0">>}, <<>>}, try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of {response, StatusCode, RespHeaders, RespBody} -> send_response(State0, Stream, StatusCode, RespHeaders, RespBody) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(early_error, [StreamID, Reason, PartialReq, Resp, Opts], Class, Exception, Stacktrace), Opts), %% We still need to send an error response, so send what we initially %% wanted to send. It's better than nothing. send_headers(State0, Stream, fin, StatusCode0, RespHeaders0) end. %% Datagrams. parse_datagram(State, Data0) -> {SessionID, Data} = cow_http3:parse_datagram(Data0), case stream_get(State, SessionID) of #stream{status={webtransport_session, _}} -> webtransport_event(State, SessionID, {datagram, Data}), loop(State); _ -> error(todo) %% @todo Might be a future WT session or an error. end. %% Erlang messages. down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) -> State = case cowboy_children:down(Children0, Pid) of %% The stream was terminated already. {ok, undefined, Children} -> State0#state{children=Children}; %% The stream is still running. {ok, StreamID, Children} -> info(State0#state{children=Children}, StreamID, Msg); %% The process was unknown. error -> cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n", [Msg, Pid], Opts), State0 end, if %% @todo % State#state.http2_status =:= closing, State#state.streams =:= #{} -> % terminate(State, {stop, normal, 'The connection is going away.'}); true -> State end. info(State=#state{opts=Opts, http3_machine=_HTTP3Machine}, StreamID, Msg) -> case stream_get(State, StreamID) of Stream=#stream{state=StreamState0} -> try cowboy_stream:info(StreamID, Msg, StreamState0) of {Commands, StreamState} -> commands(State, Stream#stream{state=StreamState}, Commands) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(info, [StreamID, Msg, StreamState0], Class, Exception, Stacktrace), Opts), reset_stream(State, Stream, {internal_error, {Class, Exception}, 'Unhandled exception in cowboy_stream:info/3.'}) end; error -> case is_lingering_stream(State, StreamID) of true -> ok; false -> cowboy:log(warning, "Received message ~p for unknown stream ~p.", [Msg, StreamID], Opts) end, State end. %% Stream handler commands. commands(State, Stream, []) -> stream_store(State, Stream); %% Error responses are sent only if a response wasn't sent already. commands(State=#state{http3_machine=HTTP3Machine}, Stream=#stream{id=StreamID}, [{error_response, StatusCode, Headers, Body}|Tail]) -> case cow_http3_machine:get_bidi_stream_local_state(StreamID, HTTP3Machine) of {ok, idle} -> commands(State, Stream, [{response, StatusCode, Headers, Body}|Tail]); _ -> commands(State, Stream, Tail) end; %% Send an informational response. commands(State0, Stream, [{inform, StatusCode, Headers}|Tail]) -> State = send_headers(State0, Stream, idle, StatusCode, Headers), commands(State, Stream, Tail); %% Send response headers. commands(State0, Stream, [{response, StatusCode, Headers, Body}|Tail]) -> State = send_response(State0, Stream, StatusCode, Headers, Body), commands(State, Stream, Tail); %% Send response headers. commands(State0, Stream, [{headers, StatusCode, Headers}|Tail]) -> State = send_headers(State0, Stream, nofin, StatusCode, Headers), commands(State, Stream, Tail); %%% Send a response body chunk. commands(State0=#state{conn=Conn}, Stream=#stream{id=StreamID}, [{data, IsFin, Data}|Tail]) -> _ = case Data of {sendfile, Offset, Bytes, Path} -> %% Temporary solution to do sendfile over QUIC. {ok, _} = ranch_transport:sendfile(?MODULE, {Conn, StreamID}, Path, Offset, Bytes, []), ok = maybe_socket_error(State0, cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), IsFin)); _ -> ok = maybe_socket_error(State0, cowboy_quicer:send(Conn, StreamID, cow_http3:data(Data), IsFin)) end, State = maybe_send_is_fin(State0, Stream, IsFin), commands(State, Stream, Tail); %%% Send trailers. commands(State0=#state{conn=Conn, http3_machine=HTTP3Machine0}, Stream=#stream{id=StreamID}, [{trailers, Trailers}|Tail]) -> State = case cow_http3_machine:prepare_trailers( StreamID, HTTP3Machine0, maps:to_list(Trailers)) of {trailers, HeaderBlock, Instrs, HTTP3Machine} -> State1 = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs), ok = maybe_socket_error(State1, cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock), fin)), State1; {no_trailers, HTTP3Machine} -> ok = maybe_socket_error(State0, cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), fin)), State0#state{http3_machine=HTTP3Machine} end, commands(State, Stream, Tail); %% Send a push promise. %% %% @todo Responses sent as a result of a push_promise request %% must not send push_promise frames themselves. %% %% @todo We should not send push_promise frames when we are %% in the closing http2_status. %commands(State0=#state{socket=Socket, transport=Transport, http3_machine=HTTP3Machine0}, % Stream, [{push, Method, Scheme, Host, Port, Path, Qs, Headers0}|Tail]) -> % Authority = case {Scheme, Port} of % {<<"http">>, 80} -> Host; % {<<"https">>, 443} -> Host; % _ -> iolist_to_binary([Host, $:, integer_to_binary(Port)]) % end, % PathWithQs = iolist_to_binary(case Qs of % <<>> -> Path; % _ -> [Path, $?, Qs] % end), % PseudoHeaders = #{ % method => Method, % scheme => Scheme, % authority => Authority, % path => PathWithQs % }, % %% We need to make sure the header value is binary before we can % %% create the Req object, as it expects them to be flat. % Headers = maps:to_list(maps:map(fun(_, V) -> iolist_to_binary(V) end, Headers0)), % %% @todo % State = case cow_http2_machine:prepare_push_promise(StreamID, HTTP3Machine0, % PseudoHeaders, Headers) of % {ok, PromisedStreamID, HeaderBlock, HTTP3Machine} -> % Transport:send(Socket, cow_http2:push_promise( % StreamID, PromisedStreamID, HeaderBlock)), % headers_frame(State0#state{http3_machine=HTTP2Machine}, % PromisedStreamID, fin, Headers, PseudoHeaders, 0); % {error, no_push} -> % State0 % end, % commands(State, Stream, Tail); %%% Read the request body. %commands(State0=#state{flow=Flow, streams=Streams}, Stream, [{flow, Size}|Tail]) -> commands(State, Stream, [{flow, _Size}|Tail]) -> %% @todo We should tell the QUIC stream to increase its window size. % #{StreamID := Stream=#stream{flow=StreamFlow}} = Streams, % State = update_window(State0#state{flow=Flow + Size, % streams=Streams#{StreamID => Stream#stream{flow=StreamFlow + Size}}}, % StreamID), commands(State, Stream, Tail); %% Supervise a child process. commands(State=#state{children=Children}, Stream=#stream{id=StreamID}, [{spawn, Pid, Shutdown}|Tail]) -> commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)}, Stream, Tail); %% Error handling. commands(State, Stream, [Error = {internal_error, _, _}|_Tail]) -> %% @todo Do we want to run the commands after an internal_error? %% @todo Do we even allow commands after? %% @todo Only reset when the stream still exists. reset_stream(State, Stream, Error); %% Use a different protocol within the stream (CONNECT :protocol). %% @todo Make sure we error out when the feature is disabled. commands(State0, Stream0=#stream{id=StreamID}, [{switch_protocol, Headers, cowboy_webtransport, WTState=#{}}|Tail]) -> State = info(stream_store(State0, Stream0), StreamID, {headers, 200, Headers}), #state{http3_machine=HTTP3Machine0} = State, Stream1 = #stream{state=StreamState} = stream_get(State, StreamID), %% The stream becomes a WT session at that point. It is the %% parent stream of all streams in this WT session. The %% cowboy_stream state is kept because it will be needed %% to terminate the stream properly. HTTP3Machine = cow_http3_machine:become_webtransport_session(StreamID, HTTP3Machine0), Stream = Stream1#stream{ status={webtransport_session, normal}, state={cowboy_webtransport, WTState#{stream_state => StreamState}} }, %% @todo We must propagate the buffer to capsule handling if any. commands(State#state{http3_machine=HTTP3Machine}, Stream, Tail); %% There are two data_delivery: stream_handlers and relay. %% The former just has the data go through stream handlers %% like normal requests. The latter relays data directly. commands(State0, Stream0=#stream{id=StreamID}, [{switch_protocol, Headers, _Mod, ModState=#{data_delivery := relay}}|Tail]) -> State = info(stream_store(State0, Stream0), StreamID, {headers, 200, Headers}), Stream1 = #stream{status=normal} = stream_get(State, StreamID), #{data_delivery_pid := RelayPid} = ModState, %% We do not set data_delivery_flow because it is managed by quicer %% and we do not have an easy way to modify it. Stream = Stream1#stream{status={relaying, normal, RelayPid}}, commands(State, Stream, Tail); commands(State0, Stream0=#stream{id=StreamID}, [{switch_protocol, Headers, _Mod, _ModState}|Tail]) -> State = info(stream_store(State0, Stream0), StreamID, {headers, 200, Headers}), Stream = stream_get(State, StreamID), commands(State, Stream, Tail); %% Set options dynamically. commands(State, Stream, [{set_options, _Opts}|Tail]) -> commands(State, Stream, Tail); commands(State, Stream, [stop|_Tail]) -> %% @todo Do we want to run the commands after a stop? %% @todo Do we even allow commands after? stop_stream(State, Stream); %% Log event. commands(State=#state{opts=Opts}, Stream, [Log={log, _, _, _}|Tail]) -> cowboy:log(Log, Opts), commands(State, Stream, Tail). send_response(State0=#state{conn=Conn, http3_machine=HTTP3Machine0}, Stream=#stream{id=StreamID}, StatusCode, Headers, Body) -> Size = case Body of {sendfile, _, Bytes0, _} -> Bytes0; _ -> iolist_size(Body) end, case Size of 0 -> State = send_headers(State0, Stream, fin, StatusCode, Headers), maybe_send_is_fin(State, Stream, fin); _ -> %% @todo Add a test for HEAD to make sure we don't send the body when %% returning {response...} from a stream handler (or {headers...} then {data...}). {ok, _IsFin, HeaderBlock, Instrs, HTTP3Machine} = cow_http3_machine:prepare_headers(StreamID, HTTP3Machine0, nofin, #{status => cow_http:status_to_integer(StatusCode)}, headers_to_list(Headers)), State = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs), %% @todo It might be better to do async sends. _ = case Body of {sendfile, Offset, Bytes, Path} -> ok = maybe_socket_error(State, cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock))), %% Temporary solution to do sendfile over QUIC. {ok, _} = maybe_socket_error(State, ranch_transport:sendfile(?MODULE, {Conn, StreamID}, Path, Offset, Bytes, [])), ok = maybe_socket_error(State, cowboy_quicer:send(Conn, StreamID, cow_http3:data(<<>>), fin)); _ -> ok = maybe_socket_error(State, cowboy_quicer:send(Conn, StreamID, [ cow_http3:headers(HeaderBlock), cow_http3:data(Body) ], fin)) end, maybe_send_is_fin(State, Stream, fin) end. maybe_send_is_fin(State=#state{http3_machine=HTTP3Machine0}, Stream=#stream{id=StreamID}, fin) -> HTTP3Machine = cow_http3_machine:close_bidi_stream_for_sending(StreamID, HTTP3Machine0), maybe_terminate_stream(State#state{http3_machine=HTTP3Machine}, Stream); maybe_send_is_fin(State, _, _) -> State. %% Temporary callback to do sendfile over QUIC. -spec send({cowboy_quicer:quicer_connection_handle(), cow_http3:stream_id()}, iodata()) -> ok | {error, any()}. send({Conn, StreamID}, IoData) -> cowboy_quicer:send(Conn, StreamID, cow_http3:data(IoData)). send_headers(State0=#state{conn=Conn, http3_machine=HTTP3Machine0}, #stream{id=StreamID}, IsFin0, StatusCode, Headers) -> {ok, IsFin, HeaderBlock, Instrs, HTTP3Machine} = cow_http3_machine:prepare_headers(StreamID, HTTP3Machine0, IsFin0, #{status => cow_http:status_to_integer(StatusCode)}, headers_to_list(Headers)), State = send_instructions(State0#state{http3_machine=HTTP3Machine}, Instrs), ok = maybe_socket_error(State, cowboy_quicer:send(Conn, StreamID, cow_http3:headers(HeaderBlock), IsFin)), State. %% The set-cookie header is special; we can only send one cookie per header. headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) -> Headers = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)), Headers ++ [{<<"set-cookie">>, Value} || Value <- SetCookies]; headers_to_list(Headers) -> maps:to_list(Headers). %% @todo We would open unidi streams here if we only open on-demand. %% No instructions. send_instructions(State, undefined) -> State; %% Decoder instructions. send_instructions(State=#state{conn=Conn, local_decoder_id=DecoderID}, {decoder_instructions, DecData}) -> ok = maybe_socket_error(State, cowboy_quicer:send(Conn, DecoderID, DecData)), State; %% Encoder instructions. send_instructions(State=#state{conn=Conn, local_encoder_id=EncoderID}, {encoder_instructions, EncData}) -> ok = maybe_socket_error(State, cowboy_quicer:send(Conn, EncoderID, EncData)), State. %% Relay data delivery commands. relay_command(State, StreamID, DataCmd = {data, _, _}) -> Stream = stream_get(State, StreamID), commands(State, Stream, [DataCmd]); relay_command(State=#state{conn=Conn}, StreamID, active) -> ok = maybe_socket_error(State, cowboy_quicer:setopt(Conn, StreamID, active, true)), State; relay_command(State=#state{conn=Conn}, StreamID, passive) -> ok = maybe_socket_error(State, cowboy_quicer:setopt(Conn, StreamID, active, false)), State. %% We mark the stream as being a WebTransport stream %% and then continue parsing the data as a WebTransport %% stream. This function is common for incoming unidi %% and bidi streams. become_webtransport_stream(State0=#state{http3_machine=HTTP3Machine0}, Stream0=#stream{id=StreamID}, StreamType, SessionID, Rest, IsFin) -> case cow_http3_machine:become_webtransport_stream(StreamID, SessionID, HTTP3Machine0) of {ok, HTTP3Machine} -> State = State0#state{http3_machine=HTTP3Machine}, Stream = Stream0#stream{status={webtransport_stream, SessionID}}, webtransport_event(State, SessionID, {stream_open, StreamID, StreamType}), %% We don't need to parse the remaining data if there isn't any. case {Rest, IsFin} of {<<>>, nofin} -> loop(stream_store(State, Stream)); _ -> parse(stream_store(State, Stream), StreamID, Rest, IsFin) end %% @todo Error conditions. end. webtransport_event(State, SessionID, Event) -> #stream{ status={webtransport_session, _}, state={cowboy_webtransport, #{session_pid := SessionPid}} } = stream_get(State, SessionID), SessionPid ! {'$webtransport_event', SessionID, Event}, ok. webtransport_commands(State, SessionID, Commands) -> case stream_get(State, SessionID) of Session = #stream{status={webtransport_session, _}} -> wt_commands(State, Session, Commands); %% The stream has been terminated, ignore pending commands. error -> State end. wt_commands(State, _, []) -> State; wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID}, [{open_stream, OpenStreamRef, StreamType, InitialData}|Tail]) -> %% Because opening the stream involves sending a short header %% we necessarily write data. The InitialData variable allows %% providing additional data to be sent in the same packet. StartF = case StreamType of bidi -> start_bidi_stream; unidi -> start_unidi_stream end, Header = cow_http3:webtransport_stream_header(SessionID, StreamType), case cowboy_quicer:StartF(Conn, [Header, InitialData]) of {ok, StreamID} -> %% @todo Pass Session directly? webtransport_event(State0, SessionID, {opened_stream_id, OpenStreamRef, StreamID}), State = stream_new_local(State0, StreamID, StreamType, {webtransport_stream, SessionID}), wt_commands(State, Session, Tail) %% @todo Handle errors. end; wt_commands(State, Session, [{close_stream, StreamID, Code}|Tail]) -> %% @todo Check that StreamID belongs to Session. error({todo, State, Session, [{close_stream, StreamID, Code}|Tail]}); wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID}, [{send, datagram, Data}|Tail]) -> case cowboy_quicer:send_datagram(Conn, cow_http3:datagram(SessionID, Data)) of ok -> wt_commands(State, Session, Tail) %% @todo Handle errors. end; wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, Data}|Tail]) -> %% @todo Check that StreamID belongs to Session. case cowboy_quicer:send(Conn, StreamID, Data, nofin) of ok -> wt_commands(State, Session, Tail) %% @todo Handle errors. end; wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, IsFin, Data}|Tail]) -> %% @todo Check that StreamID belongs to Session. case cowboy_quicer:send(Conn, StreamID, Data, IsFin) of ok -> wt_commands(State, Session, Tail) %% @todo Handle errors. end; wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID}, [initiate_close|Tail]) -> %% We must send a WT_DRAIN_SESSION capsule on the CONNECT stream. Capsule = cow_capsule:wt_drain_session(), case cowboy_quicer:send(Conn, SessionID, Capsule, nofin) of ok -> wt_commands(State, Session, Tail) %% @todo Handle errors. end; wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID}, [Cmd|Tail]) when Cmd =:= close; element(1, Cmd) =:= close -> %% We must send a WT_CLOSE_SESSION capsule on the CONNECT stream. {AppCode, AppMsg} = case Cmd of close -> {0, <<>>}; {close, AppCode0} -> {AppCode0, <<>>}; {close, AppCode0, AppMsg0} -> {AppCode0, AppMsg0} end, Capsule = cow_capsule:wt_close_session(AppCode, AppMsg), case cowboy_quicer:send(Conn, SessionID, Capsule, fin) of ok -> State = webtransport_terminate_session(State0, Session), %% @todo Because the handler is in a separate process %% we must wait for it to stop and eventually %% kill the process if it takes too long. %% @todo We may need to fully close the CONNECT stream (if remote doesn't reset it). wt_commands(State, Session, Tail) %% @todo Handle errors. end. webtransport_terminate_session(State=#state{conn=Conn, http3_machine=HTTP3Machine0, streams=Streams0, lingering_streams=Lingering0}, #stream{id=SessionID}) -> %% Reset/abort the WT streams. Streams = maps:filtermap(fun (_, #stream{id=StreamID, status={webtransport_session, _}}) when StreamID =:= SessionID -> %% We remove the session stream but do the shutdown outside this function. false; (StreamID, #stream{status={webtransport_stream, StreamSessionID}}) when StreamSessionID =:= SessionID -> cowboy_quicer:shutdown_stream(Conn, StreamID, both, cow_http3:error_to_code(wt_session_gone)), false; (_, _) -> true end, Streams0), %% Keep the streams in lingering state. %% We only keep up to 100 streams in this state. @todo Make it configurable? Terminated = maps:keys(Streams0) -- maps:keys(Streams), Lingering = lists:sublist(Terminated ++ Lingering0, 100), %% Update the HTTP3 state machine. HTTP3Machine = cow_http3_machine:close_webtransport_session(SessionID, HTTP3Machine0), State#state{ http3_machine=HTTP3Machine, streams=Streams, lingering_streams=Lingering }. stream_peer_send_shutdown(State=#state{conn=Conn}, StreamID) -> case stream_get(State, StreamID) of %% Cleanly terminating the CONNECT stream is equivalent %% to an application error code of 0 and empty message. Stream = #stream{status={webtransport_session, _}} -> webtransport_event(State, StreamID, {closed, 0, <<>>}), %% Shutdown the CONNECT stream fully. cowboy_quicer:shutdown_stream(Conn, StreamID), webtransport_terminate_session(State, Stream); _ -> State end. reset_stream(State0=#state{conn=Conn, http3_machine=HTTP3Machine0}, Stream=#stream{id=StreamID}, Error) -> Reason = case Error of {internal_error, _, _} -> h3_internal_error; {stream_error, Reason0, _} -> Reason0 end, %% @todo Do we want to close both sides? %% @todo Should we close the send side if the receive side was already closed? cowboy_quicer:shutdown_stream(Conn, StreamID, both, cow_http3:error_to_code(Reason)), State1 = case cow_http3_machine:reset_stream(StreamID, HTTP3Machine0) of {ok, HTTP3Machine} -> terminate_stream(State0#state{http3_machine=HTTP3Machine}, Stream, Error); {error, not_found} -> terminate_stream(State0, Stream, Error) end, %% @todo % case reset_rate(State1) of % {ok, State} -> % State; % error -> % terminate(State1, {connection_error, enhance_your_calm, % 'Stream reset rate larger than configuration allows. Flood? (CVE-2019-9514)'}) % end. State1. stop_stream(State0=#state{http3_machine=HTTP3Machine}, Stream=#stream{id=StreamID}) -> %% We abort reading when stopping the stream but only %% if the client was not finished sending data. %% We mark the stream as 'stopping' either way. State = case cow_http3_machine:get_bidi_stream_remote_state(StreamID, HTTP3Machine) of {ok, fin} -> stream_store(State0, Stream#stream{status=stopping}); {error, not_found} -> stream_store(State0, Stream#stream{status=stopping}); _ -> stream_abort_receive(State0, Stream, h3_no_error) end, %% Then we may need to send a response or terminate it %% if the stream handler did not do so already. case cow_http3_machine:get_bidi_stream_local_state(StreamID, HTTP3Machine) of %% When the stream terminates normally (without resetting the stream) %% and no response was sent, we need to send a proper response back to the client. {ok, idle} -> info(State, StreamID, {response, 204, #{}, <<>>}); %% When a response was sent but not terminated, we need to close the stream. %% We send a final DATA frame to complete the stream. {ok, nofin} -> info(State, StreamID, {data, fin, <<>>}); %% When a response was sent fully we can terminate the stream, %% regardless of the stream being in half-closed or closed state. _ -> terminate_stream(State, Stream, normal) end. maybe_terminate_stream(State, Stream=#stream{status=stopping}) -> terminate_stream(State, Stream, normal); %% The Stream will be stored in the State at the end of commands processing. maybe_terminate_stream(State, _) -> State. terminate_stream(State=#state{streams=Streams0, children=Children0}, #stream{id=StreamID, state=StreamState}, Reason) -> Streams = maps:remove(StreamID, Streams0), terminate_stream_handler(State, StreamID, Reason, StreamState), Children = cowboy_children:shutdown(Children0, StreamID), stream_linger(State#state{streams=Streams, children=Children}, StreamID). terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) -> try cowboy_stream:terminate(StreamID, Reason, StreamState) catch Class:Exception:Stacktrace -> cowboy:log(cowboy_stream:make_error_log(terminate, [StreamID, Reason, StreamState], Class, Exception, Stacktrace), Opts) end. ignored_frame(State=#state{http3_machine=HTTP3Machine0}, #stream{id=StreamID}) -> case cow_http3_machine:ignored_frame(StreamID, HTTP3Machine0) of {ok, HTTP3Machine} -> State#state{http3_machine=HTTP3Machine}; {error, Error={connection_error, _, _}, HTTP3Machine} -> terminate(State#state{http3_machine=HTTP3Machine}, Error) end. stream_abort_receive(State=#state{conn=Conn}, Stream=#stream{id=StreamID}, Reason) -> cowboy_quicer:shutdown_stream(Conn, StreamID, receiving, cow_http3:error_to_code(Reason)), stream_store(State, Stream#stream{status=stopping}). %% @todo Graceful connection shutdown. %% We terminate the connection immediately if it hasn't fully been initialized. -spec goaway(#state{}, {goaway, _}) -> no_return(). goaway(State, {goaway, _}) -> terminate(State, {stop, goaway, 'The connection is going away.'}). %% Function copied from cowboy_http. maybe_socket_error(State, {error, closed}) -> terminate(State, {socket_error, closed, 'The socket has been closed.'}); maybe_socket_error(State, Reason) -> maybe_socket_error(State, Reason, 'An error has occurred on the socket.'). maybe_socket_error(_, Result = ok, _) -> Result; maybe_socket_error(_, Result = {ok, _}, _) -> Result; maybe_socket_error(State, {error, Reason}, Human) -> terminate(State, {socket_error, Reason, Human}). -spec terminate(#state{} | undefined, _) -> no_return(). terminate(undefined, Reason) -> exit({shutdown, Reason}); terminate(State=#state{conn=Conn, %http3_status=Status, %http3_machine=HTTP3Machine, streams=Streams, children=Children}, Reason) -> % if % Status =:= connected; Status =:= closing_initiated -> %% @todo % %% We are terminating so it's OK if we can't send the GOAWAY anymore. % _ = cowboy_quicer:send(Conn, ControlID, cow_http3:goaway( % cow_http3_machine:get_last_streamid(HTTP3Machine))), %% We already sent the GOAWAY frame. % Status =:= closing -> % ok % end, terminate_all_streams(State, maps:to_list(Streams), Reason), cowboy_children:terminate(Children), % terminate_linger(State), _ = cowboy_quicer:shutdown(Conn, cow_http3:error_to_code(terminate_reason(Reason))), exit({shutdown, Reason}). terminate_reason({connection_error, Reason, _}) -> Reason; terminate_reason({stop, _, _}) -> h3_no_error; terminate_reason({socket_error, _, _}) -> h3_internal_error. %terminate_reason({internal_error, _, _}) -> internal_error. terminate_all_streams(_, [], _) -> ok; terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reason) -> terminate_stream_handler(State, StreamID, Reason, StreamState), terminate_all_streams(State, Tail, Reason). stream_get(#state{streams=Streams}, StreamID) -> maps:get(StreamID, Streams, error). stream_new_local(State, StreamID, StreamType, Status) -> stream_new(State, StreamID, StreamType, unidi_local, Status). stream_new_remote(State, StreamID, StreamType) -> Status = case StreamType of unidi -> header; bidi -> normal end, stream_new(State, StreamID, StreamType, unidi_remote, Status). stream_new(State=#state{http3_machine=HTTP3Machine0, streams=Streams}, StreamID, StreamType, UnidiType, Status) -> {HTTP3Machine, Status} = case StreamType of unidi -> {cow_http3_machine:init_unidi_stream(StreamID, UnidiType, HTTP3Machine0), Status}; bidi -> {cow_http3_machine:init_bidi_stream(StreamID, HTTP3Machine0), Status} end, Stream = #stream{id=StreamID, status=Status}, State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamID => Stream}}. %% Stream closed message for a local (write-only) unidi stream. stream_closed(State=#state{local_control_id=StreamID}, StreamID, _) -> stream_closed1(State, StreamID); stream_closed(State=#state{local_encoder_id=StreamID}, StreamID, _) -> stream_closed1(State, StreamID); stream_closed(State=#state{local_decoder_id=StreamID}, StreamID, _) -> stream_closed1(State, StreamID); stream_closed(State=#state{opts=Opts, streams=Streams0, children=Children0}, StreamID, ErrorCode) -> case maps:take(StreamID, Streams0) of %% In the WT session's case, streams will be %% removed in webtransport_terminate_session. {Stream=#stream{status={webtransport_session, _}}, _} -> webtransport_event(State, StreamID, closed_abruptly), webtransport_terminate_session(State, Stream); {#stream{state=undefined}, Streams} -> %% Unidi stream has no handler/children. stream_closed1(State#state{streams=Streams}, StreamID); %% We only stop bidi streams if the stream was closed with an error %% or the stream was already in the process of stopping. {#stream{status=Status, state=StreamState}, Streams} when Status =:= stopping; ErrorCode =/= 0 -> terminate_stream_handler(State, StreamID, closed, StreamState), Children = cowboy_children:shutdown(Children0, StreamID), stream_closed1(State#state{streams=Streams, children=Children}, StreamID); %% Don't remove a stream that terminated properly but %% has chosen to remain up (custom stream handlers). {_, _} -> stream_closed1(State, StreamID); %% Stream closed message for a stream that has been reset. Ignore. error -> case is_lingering_stream(State, StreamID) of true -> ok; false -> %% We avoid logging the data as it could be quite large. cowboy:log(warning, "Received stream_closed for unknown stream ~p. ~p ~p", [StreamID, self(), Streams0], Opts) end, State end. stream_closed1(State=#state{http3_machine=HTTP3Machine0}, StreamID) -> case cow_http3_machine:close_stream(StreamID, HTTP3Machine0) of {ok, HTTP3Machine} -> State#state{http3_machine=HTTP3Machine}; {error, Error={connection_error, _, _}, HTTP3Machine} -> terminate(State#state{http3_machine=HTTP3Machine}, Error) end. stream_store(State=#state{streams=Streams}, Stream=#stream{id=StreamID}) -> State#state{streams=Streams#{StreamID => Stream}}. stream_linger(State=#state{lingering_streams=Lingering0}, StreamID) -> %% We only keep up to 100 streams in this state. @todo Make it configurable? Lingering = [StreamID|lists:sublist(Lingering0, 100 - 1)], State#state{lingering_streams=Lingering}. is_lingering_stream(#state{lingering_streams=Lingering}, StreamID) -> lists:member(StreamID, Lingering). -endif. ================================================ FILE: src/cowboy_loop.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_loop). -behaviour(cowboy_sub_protocol). -export([upgrade/4]). -export([upgrade/5]). -export([loop/5]). -export([system_continue/3]). -export([system_terminate/4]). -export([system_code_change/4]). %% From gen_server. -define(is_timeout(X), ((X) =:= infinity orelse (is_integer(X) andalso (X) >= 0))). -callback init(Req, any()) -> {ok | module(), Req, any()} | {module(), Req, any(), any()} when Req::cowboy_req:req(). -callback info(any(), Req, State) -> {ok, Req, State} | {ok, Req, State, hibernate} | {stop, Req, State} when Req::cowboy_req:req(), State::any(). -callback terminate(any(), cowboy_req:req(), any()) -> ok. -optional_callbacks([terminate/3]). -spec upgrade(Req, Env, module(), any()) -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade(Req, Env, Handler, HandlerState) -> loop(Req, Env, Handler, HandlerState, infinity). -spec upgrade(Req, Env, module(), any(), hibernate | timeout()) -> {suspend, ?MODULE, loop, [any()]} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade(Req, Env, Handler, HandlerState, hibernate) -> suspend(Req, Env, Handler, HandlerState); upgrade(Req, Env, Handler, HandlerState, Timeout) when ?is_timeout(Timeout) -> loop(Req, Env, Handler, HandlerState, Timeout). -spec loop(Req, Env, module(), any(), timeout()) -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). %% @todo Handle system messages. loop(Req=#{pid := Parent}, Env, Handler, HandlerState, Timeout) -> receive %% System messages. {'EXIT', Parent, Reason} -> terminate(Req, Env, Handler, HandlerState, Reason); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Req, Env, Handler, HandlerState, Timeout}); %% Calls from supervisor module. {'$gen_call', From, Call} -> cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE), loop(Req, Env, Handler, HandlerState, Timeout); Message -> call(Req, Env, Handler, HandlerState, Timeout, Message) after Timeout -> call(Req, Env, Handler, HandlerState, Timeout, timeout) end. call(Req0, Env, Handler, HandlerState0, Timeout, Message) -> try Handler:info(Message, Req0, HandlerState0) of {ok, Req, HandlerState} -> loop(Req, Env, Handler, HandlerState, Timeout); {ok, Req, HandlerState, hibernate} -> suspend(Req, Env, Handler, HandlerState); {ok, Req, HandlerState, NewTimeout} when ?is_timeout(NewTimeout) -> loop(Req, Env, Handler, HandlerState, NewTimeout); {stop, Req, HandlerState} -> terminate(Req, Env, Handler, HandlerState, stop) catch Class:Reason:Stacktrace -> cowboy_handler:terminate({crash, Class, Reason}, Req0, HandlerState0, Handler), erlang:raise(Class, Reason, Stacktrace) end. suspend(Req, Env, Handler, HandlerState) -> {suspend, ?MODULE, loop, [Req, Env, Handler, HandlerState, infinity]}. terminate(Req, Env, Handler, HandlerState, Reason) -> Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler), {ok, Req, Env#{result => Result}}. %% System callbacks. -spec system_continue(_, _, {Req, Env, module(), any(), timeout()}) -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). system_continue(_, _, {Req, Env, Handler, HandlerState, Timeout}) -> loop(Req, Env, Handler, HandlerState, Timeout). -spec system_terminate(any(), _, _, {Req, Env, module(), any(), timeout()}) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). system_terminate(Reason, _, _, {Req, Env, Handler, HandlerState, _}) -> terminate(Req, Env, Handler, HandlerState, Reason). -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{cowboy_req:req(), cowboy_middleware:env(), module(), any()}. system_code_change(Misc, _, _, _) -> {ok, Misc}. ================================================ FILE: src/cowboy_metrics_h.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_metrics_h). -behavior(cowboy_stream). -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). -type proc_metrics() :: #{pid() => #{ %% Time at which the process spawned. spawn := integer(), %% Time at which the process exited. exit => integer(), %% Reason for the process exit. reason => any() }}. -type informational_metrics() :: #{ %% Informational response status. status := cowboy:http_status(), %% Headers sent with the informational response. headers := cowboy:http_headers(), %% Time when the informational response was sent. time := integer() }. -type metrics() :: #{ %% The identifier for this listener. ref := ranch:ref(), %% The pid for this connection. pid := pid(), %% The streamid also indicates the total number of requests on %% this connection (StreamID div 2 + 1). streamid := cowboy_stream:streamid(), %% The terminate reason is always useful. reason := cowboy_stream:reason(), %% A filtered Req object or a partial Req object %% depending on how far the request got to. req => cowboy_req:req(), partial_req => cowboy_stream:partial_req(), %% Response status. resp_status := cowboy:http_status(), %% Filtered response headers. resp_headers := cowboy:http_headers(), %% Start/end of the processing of the request. %% %% This represents the time from this stream handler's init %% to terminate. req_start => integer(), req_end => integer(), %% Start/end of the receiving of the request body. %% Begins when the first packet has been received. req_body_start => integer(), req_body_end => integer(), %% Start/end of the sending of the response. %% Begins when we send the headers and ends on the final %% packet of the response body. If everything is sent at %% once these values are identical. resp_start => integer(), resp_end => integer(), %% For early errors all we get is the time we received it. early_error_time => integer(), %% Start/end of spawned processes. This is where most of %% the user code lies, excluding stream handlers. On a %% default Cowboy configuration there should be only one %% process: the request process. procs => proc_metrics(), %% Informational responses sent before the final response. informational => [informational_metrics()], %% Length of the request and response bodies. This does %% not include the framing. req_body_length => non_neg_integer(), resp_body_length => non_neg_integer(), %% Additional metadata set by the user. user_data => map() }. -export_type([metrics/0]). -type metrics_callback() :: fun((metrics()) -> any()). -export_type([metrics_callback/0]). -record(state, { next :: any(), callback :: fun((metrics()) -> any()), resp_headers_filter :: undefined | fun((cowboy:http_headers()) -> cowboy:http_headers()), req :: map(), resp_status :: undefined | cowboy:http_status(), resp_headers :: undefined | cowboy:http_headers(), ref :: ranch:ref(), req_start :: integer(), req_end :: undefined | integer(), req_body_start :: undefined | integer(), req_body_end :: undefined | integer(), resp_start :: undefined | integer(), resp_end :: undefined | integer(), procs = #{} :: proc_metrics(), informational = [] :: [informational_metrics()], req_body_length = 0 :: non_neg_integer(), resp_body_length = 0 :: non_neg_integer(), user_data = #{} :: map() }). -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> {[{spawn, pid(), timeout()}], #state{}}. init(StreamID, Req=#{ref := Ref}, Opts=#{metrics_callback := Fun}) -> ReqStart = erlang:monotonic_time(), {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), FilteredReq = case maps:get(metrics_req_filter, Opts, undefined) of undefined -> Req; ReqFilter -> ReqFilter(Req) end, RespHeadersFilter = maps:get(metrics_resp_headers_filter, Opts, undefined), {Commands, fold(Commands, #state{ next=Next, callback=Fun, resp_headers_filter=RespHeadersFilter, req=FilteredReq, ref=Ref, req_start=ReqStart })}. -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) -> {cowboy_stream:commands(), State} when State::#state{}. data(StreamID, IsFin=fin, Data, State=#state{req_body_start=undefined}) -> ReqBody = erlang:monotonic_time(), do_data(StreamID, IsFin, Data, State#state{ req_body_start=ReqBody, req_body_end=ReqBody, req_body_length=byte_size(Data) }); data(StreamID, IsFin=fin, Data, State=#state{req_body_length=ReqBodyLen}) -> ReqBodyEnd = erlang:monotonic_time(), do_data(StreamID, IsFin, Data, State#state{ req_body_end=ReqBodyEnd, req_body_length=ReqBodyLen + byte_size(Data) }); data(StreamID, IsFin, Data, State=#state{req_body_start=undefined}) -> ReqBodyStart = erlang:monotonic_time(), do_data(StreamID, IsFin, Data, State#state{ req_body_start=ReqBodyStart, req_body_length=byte_size(Data) }); data(StreamID, IsFin, Data, State=#state{req_body_length=ReqBodyLen}) -> do_data(StreamID, IsFin, Data, State#state{ req_body_length=ReqBodyLen + byte_size(Data) }). do_data(StreamID, IsFin, Data, State0=#state{next=Next0}) -> {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), {Commands, fold(Commands, State0#state{next=Next})}. -spec info(cowboy_stream:streamid(), any(), State) -> {cowboy_stream:commands(), State} when State::#state{}. info(StreamID, Info={'EXIT', Pid, Reason}, State0=#state{procs=Procs}) -> ProcEnd = erlang:monotonic_time(), P = maps:get(Pid, Procs), State = State0#state{procs=Procs#{Pid => P#{ exit => ProcEnd, reason => Reason }}}, do_info(StreamID, Info, State); info(StreamID, Info, State) -> do_info(StreamID, Info, State). do_info(StreamID, Info, State0=#state{next=Next0}) -> {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), {Commands, fold(Commands, State0#state{next=Next})}. fold([], State) -> State; fold([{spawn, Pid, _}|Tail], State0=#state{procs=Procs}) -> ProcStart = erlang:monotonic_time(), State = State0#state{procs=Procs#{Pid => #{spawn => ProcStart}}}, fold(Tail, State); fold([{inform, Status, Headers}|Tail], State=#state{informational=Infos}) -> Time = erlang:monotonic_time(), fold(Tail, State#state{informational=[#{ status => Status, headers => Headers, time => Time }|Infos]}); fold([{response, Status, Headers, Body}|Tail], State=#state{resp_headers_filter=RespHeadersFilter}) -> Resp = erlang:monotonic_time(), fold(Tail, State#state{ resp_status=Status, resp_headers=case RespHeadersFilter of undefined -> Headers; _ -> RespHeadersFilter(Headers) end, resp_start=Resp, resp_end=Resp, resp_body_length=resp_body_length(Body) }); fold([{error_response, Status, Headers, Body}|Tail], State=#state{resp_status=RespStatus}) -> %% The error_response command only results in a response %% if no response was sent before. case RespStatus of undefined -> fold([{response, Status, Headers, Body}|Tail], State); _ -> fold(Tail, State) end; fold([{headers, Status, Headers}|Tail], State=#state{resp_headers_filter=RespHeadersFilter}) -> RespStart = erlang:monotonic_time(), fold(Tail, State#state{ resp_status=Status, resp_headers=case RespHeadersFilter of undefined -> Headers; _ -> RespHeadersFilter(Headers) end, resp_start=RespStart }); %% @todo It might be worthwhile to keep the sendfile information around, %% especially if these frames ultimately result in a sendfile syscall. fold([{data, nofin, Data}|Tail], State=#state{resp_body_length=RespBodyLen}) -> fold(Tail, State#state{ resp_body_length=RespBodyLen + resp_body_length(Data) }); fold([{data, fin, Data}|Tail], State=#state{resp_body_length=RespBodyLen}) -> RespEnd = erlang:monotonic_time(), fold(Tail, State#state{ resp_end=RespEnd, resp_body_length=RespBodyLen + resp_body_length(Data) }); fold([{set_options, SetOpts}|Tail], State0=#state{user_data=OldUserData}) -> State = case SetOpts of #{metrics_user_data := NewUserData} -> State0#state{user_data=maps:merge(OldUserData, NewUserData)}; _ -> State0 end, fold(Tail, State); fold([_|Tail], State) -> fold(Tail, State). -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). terminate(StreamID, Reason, #state{next=Next, callback=Fun, req=Req, resp_status=RespStatus, resp_headers=RespHeaders, ref=Ref, req_start=ReqStart, req_body_start=ReqBodyStart, req_body_end=ReqBodyEnd, resp_start=RespStart, resp_end=RespEnd, procs=Procs, informational=Infos, user_data=UserData, req_body_length=ReqBodyLen, resp_body_length=RespBodyLen}) -> Res = cowboy_stream:terminate(StreamID, Reason, Next), ReqEnd = erlang:monotonic_time(), Metrics = #{ ref => Ref, pid => self(), streamid => StreamID, reason => Reason, req => Req, resp_status => RespStatus, resp_headers => RespHeaders, req_start => ReqStart, req_end => ReqEnd, req_body_start => ReqBodyStart, req_body_end => ReqBodyEnd, resp_start => RespStart, resp_end => RespEnd, procs => Procs, informational => lists:reverse(Infos), req_body_length => ReqBodyLen, resp_body_length => RespBodyLen, user_data => UserData }, Fun(Metrics), Res. -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp when Resp::cowboy_stream:resp_command(). early_error(StreamID, Reason, PartialReq=#{ref := Ref}, Resp0, Opts=#{metrics_callback := Fun}) -> Time = erlang:monotonic_time(), Resp = {response, RespStatus, RespHeaders, RespBody} = cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp0, Opts), %% As far as metrics go we are limited in what we can provide %% in this case. Metrics = #{ ref => Ref, pid => self(), streamid => StreamID, reason => Reason, partial_req => PartialReq, resp_status => RespStatus, resp_headers => RespHeaders, early_error_time => Time, resp_body_length => resp_body_length(RespBody) }, Fun(Metrics), Resp. resp_body_length({sendfile, _, Len, _}) -> Len; resp_body_length(Data) -> iolist_size(Data). ================================================ FILE: src/cowboy_middleware.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_middleware). -type env() :: #{atom() => any()}. -export_type([env/0]). -callback execute(Req, Env) -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} when Req::cowboy_req:req(), Env::env(). ================================================ FILE: src/cowboy_quicer.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% QUIC transport using the emqx/quicer NIF. -module(cowboy_quicer). -ifdef(COWBOY_QUICER). %% Connection. -export([peername/1]). -export([sockname/1]). -export([peercert/1]). -export([shutdown/2]). %% Streams. -export([start_bidi_stream/2]). -export([start_unidi_stream/2]). -export([setopt/4]). -export([send/3]). -export([send/4]). -export([send_datagram/2]). -export([shutdown_stream/2]). -export([shutdown_stream/4]). %% Messages. -export([handle/1]). %% @todo Make quicer export these types. -type quicer_connection_handle() :: reference(). -export_type([quicer_connection_handle/0]). -type quicer_app_errno() :: non_neg_integer(). -include_lib("quicer/include/quicer.hrl"). %% Connection. -spec peername(quicer_connection_handle()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, any()}. peername(Conn) -> quicer:peername(Conn). -spec sockname(quicer_connection_handle()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, any()}. sockname(Conn) -> quicer:sockname(Conn). -spec peercert(quicer_connection_handle()) -> {ok, public_key:der_encoded()} | {error, any()}. peercert(Conn) -> quicer_nif:peercert(Conn). -spec shutdown(quicer_connection_handle(), quicer_app_errno()) -> ok | {error, any()}. shutdown(Conn, ErrorCode) -> quicer:shutdown_connection(Conn, ?QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, ErrorCode). %% Streams. -spec start_bidi_stream(quicer_connection_handle(), iodata()) -> {ok, cow_http3:stream_id()} | {error, any()}. start_bidi_stream(Conn, InitialData) -> start_stream(Conn, InitialData, ?QUIC_STREAM_OPEN_FLAG_NONE). -spec start_unidi_stream(quicer_connection_handle(), iodata()) -> {ok, cow_http3:stream_id()} | {error, any()}. start_unidi_stream(Conn, InitialData) -> start_stream(Conn, InitialData, ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL). start_stream(Conn, InitialData, OpenFlag) -> case quicer:start_stream(Conn, #{ active => true, open_flag => OpenFlag}) of {ok, StreamRef} -> case quicer:send(StreamRef, InitialData) of {ok, _} -> {ok, StreamID} = quicer:get_stream_id(StreamRef), put({quicer_stream, StreamID}, StreamRef), {ok, StreamID}; Error -> Error end; {error, Reason1, Reason2} -> {error, {Reason1, Reason2}}; Error -> Error end. -spec setopt(quicer_connection_handle(), cow_http3:stream_id(), active, boolean()) -> ok | {error, any()}. setopt(_Conn, StreamID, active, Value) -> StreamRef = get({quicer_stream, StreamID}), quicer:setopt(StreamRef, active, Value). -spec send(quicer_connection_handle(), cow_http3:stream_id(), iodata()) -> ok | {error, any()}. send(Conn, StreamID, Data) -> send(Conn, StreamID, Data, nofin). -spec send(quicer_connection_handle(), cow_http3:stream_id(), iodata(), cow_http:fin()) -> ok | {error, any()}. send(_Conn, StreamID, Data, IsFin) -> StreamRef = get({quicer_stream, StreamID}), Size = iolist_size(Data), case quicer:send(StreamRef, Data, send_flag(IsFin)) of {ok, Size} -> ok; {error, Reason1, Reason2} -> {error, {Reason1, Reason2}}; Error -> Error end. send_flag(nofin) -> ?QUIC_SEND_FLAG_NONE; send_flag(fin) -> ?QUIC_SEND_FLAG_FIN. -spec send_datagram(quicer_connection_handle(), iodata()) -> ok | {error, any()}. send_datagram(Conn, Data) -> %% @todo Fix/ignore the Dialyzer error instead of doing this. DataBin = iolist_to_binary(Data), Size = byte_size(DataBin), case quicer:send_dgram(Conn, DataBin) of {ok, Size} -> ok; %% @todo Handle error cases. Error -> Error end. -spec shutdown_stream(quicer_connection_handle(), cow_http3:stream_id()) -> ok. shutdown_stream(_Conn, StreamID) -> StreamRef = get({quicer_stream, StreamID}), _ = quicer:shutdown_stream(StreamRef), ok. -spec shutdown_stream(quicer_connection_handle(), cow_http3:stream_id(), both | receiving, quicer_app_errno()) -> ok. shutdown_stream(_Conn, StreamID, Dir, ErrorCode) -> StreamRef = get({quicer_stream, StreamID}), _ = quicer:shutdown_stream(StreamRef, shutdown_flag(Dir), ErrorCode, infinity), ok. %% @todo Are these flags correct for what we want? shutdown_flag(both) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT; shutdown_flag(receiving) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE. %% Messages. %% @todo Probably should have the Conn given as argument too? -spec handle({quic, _, _, _}) -> {data, cow_http3:stream_id(), cow_http:fin(), binary()} | {datagram, binary()} | {stream_started, cow_http3:stream_id(), unidi | bidi} | {stream_closed, cow_http3:stream_id(), quicer_app_errno()} | closed | {peer_send_shutdown, cow_http3:stream_id()} | ok | unknown | {socket_error, any()}. handle({quic, Data, StreamRef, #{flags := Flags}}) when is_binary(Data) -> {ok, StreamID} = quicer:get_stream_id(StreamRef), IsFin = case Flags band ?QUIC_RECEIVE_FLAG_FIN of ?QUIC_RECEIVE_FLAG_FIN -> fin; _ -> nofin end, {data, StreamID, IsFin, Data}; %% @todo Match on Conn. handle({quic, Data, _Conn, Flags}) when is_binary(Data), is_integer(Flags) -> {datagram, Data}; %% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED. handle({quic, new_stream, StreamRef, #{flags := Flags}}) -> case quicer:setopt(StreamRef, active, true) of ok -> {ok, StreamID} = quicer:get_stream_id(StreamRef), put({quicer_stream, StreamID}, StreamRef), StreamType = case quicer:is_unidirectional(Flags) of true -> unidi; false -> bidi end, {stream_started, StreamID, StreamType}; {error, Reason} -> {socket_error, Reason} end; %% QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE. handle({quic, stream_closed, StreamRef, #{error := ErrorCode}}) -> {ok, StreamID} = quicer:get_stream_id(StreamRef), {stream_closed, StreamID, ErrorCode}; %% QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE. handle({quic, closed, Conn, _Flags}) -> _ = quicer:close_connection(Conn), closed; %% The following events are currently ignored either because %% I do not know what they do or because we do not need to %% take action. handle({quic, streams_available, _Conn, _Props}) -> ok; handle({quic, dgram_state_changed, _Conn, _Props}) -> ok; %% QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT handle({quic, transport_shutdown, _Conn, _Flags}) -> ok; handle({quic, peer_send_shutdown, StreamRef, undefined}) -> {ok, StreamID} = quicer:get_stream_id(StreamRef), {peer_send_shutdown, StreamID}; handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) -> ok; handle({quic, shutdown, _Conn, success}) -> ok; handle(_Msg) -> unknown. -endif. ================================================ FILE: src/cowboy_req.erl ================================================ %% Copyright (c) Loïc Hoguin %% Copyright (c) Anthony Ramine %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_req). %% Request. -export([method/1]). -export([version/1]). -export([peer/1]). -export([sock/1]). -export([cert/1]). -export([scheme/1]). -export([host/1]). -export([host_info/1]). -export([port/1]). -export([path/1]). -export([path_info/1]). -export([qs/1]). -export([parse_qs/1]). -export([match_qs/2]). -export([uri/1]). -export([uri/2]). -export([binding/2]). -export([binding/3]). -export([bindings/1]). -export([header/2]). -export([header/3]). -export([headers/1]). -export([parse_header/2]). -export([parse_header/3]). -export([filter_cookies/2]). -export([parse_cookies/1]). -export([match_cookies/2]). %% Request body. -export([has_body/1]). -export([body_length/1]). -export([read_body/1]). -export([read_body/2]). -export([read_urlencoded_body/1]). -export([read_urlencoded_body/2]). -export([read_and_match_urlencoded_body/2]). -export([read_and_match_urlencoded_body/3]). %% Multipart. -export([read_part/1]). -export([read_part/2]). -export([read_part_body/1]). -export([read_part_body/2]). %% Response. -export([set_resp_cookie/3]). -export([set_resp_cookie/4]). -export([resp_header/2]). -export([resp_header/3]). -export([resp_headers/1]). -export([set_resp_header/3]). -export([set_resp_headers/2]). -export([has_resp_header/2]). -export([delete_resp_header/2]). -export([set_resp_body/2]). %% @todo set_resp_body/3 with a ContentType or even Headers argument, to set content headers. -export([has_resp_body/1]). -export([inform/2]). -export([inform/3]). -export([reply/2]). -export([reply/3]). -export([reply/4]). -export([stream_reply/2]). -export([stream_reply/3]). %% @todo stream_body/2 (nofin) -export([stream_body/3]). %% @todo stream_events/2 (nofin) -export([stream_events/3]). -export([stream_trailers/2]). -export([push/3]). -export([push/4]). %% Stream handlers. -export([cast/2]). %% Internal. -export([response_headers/2]). -type read_body_opts() :: #{ length => non_neg_integer() | infinity, period => non_neg_integer(), timeout => timeout() }. -export_type([read_body_opts/0]). %% While sendfile allows a Len of 0 that means "everything past Offset", %% Cowboy expects the real length as it is used as metadata. -type resp_body() :: iodata() | {sendfile, non_neg_integer(), non_neg_integer(), file:name_all()}. -export_type([resp_body/0]). -type push_opts() :: #{ method => binary(), scheme => binary(), host => binary(), port => inet:port_number(), qs => binary() }. -export_type([push_opts/0]). -type req() :: #{ %% Public interface. method := binary(), version := cowboy:http_version() | atom(), scheme := binary(), host := binary(), port := inet:port_number(), path := binary(), qs := binary(), headers := cowboy:http_headers(), peer := {inet:ip_address(), inet:port_number()}, sock := {inet:ip_address(), inet:port_number()}, cert := binary() | undefined, %% Private interface. ref := ranch:ref(), pid := pid(), streamid := cowboy_stream:streamid(), host_info => cowboy_router:tokens(), path_info => cowboy_router:tokens(), bindings => cowboy_router:bindings(), has_body := boolean(), body_length := non_neg_integer() | undefined, has_read_body => true, multipart => {binary(), binary()} | done, has_sent_resp => headers | true, resp_cookies => #{iodata() => iodata()}, resp_headers => #{binary() => iodata()}, resp_body => resp_body(), proxy_header => ranch_proxy_header:proxy_info(), media_type => {binary(), binary(), [{binary(), binary()}]}, language => binary() | undefined, charset => binary() | undefined, range => {binary(), binary() | [{non_neg_integer(), non_neg_integer() | infinity} | neg_integer()]}, websocket_version => 7 | 8 | 13, %% The user is encouraged to use the Req to store information %% when no better solution is available. _ => _ }. -export_type([req/0]). %% Request. -spec method(req()) -> binary(). method(#{method := Method}) -> Method. -spec version(req()) -> cowboy:http_version(). version(#{version := Version}) -> Version. -spec peer(req()) -> {inet:ip_address(), inet:port_number()}. peer(#{peer := Peer}) -> Peer. -spec sock(req()) -> {inet:ip_address(), inet:port_number()}. sock(#{sock := Sock}) -> Sock. -spec cert(req()) -> binary() | undefined. cert(#{cert := Cert}) -> Cert. -spec scheme(req()) -> binary(). scheme(#{scheme := Scheme}) -> Scheme. -spec host(req()) -> binary(). host(#{host := Host}) -> Host. %% @todo The host_info is undefined if cowboy_router isn't used. Do we want to crash? -spec host_info(req()) -> cowboy_router:tokens() | undefined. host_info(#{host_info := HostInfo}) -> HostInfo. -spec port(req()) -> inet:port_number(). port(#{port := Port}) -> Port. -spec path(req()) -> binary(). path(#{path := Path}) -> Path. %% @todo The path_info is undefined if cowboy_router isn't used. Do we want to crash? -spec path_info(req()) -> cowboy_router:tokens() | undefined. path_info(#{path_info := PathInfo}) -> PathInfo. -spec qs(req()) -> binary(). qs(#{qs := Qs}) -> Qs. %% @todo Might be useful to limit the number of keys. -spec parse_qs(req()) -> [{binary(), binary() | true}]. parse_qs(#{qs := Qs}) -> try cow_qs:parse_qs(Qs) catch _:_:Stacktrace -> erlang:raise(exit, {request_error, qs, 'Malformed query string; application/x-www-form-urlencoded expected.' }, Stacktrace) end. -spec match_qs(cowboy:fields(), req()) -> map(). match_qs(Fields, Req) -> case filter(Fields, kvlist_to_map(Fields, parse_qs(Req))) of {ok, Map} -> Map; {error, Errors} -> exit({request_error, {match_qs, Errors}, 'Query string validation constraints failed for the reasons provided.'}) end. -spec uri(req()) -> iodata(). uri(Req) -> uri(Req, #{}). -spec uri(req(), map()) -> iodata(). uri(#{scheme := Scheme0, host := Host0, port := Port0, path := Path0, qs := Qs0}, Opts) -> Scheme = case maps:get(scheme, Opts, Scheme0) of S = undefined -> S; S -> iolist_to_binary(S) end, Host = maps:get(host, Opts, Host0), Port = maps:get(port, Opts, Port0), {Path, Qs} = case maps:get(path, Opts, Path0) of <<"*">> -> {<<>>, <<>>}; P -> {P, maps:get(qs, Opts, Qs0)} end, Fragment = maps:get(fragment, Opts, undefined), [uri_host(Scheme, Scheme0, Port, Host), uri_path(Path), uri_qs(Qs), uri_fragment(Fragment)]. uri_host(_, _, _, undefined) -> <<>>; uri_host(Scheme, Scheme0, Port, Host) -> case iolist_size(Host) of 0 -> <<>>; _ -> [uri_scheme(Scheme), <<"//">>, Host, uri_port(Scheme, Scheme0, Port)] end. uri_scheme(undefined) -> <<>>; uri_scheme(Scheme) -> case iolist_size(Scheme) of 0 -> Scheme; _ -> [Scheme, $:] end. uri_port(_, _, undefined) -> <<>>; uri_port(undefined, <<"http">>, 80) -> <<>>; uri_port(undefined, <<"https">>, 443) -> <<>>; uri_port(<<"http">>, _, 80) -> <<>>; uri_port(<<"https">>, _, 443) -> <<>>; uri_port(_, _, Port) -> [$:, integer_to_binary(Port)]. uri_path(undefined) -> <<>>; uri_path(Path) -> Path. uri_qs(undefined) -> <<>>; uri_qs(Qs) -> case iolist_size(Qs) of 0 -> Qs; _ -> [$?, Qs] end. uri_fragment(undefined) -> <<>>; uri_fragment(Fragment) -> case iolist_size(Fragment) of 0 -> Fragment; _ -> [$#, Fragment] end. -ifdef(TEST). uri1_test() -> <<"http://localhost/path">> = iolist_to_binary(uri(#{ scheme => <<"http">>, host => <<"localhost">>, port => 80, path => <<"/path">>, qs => <<>>})), <<"http://localhost:443/path">> = iolist_to_binary(uri(#{ scheme => <<"http">>, host => <<"localhost">>, port => 443, path => <<"/path">>, qs => <<>>})), <<"http://localhost:8080/path">> = iolist_to_binary(uri(#{ scheme => <<"http">>, host => <<"localhost">>, port => 8080, path => <<"/path">>, qs => <<>>})), <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(#{ scheme => <<"http">>, host => <<"localhost">>, port => 8080, path => <<"/path">>, qs => <<"dummy=2785">>})), <<"https://localhost/path">> = iolist_to_binary(uri(#{ scheme => <<"https">>, host => <<"localhost">>, port => 443, path => <<"/path">>, qs => <<>>})), <<"https://localhost:8443/path">> = iolist_to_binary(uri(#{ scheme => <<"https">>, host => <<"localhost">>, port => 8443, path => <<"/path">>, qs => <<>>})), <<"https://localhost:8443/path?dummy=2785">> = iolist_to_binary(uri(#{ scheme => <<"https">>, host => <<"localhost">>, port => 8443, path => <<"/path">>, qs => <<"dummy=2785">>})), ok. uri2_test() -> Req = #{ scheme => <<"http">>, host => <<"localhost">>, port => 8080, path => <<"/path">>, qs => <<"dummy=2785">> }, <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{})), %% Disable individual components. <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => undefined})), <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => undefined})), <<"http://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req, #{port => undefined})), <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => undefined})), <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => undefined})), <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => undefined})), <<"http://localhost:8080">> = iolist_to_binary(uri(Req, #{path => undefined, qs => undefined})), <<>> = iolist_to_binary(uri(Req, #{host => undefined, path => undefined, qs => undefined})), %% Empty values. <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => <<>>})), <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => ""})), <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => [<<>>]})), <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => <<>>})), <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => ""})), <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => [<<>>]})), <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => <<>>})), <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => ""})), <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => [<<>>]})), <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => <<>>})), <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => ""})), <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => [<<>>]})), <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => <<>>})), <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => ""})), <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => [<<>>]})), %% Port is integer() | undefined. {'EXIT', _} = (catch iolist_to_binary(uri(Req, #{port => <<>>}))), {'EXIT', _} = (catch iolist_to_binary(uri(Req, #{port => ""}))), {'EXIT', _} = (catch iolist_to_binary(uri(Req, #{port => [<<>>]}))), %% Update components. <<"https://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => "https"})), <<"http://example.org:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => "example.org"})), <<"http://localhost:123/path?dummy=2785">> = iolist_to_binary(uri(Req, #{port => 123})), <<"http://localhost:8080/custom?dummy=2785">> = iolist_to_binary(uri(Req, #{path => "/custom"})), <<"http://localhost:8080/path?smart=42">> = iolist_to_binary(uri(Req, #{qs => "smart=42"})), <<"http://localhost:8080/path?dummy=2785#intro">> = iolist_to_binary(uri(Req, #{fragment => "intro"})), %% Interesting combinations. <<"http://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req, #{port => 80})), <<"https://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => "https", port => 443})), ok. -endif. -spec binding(atom(), req()) -> any() | undefined. binding(Name, Req) -> binding(Name, Req, undefined). -spec binding(atom(), req(), Default) -> any() | Default when Default::any(). binding(Name, #{bindings := Bindings}, Default) when is_atom(Name) -> case Bindings of #{Name := Value} -> Value; _ -> Default end; binding(Name, _, Default) when is_atom(Name) -> Default. -spec bindings(req()) -> cowboy_router:bindings(). bindings(#{bindings := Bindings}) -> Bindings; bindings(_) -> #{}. -spec header(binary(), req()) -> binary() | undefined. header(Name, Req) -> header(Name, Req, undefined). -spec header(binary(), req(), Default) -> binary() | Default when Default::any(). header(Name, #{headers := Headers}, Default) -> maps:get(Name, Headers, Default). -spec headers(req()) -> cowboy:http_headers(). headers(#{headers := Headers}) -> Headers. -spec parse_header(binary(), Req) -> any() when Req::req(). parse_header(Name = <<"content-length">>, Req) -> parse_header(Name, Req, 0); parse_header(Name = <<"cookie">>, Req) -> parse_header(Name, Req, []); parse_header(Name, Req) -> parse_header(Name, Req, undefined). -spec parse_header(binary(), Req, any()) -> any() when Req::req(). parse_header(Name, Req, Default) -> try parse_header(Name, Req, Default, parse_header_fun(Name)) catch _:_:Stacktrace -> erlang:raise(exit, {request_error, {header, Name}, 'Malformed header. Please consult the relevant specification.' }, Stacktrace) end. parse_header_fun(<<"accept">>) -> fun cow_http_hd:parse_accept/1; parse_header_fun(<<"accept-charset">>) -> fun cow_http_hd:parse_accept_charset/1; parse_header_fun(<<"accept-encoding">>) -> fun cow_http_hd:parse_accept_encoding/1; parse_header_fun(<<"accept-language">>) -> fun cow_http_hd:parse_accept_language/1; parse_header_fun(<<"access-control-request-headers">>) -> fun cow_http_hd:parse_access_control_request_headers/1; parse_header_fun(<<"access-control-request-method">>) -> fun cow_http_hd:parse_access_control_request_method/1; parse_header_fun(<<"authorization">>) -> fun cow_http_hd:parse_authorization/1; parse_header_fun(<<"connection">>) -> fun cow_http_hd:parse_connection/1; parse_header_fun(<<"content-encoding">>) -> fun cow_http_hd:parse_content_encoding/1; parse_header_fun(<<"content-language">>) -> fun cow_http_hd:parse_content_language/1; parse_header_fun(<<"content-length">>) -> fun cow_http_hd:parse_content_length/1; parse_header_fun(<<"content-type">>) -> fun cow_http_hd:parse_content_type/1; parse_header_fun(<<"cookie">>) -> fun cow_cookie:parse_cookie/1; parse_header_fun(<<"expect">>) -> fun cow_http_hd:parse_expect/1; parse_header_fun(<<"if-match">>) -> fun cow_http_hd:parse_if_match/1; parse_header_fun(<<"if-modified-since">>) -> fun cow_http_hd:parse_if_modified_since/1; parse_header_fun(<<"if-none-match">>) -> fun cow_http_hd:parse_if_none_match/1; parse_header_fun(<<"if-range">>) -> fun cow_http_hd:parse_if_range/1; parse_header_fun(<<"if-unmodified-since">>) -> fun cow_http_hd:parse_if_unmodified_since/1; parse_header_fun(<<"max-forwards">>) -> fun cow_http_hd:parse_max_forwards/1; parse_header_fun(<<"origin">>) -> fun cow_http_hd:parse_origin/1; parse_header_fun(<<"proxy-authorization">>) -> fun cow_http_hd:parse_proxy_authorization/1; parse_header_fun(<<"range">>) -> fun cow_http_hd:parse_range/1; parse_header_fun(<<"sec-websocket-extensions">>) -> fun cow_http_hd:parse_sec_websocket_extensions/1; parse_header_fun(<<"sec-websocket-protocol">>) -> fun cow_http_hd:parse_sec_websocket_protocol_req/1; parse_header_fun(<<"sec-websocket-version">>) -> fun cow_http_hd:parse_sec_websocket_version_req/1; parse_header_fun(<<"trailer">>) -> fun cow_http_hd:parse_trailer/1; parse_header_fun(<<"upgrade">>) -> fun cow_http_hd:parse_upgrade/1; parse_header_fun(<<"wt-available-protocols">>) -> fun cow_http_hd:parse_wt_available_protocols/1; parse_header_fun(<<"x-forwarded-for">>) -> fun cow_http_hd:parse_x_forwarded_for/1. parse_header(Name, Req, Default, ParseFun) -> case header(Name, Req) of undefined -> Default; Value -> ParseFun(Value) end. -spec filter_cookies([atom() | binary()], Req) -> Req when Req::req(). filter_cookies(Names0, Req=#{headers := Headers}) -> Names = [if is_atom(N) -> atom_to_binary(N, utf8); true -> N end || N <- Names0], case header(<<"cookie">>, Req) of undefined -> Req; Value0 -> Cookies0 = binary:split(Value0, <<$;>>, [global]), Cookies = lists:filter(fun(Cookie) -> lists:member(cookie_name(Cookie), Names) end, Cookies0), Value = iolist_to_binary(lists:join($;, Cookies)), Req#{headers => Headers#{<<"cookie">> => Value}} end. %% This is a specialized function to extract a cookie name %% regardless of whether the name is valid or not. We skip %% whitespace at the beginning and take whatever's left to %% be the cookie name, up to the = sign. cookie_name(<<$\s, Rest/binary>>) -> cookie_name(Rest); cookie_name(<<$\t, Rest/binary>>) -> cookie_name(Rest); cookie_name(Name) -> cookie_name(Name, <<>>). cookie_name(<<>>, Name) -> Name; cookie_name(<<$=, _/bits>>, Name) -> Name; cookie_name(<>, Acc) -> cookie_name(Rest, <>). -spec parse_cookies(req()) -> [{binary(), binary()}]. parse_cookies(Req) -> parse_header(<<"cookie">>, Req). -spec match_cookies(cowboy:fields(), req()) -> map(). match_cookies(Fields, Req) -> case filter(Fields, kvlist_to_map(Fields, parse_cookies(Req))) of {ok, Map} -> Map; {error, Errors} -> exit({request_error, {match_cookies, Errors}, 'Cookie validation constraints failed for the reasons provided.'}) end. %% Request body. -spec has_body(req()) -> boolean(). has_body(#{has_body := HasBody}) -> HasBody. %% The length may not be known if HTTP/1.1 with a transfer-encoding; %% or HTTP/2 with no content-length header. The length is always %% known once the body has been completely read. -spec body_length(req()) -> undefined | non_neg_integer(). body_length(#{body_length := Length}) -> Length. -spec read_body(Req) -> {ok, binary(), Req} | {more, binary(), Req} when Req::req(). read_body(Req) -> read_body(Req, #{}). -spec read_body(Req, read_body_opts()) -> {ok, binary(), Req} | {more, binary(), Req} when Req::req(). read_body(Req=#{has_body := false}, _) -> {ok, <<>>, Req}; read_body(Req=#{has_read_body := true}, _) -> {ok, <<>>, Req}; read_body(Req, Opts) -> Length = maps:get(length, Opts, 8000000), Period = maps:get(period, Opts, 15000), DefaultTimeout = case Period of infinity -> infinity; %% infinity + 1000 = infinity. _ -> Period + 1000 end, Timeout = maps:get(timeout, Opts, DefaultTimeout), Ref = make_ref(), cast({read_body, self(), Ref, Length, Period}, Req), receive {request_body, Ref, nofin, Body} -> {more, Body, Req}; {request_body, Ref, fin, BodyLength, Body} -> {ok, Body, set_body_length(Req, BodyLength)} after Timeout -> exit(timeout) end. set_body_length(Req=#{headers := Headers}, BodyLength) -> Req#{ headers => Headers#{<<"content-length">> => integer_to_binary(BodyLength)}, body_length => BodyLength, has_read_body => true }. -spec read_urlencoded_body(Req) -> {ok, [{binary(), binary() | true}], Req} when Req::req(). read_urlencoded_body(Req) -> read_urlencoded_body(Req, #{length => 64000, period => 5000}). -spec read_urlencoded_body(Req, read_body_opts()) -> {ok, [{binary(), binary() | true}], Req} when Req::req(). read_urlencoded_body(Req0, Opts) -> case read_body(Req0, Opts) of {ok, Body, Req} -> try {ok, cow_qs:parse_qs(Body), Req} catch _:_:Stacktrace -> erlang:raise(exit, {request_error, urlencoded_body, 'Malformed body; application/x-www-form-urlencoded expected.' }, Stacktrace) end; {more, Body, _} -> Length = maps:get(length, Opts, 64000), if byte_size(Body) < Length -> exit({request_error, timeout, 'The request body was not received within the configured time.'}); true -> exit({request_error, payload_too_large, 'The request body is larger than allowed by configuration.'}) end end. -spec read_and_match_urlencoded_body(cowboy:fields(), Req) -> {ok, map(), Req} when Req::req(). read_and_match_urlencoded_body(Fields, Req) -> read_and_match_urlencoded_body(Fields, Req, #{length => 64000, period => 5000}). -spec read_and_match_urlencoded_body(cowboy:fields(), Req, read_body_opts()) -> {ok, map(), Req} when Req::req(). read_and_match_urlencoded_body(Fields, Req0, Opts) -> {ok, Qs, Req} = read_urlencoded_body(Req0, Opts), case filter(Fields, kvlist_to_map(Fields, Qs)) of {ok, Map} -> {ok, Map, Req}; {error, Errors} -> exit({request_error, {read_and_match_urlencoded_body, Errors}, 'Urlencoded request body validation constraints failed for the reasons provided.'}) end. %% Multipart. -spec read_part(Req) -> {ok, cowboy:http_headers(), Req} | {done, Req} when Req::req(). read_part(Req) -> read_part(Req, #{length => 64000, period => 5000}). -spec read_part(Req, read_body_opts()) -> {ok, cowboy:http_headers(), Req} | {done, Req} when Req::req(). read_part(Req, Opts) -> case maps:is_key(multipart, Req) of true -> {Data, Req2} = stream_multipart(Req, Opts, headers), read_part(Data, Opts, Req2); false -> read_part(init_multipart(Req), Opts) end. read_part(Buffer, Opts, Req=#{multipart := {Boundary, _}}) -> try cow_multipart:parse_headers(Buffer, Boundary) of more -> {Data, Req2} = stream_multipart(Req, Opts, headers), read_part(<< Buffer/binary, Data/binary >>, Opts, Req2); {more, Buffer2} -> {Data, Req2} = stream_multipart(Req, Opts, headers), read_part(<< Buffer2/binary, Data/binary >>, Opts, Req2); {ok, Headers0, Rest} -> Headers = maps:from_list(Headers0), %% Reject multipart content containing duplicate headers. true = map_size(Headers) =:= length(Headers0), {ok, Headers, Req#{multipart => {Boundary, Rest}}}; %% Ignore epilogue. {done, _} -> {done, Req#{multipart => done}} catch _:_:Stacktrace -> erlang:raise(exit, {request_error, {multipart, headers}, 'Malformed body; multipart expected.' }, Stacktrace) end. -spec read_part_body(Req) -> {ok, binary(), Req} | {more, binary(), Req} when Req::req(). read_part_body(Req) -> read_part_body(Req, #{}). -spec read_part_body(Req, read_body_opts()) -> {ok, binary(), Req} | {more, binary(), Req} when Req::req(). read_part_body(Req, Opts) -> case maps:is_key(multipart, Req) of true -> read_part_body(<<>>, Opts, Req, <<>>); false -> read_part_body(init_multipart(Req), Opts) end. read_part_body(Buffer, Opts, Req=#{multipart := {Boundary, _}}, Acc) -> Length = maps:get(length, Opts, 8000000), case byte_size(Acc) > Length of true -> {more, Acc, Req#{multipart => {Boundary, Buffer}}}; false -> {Data, Req2} = stream_multipart(Req, Opts, body), case cow_multipart:parse_body(<< Buffer/binary, Data/binary >>, Boundary) of {ok, Body} -> read_part_body(<<>>, Opts, Req2, << Acc/binary, Body/binary >>); {ok, Body, Rest} -> read_part_body(Rest, Opts, Req2, << Acc/binary, Body/binary >>); done -> {ok, Acc, Req2}; {done, Body} -> {ok, << Acc/binary, Body/binary >>, Req2}; {done, Body, Rest} -> {ok, << Acc/binary, Body/binary >>, Req2#{multipart => {Boundary, Rest}}} end end. init_multipart(Req) -> {<<"multipart">>, _, Params} = parse_header(<<"content-type">>, Req), case lists:keyfind(<<"boundary">>, 1, Params) of {_, Boundary} -> Req#{multipart => {Boundary, <<>>}}; false -> exit({request_error, {multipart, boundary}, 'Missing boundary parameter for multipart media type.'}) end. stream_multipart(Req=#{multipart := done}, _, _) -> {<<>>, Req}; stream_multipart(Req=#{multipart := {_, <<>>}}, Opts, Type) -> case read_body(Req, Opts) of {more, Data, Req2} -> {Data, Req2}; %% We crash when the data ends unexpectedly. {ok, <<>>, _} -> exit({request_error, {multipart, Type}, 'Malformed body; multipart expected.'}); {ok, Data, Req2} -> {Data, Req2} end; stream_multipart(Req=#{multipart := {Boundary, Buffer}}, _, _) -> {Buffer, Req#{multipart => {Boundary, <<>>}}}. %% Response. -spec set_resp_cookie(iodata(), iodata(), Req) -> Req when Req::req(). set_resp_cookie(Name, Value, Req) -> set_resp_cookie(Name, Value, Req, #{}). %% The cookie name cannot contain any of the following characters: %% =,;\s\t\r\n\013\014 %% %% The cookie value cannot contain any of the following characters: %% ,; \t\r\n\013\014 -spec set_resp_cookie(binary(), iodata(), Req, cow_cookie:cookie_opts()) -> Req when Req::req(). set_resp_cookie(Name, Value, Req, Opts) -> Cookie = cow_cookie:setcookie(Name, Value, Opts), RespCookies = maps:get(resp_cookies, Req, #{}), Req#{resp_cookies => RespCookies#{Name => Cookie}}. %% @todo We could add has_resp_cookie and unset_resp_cookie now. -spec set_resp_header(binary(), iodata(), Req) -> Req when Req::req(). set_resp_header(<<"set-cookie">>, _, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); set_resp_header(Name, Value, Req=#{resp_headers := RespHeaders}) -> Req#{resp_headers => RespHeaders#{Name => Value}}; set_resp_header(Name,Value, Req) -> Req#{resp_headers => #{Name => Value}}. -spec set_resp_headers(cowboy:http_headers() | [{binary(), iodata()}], Req) -> Req when Req::req(). set_resp_headers(Headers, Req) when is_list(Headers) -> set_resp_headers_list(Headers, Req, #{}); set_resp_headers(#{<<"set-cookie">> := _}, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); set_resp_headers(Headers, Req=#{resp_headers := RespHeaders}) -> Req#{resp_headers => maps:merge(RespHeaders, Headers)}; set_resp_headers(Headers, Req) -> Req#{resp_headers => Headers}. set_resp_headers_list([], Req, Acc) -> set_resp_headers(Acc, Req); set_resp_headers_list([{<<"set-cookie">>, _}|_], _, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); set_resp_headers_list([{Name, Value}|Tail], Req, Acc) -> case Acc of #{Name := ValueAcc} -> set_resp_headers_list(Tail, Req, Acc#{Name => [ValueAcc, <<", ">>, Value]}); _ -> set_resp_headers_list(Tail, Req, Acc#{Name => Value}) end. -spec resp_header(binary(), req()) -> binary() | undefined. resp_header(Name, Req) -> resp_header(Name, Req, undefined). -spec resp_header(binary(), req(), Default) -> binary() | Default when Default::any(). resp_header(Name, #{resp_headers := Headers}, Default) -> maps:get(Name, Headers, Default); resp_header(_, #{}, Default) -> Default. -spec resp_headers(req()) -> cowboy:http_headers(). resp_headers(#{resp_headers := RespHeaders}) -> RespHeaders; resp_headers(#{}) -> #{}. -spec set_resp_body(resp_body(), Req) -> Req when Req::req(). set_resp_body(Body, Req) -> Req#{resp_body => Body}. -spec has_resp_header(binary(), req()) -> boolean(). has_resp_header(Name, #{resp_headers := RespHeaders}) -> maps:is_key(Name, RespHeaders); has_resp_header(_, _) -> false. -spec has_resp_body(req()) -> boolean(). has_resp_body(#{resp_body := {sendfile, _, _, _}}) -> true; has_resp_body(#{resp_body := RespBody}) -> iolist_size(RespBody) > 0; has_resp_body(_) -> false. -spec delete_resp_header(binary(), Req) -> Req when Req::req(). delete_resp_header(Name, Req=#{resp_headers := RespHeaders}) -> Req#{resp_headers => maps:remove(Name, RespHeaders)}; %% There are no resp headers so we have nothing to delete. delete_resp_header(_, Req) -> Req. -spec inform(cowboy:http_status(), req()) -> ok. inform(Status, Req) -> inform(Status, #{}, Req). -spec inform(cowboy:http_status(), cowboy:http_headers(), req()) -> ok. inform(_, _, #{has_sent_resp := _}) -> exit({response_error, response_already_sent, 'The final response has already been sent.'}); inform(_, #{<<"set-cookie">> := _}, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); inform(Status, Headers, Req) when is_integer(Status); is_binary(Status) -> cast({inform, Status, Headers}, Req). -spec reply(cowboy:http_status(), Req) -> Req when Req::req(). reply(Status, Req) -> reply(Status, #{}, Req). -spec reply(cowboy:http_status(), cowboy:http_headers(), Req) -> Req when Req::req(). reply(Status, Headers, Req=#{resp_body := Body}) -> reply(Status, Headers, Body, Req); reply(Status, Headers, Req) -> reply(Status, Headers, <<>>, Req). -spec reply(cowboy:http_status(), cowboy:http_headers(), resp_body(), Req) -> Req when Req::req(). reply(_, _, _, #{has_sent_resp := _}) -> exit({response_error, response_already_sent, 'The final response has already been sent.'}); reply(_, #{<<"set-cookie">> := _}, _, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); reply(Status, Headers, {sendfile, _, 0, _}, Req) when is_integer(Status); is_binary(Status) -> do_reply(Status, Headers#{ <<"content-length">> => <<"0">> }, <<>>, Req); reply(Status, Headers, SendFile = {sendfile, _, Len, _}, Req) when is_integer(Status); is_binary(Status) -> do_reply(Status, Headers#{ <<"content-length">> => integer_to_binary(Len) }, SendFile, Req); %% 204 responses must not include content-length. 304 responses may %% but only when set explicitly. (RFC7230 3.3.1, RFC7230 3.3.2) %% Neither status code must include a response body. (RFC7230 3.3) reply(Status, Headers, Body, Req) when Status =:= 204; Status =:= 304 -> do_reply_ensure_no_body(Status, Headers, Body, Req); reply(Status = <<"204",_/bits>>, Headers, Body, Req) -> do_reply_ensure_no_body(Status, Headers, Body, Req); reply(Status = <<"304",_/bits>>, Headers, Body, Req) -> do_reply_ensure_no_body(Status, Headers, Body, Req); reply(Status, Headers, Body, Req) when is_integer(Status); is_binary(Status) -> do_reply(Status, Headers#{ <<"content-length">> => integer_to_binary(iolist_size(Body)) }, Body, Req). do_reply_ensure_no_body(Status, Headers, Body, Req) -> case iolist_size(Body) of 0 -> do_reply(Status, Headers, Body, Req); _ -> exit({response_error, payload_too_large, '204 and 304 responses must not include a body. (RFC7230 3.3)'}) end. %% Don't send any body for HEAD responses. While the protocol code is %% supposed to enforce this rule, we prefer to avoid copying too much %% data around if we can avoid it. do_reply(Status, Headers, _, Req=#{method := <<"HEAD">>}) -> cast({response, Status, response_headers(Headers, Req), <<>>}, Req), done_replying(Req, true); do_reply(Status, Headers, Body, Req) -> cast({response, Status, response_headers(Headers, Req), Body}, Req), done_replying(Req, true). done_replying(Req, HasSentResp) -> maps:without([resp_cookies, resp_headers, resp_body], Req#{has_sent_resp => HasSentResp}). -spec stream_reply(cowboy:http_status(), Req) -> Req when Req::req(). stream_reply(Status, Req) -> stream_reply(Status, #{}, Req). -spec stream_reply(cowboy:http_status(), cowboy:http_headers(), Req) -> Req when Req::req(). stream_reply(_, _, #{has_sent_resp := _}) -> exit({response_error, response_already_sent, 'The final response has already been sent.'}); stream_reply(_, #{<<"set-cookie">> := _}, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); %% 204 and 304 responses must NOT send a body. We therefore %% transform the call to a full response and expect the user %% to NOT call stream_body/3 afterwards. (RFC7230 3.3) stream_reply(Status, Headers=#{}, Req) when Status =:= 204; Status =:= 304 -> reply(Status, Headers, <<>>, Req); stream_reply(Status = <<"204",_/bits>>, Headers=#{}, Req) -> reply(Status, Headers, <<>>, Req); stream_reply(Status = <<"304",_/bits>>, Headers=#{}, Req) -> reply(Status, Headers, <<>>, Req); stream_reply(Status, Headers=#{}, Req) when is_integer(Status); is_binary(Status) -> cast({headers, Status, response_headers(Headers, Req)}, Req), done_replying(Req, headers). -spec stream_body(resp_body(), fin | nofin, req()) -> ok. %% Error out if headers were not sent. %% Don't send any body for HEAD responses. stream_body(_, _, #{method := <<"HEAD">>, has_sent_resp := headers}) -> ok; %% Don't send a message if the data is empty, except for the %% very last message with IsFin=fin. When using sendfile this %% is converted to a data tuple, however. stream_body({sendfile, _, 0, _}, nofin, _) -> ok; stream_body({sendfile, _, 0, _}, IsFin=fin, Req=#{has_sent_resp := headers}) -> stream_body({data, self(), IsFin, <<>>}, Req); stream_body({sendfile, O, B, P}, IsFin, Req=#{has_sent_resp := headers}) when is_integer(O), O >= 0, is_integer(B), B > 0 -> stream_body({data, self(), IsFin, {sendfile, O, B, P}}, Req); stream_body(Data, IsFin=nofin, Req=#{has_sent_resp := headers}) when not is_tuple(Data) -> case iolist_size(Data) of 0 -> ok; _ -> stream_body({data, self(), IsFin, Data}, Req) end; stream_body(Data, IsFin, Req=#{has_sent_resp := headers}) when not is_tuple(Data) -> stream_body({data, self(), IsFin, Data}, Req). %% @todo Do we need a timeout? stream_body(Msg, Req=#{pid := Pid}) -> cast(Msg, Req), receive {data_ack, Pid} -> ok end. -spec stream_events(cow_sse:event() | [cow_sse:event()], fin | nofin, req()) -> ok. stream_events(Event, IsFin, Req) when is_map(Event) -> stream_events([Event], IsFin, Req); stream_events(Events, IsFin, Req=#{has_sent_resp := headers}) -> stream_body({data, self(), IsFin, cow_sse:events(Events)}, Req). -spec stream_trailers(cowboy:http_headers(), req()) -> ok. stream_trailers(#{<<"set-cookie">> := _}, _) -> exit({response_error, invalid_header, 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); stream_trailers(Trailers, Req=#{has_sent_resp := headers}) -> cast({trailers, Trailers}, Req). -spec push(iodata(), cowboy:http_headers(), req()) -> ok. push(Path, Headers, Req) -> push(Path, Headers, Req, #{}). %% @todo Optimization: don't send anything at all for HTTP/1.0 and HTTP/1.1. %% @todo Path, Headers, Opts, everything should be in proper binary, %% or normalized when creating the Req object. -spec push(iodata(), cowboy:http_headers(), req(), push_opts()) -> ok. push(_, _, #{has_sent_resp := _}, _) -> exit({response_error, response_already_sent, 'The final response has already been sent.'}); push(Path, Headers, Req=#{scheme := Scheme0, host := Host0, port := Port0}, Opts) -> Method = maps:get(method, Opts, <<"GET">>), Scheme = maps:get(scheme, Opts, Scheme0), Host = maps:get(host, Opts, Host0), Port = maps:get(port, Opts, Port0), Qs = maps:get(qs, Opts, <<>>), cast({push, Method, Scheme, Host, Port, Path, Qs, Headers}, Req). %% Stream handlers. -spec cast(any(), req()) -> ok. cast(Msg, #{pid := Pid, streamid := StreamID}) -> Pid ! {{Pid, StreamID}, Msg}, ok. %% Internal. %% @todo What about set-cookie headers set through set_resp_header or reply? -spec response_headers(Headers, req()) -> Headers when Headers::cowboy:http_headers(). response_headers(Headers0, Req) -> RespHeaders = maps:get(resp_headers, Req, #{}), Headers = maps:merge(#{ <<"date">> => cowboy_clock:rfc1123(), <<"server">> => <<"Cowboy">> }, maps:merge(RespHeaders, Headers0)), %% The set-cookie header is special; we can only send one cookie per header. %% We send the list of values for many cookies in one key of the map, %% and let the protocols deal with it directly. case maps:get(resp_cookies, Req, undefined) of undefined -> Headers; RespCookies -> Headers#{<<"set-cookie">> => maps:values(RespCookies)} end. %% Create map, convert keys to atoms and group duplicate keys into lists. %% Keys that are not found in the user provided list are entirely skipped. %% @todo Can probably be done directly while parsing. kvlist_to_map(Fields, KvList) -> Keys = [case K of {Key, _} -> Key; {Key, _, _} -> Key; Key -> Key end || K <- Fields], kvlist_to_map(Keys, KvList, #{}). kvlist_to_map(_, [], Map) -> Map; kvlist_to_map(Keys, [{Key, Value}|Tail], Map) -> try binary_to_existing_atom(Key, utf8) of Atom -> case lists:member(Atom, Keys) of true -> case maps:find(Atom, Map) of {ok, MapValue} when is_list(MapValue) -> kvlist_to_map(Keys, Tail, Map#{Atom => [Value|MapValue]}); {ok, MapValue} -> kvlist_to_map(Keys, Tail, Map#{Atom => [Value, MapValue]}); error -> kvlist_to_map(Keys, Tail, Map#{Atom => Value}) end; false -> kvlist_to_map(Keys, Tail, Map) end catch error:badarg -> kvlist_to_map(Keys, Tail, Map) end. filter(Fields, Map0) -> filter(Fields, Map0, #{}). %% Loop through fields, if value is missing and no default, %% record the error; else if value is missing and has a %% default, set default; otherwise apply constraints. If %% constraint fails, record the error. %% %% When there is an error at the end, crash. filter([], Map, Errors) -> case maps:size(Errors) of 0 -> {ok, Map}; _ -> {error, Errors} end; filter([{Key, Constraints}|Tail], Map, Errors) -> case maps:find(Key, Map) of {ok, Value} -> filter_constraints(Tail, Map, Errors, Key, Value, Constraints); error -> filter(Tail, Map, Errors#{Key => required}) end; filter([{Key, Constraints, Default}|Tail], Map, Errors) -> case maps:find(Key, Map) of {ok, Value} -> filter_constraints(Tail, Map, Errors, Key, Value, Constraints); error -> filter(Tail, Map#{Key => Default}, Errors) end; filter([Key|Tail], Map, Errors) -> case maps:is_key(Key, Map) of true -> filter(Tail, Map, Errors); false -> filter(Tail, Map, Errors#{Key => required}) end. filter_constraints(Tail, Map, Errors, Key, Value0, Constraints) -> case cowboy_constraints:validate(Value0, Constraints) of {ok, Value} -> filter(Tail, Map#{Key => Value}, Errors); {error, Reason} -> filter(Tail, Map, Errors#{Key => Reason}) end. ================================================ FILE: src/cowboy_rest.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% Originally based on the Webmachine Diagram from Alan Dean and %% Justin Sheehy. -module(cowboy_rest). -behaviour(cowboy_sub_protocol). -export([upgrade/4]). -export([upgrade/5]). -type switch_handler() :: {switch_handler, module()} | {switch_handler, module(), any()}. %% Common handler callbacks. -callback init(Req, any()) -> {ok | module(), Req, any()} | {module(), Req, any(), any()} when Req::cowboy_req:req(). -callback terminate(any(), cowboy_req:req(), any()) -> ok. -optional_callbacks([terminate/3]). %% REST handler callbacks. -callback allowed_methods(Req, State) -> {[binary()], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([allowed_methods/2]). -callback allow_missing_post(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([allow_missing_post/2]). -callback charsets_provided(Req, State) -> {[binary()], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([charsets_provided/2]). -callback content_types_accepted(Req, State) -> {[{'*' | binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([content_types_accepted/2]). -callback content_types_provided(Req, State) -> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([content_types_provided/2]). -callback delete_completed(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([delete_completed/2]). -callback delete_resource(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([delete_resource/2]). -callback expires(Req, State) -> {calendar:datetime() | binary() | undefined, Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([expires/2]). -callback forbidden(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([forbidden/2]). -callback generate_etag(Req, State) -> {binary() | {weak | strong, binary()} | undefined, Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([generate_etag/2]). -callback is_authorized(Req, State) -> {true | {false, iodata()}, Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([is_authorized/2]). -callback is_conflict(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([is_conflict/2]). -callback known_methods(Req, State) -> {[binary()], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([known_methods/2]). -callback languages_provided(Req, State) -> {[binary()], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([languages_provided/2]). -callback last_modified(Req, State) -> {calendar:datetime() | undefined, Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([last_modified/2]). -callback malformed_request(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([malformed_request/2]). -callback moved_permanently(Req, State) -> {{true, iodata()} | false, Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([moved_permanently/2]). -callback moved_temporarily(Req, State) -> {{true, iodata()} | false, Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([moved_temporarily/2]). -callback multiple_choices(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([multiple_choices/2]). -callback options(Req, State) -> {ok, Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([options/2]). -callback previously_existed(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([previously_existed/2]). -callback range_satisfiable(Req, State) -> {boolean() | {false, non_neg_integer() | iodata()}, Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([range_satisfiable/2]). -callback ranges_provided(Req, State) -> {[{binary(), atom()}], Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([ranges_provided/2]). -callback rate_limited(Req, State) -> {{true, non_neg_integer() | calendar:datetime()} | false, Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([rate_limited/2]). -callback resource_exists(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([resource_exists/2]). -callback service_available(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([service_available/2]). -callback uri_too_long(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([uri_too_long/2]). -callback valid_content_headers(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([valid_content_headers/2]). -callback valid_entity_length(Req, State) -> {boolean(), Req, State} | {stop, Req, State} | {switch_handler(), Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([valid_entity_length/2]). -callback variances(Req, State) -> {[binary()], Req, State} when Req::cowboy_req:req(), State::any(). -optional_callbacks([variances/2]). %% End of REST callbacks. Whew! -record(state, { method = undefined :: binary(), %% Handler. handler :: atom(), handler_state :: any(), %% Media type. content_types_p = [] :: [{binary() | {binary(), binary(), [{binary(), binary()}] | '*'}, atom()}], content_type_a :: undefined | {binary() | {binary(), binary(), [{binary(), binary()}] | '*'}, atom()}, %% Language. languages_p = [] :: [binary()], language_a :: undefined | binary(), %% Charset. charsets_p = undefined :: undefined | [binary()], charset_a :: undefined | binary(), %% Range units. ranges_a = [] :: [{binary(), atom()}], %% Whether the resource exists. exists = false :: boolean(), %% Cached resource calls. etag :: undefined | no_call | {strong | weak, binary()}, last_modified :: undefined | no_call | calendar:datetime(), expires :: undefined | no_call | calendar:datetime() | binary() }). -spec upgrade(Req, Env, module(), any()) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade(Req0, Env, Handler, HandlerState0) -> Method = cowboy_req:method(Req0), case service_available(Req0, #state{method=Method, handler=Handler, handler_state=HandlerState0}) of {ok, Req, Result} -> {ok, Req, Env#{result => Result}}; {Mod, Req, HandlerState} -> Mod:upgrade(Req, Env, Handler, HandlerState); {Mod, Req, HandlerState, Opts} -> Mod:upgrade(Req, Env, Handler, HandlerState, Opts) end. -spec upgrade(Req, Env, module(), any(), any()) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). %% cowboy_rest takes no options. upgrade(Req, Env, Handler, HandlerState, _Opts) -> upgrade(Req, Env, Handler, HandlerState). service_available(Req, State) -> expect(Req, State, service_available, true, fun known_methods/2, 503). %% known_methods/2 should return a list of binary methods. known_methods(Req, State=#state{method=Method}) -> case call(Req, State, known_methods) of no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>; Method =:= <<"POST">>; Method =:= <<"PUT">>; Method =:= <<"PATCH">>; Method =:= <<"DELETE">>; Method =:= <<"OPTIONS">> -> uri_too_long(Req, State); no_call -> respond(Req, State, 501); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {List, Req2, State2} -> case lists:member(Method, List) of true -> uri_too_long(Req2, State2); false -> respond(Req2, State2, 501) end end. uri_too_long(Req, State) -> expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414). %% allowed_methods/2 should return a list of binary methods. allowed_methods(Req, State=#state{method=Method}) -> case call(Req, State, allowed_methods) of no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>; Method =:= <<"OPTIONS">> -> Req2 = cowboy_req:set_resp_header(<<"allow">>, <<"HEAD, GET, OPTIONS">>, Req), malformed_request(Req2, State); no_call -> Req2 = cowboy_req:set_resp_header(<<"allow">>, <<"HEAD, GET, OPTIONS">>, Req), respond(Req2, State, 405); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {List, Req2, State2} -> Req3 = cowboy_req:set_resp_header(<<"allow">>, cow_http_hd:allow(List), Req2), case lists:member(Method, List) of true -> malformed_request(Req3, State2); false -> respond(Req3, State2, 405) end end. malformed_request(Req, State) -> expect(Req, State, malformed_request, false, fun is_authorized/2, 400). %% is_authorized/2 should return true or {false, WwwAuthenticateHeader}. is_authorized(Req, State) -> case call(Req, State, is_authorized) of no_call -> forbidden(Req, State); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {true, Req2, State2} -> forbidden(Req2, State2); {{false, AuthHead}, Req2, State2} -> Req3 = cowboy_req:set_resp_header( <<"www-authenticate">>, AuthHead, Req2), respond(Req3, State2, 401) end. forbidden(Req, State) -> expect(Req, State, forbidden, false, fun rate_limited/2, 403). rate_limited(Req, State) -> case call(Req, State, rate_limited) of no_call -> valid_content_headers(Req, State); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {false, Req2, State2} -> valid_content_headers(Req2, State2); {{true, RetryAfter0}, Req2, State2} -> RetryAfter = if is_integer(RetryAfter0), RetryAfter0 >= 0 -> integer_to_binary(RetryAfter0); is_tuple(RetryAfter0) -> cowboy_clock:rfc1123(RetryAfter0) end, Req3 = cowboy_req:set_resp_header(<<"retry-after">>, RetryAfter, Req2), respond(Req3, State2, 429) end. valid_content_headers(Req, State) -> expect(Req, State, valid_content_headers, true, fun valid_entity_length/2, 501). valid_entity_length(Req, State) -> expect(Req, State, valid_entity_length, true, fun options/2, 413). %% If you need to add additional headers to the response at this point, %% you should do it directly in the options/2 call using set_resp_headers. options(Req, State=#state{method= <<"OPTIONS">>}) -> case call(Req, State, options) of no_call -> respond(Req, State, 200); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {ok, Req2, State2} -> respond(Req2, State2, 200) end; options(Req, State) -> content_types_provided(Req, State). %% content_types_provided/2 should return a list of content types and their %% associated callback function as a tuple: {{Type, SubType, Params}, Fun}. %% Type and SubType are the media type as binary. Params is a list of %% Key/Value tuple, with Key and Value a binary. Fun is the name of the %% callback that will be used to return the content of the response. It is %% given as an atom. %% %% An example of such return value would be: %% {{<<"text">>, <<"html">>, []}, to_html} %% %% Note that it is also possible to return a binary content type that will %% then be parsed by Cowboy. However note that while this may make your %% resources a little more readable, this is a lot less efficient. %% %% An example of such return value would be: %% {<<"text/html">>, to_html} content_types_provided(Req, State) -> case call(Req, State, content_types_provided) of no_call -> State2 = State#state{ content_types_p=[{{<<"text">>, <<"html">>, '*'}, to_html}]}, try cowboy_req:parse_header(<<"accept">>, Req) of undefined -> languages_provided( Req#{media_type => {<<"text">>, <<"html">>, []}}, State2#state{content_type_a={{<<"text">>, <<"html">>, []}, to_html}}); Accept -> choose_media_type(Req, State2, prioritize_accept(Accept)) catch _:_ -> respond(Req, State2, 400) end; {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {[], Req2, State2} -> not_acceptable(Req2, State2); {CTP, Req2, State2} -> CTP2 = [normalize_content_types(P, provide) || P <- CTP], State3 = State2#state{content_types_p=CTP2}, try cowboy_req:parse_header(<<"accept">>, Req2) of undefined -> {PMT0, _Fun} = HeadCTP = hd(CTP2), %% We replace the wildcard by an empty list of parameters. PMT = case PMT0 of {Type, SubType, '*'} -> {Type, SubType, []}; _ -> PMT0 end, languages_provided( Req2#{media_type => PMT}, State3#state{content_type_a=HeadCTP}); Accept -> choose_media_type(Req2, State3, prioritize_accept(Accept)) catch _:_ -> respond(Req2, State3, 400) end end. normalize_content_types({ContentType, Callback}, _) when is_binary(ContentType) -> {cow_http_hd:parse_content_type(ContentType), Callback}; normalize_content_types(Normalized = {{Type, SubType, _}, _}, _) when is_binary(Type), is_binary(SubType) -> Normalized; %% Wildcard for content_types_accepted. normalize_content_types(Normalized = {'*', _}, accept) -> Normalized. prioritize_accept(Accept) -> lists:sort( fun ({MediaTypeA, Quality, _AcceptParamsA}, {MediaTypeB, Quality, _AcceptParamsB}) -> %% Same quality, check precedence in more details. prioritize_mediatype(MediaTypeA, MediaTypeB); ({_MediaTypeA, QualityA, _AcceptParamsA}, {_MediaTypeB, QualityB, _AcceptParamsB}) -> %% Just compare the quality. QualityA > QualityB end, Accept). %% Media ranges can be overridden by more specific media ranges or %% specific media types. If more than one media range applies to a given %% type, the most specific reference has precedence. %% %% We always choose B over A when we can't decide between the two. prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) -> case TypeB of TypeA -> case SubTypeB of SubTypeA -> length(ParamsA) > length(ParamsB); <<"*">> -> true; _Any -> false end; <<"*">> -> true; _Any -> false end. %% Ignoring the rare AcceptParams. Not sure what should be done about them. choose_media_type(Req, State, []) -> not_acceptable(Req, State); choose_media_type(Req, State=#state{content_types_p=CTP}, [MediaType|Tail]) -> match_media_type(Req, State, Tail, CTP, MediaType). match_media_type(Req, State, Accept, [], _MediaType) -> choose_media_type(Req, State, Accept); match_media_type(Req, State, Accept, CTP, MediaType = {{<<"*">>, <<"*">>, _Params_A}, _QA, _APA}) -> match_media_type_params(Req, State, Accept, CTP, MediaType); match_media_type(Req, State, Accept, CTP = [{{Type, SubType_P, _PP}, _Fun}|_Tail], MediaType = {{Type, SubType_A, _PA}, _QA, _APA}) when SubType_P =:= SubType_A; SubType_A =:= <<"*">> -> match_media_type_params(Req, State, Accept, CTP, MediaType); match_media_type(Req, State, Accept, [_Any|Tail], MediaType) -> match_media_type(Req, State, Accept, Tail, MediaType). match_media_type_params(Req, State, Accept, [Provided = {{TP, STP, '*'}, _Fun}|Tail], MediaType = {{TA, _STA, Params_A0}, _QA, _APA}) -> case lists:keytake(<<"charset">>, 1, Params_A0) of {value, {_, Charset}, Params_A} when TA =:= <<"text">> -> %% When we match against a wildcard, the media type is text %% and has a charset parameter, we call charsets_provided %% and check that the charset is provided. If the callback %% is not exported, we accept inconditionally but ignore %% the given charset so as to not send a wrong value back. case call(Req, State, charsets_provided) of no_call -> languages_provided(Req#{media_type => {TP, STP, Params_A0}}, State#state{content_type_a=Provided}); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {CP, Req2, State2} -> State3 = State2#state{charsets_p=CP}, case lists:member(Charset, CP) of false -> match_media_type(Req2, State3, Accept, Tail, MediaType); true -> languages_provided(Req2#{media_type => {TP, STP, Params_A}}, State3#state{content_type_a=Provided, charset_a=Charset}) end end; _ -> languages_provided(Req#{media_type => {TP, STP, Params_A0}}, State#state{content_type_a=Provided}) end; match_media_type_params(Req, State, Accept, [Provided = {PMT = {TP, STP, Params_P0}, Fun}|Tail], MediaType = {{_TA, _STA, Params_A}, _QA, _APA}) -> case lists:sort(Params_P0) =:= lists:sort(Params_A) of true when TP =:= <<"text">> -> %% When a charset was provided explicitly in both the charset header %% and the media types provided and the negotiation is successful, %% we keep the charset and don't call charsets_provided. This only %% applies to text media types, however. {Charset, Params_P} = case lists:keytake(<<"charset">>, 1, Params_P0) of false -> {undefined, Params_P0}; {value, {_, Charset0}, Params_P1} -> {Charset0, Params_P1} end, languages_provided(Req#{media_type => {TP, STP, Params_P}}, State#state{content_type_a={{TP, STP, Params_P}, Fun}, charset_a=Charset}); true -> languages_provided(Req#{media_type => PMT}, State#state{content_type_a=Provided}); false -> match_media_type(Req, State, Accept, Tail, MediaType) end. %% languages_provided should return a list of binary values indicating %% which languages are accepted by the resource. %% %% @todo I suppose we should also ask the resource if it wants to %% set a language itself or if it wants it to be automatically chosen. languages_provided(Req, State) -> case call(Req, State, languages_provided) of no_call -> charsets_provided(Req, State); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {[], Req2, State2} -> not_acceptable(Req2, State2); {LP, Req2, State2} -> State3 = State2#state{languages_p=LP}, case cowboy_req:parse_header(<<"accept-language">>, Req2) of undefined -> set_language(Req2, State3#state{language_a=hd(LP)}); AcceptLanguage -> AcceptLanguage2 = prioritize_languages(AcceptLanguage), choose_language(Req2, State3, AcceptLanguage2) end end. %% A language-range matches a language-tag if it exactly equals the tag, %% or if it exactly equals a prefix of the tag such that the first tag %% character following the prefix is "-". The special range "*", if %% present in the Accept-Language field, matches every tag not matched %% by any other range present in the Accept-Language field. %% %% @todo The last sentence probably means we should always put '*' %% at the end of the list. prioritize_languages(AcceptLanguages) -> lists:sort( fun ({_TagA, QualityA}, {_TagB, QualityB}) -> QualityA > QualityB end, AcceptLanguages). choose_language(Req, State, []) -> not_acceptable(Req, State); choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) -> match_language(Req, State, Tail, LP, Language). match_language(Req, State, Accept, [], _Language) -> choose_language(Req, State, Accept); match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) -> set_language(Req, State#state{language_a=Provided}); match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) -> set_language(Req, State#state{language_a=Provided}); match_language(Req, State, Accept, [Provided|Tail], Language = {Tag, _Quality}) -> Length = byte_size(Tag), case Provided of << Tag:Length/binary, $-, _Any/bits >> -> set_language(Req, State#state{language_a=Provided}); _Any -> match_language(Req, State, Accept, Tail, Language) end. set_language(Req, State=#state{language_a=Language}) -> Req2 = cowboy_req:set_resp_header(<<"content-language">>, Language, Req), charsets_provided(Req2#{language => Language}, State). %% charsets_provided should return a list of binary values indicating %% which charsets are accepted by the resource. %% %% A charset may have been selected while negotiating the accept header. %% There's no need to select one again. charsets_provided(Req, State=#state{charset_a=Charset}) when Charset =/= undefined -> set_content_type(Req, State); %% If charsets_p is defined, use it instead of calling charsets_provided %% again. We also call this clause during normal execution to avoid %% duplicating code. charsets_provided(Req, State=#state{charsets_p=[]}) -> not_acceptable(Req, State); charsets_provided(Req, State=#state{charsets_p=CP}) when CP =/= undefined -> case cowboy_req:parse_header(<<"accept-charset">>, Req) of undefined -> set_content_type(Req, State#state{charset_a=hd(CP)}); AcceptCharset0 -> AcceptCharset = prioritize_charsets(AcceptCharset0), choose_charset(Req, State, AcceptCharset) end; charsets_provided(Req, State) -> case call(Req, State, charsets_provided) of no_call -> set_content_type(Req, State); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {CP, Req2, State2} -> charsets_provided(Req2, State2#state{charsets_p=CP}) end. prioritize_charsets(AcceptCharsets) -> lists:sort( fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) -> QualityA > QualityB end, AcceptCharsets). choose_charset(Req, State, []) -> not_acceptable(Req, State); %% A q-value of 0 means not acceptable. choose_charset(Req, State, [{_, 0}|Tail]) -> choose_charset(Req, State, Tail); choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) -> match_charset(Req, State, Tail, CP, Charset). match_charset(Req, State, Accept, [], _Charset) -> choose_charset(Req, State, Accept); match_charset(Req, State, _Accept, [Provided|_], {<<"*">>, _}) -> set_content_type(Req, State#state{charset_a=Provided}); match_charset(Req, State, _Accept, [Provided|_], {Provided, _}) -> set_content_type(Req, State#state{charset_a=Provided}); match_charset(Req, State, Accept, [_|Tail], Charset) -> match_charset(Req, State, Accept, Tail, Charset). set_content_type(Req, State=#state{ content_type_a={{Type, SubType, Params}, _Fun}, charset_a=Charset}) -> ParamsBin = set_content_type_build_params(Params, []), ContentType = [Type, <<"/">>, SubType, ParamsBin], ContentType2 = case {Type, Charset} of {<<"text">>, Charset} when Charset =/= undefined -> [ContentType, <<"; charset=">>, Charset]; _ -> ContentType end, Req2 = cowboy_req:set_resp_header(<<"content-type">>, ContentType2, Req), encodings_provided(Req2#{charset => Charset}, State). set_content_type_build_params('*', []) -> <<>>; set_content_type_build_params([], []) -> <<>>; set_content_type_build_params([], Acc) -> lists:reverse(Acc); set_content_type_build_params([{Attr, Value}|Tail], Acc) -> set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]). %% @todo Match for identity as we provide nothing else for now. %% @todo Don't forget to set the Content-Encoding header when we reply a body %% and the found encoding is something other than identity. encodings_provided(Req, State) -> ranges_provided(Req, State). not_acceptable(Req, State) -> respond(Req, State, 406). ranges_provided(Req, State) -> case call(Req, State, ranges_provided) of no_call -> variances(Req, State); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {[], Req2, State2} -> Req3 = cowboy_req:set_resp_header(<<"accept-ranges">>, <<"none">>, Req2), variances(Req3, State2#state{ranges_a=[]}); {RP, Req2, State2} -> <<", ", AcceptRanges/binary>> = <<<<", ", R/binary>> || {R, _} <- RP>>, Req3 = cowboy_req:set_resp_header(<<"accept-ranges">>, AcceptRanges, Req2), variances(Req3, State2#state{ranges_a=RP}) end. %% variances/2 should return a list of headers that will be added %% to the Vary response header. The Accept, Accept-Language, %% Accept-Charset and Accept-Encoding headers do not need to be %% specified. %% %% @todo Do Accept-Encoding too when we handle it. %% @todo Does the order matter? variances(Req, State=#state{content_types_p=CTP, languages_p=LP, charsets_p=CP}) -> Variances = case CTP of [] -> []; [_] -> []; [_|_] -> [<<"accept">>] end, Variances2 = case LP of [] -> Variances; [_] -> Variances; [_|_] -> [<<"accept-language">>|Variances] end, Variances3 = case CP of undefined -> Variances2; [] -> Variances2; [_] -> Variances2; [_|_] -> [<<"accept-charset">>|Variances2] end, try variances(Req, State, Variances3) of {Variances4, Req2, State2} -> case [[<<", ">>, V] || V <- Variances4] of [] -> resource_exists(Req2, State2); [[<<", ">>, H]|Variances5] -> Req3 = cowboy_req:set_resp_header( <<"vary">>, [H|Variances5], Req2), resource_exists(Req3, State2) end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. variances(Req, State, Variances) -> case unsafe_call(Req, State, variances) of no_call -> {Variances, Req, State}; {HandlerVariances, Req2, State2} -> {Variances ++ HandlerVariances, Req2, State2} end. resource_exists(Req, State) -> expect(Req, State, resource_exists, true, fun if_match_exists/2, fun if_match_must_not_exist/2). if_match_exists(Req, State) -> State2 = State#state{exists=true}, case cowboy_req:parse_header(<<"if-match">>, Req) of undefined -> if_unmodified_since_exists(Req, State2); '*' -> if_unmodified_since_exists(Req, State2); ETagsList -> if_match(Req, State2, ETagsList) end. if_match(Req, State, EtagsList) -> try generate_etag(Req, State) of %% Strong Etag comparison: weak Etag never matches. {{weak, _}, Req2, State2} -> precondition_failed(Req2, State2); {Etag, Req2, State2} -> case lists:member(Etag, EtagsList) of true -> if_none_match_exists(Req2, State2); %% Etag may be `undefined' which cannot be a member. false -> precondition_failed(Req2, State2) end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. if_match_must_not_exist(Req, State) -> case cowboy_req:header(<<"if-match">>, Req) of undefined -> is_put_to_missing_resource(Req, State); _ -> precondition_failed(Req, State) end. if_unmodified_since_exists(Req, State) -> try cowboy_req:parse_header(<<"if-unmodified-since">>, Req) of undefined -> if_none_match_exists(Req, State); IfUnmodifiedSince -> if_unmodified_since(Req, State, IfUnmodifiedSince) catch _:_ -> if_none_match_exists(Req, State) end. %% If LastModified is the atom 'no_call', we continue. if_unmodified_since(Req, State, IfUnmodifiedSince) -> try last_modified(Req, State) of {LastModified, Req2, State2} -> case LastModified > IfUnmodifiedSince of true -> precondition_failed(Req2, State2); false -> if_none_match_exists(Req2, State2) end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. if_none_match_exists(Req, State) -> case cowboy_req:parse_header(<<"if-none-match">>, Req) of undefined -> if_modified_since_exists(Req, State); '*' -> precondition_is_head_get(Req, State); EtagsList -> if_none_match(Req, State, EtagsList) end. if_none_match(Req, State, EtagsList) -> try generate_etag(Req, State) of {Etag, Req2, State2} -> case Etag of undefined -> precondition_failed(Req2, State2); Etag -> case is_weak_match(Etag, EtagsList) of true -> precondition_is_head_get(Req2, State2); false -> method(Req2, State2) end end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. %% Weak Etag comparison: only check the opaque tag. is_weak_match(_, []) -> false; is_weak_match({_, Tag}, [{_, Tag}|_]) -> true; is_weak_match(Etag, [_|Tail]) -> is_weak_match(Etag, Tail). precondition_is_head_get(Req, State=#state{method=Method}) when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> not_modified(Req, State); precondition_is_head_get(Req, State) -> precondition_failed(Req, State). if_modified_since_exists(Req, State) -> try cowboy_req:parse_header(<<"if-modified-since">>, Req) of undefined -> method(Req, State); IfModifiedSince -> if_modified_since_now(Req, State, IfModifiedSince) catch _:_ -> method(Req, State) end. if_modified_since_now(Req, State, IfModifiedSince) -> case IfModifiedSince > erlang:universaltime() of true -> method(Req, State); false -> if_modified_since(Req, State, IfModifiedSince) end. if_modified_since(Req, State, IfModifiedSince) -> try last_modified(Req, State) of {undefined, Req2, State2} -> method(Req2, State2); {LastModified, Req2, State2} -> case LastModified > IfModifiedSince of true -> method(Req2, State2); false -> not_modified(Req2, State2) end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. not_modified(Req, State) -> Req2 = cowboy_req:delete_resp_header(<<"content-type">>, Req), try set_resp_etag(Req2, State) of {Req3, State2} -> try set_resp_expires(Req3, State2) of {Req4, State3} -> respond(Req4, State3, 304) catch Class:Reason:Stacktrace -> error_terminate(Req, State2, Class, Reason, Stacktrace) end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. precondition_failed(Req, State) -> respond(Req, State, 412). is_put_to_missing_resource(Req, State=#state{method= <<"PUT">>}) -> moved_permanently(Req, State, fun is_conflict/2); is_put_to_missing_resource(Req, State) -> previously_existed(Req, State). %% moved_permanently/2 should return either false or {true, Location} %% with Location the full new URI of the resource. moved_permanently(Req, State, OnFalse) -> case call(Req, State, moved_permanently) of {{true, Location}, Req2, State2} -> Req3 = cowboy_req:set_resp_header( <<"location">>, Location, Req2), respond(Req3, State2, 301); {false, Req2, State2} -> OnFalse(Req2, State2); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); no_call -> OnFalse(Req, State) end. previously_existed(Req, State) -> expect(Req, State, previously_existed, false, fun (R, S) -> is_post_to_missing_resource(R, S, 404) end, fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end). %% moved_temporarily/2 should return either false or {true, Location} %% with Location the full new URI of the resource. moved_temporarily(Req, State) -> case call(Req, State, moved_temporarily) of {{true, Location}, Req2, State2} -> Req3 = cowboy_req:set_resp_header( <<"location">>, Location, Req2), respond(Req3, State2, 307); {false, Req2, State2} -> is_post_to_missing_resource(Req2, State2, 410); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); no_call -> is_post_to_missing_resource(Req, State, 410) end. is_post_to_missing_resource(Req, State=#state{method= <<"POST">>}, OnFalse) -> allow_missing_post(Req, State, OnFalse); is_post_to_missing_resource(Req, State, OnFalse) -> respond(Req, State, OnFalse). allow_missing_post(Req, State, OnFalse) -> expect(Req, State, allow_missing_post, true, fun accept_resource/2, OnFalse). method(Req, State=#state{method= <<"DELETE">>}) -> delete_resource(Req, State); method(Req, State=#state{method= <<"PUT">>}) -> is_conflict(Req, State); method(Req, State=#state{method=Method}) when Method =:= <<"POST">>; Method =:= <<"PATCH">> -> accept_resource(Req, State); method(Req, State=#state{method=Method}) when Method =:= <<"GET">>; Method =:= <<"HEAD">> -> set_resp_body_etag(Req, State); method(Req, State) -> multiple_choices(Req, State). %% delete_resource/2 should start deleting the resource and return. delete_resource(Req, State) -> expect(Req, State, delete_resource, false, 500, fun delete_completed/2). %% delete_completed/2 indicates whether the resource has been deleted yet. delete_completed(Req, State) -> expect(Req, State, delete_completed, true, fun has_resp_body/2, 202). is_conflict(Req, State) -> expect(Req, State, is_conflict, false, fun accept_resource/2, 409). %% content_types_accepted should return a list of media types and their %% associated callback functions in the same format as content_types_provided. %% %% The callback will then be called and is expected to process the content %% pushed to the resource in the request body. %% %% content_types_accepted SHOULD return a different list %% for each HTTP method. accept_resource(Req, State) -> case call(Req, State, content_types_accepted) of no_call -> respond(Req, State, 415); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {CTA, Req2, State2} -> CTA2 = [normalize_content_types(P, accept) || P <- CTA], try cowboy_req:parse_header(<<"content-type">>, Req2) of %% We do not match against the boundary parameter for multipart. {Type = <<"multipart">>, SubType, Params} -> ContentType = {Type, SubType, lists:keydelete(<<"boundary">>, 1, Params)}, choose_content_type(Req2, State2, ContentType, CTA2); ContentType -> choose_content_type(Req2, State2, ContentType, CTA2) catch _:_ -> respond(Req2, State2, 415) end end. %% The special content type '*' will always match. It can be used as a %% catch-all content type for accepting any kind of request content. %% Note that because it will always match, it should be the last of the %% list of content types, otherwise it'll shadow the ones following. choose_content_type(Req, State, _ContentType, []) -> respond(Req, State, 415); choose_content_type(Req, State, ContentType, [{Accepted, Fun}|_Tail]) when Accepted =:= '*'; Accepted =:= ContentType -> process_content_type(Req, State, Fun); %% The special parameter '*' will always match any kind of content type %% parameters. %% Note that because it will always match, it should be the last of the %% list for specific content type, otherwise it'll shadow the ones following. choose_content_type(Req, State, {Type, SubType, Param}, [{{Type, SubType, AcceptedParam}, Fun}|_Tail]) when AcceptedParam =:= '*'; AcceptedParam =:= Param -> process_content_type(Req, State, Fun); choose_content_type(Req, State, ContentType, [_Any|Tail]) -> choose_content_type(Req, State, ContentType, Tail). process_content_type(Req, State=#state{method=Method, exists=Exists}, Fun) -> try case call(Req, State, Fun) of {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {true, Req2, State2} when Exists -> has_resp_body(Req2, State2); {true, Req2, State2} -> maybe_created(Req2, State2); {false, Req2, State2} -> respond(Req2, State2, 400); {{created, ResURL}, Req2, State2} when Method =:= <<"POST">> -> Req3 = cowboy_req:set_resp_header( <<"location">>, ResURL, Req2), respond(Req3, State2, 201); {{see_other, ResURL}, Req2, State2} when Method =:= <<"POST">> -> Req3 = cowboy_req:set_resp_header( <<"location">>, ResURL, Req2), respond(Req3, State2, 303); {{true, ResURL}, Req2, State2} when Method =:= <<"POST">> -> Req3 = cowboy_req:set_resp_header( <<"location">>, ResURL, Req2), if Exists -> respond(Req3, State2, 303); true -> respond(Req3, State2, 201) end end catch Class:Reason = {case_clause, no_call}:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. %% If PUT was used then the resource has been created at the current URL. %% Otherwise, if a location header has been set then the resource has been %% created at a new URL. If not, send a 200 or 204 as expected from a %% POST or PATCH request. maybe_created(Req, State=#state{method= <<"PUT">>}) -> respond(Req, State, 201); maybe_created(Req, State) -> case cowboy_req:has_resp_header(<<"location">>, Req) of true -> respond(Req, State, 201); false -> has_resp_body(Req, State) end. has_resp_body(Req, State) -> case cowboy_req:has_resp_body(Req) of true -> multiple_choices(Req, State); false -> respond(Req, State, 204) end. %% Set the Etag header if any for the response provided. set_resp_body_etag(Req, State) -> try set_resp_etag(Req, State) of {Req2, State2} -> set_resp_body_last_modified(Req2, State2) catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. %% Set the Last-Modified header if any for the response provided. set_resp_body_last_modified(Req, State) -> try last_modified(Req, State) of {LastModified, Req2, State2} -> case LastModified of LastModified when is_atom(LastModified) -> set_resp_body_expires(Req2, State2); LastModified -> LastModifiedBin = cowboy_clock:rfc1123(LastModified), Req3 = cowboy_req:set_resp_header( <<"last-modified">>, LastModifiedBin, Req2), set_resp_body_expires(Req3, State2) end catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. %% Set the Expires header if any for the response provided. set_resp_body_expires(Req, State) -> try set_resp_expires(Req, State) of {Req2, State2} -> if_range(Req2, State2) catch Class:Reason:Stacktrace -> error_terminate(Req, State, Class, Reason, Stacktrace) end. %% When both the if-range and range headers are set, we perform %% a strong comparison. If it fails, we send a full response. if_range(Req=#{headers := #{<<"if-range">> := _, <<"range">> := _}}, State=#state{etag=Etag}) -> try cowboy_req:parse_header(<<"if-range">>, Req) of %% Strong etag comparison is an exact match with the generate_etag result. Etag={strong, _} -> range(Req, State); %% We cannot do a strong date comparison because we have %% no way of knowing whether the representation changed %% twice during the second covered by the presented %% validator. (RFC7232 2.2.2) _ -> set_resp_body(Req, State) catch _:_ -> set_resp_body(Req, State) end; if_range(Req, State) -> range(Req, State). %% @todo This can probably be moved to if_range directly. range(Req, State=#state{ranges_a=[]}) -> set_resp_body(Req, State); range(Req, State) -> try cowboy_req:parse_header(<<"range">>, Req) of undefined -> set_resp_body(Req, State); %% @todo Maybe change parse_header to return <<"bytes">> in 3.0. {bytes, BytesRange} -> choose_range(Req, State, {<<"bytes">>, BytesRange}); Range -> choose_range(Req, State, Range) catch _:_ -> %% We send a 416 response back when we can't parse the %% range header at all. I'm not sure this is the right %% way to go but at least this can help clients identify %% what went wrong when their range requests never work. range_not_satisfiable(Req, State, undefined) end. choose_range(Req, State=#state{ranges_a=RangesAccepted}, Range={RangeUnit, _}) -> case lists:keyfind(RangeUnit, 1, RangesAccepted) of {_, Callback} -> %% We pass the selected range onward in the Req. range_satisfiable(Req#{range => Range}, State, Callback); false -> set_resp_body(Req, State) end. range_satisfiable(Req, State, Callback) -> case call(Req, State, range_satisfiable) of no_call -> set_ranged_body(Req, State, Callback); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {true, Req2, State2} -> set_ranged_body(Req2, State2, Callback); {false, Req2, State2} -> range_not_satisfiable(Req2, State2, undefined); {{false, Int}, Req2, State2} when is_integer(Int) -> range_not_satisfiable(Req2, State2, [<<"*/">>, integer_to_binary(Int)]); {{false, Iodata}, Req2, State2} when is_binary(Iodata); is_list(Iodata) -> range_not_satisfiable(Req2, State2, Iodata) end. %% When the callback selected is 'auto' and the range unit %% is bytes, we call the normal provide callback and split %% the content automatically. set_ranged_body(Req=#{range := {<<"bytes">>, _}}, State, auto) -> set_ranged_body_auto(Req, State); set_ranged_body(Req, State, Callback) -> set_ranged_body_callback(Req, State, Callback). set_ranged_body_auto(Req, State=#state{handler=Handler, content_type_a={_, Callback}}) -> try case call(Req, State, Callback) of {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {Body, Req2, State2} -> maybe_set_ranged_body_auto(Req2, State2, Body) end catch Class:{case_clause, no_call}:Stacktrace -> error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, 'A callback specified in content_types_provided/2 is not exported.'}, Stacktrace) end. maybe_set_ranged_body_auto(Req=#{range := {_, Ranges}}, State, Body) -> Size = case Body of {sendfile, _, Bytes, _} -> Bytes; _ -> iolist_size(Body) end, Checks = [case Range of {From, infinity} -> From < Size; {From, To} -> (From < Size) andalso (From =< To) andalso (To =< Size); Neg -> (Neg =/= 0) andalso (-Neg < Size) end || Range <- Ranges], case lists:usort(Checks) of [true] -> set_ranged_body_auto(Req, State, Body); _ -> range_not_satisfiable(Req, State, [<<"*/">>, integer_to_binary(Size)]) end. %% We might also want to have some checks about range order, %% number of ranges, and perhaps also join ranges that are %% too close into one contiguous range. Some of these can %% be done before calling the ProvideCallback. set_ranged_body_auto(Req=#{range := {_, Ranges}}, State, Body) -> Parts = [ranged_partition(Range, Body) || Range <- Ranges], case Parts of [OnePart] -> set_one_ranged_body(Req, State, OnePart); _ when is_tuple(Body) -> send_multipart_ranged_body(Req, State, Parts); _ -> set_multipart_ranged_body(Req, State, Parts) end. ranged_partition(Range, {sendfile, Offset0, Bytes0, Path}) -> {From, To, Offset, Bytes} = case Range of {From0, infinity} -> {From0, Bytes0 - 1, Offset0 + From0, Bytes0 - From0}; {From0, To0} -> {From0, To0, Offset0 + From0, 1 + To0 - From0}; Neg -> {Bytes0 + Neg, Bytes0 - 1, Offset0 + Bytes0 + Neg, -Neg} end, {{From, To, Bytes0}, {sendfile, Offset, Bytes, Path}}; ranged_partition(Range, Data0) -> Total = iolist_size(Data0), {From, To, Data} = case Range of {From0, infinity} -> {_, Data1} = cow_iolists:split(From0, Data0), {From0, Total - 1, Data1}; {From0, To0} -> {_, Data1} = cow_iolists:split(From0, Data0), {Data2, _} = cow_iolists:split(To0 - From0 + 1, Data1), {From0, To0, Data2}; Neg -> {_, Data1} = cow_iolists:split(Total + Neg, Data0), {Total + Neg, Total - 1, Data1} end, {{From, To, Total}, Data}. -ifdef(TEST). ranged_partition_test_() -> Tests = [ %% Sendfile with open-ended range. {{0, infinity}, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, {{6, infinity}, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, {{11, infinity}, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, %% Sendfile with open-ended range. Sendfile tuple has an offset originally. {{0, infinity}, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, {{6, infinity}, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, {{11, infinity}, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, %% Sendfile with a specific range. {{0, 11}, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, {{6, 11}, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, {{11, 11}, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, {{1, 10}, {sendfile, 0, 12, "t"}, {{1, 10, 12}, {sendfile, 1, 10, "t"}}}, %% Sendfile with a specific range. Sendfile tuple has an offset originally. {{0, 11}, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, {{6, 11}, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, {{11, 11}, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, {{1, 10}, {sendfile, 3, 12, "t"}, {{1, 10, 12}, {sendfile, 4, 10, "t"}}}, %% Sendfile with negative range. {-12, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, {-6, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, {-1, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, %% Sendfile with negative range. Sendfile tuple has an offset originally. {-12, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, {-6, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, {-1, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, %% Iodata with open-ended range. {{0, infinity}, <<"Hello world!">>, {{0, 11, 12}, <<"Hello world!">>}}, {{6, infinity}, <<"Hello world!">>, {{6, 11, 12}, <<"world!">>}}, {{11, infinity}, <<"Hello world!">>, {{11, 11, 12}, <<"!">>}}, %% Iodata with a specific range. The resulting data is %% wrapped in a list because of how cow_iolists:split/2 works. {{0, 11}, <<"Hello world!">>, {{0, 11, 12}, [<<"Hello world!">>]}}, {{6, 11}, <<"Hello world!">>, {{6, 11, 12}, [<<"world!">>]}}, {{11, 11}, <<"Hello world!">>, {{11, 11, 12}, [<<"!">>]}}, {{1, 10}, <<"Hello world!">>, {{1, 10, 12}, [<<"ello world">>]}}, %% Iodata with negative range. {-12, <<"Hello world!">>, {{0, 11, 12}, <<"Hello world!">>}}, {-6, <<"Hello world!">>, {{6, 11, 12}, <<"world!">>}}, {-1, <<"Hello world!">>, {{11, 11, 12}, <<"!">>}} ], [{iolist_to_binary(io_lib:format("range ~p data ~p", [VR, VD])), fun() -> R = ranged_partition(VR, VD) end} || {VR, VD, R} <- Tests]. -endif. set_ranged_body_callback(Req, State=#state{handler=Handler}, Callback) -> try case call(Req, State, Callback) of {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); %% When we receive a single range, we send it directly. {[OneRange], Req2, State2} -> set_one_ranged_body(Req2, State2, OneRange); %% When we receive multiple ranges we have to send them as multipart/byteranges. %% This also applies to non-bytes units. (RFC7233 A) If users don't want to use %% this for non-bytes units they can always return a single range with a binary %% content-range information. {Ranges, Req2, State2} when length(Ranges) > 1 -> %% We have to check whether there are sendfile tuples in the %% ranges to be sent. If there are we must use stream_reply. HasSendfile = [] =/= [true || {_, {sendfile, _, _, _}} <- Ranges], case HasSendfile of true -> send_multipart_ranged_body(Req2, State2, Ranges); false -> set_multipart_ranged_body(Req2, State2, Ranges) end end catch Class:{case_clause, no_call}:Stacktrace -> error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, 'A callback specified in ranges_provided/2 is not exported.'}, Stacktrace) end. set_one_ranged_body(Req0, State, OneRange) -> {ContentRange, Body} = prepare_range(Req0, OneRange), Req1 = cowboy_req:set_resp_header(<<"content-range">>, ContentRange, Req0), Req = cowboy_req:set_resp_body(Body, Req1), respond(Req, State, 206). set_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> Boundary = cow_multipart:boundary(), ContentType = cowboy_req:resp_header(<<"content-type">>, Req), {FirstContentRange, FirstPartBody} = prepare_range(Req, FirstRange), FirstPartHead = cow_multipart:first_part(Boundary, [ {<<"content-type">>, ContentType}, {<<"content-range">>, FirstContentRange} ]), MoreParts = [begin {NextContentRange, NextPartBody} = prepare_range(Req, NextRange), NextPartHead = cow_multipart:part(Boundary, [ {<<"content-type">>, ContentType}, {<<"content-range">>, NextContentRange} ]), [NextPartHead, NextPartBody] end || NextRange <- MoreRanges], Body = [FirstPartHead, FirstPartBody, MoreParts, cow_multipart:close(Boundary)], Req2 = cowboy_req:set_resp_header(<<"content-type">>, [<<"multipart/byteranges; boundary=">>, Boundary], Req), Req3 = cowboy_req:set_resp_body(Body, Req2), respond(Req3, State, 206). %% Similar to set_multipart_ranged_body except we have to stream %% the data because the parts contain sendfile tuples. send_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> Boundary = cow_multipart:boundary(), ContentType = cowboy_req:resp_header(<<"content-type">>, Req), Req2 = cowboy_req:set_resp_header(<<"content-type">>, [<<"multipart/byteranges; boundary=">>, Boundary], Req), Req3 = cowboy_req:stream_reply(206, Req2), {FirstContentRange, FirstPartBody} = prepare_range(Req, FirstRange), FirstPartHead = cow_multipart:first_part(Boundary, [ {<<"content-type">>, ContentType}, {<<"content-range">>, FirstContentRange} ]), cowboy_req:stream_body(FirstPartHead, nofin, Req3), cowboy_req:stream_body(FirstPartBody, nofin, Req3), _ = [begin {NextContentRange, NextPartBody} = prepare_range(Req, NextRange), NextPartHead = cow_multipart:part(Boundary, [ {<<"content-type">>, ContentType}, {<<"content-range">>, NextContentRange} ]), cowboy_req:stream_body(NextPartHead, nofin, Req3), cowboy_req:stream_body(NextPartBody, nofin, Req3), [NextPartHead, NextPartBody] end || NextRange <- MoreRanges], cowboy_req:stream_body(cow_multipart:close(Boundary), fin, Req3), terminate(Req3, State). prepare_range(#{range := {RangeUnit, _}}, {{From, To, Total0}, Body}) -> Total = case Total0 of '*' -> <<"*">>; _ -> integer_to_binary(Total0) end, ContentRange = [RangeUnit, $\s, integer_to_binary(From), $-, integer_to_binary(To), $/, Total], {ContentRange, Body}; prepare_range(#{range := {RangeUnit, _}}, {RangeData, Body}) -> {[RangeUnit, $\s, RangeData], Body}. %% We send the content-range header when we can on error. range_not_satisfiable(Req, State, undefined) -> respond(Req, State, 416); range_not_satisfiable(Req0=#{range := {RangeUnit, _}}, State, RangeData) -> Req = cowboy_req:set_resp_header(<<"content-range">>, [RangeUnit, $\s, RangeData], Req0), respond(Req, State, 416). %% Set the response headers and call the callback found using %% content_types_provided/2 to obtain the request body and add %% it to the response. set_resp_body(Req, State=#state{handler=Handler, content_type_a={_, Callback}}) -> try case call(Req, State, Callback) of {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {Body, Req2, State2} -> Req3 = cowboy_req:set_resp_body(Body, Req2), multiple_choices(Req3, State2) end catch Class:{case_clause, no_call}:Stacktrace -> error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, 'A callback specified in content_types_provided/2 is not exported.'}, Stacktrace) end. multiple_choices(Req, State) -> expect(Req, State, multiple_choices, false, 200, 300). %% Response utility functions. set_resp_etag(Req, State) -> {Etag, Req2, State2} = generate_etag(Req, State), case Etag of undefined -> {Req2, State2}; Etag -> Req3 = cowboy_req:set_resp_header( <<"etag">>, encode_etag(Etag), Req2), {Req3, State2} end. -spec encode_etag({strong | weak, binary()}) -> iolist(). encode_etag({strong, Etag}) -> [$",Etag,$"]; encode_etag({weak, Etag}) -> ["W/\"",Etag,$"]. set_resp_expires(Req, State) -> {Expires, Req2, State2} = expires(Req, State), case Expires of Expires when is_atom(Expires) -> {Req2, State2}; Expires when is_binary(Expires) -> Req3 = cowboy_req:set_resp_header( <<"expires">>, Expires, Req2), {Req3, State2}; Expires -> ExpiresBin = cowboy_clock:rfc1123(Expires), Req3 = cowboy_req:set_resp_header( <<"expires">>, ExpiresBin, Req2), {Req3, State2} end. %% Info retrieval. No logic. generate_etag(Req, State=#state{etag=no_call}) -> {undefined, Req, State}; generate_etag(Req, State=#state{etag=undefined}) -> case unsafe_call(Req, State, generate_etag) of no_call -> {undefined, Req, State#state{etag=no_call}}; %% We allow the callback to return 'undefined' %% to allow conditionally generating etags. We %% handle 'undefined' the same as if the function %% was not exported. {undefined, Req2, State2} -> {undefined, Req2, State2#state{etag=no_call}}; {Etag, Req2, State2} when is_binary(Etag) -> Etag2 = cow_http_hd:parse_etag(Etag), {Etag2, Req2, State2#state{etag=Etag2}}; {Etag, Req2, State2} -> {Etag, Req2, State2#state{etag=Etag}} end; generate_etag(Req, State=#state{etag=Etag}) -> {Etag, Req, State}. last_modified(Req, State=#state{last_modified=no_call}) -> {undefined, Req, State}; last_modified(Req, State=#state{last_modified=undefined}) -> case unsafe_call(Req, State, last_modified) of no_call -> {undefined, Req, State#state{last_modified=no_call}}; %% We allow the callback to return 'undefined', %% in which case the generated header would be missing %% as if the callback was not called. {undefined, Req2, State2} -> {undefined, Req2, State2#state{last_modified=no_call}}; {LastModified, Req2, State2} -> {LastModified, Req2, State2#state{last_modified=LastModified}} end; last_modified(Req, State=#state{last_modified=LastModified}) -> {LastModified, Req, State}. expires(Req, State=#state{expires=no_call}) -> {undefined, Req, State}; expires(Req, State=#state{expires=undefined}) -> case unsafe_call(Req, State, expires) of no_call -> {undefined, Req, State#state{expires=no_call}}; {Expires, Req2, State2} -> {Expires, Req2, State2#state{expires=Expires}} end; expires(Req, State=#state{expires=Expires}) -> {Expires, Req, State}. %% REST primitives. expect(Req, State, Callback, Expected, OnTrue, OnFalse) -> case call(Req, State, Callback) of no_call -> next(Req, State, OnTrue); {stop, Req2, State2} -> terminate(Req2, State2); {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> switch_handler(Switch, Req2, State2); {Expected, Req2, State2} -> next(Req2, State2, OnTrue); {_Unexpected, Req2, State2} -> next(Req2, State2, OnFalse) end. call(Req0, State=#state{handler=Handler, handler_state=HandlerState0}, Callback) -> case erlang:function_exported(Handler, Callback, 2) of true -> try Handler:Callback(Req0, HandlerState0) of no_call -> no_call; {Result, Req, HandlerState} -> {Result, Req, State#state{handler_state=HandlerState}} catch Class:Reason:Stacktrace -> error_terminate(Req0, State, Class, Reason, Stacktrace) end; false -> no_call end. unsafe_call(Req0, State=#state{handler=Handler, handler_state=HandlerState0}, Callback) -> case erlang:function_exported(Handler, Callback, 2) of false -> no_call; true -> case Handler:Callback(Req0, HandlerState0) of no_call -> no_call; {Result, Req, HandlerState} -> {Result, Req, State#state{handler_state=HandlerState}} end end. next(Req, State, Next) when is_function(Next) -> Next(Req, State); next(Req, State, StatusCode) when is_integer(StatusCode) -> respond(Req, State, StatusCode). respond(Req0, State, StatusCode) -> %% We remove the content-type header when there is no body, %% except when the status code is 200 because it might have %% been intended (for example sending an empty file). Req = case cowboy_req:has_resp_body(Req0) of true when StatusCode =:= 200 -> Req0; true -> Req0; false -> cowboy_req:delete_resp_header(<<"content-type">>, Req0) end, terminate(cowboy_req:reply(StatusCode, Req), State). switch_handler({switch_handler, Mod}, Req, #state{handler_state=HandlerState}) -> {Mod, Req, HandlerState}; switch_handler({switch_handler, Mod, Opts}, Req, #state{handler_state=HandlerState}) -> {Mod, Req, HandlerState, Opts}. -spec error_terminate(cowboy_req:req(), #state{}, atom(), any(), any()) -> no_return(). error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class, Reason, Stacktrace) -> cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler), erlang:raise(Class, Reason, Stacktrace). terminate(Req, #state{handler=Handler, handler_state=HandlerState}) -> %% @todo I don't think the result is used anywhere? Result = cowboy_handler:terminate(normal, Req, HandlerState, Handler), {ok, Req, Result}. ================================================ FILE: src/cowboy_router.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% Routing middleware. %% %% Resolve the handler to be used for the request based on the %% routing information found in the dispatch environment value. %% When found, the handler module and associated data are added to %% the environment as the handler and handler_opts values %% respectively. %% %% If the route cannot be found, processing stops with either %% a 400 or a 404 reply. -module(cowboy_router). -behaviour(cowboy_middleware). -export([compile/1]). -export([execute/2]). -type bindings() :: #{atom() => any()}. -type tokens() :: [binary()]. -export_type([bindings/0]). -export_type([tokens/0]). -type route_match() :: '_' | iodata(). -type route_path() :: {Path::route_match(), Handler::module(), Opts::any()} | {Path::route_match(), cowboy:fields(), Handler::module(), Opts::any()}. -type route_rule() :: {Host::route_match(), Paths::[route_path()]} | {Host::route_match(), cowboy:fields(), Paths::[route_path()]}. -type routes() :: [route_rule()]. -export_type([routes/0]). -type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()]. -type dispatch_path() :: {dispatch_match(), cowboy:fields(), module(), any()}. -type dispatch_rule() :: {Host::dispatch_match(), cowboy:fields(), Paths::[dispatch_path()]}. -opaque dispatch_rules() :: [dispatch_rule()]. -export_type([dispatch_rules/0]). -spec compile(routes()) -> dispatch_rules(). compile(Routes) -> compile(Routes, []). compile([], Acc) -> lists:reverse(Acc); compile([{Host, Paths}|Tail], Acc) -> compile([{Host, [], Paths}|Tail], Acc); compile([{HostMatch, Fields, Paths}|Tail], Acc) -> HostRules = case HostMatch of '_' -> '_'; _ -> compile_host(HostMatch) end, PathRules = compile_paths(Paths, []), Hosts = case HostRules of '_' -> [{'_', Fields, PathRules}]; _ -> [{R, Fields, PathRules} || R <- HostRules] end, compile(Tail, Hosts ++ Acc). compile_host(HostMatch) when is_list(HostMatch) -> compile_host(list_to_binary(HostMatch)); compile_host(HostMatch) when is_binary(HostMatch) -> compile_rules(HostMatch, $., [], [], <<>>). compile_paths([], Acc) -> lists:reverse(Acc); compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) -> compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc); compile_paths([{PathMatch, Fields, Handler, Opts}|Tail], Acc) when is_list(PathMatch) -> compile_paths([{iolist_to_binary(PathMatch), Fields, Handler, Opts}|Tail], Acc); compile_paths([{'_', Fields, Handler, Opts}|Tail], Acc) -> compile_paths(Tail, [{'_', Fields, Handler, Opts}] ++ Acc); compile_paths([{<<"*">>, Fields, Handler, Opts}|Tail], Acc) -> compile_paths(Tail, [{<<"*">>, Fields, Handler, Opts}|Acc]); compile_paths([{<< $/, PathMatch/bits >>, Fields, Handler, Opts}|Tail], Acc) -> PathRules = compile_rules(PathMatch, $/, [], [], <<>>), Paths = [{lists:reverse(R), Fields, Handler, Opts} || R <- PathRules], compile_paths(Tail, Paths ++ Acc); compile_paths([{PathMatch, _, _, _}|_], _) -> error({badarg, "The following route MUST begin with a slash: " ++ binary_to_list(PathMatch)}). compile_rules(<<>>, _, Segments, Rules, <<>>) -> [Segments|Rules]; compile_rules(<<>>, _, Segments, Rules, Acc) -> [[Acc|Segments]|Rules]; compile_rules(<< S, Rest/bits >>, S, Segments, Rules, <<>>) -> compile_rules(Rest, S, Segments, Rules, <<>>); compile_rules(<< S, Rest/bits >>, S, Segments, Rules, Acc) -> compile_rules(Rest, S, [Acc|Segments], Rules, <<>>); %% Colon on path segment start is special, otherwise allow. compile_rules(<< $:, Rest/bits >>, S, Segments, Rules, <<>>) -> {NameBin, Rest2} = compile_binding(Rest, S, <<>>), Name = binary_to_atom(NameBin, utf8), compile_rules(Rest2, S, Segments, Rules, Name); compile_rules(<< $[, $., $., $., $], Rest/bits >>, S, Segments, Rules, Acc) when Acc =:= <<>> -> compile_rules(Rest, S, ['...'|Segments], Rules, Acc); compile_rules(<< $[, $., $., $., $], Rest/bits >>, S, Segments, Rules, Acc) -> compile_rules(Rest, S, ['...', Acc|Segments], Rules, Acc); compile_rules(<< $[, S, Rest/bits >>, S, Segments, Rules, Acc) -> compile_brackets(Rest, S, [Acc|Segments], Rules); compile_rules(<< $[, Rest/bits >>, S, Segments, Rules, <<>>) -> compile_brackets(Rest, S, Segments, Rules); %% Open bracket in the middle of a segment. compile_rules(<< $[, _/bits >>, _, _, _, _) -> error(badarg); %% Missing an open bracket. compile_rules(<< $], _/bits >>, _, _, _, _) -> error(badarg); compile_rules(<< C, Rest/bits >>, S, Segments, Rules, Acc) -> compile_rules(Rest, S, Segments, Rules, << Acc/binary, C >>). %% Everything past $: until the segment separator ($. for hosts, %% $/ for paths) or $[ or $] or end of binary is the binding name. compile_binding(<<>>, _, <<>>) -> error(badarg); compile_binding(Rest = <<>>, _, Acc) -> {Acc, Rest}; compile_binding(Rest = << C, _/bits >>, S, Acc) when C =:= S; C =:= $[; C =:= $] -> {Acc, Rest}; compile_binding(<< C, Rest/bits >>, S, Acc) -> compile_binding(Rest, S, << Acc/binary, C >>). compile_brackets(Rest, S, Segments, Rules) -> {Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0), Rules1 = compile_rules(Rest2, S, Segments, [], <<>>), Rules2 = compile_rules(<< Bracket/binary, Rest2/binary >>, S, Segments, [], <<>>), Rules ++ Rules2 ++ Rules1. %% Missing a close bracket. compile_brackets_split(<<>>, _, _) -> error(badarg); %% Make sure we don't confuse the closing bracket we're looking for. compile_brackets_split(<< C, Rest/bits >>, Acc, N) when C =:= $[ -> compile_brackets_split(Rest, << Acc/binary, C >>, N + 1); compile_brackets_split(<< C, Rest/bits >>, Acc, N) when C =:= $], N > 0 -> compile_brackets_split(Rest, << Acc/binary, C >>, N - 1); %% That's the right one. compile_brackets_split(<< $], Rest/bits >>, Acc, 0) -> {Acc, Rest}; compile_brackets_split(<< C, Rest/bits >>, Acc, N) -> compile_brackets_split(Rest, << Acc/binary, C >>, N). -spec execute(Req, Env) -> {ok, Req, Env} | {stop, Req} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). execute(Req=#{host := Host, path := Path}, Env=#{dispatch := Dispatch0}) -> Dispatch = case Dispatch0 of {persistent_term, Key} -> persistent_term:get(Key); _ -> Dispatch0 end, case match(Dispatch, Host, Path) of {ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} -> {ok, Req#{ host_info => HostInfo, path_info => PathInfo, bindings => Bindings }, Env#{ handler => Handler, handler_opts => HandlerOpts }}; {error, notfound, host} -> {stop, cowboy_req:reply(400, Req)}; {error, badrequest, path} -> {stop, cowboy_req:reply(400, Req)}; {error, notfound, path} -> {stop, cowboy_req:reply(404, Req)} end. %% Internal. %% Match hostname tokens and path tokens against dispatch rules. %% %% It is typically used for matching tokens for the hostname and path of %% the request against a global dispatch rule for your listener. %% %% Dispatch rules are a list of {Hostname, PathRules} tuples, with %% PathRules being a list of {Path, HandlerMod, HandlerOpts}. %% %% Hostname and Path are match rules and can be either the %% atom '_', which matches everything, `<<"*">>', which match the %% wildcard path, or a list of tokens. %% %% Each token can be either a binary, the atom '_', %% the atom '...' or a named atom. A binary token must match exactly, %% '_' matches everything for a single token, '...' matches %% everything for the rest of the tokens and a named atom will bind the %% corresponding token value and return it. %% %% The list of hostname tokens is reversed before matching. For example, if %% we were to match "www.ninenines.eu", we would first match "eu", then %% "ninenines", then "www". This means that in the context of hostnames, %% the '...' atom matches properly the lower levels of the domain %% as would be expected. %% %% When a result is found, this function will return the handler module and %% options found in the dispatch list, a key-value list of bindings and %% the tokens that were matched by the '...' atom for both the %% hostname and path. -spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary()) -> {ok, module(), any(), bindings(), HostInfo::undefined | tokens(), PathInfo::undefined | tokens()} | {error, notfound, host} | {error, notfound, path} | {error, badrequest, path}. match([], _, _) -> {error, notfound, host}; %% If the host is '_' then there can be no constraints. match([{'_', [], PathMatchs}|_Tail], _, Path) -> match_path(PathMatchs, undefined, Path, #{}); match([{HostMatch, Fields, PathMatchs}|Tail], Tokens, Path) when is_list(Tokens) -> case list_match(Tokens, HostMatch, #{}) of false -> match(Tail, Tokens, Path); {true, Bindings, HostInfo} -> HostInfo2 = case HostInfo of undefined -> undefined; _ -> lists:reverse(HostInfo) end, case check_constraints(Fields, Bindings) of {ok, Bindings2} -> match_path(PathMatchs, HostInfo2, Path, Bindings2); nomatch -> match(Tail, Tokens, Path) end end; match(Dispatch, Host, Path) -> match(Dispatch, split_host(Host), Path). -spec match_path([dispatch_path()], HostInfo::undefined | tokens(), binary() | tokens(), bindings()) -> {ok, module(), any(), bindings(), HostInfo::undefined | tokens(), PathInfo::undefined | tokens()} | {error, notfound, path} | {error, badrequest, path}. match_path([], _, _, _) -> {error, notfound, path}; %% If the path is '_' then there can be no constraints. match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) -> {ok, Handler, Opts, Bindings, HostInfo, undefined}; match_path([{<<"*">>, _, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) -> {ok, Handler, Opts, Bindings, HostInfo, undefined}; match_path([_|Tail], HostInfo, <<"*">>, Bindings) -> match_path(Tail, HostInfo, <<"*">>, Bindings); match_path([{PathMatch, Fields, Handler, Opts}|Tail], HostInfo, Tokens, Bindings) when is_list(Tokens) -> case list_match(Tokens, PathMatch, Bindings) of false -> match_path(Tail, HostInfo, Tokens, Bindings); {true, PathBinds, PathInfo} -> case check_constraints(Fields, PathBinds) of {ok, PathBinds2} -> {ok, Handler, Opts, PathBinds2, HostInfo, PathInfo}; nomatch -> match_path(Tail, HostInfo, Tokens, Bindings) end end; match_path(_Dispatch, _HostInfo, badrequest, _Bindings) -> {error, badrequest, path}; match_path(Dispatch, HostInfo, Path, Bindings) -> match_path(Dispatch, HostInfo, split_path(Path), Bindings). check_constraints([], Bindings) -> {ok, Bindings}; check_constraints([Field|Tail], Bindings) when is_atom(Field) -> check_constraints(Tail, Bindings); check_constraints([Field|Tail], Bindings) -> Name = element(1, Field), case Bindings of #{Name := Value0} -> Constraints = element(2, Field), case cowboy_constraints:validate(Value0, Constraints) of {ok, Value} -> check_constraints(Tail, Bindings#{Name => Value}); {error, _} -> nomatch end; _ -> check_constraints(Tail, Bindings) end. -spec split_host(binary()) -> tokens(). split_host(Host) -> split_host(Host, []). split_host(Host, Acc) -> case binary:match(Host, <<".">>) of nomatch when Host =:= <<>> -> Acc; nomatch -> [Host|Acc]; {Pos, _} -> << Segment:Pos/binary, _:8, Rest/bits >> = Host, false = byte_size(Segment) == 0, split_host(Rest, [Segment|Acc]) end. %% Following RFC2396, this function may return path segments containing any %% character, including / if, and only if, a / was escaped %% and part of a path segment. -spec split_path(binary()) -> tokens() | badrequest. split_path(<< $/, Path/bits >>) -> split_path(Path, []); split_path(_) -> badrequest. split_path(Path, Acc) -> try case binary:match(Path, <<"/">>) of nomatch when Path =:= <<>> -> remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- Acc]), []); nomatch -> remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- [Path|Acc]]), []); {Pos, _} -> << Segment:Pos/binary, _:8, Rest/bits >> = Path, split_path(Rest, [Segment|Acc]) end catch error:_ -> badrequest end. remove_dot_segments([], Acc) -> lists:reverse(Acc); remove_dot_segments([<<".">>|Segments], Acc) -> remove_dot_segments(Segments, Acc); remove_dot_segments([<<"..">>|Segments], Acc=[]) -> remove_dot_segments(Segments, Acc); remove_dot_segments([<<"..">>|Segments], [_|Acc]) -> remove_dot_segments(Segments, Acc); remove_dot_segments([S|Segments], Acc) -> remove_dot_segments(Segments, [S|Acc]). -ifdef(TEST). remove_dot_segments_test_() -> Tests = [ {[<<"a">>, <<"b">>, <<"c">>, <<".">>, <<"..">>, <<"..">>, <<"g">>], [<<"a">>, <<"g">>]}, {[<<"mid">>, <<"content=5">>, <<"..">>, <<"6">>], [<<"mid">>, <<"6">>]}, {[<<"..">>, <<"a">>], [<<"a">>]} ], [fun() -> R = remove_dot_segments(S, []) end || {S, R} <- Tests]. -endif. -spec list_match(tokens(), dispatch_match(), bindings()) -> {true, bindings(), undefined | tokens()} | false. %% Atom '...' matches any trailing path, stop right now. list_match(List, ['...'], Binds) -> {true, Binds, List}; %% Atom '_' matches anything, continue. list_match([_E|Tail], ['_'|TailMatch], Binds) -> list_match(Tail, TailMatch, Binds); %% Both values match, continue. list_match([E|Tail], [E|TailMatch], Binds) -> list_match(Tail, TailMatch, Binds); %% Bind E to the variable name V and continue, %% unless V was already defined and E isn't identical to the previous value. list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) -> case Binds of %% @todo This isn't right, the constraint must be applied FIRST %% otherwise we can't check for example ints in both host/path. #{V := E} -> list_match(Tail, TailMatch, Binds); #{V := _} -> false; _ -> list_match(Tail, TailMatch, Binds#{V => E}) end; %% Match complete. list_match([], [], Binds) -> {true, Binds, undefined}; %% Values don't match, stop. list_match(_List, _Match, _Binds) -> false. %% Tests. -ifdef(TEST). compile_test_() -> Tests = [ %% Match any host and path. {[{'_', [{'_', h, o}]}], [{'_', [], [{'_', [], h, o}]}]}, {[{"cowboy.example.org", [{"/", ha, oa}, {"/path/to/resource", hb, ob}]}], [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [ {[], [], ha, oa}, {[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]}, {[{'_', [{"/path/to/resource/", h, o}]}], [{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]}, % Cyrillic from a latin1 encoded file. {[{'_', [{[47,208,191,209,131,209,130,209,140,47,208,186,47,209,128, 208,181,209,129,209,131,209,128,209,129,209,131,47], h, o}]}], [{'_', [], [{[<<208,191,209,131,209,130,209,140>>, <<208,186>>, <<209,128,208,181,209,129,209,131,209,128,209,129,209,131>>], [], h, o}]}]}, {[{"cowboy.example.org.", [{'_', h, o}]}], [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]}, {[{".cowboy.example.org", [{'_', h, o}]}], [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]}, % Cyrillic from a latin1 encoded file. {[{[208,189,208,181,208,186,208,184,208,185,46,209,129,208,176, 208,185,209,130,46,209,128,209,132,46], [{'_', h, o}]}], [{[<<209,128,209,132>>, <<209,129,208,176,208,185,209,130>>, <<208,189,208,181,208,186,208,184,208,185>>], [], [{'_', [], h, o}]}]}, {[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}], [{[<<"org">>, <<"example">>, subdomain], [], [ {[<<"hats">>, name, <<"prices">>], [], h, o}]}]}, {[{"ninenines.:_", [{"/hats/:_", h, o}]}], [{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]}, {[{"[www.]ninenines.eu", [{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [ {[<<"eu">>, <<"ninenines">>], [], [ {[<<"horses">>], [], h, o}, {[<<"hats">>], [], h, o}, {[<<"hats">>, <<"page">>, number], [], h, o}]}, {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [ {[<<"horses">>], [], h, o}, {[<<"hats">>], [], h, o}, {[<<"hats">>, <<"page">>, number], [], h, o}]}]}, {[{'_', [{"/hats/:page/:number", h, o}]}], [{'_', [], [ {[<<"hats">>, page, number], [], h, o}]}]}, {[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [ {[<<"hats">>], [], h, o}, {[<<"hats">>, <<"page">>], [], h, o}, {[<<"hats">>, <<"page">>, number], [], h, o}]}]}, {[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}], [{[<<"eu">>, <<"ninenines">>, '...'], [], [ {[<<"hats">>, '...'], [], h, o}]}]}, %% Path segment containing a colon. {[{'_', [{"/foo/bar:blah", h, o}]}], [{'_', [], [ {[<<"foo">>, <<"bar:blah">>], [], h, o}]}]} ], [{lists:flatten(io_lib:format("~p", [Rt])), fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests]. split_host_test_() -> Tests = [ {<<"">>, []}, {<<"*">>, [<<"*">>]}, {<<"cowboy.ninenines.eu">>, [<<"eu">>, <<"ninenines">>, <<"cowboy">>]}, {<<"ninenines.eu">>, [<<"eu">>, <<"ninenines">>]}, {<<"ninenines.eu.">>, [<<"eu">>, <<"ninenines">>]}, {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>, [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>, <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>, <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>, <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]} ], [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests]. split_path_test_() -> Tests = [ {<<"/">>, []}, {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]}, {<<"/users">>, [<<"users">>]}, {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]}, {<<"/users/a%20b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]} ], [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests]. match_test_() -> Dispatch = [ {[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [ {[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []} ]}, {[<<"eu">>, <<"ninenines">>], [], [ {[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []}, {'_', [], match_extend, []} ]}, {[var, <<"ninenines">>], [], [ {[<<"threads">>, var], [], match_duplicate_vars, [we, {expect, two}, var, here]} ]}, {[ext, <<"erlang">>], [], [ {'_', [], match_erlang_ext, []} ]}, {'_', [], [ {[<<"users">>, id, <<"friends">>], [], match_users_friends, []}, {'_', [], match_any, []} ]} ], Tests = [ {<<"any">>, <<"/">>, {ok, match_any, [], #{}}}, {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>, {ok, match_any_subdomain_users, [], #{}}}, {<<"www.ninenines.eu">>, <<"/users/42/mails">>, {ok, match_any, [], #{}}}, {<<"www.ninenines.eu">>, <<"/">>, {ok, match_any, [], #{}}}, {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>, {error, notfound, path}}, {<<"ninenines.eu">>, <<"/">>, {ok, match_extend, [], #{}}}, {<<"ninenines.eu">>, <<"/users/42/friends">>, {ok, match_extend_users_friends, [], #{id => <<"42">>}}}, {<<"erlang.fr">>, '_', {ok, match_erlang_ext, [], #{ext => <<"fr">>}}}, {<<"any">>, <<"/users/444/friends">>, {ok, match_users_friends, [], #{id => <<"444">>}}}, {<<"any">>, <<"/users//friends">>, {ok, match_users_friends, [], #{id => <<>>}}} ], [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> {ok, Handler, Opts, Binds, undefined, undefined} = match(Dispatch, H, P) end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests]. match_info_test_() -> Dispatch = [ {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [ {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []} ]}, {[<<"eu">>, <<"ninenines">>, '...'], [], [ {'_', [], match_any, []} ]} ], Tests = [ {<<"ninenines.eu">>, <<"/">>, {ok, match_any, [], #{}, [], undefined}}, {<<"bugs.ninenines.eu">>, <<"/">>, {ok, match_any, [], #{}, [<<"bugs">>], undefined}}, {<<"cowboy.bugs.ninenines.eu">>, <<"/">>, {ok, match_any, [], #{}, [<<"cowboy">>, <<"bugs">>], undefined}}, {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>, {ok, match_path, [], #{}, undefined, []}}, {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>, {ok, match_path, [], #{}, undefined, [<<"path_info">>]}}, {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>, {ok, match_path, [], #{}, undefined, [<<"foo">>, <<"bar">>]}} ], [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> R = match(Dispatch, H, P) end} || {H, P, R} <- Tests]. match_constraints_test() -> Dispatch0 = [{'_', [], [{[<<"path">>, value], [{value, int}], match, []}]}], {ok, _, [], #{value := 123}, _, _} = match(Dispatch0, <<"ninenines.eu">>, <<"/path/123">>), {ok, _, [], #{value := 123}, _, _} = match(Dispatch0, <<"ninenines.eu">>, <<"/path/123/">>), {error, notfound, path} = match(Dispatch0, <<"ninenines.eu">>, <<"/path/NaN/">>), Dispatch1 = [{'_', [], [{[<<"path">>, value, <<"more">>], [{value, nonempty}], match, []}]}], {ok, _, [], #{value := <<"something">>}, _, _} = match(Dispatch1, <<"ninenines.eu">>, <<"/path/something/more">>), {error, notfound, path} = match(Dispatch1, <<"ninenines.eu">>, <<"/path//more">>), Dispatch2 = [{'_', [], [{[<<"path">>, username], [{username, fun(_, Value) -> case cowboy_bstr:to_lower(Value) of Value -> {ok, Value}; _ -> {error, not_lowercase} end end}], match, []}]}], {ok, _, [], #{username := <<"essen">>}, _, _} = match(Dispatch2, <<"ninenines.eu">>, <<"/path/essen">>), {error, notfound, path} = match(Dispatch2, <<"ninenines.eu">>, <<"/path/ESSEN">>), ok. match_same_bindings_test() -> Dispatch = [{[same, same], [], [{'_', [], match, []}]}], {ok, _, [], #{same := <<"eu">>}, _, _} = match(Dispatch, <<"eu.eu">>, <<"/">>), {error, notfound, host} = match(Dispatch, <<"ninenines.eu">>, <<"/">>), Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [], [{[<<"path">>, user], [], match, []}]}], {ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2, <<"essen.ninenines.eu">>, <<"/path/essen">>), {ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2, <<"essen.ninenines.eu">>, <<"/path/essen/">>), {error, notfound, path} = match(Dispatch2, <<"essen.ninenines.eu">>, <<"/path/notessen">>), Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}], {ok, _, [], #{same := <<"path">>}, _, _} = match(Dispatch3, <<"ninenines.eu">>, <<"/path/path">>), {error, notfound, path} = match(Dispatch3, <<"ninenines.eu">>, <<"/path/to">>), ok. -endif. ================================================ FILE: src/cowboy_static.erl ================================================ %% Copyright (c) Loïc Hoguin %% Copyright (c) Magnus Klaar %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_static). -export([init/2]). -export([malformed_request/2]). -export([forbidden/2]). -export([content_types_provided/2]). -export([charsets_provided/2]). -export([ranges_provided/2]). -export([resource_exists/2]). -export([last_modified/2]). -export([generate_etag/2]). -export([get_file/2]). -type extra_charset() :: {charset, module(), function()} | {charset, binary()}. -type extra_etag() :: {etag, module(), function()} | {etag, false}. -type extra_mimetypes() :: {mimetypes, module(), function()} | {mimetypes, binary() | {binary(), binary(), '*' | [{binary(), binary()}]}}. -type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()]. -type opts() :: {file | dir, string() | binary()} | {file | dir, string() | binary(), extra()} | {priv_file | priv_dir, atom(), string() | binary()} | {priv_file | priv_dir, atom(), string() | binary(), extra()}. -export_type([opts/0]). -include_lib("kernel/include/file.hrl"). -type state() :: {binary(), {direct | archive, #file_info{}} | {error, atom()}, extra()}. %% Resolve the file that will be sent and get its file information. %% If the handler is configured to manage a directory, check that the %% requested file is inside the configured directory. -spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req(). init(Req, {Name, Path}) -> init_opts(Req, {Name, Path, []}); init(Req, {Name, App, Path}) when Name =:= priv_file; Name =:= priv_dir -> init_opts(Req, {Name, App, Path, []}); init(Req, Opts) -> init_opts(Req, Opts). init_opts(Req, {priv_file, App, Path, Extra}) -> {PrivPath, HowToAccess} = priv_path(App, Path), init_info(Req, absname(PrivPath), HowToAccess, Extra); init_opts(Req, {file, Path, Extra}) -> init_info(Req, absname(Path), direct, Extra); init_opts(Req, {priv_dir, App, Path, Extra}) -> {PrivPath, HowToAccess} = priv_path(App, Path), init_dir(Req, PrivPath, HowToAccess, Extra); init_opts(Req, {dir, Path, Extra}) -> init_dir(Req, Path, direct, Extra). priv_path(App, Path) -> case code:priv_dir(App) of {error, bad_name} -> error({badarg, "Can't resolve the priv_dir of application " ++ atom_to_list(App)}); PrivDir when is_list(Path) -> { PrivDir ++ "/" ++ Path, how_to_access_app_priv(PrivDir) }; PrivDir when is_binary(Path) -> { << (list_to_binary(PrivDir))/binary, $/, Path/binary >>, how_to_access_app_priv(PrivDir) } end. how_to_access_app_priv(PrivDir) -> %% If the priv directory is not a directory, it must be %% inside an Erlang application .ez archive. We call %% how_to_access_app_priv1() to find the corresponding archive. case filelib:is_dir(PrivDir) of true -> direct; false -> how_to_access_app_priv1(PrivDir) end. how_to_access_app_priv1(Dir) -> %% We go "up" by one path component at a time and look for a %% regular file. Archive = filename:dirname(Dir), case Archive of Dir -> %% filename:dirname() returned its argument: %% we reach the root directory. We found no %% archive so we return 'direct': the given priv %% directory doesn't exist. direct; _ -> case filelib:is_regular(Archive) of true -> {archive, Archive}; false -> how_to_access_app_priv1(Archive) end end. absname(Path) when is_list(Path) -> filename:absname(list_to_binary(Path)); absname(Path) when is_binary(Path) -> filename:absname(Path). init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) -> init_dir(Req, list_to_binary(Path), HowToAccess, Extra); init_dir(Req, Path, HowToAccess, Extra) -> Dir = fullpath(filename:absname(Path)), case cowboy_req:path_info(Req) of %% When dir/priv_dir are used and there is no path_info %% this is a configuration error and we abort immediately. undefined -> {ok, cowboy_req:reply(500, Req), error}; PathInfo -> case validate_reserved(PathInfo) of error -> {cowboy_rest, Req, error}; ok -> Filepath = filename:join([Dir|PathInfo]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> init_info(Req, Filepath, HowToAccess, Extra); << Dir:Len/binary >> -> init_info(Req, Filepath, HowToAccess, Extra); _ -> {cowboy_rest, Req, error} end end end. validate_reserved([]) -> ok; validate_reserved([P|Tail]) -> case validate_reserved1(P) of ok -> validate_reserved(Tail); error -> error end. %% We always reject forward slash, backward slash and NUL as %% those have special meanings across the supported platforms. %% We could support the backward slash on some platforms but %% for the sake of consistency and simplicity we don't. validate_reserved1(<<>>) -> ok; validate_reserved1(<<$/, _/bits>>) -> error; validate_reserved1(<<$\\, _/bits>>) -> error; validate_reserved1(<<0, _/bits>>) -> error; validate_reserved1(<<_, Rest/bits>>) -> validate_reserved1(Rest). fullpath(Path) -> fullpath(filename:split(Path), []). fullpath([], Acc) -> filename:join(lists:reverse(Acc)); fullpath([<<".">>|Tail], Acc) -> fullpath(Tail, Acc); fullpath([<<"..">>|Tail], Acc=[_]) -> fullpath(Tail, Acc); fullpath([<<"..">>|Tail], [_|Acc]) -> fullpath(Tail, Acc); fullpath([Segment|Tail], Acc) -> fullpath(Tail, [Segment|Acc]). init_info(Req, Path, HowToAccess, Extra) -> Info = read_file_info(Path, HowToAccess), {cowboy_rest, Req, {Path, Info, Extra}}. read_file_info(Path, direct) -> case file:read_file_info(Path, [raw, {time, universal}]) of {ok, Info} -> {direct, Info}; Error -> Error end; read_file_info(Path, {archive, Archive}) -> case file:read_file_info(Archive, [raw, {time, universal}]) of {ok, ArchiveInfo} -> %% The Erlang application archive is fine. %% Now check if the requested file is in that %% archive. We also need the file_info to merge %% them with the archive's one. PathS = binary_to_list(Path), case erl_prim_loader:read_file_info(PathS) of {ok, ContainedFileInfo} -> Info = fix_archived_file_info( ArchiveInfo, ContainedFileInfo), {archive, Info}; error -> {error, enoent} end; Error -> Error end. fix_archived_file_info(ArchiveInfo, ContainedFileInfo) -> %% We merge the archive and content #file_info because we are %% interested by the timestamps of the archive, but the type and %% size of the contained file/directory. %% %% We reset the access to 'read', because we won't rewrite the %% archive. ArchiveInfo#file_info{ size = ContainedFileInfo#file_info.size, type = ContainedFileInfo#file_info.type, access = read }. -ifdef(TEST). fullpath_test_() -> Tests = [ {<<"/home/cowboy">>, <<"/home/cowboy">>}, {<<"/home/cowboy">>, <<"/home/cowboy/">>}, {<<"/home/cowboy">>, <<"/home/cowboy/./">>}, {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>}, {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>}, {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>}, {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>}, {<<"/">>, <<"/home/cowboy/../../../../../..">>}, {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>} ], [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests]. good_path_check_test_() -> Tests = [ <<"/home/cowboy/file">>, <<"/home/cowboy/file/">>, <<"/home/cowboy/./file">>, <<"/home/cowboy/././././././file">>, <<"/home/cowboy/abc/../file">>, <<"/home/cowboy/abc/../file">>, <<"/home/cowboy/abc/./.././file">> ], [{P, fun() -> case fullpath(P) of << "/home/cowboy/", _/bits >> -> ok end end} || P <- Tests]. bad_path_check_test_() -> Tests = [ <<"/home/cowboy/../../../../../../file">>, <<"/home/cowboy/../../etc/passwd">> ], [{P, fun() -> error = case fullpath(P) of << "/home/cowboy/", _/bits >> -> ok; _ -> error end end} || P <- Tests]. good_path_win32_check_test_() -> Tests = case os:type() of {unix, _} -> []; {win32, _} -> [ <<"c:/home/cowboy/file">>, <<"c:/home/cowboy/file/">>, <<"c:/home/cowboy/./file">>, <<"c:/home/cowboy/././././././file">>, <<"c:/home/cowboy/abc/../file">>, <<"c:/home/cowboy/abc/../file">>, <<"c:/home/cowboy/abc/./.././file">> ] end, [{P, fun() -> case fullpath(P) of << "c:/home/cowboy/", _/bits >> -> ok end end} || P <- Tests]. bad_path_win32_check_test_() -> Tests = case os:type() of {unix, _} -> []; {win32, _} -> [ <<"c:/home/cowboy/../../secretfile.bat">>, <<"c:/home/cowboy/c:/secretfile.bat">>, <<"c:/home/cowboy/..\\..\\secretfile.bat">>, <<"c:/home/cowboy/c:\\secretfile.bat">> ] end, [{P, fun() -> error = case fullpath(P) of << "c:/home/cowboy/", _/bits >> -> ok; _ -> error end end} || P <- Tests]. -endif. %% Reject requests that tried to access a file outside %% the target directory, or used reserved characters. -spec malformed_request(Req, State) -> {boolean(), Req, State}. malformed_request(Req, State) -> {State =:= error, Req, State}. %% Directories, files that can't be accessed at all and %% files with no read flag are forbidden. -spec forbidden(Req, State) -> {boolean(), Req, State} when State::state(). forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) -> {true, Req, State}; forbidden(Req, State={_, {error, eacces}, _}) -> {true, Req, State}; forbidden(Req, State={_, {_, #file_info{access=Access}}, _}) when Access =:= write; Access =:= none -> {true, Req, State}; forbidden(Req, State) -> {false, Req, State}. %% Detect the mimetype of the file. -spec content_types_provided(Req, State) -> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, get_file}], Req, State} when State::state(). content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) -> case lists:keyfind(mimetypes, 1, Extra) of false -> {[{cow_mimetypes:web(Path), get_file}], Req, State}; {mimetypes, Module, Function} -> {[{Module:Function(Path), get_file}], Req, State}; {mimetypes, Type} -> {[{Type, get_file}], Req, State} end. %% Detect the charset of the file. -spec charsets_provided(Req, State) -> {[binary()], Req, State} | no_call when State::state(). charsets_provided(Req, State={Path, _, Extra}) -> case lists:keyfind(charset, 1, Extra) of %% We simulate the callback not being exported. false -> no_call; {charset, Module, Function} -> {[Module:Function(Path)], Req, State}; {charset, Charset} when is_binary(Charset) -> {[Charset], Req, State} end. %% Enable support for range requests. -spec ranges_provided(Req, State) -> {[{binary(), auto}], Req, State} when State::state(). ranges_provided(Req, State) -> {[{<<"bytes">>, auto}], Req, State}. %% Assume the resource doesn't exist if it's not a regular file. -spec resource_exists(Req, State) -> {boolean(), Req, State} when State::state(). resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) -> {true, Req, State}; resource_exists(Req, State) -> {false, Req, State}. %% Generate an etag for the file. -spec generate_etag(Req, State) -> {{strong | weak, binary() | undefined}, Req, State} when State::state(). generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}}, Extra}) -> case lists:keyfind(etag, 1, Extra) of false -> {generate_default_etag(Size, Mtime), Req, State}; {etag, Module, Function} -> {Module:Function(Path, Size, Mtime), Req, State}; {etag, false} -> {undefined, Req, State} end. generate_default_etag(Size, Mtime) -> {strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}. %% Return the time of last modification of the file. -spec last_modified(Req, State) -> {calendar:datetime(), Req, State} when State::state(). last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) -> {Modified, Req, State}. %% Stream the file. -spec get_file(Req, State) -> {{sendfile, 0, non_neg_integer(), binary()} | binary(), Req, State} when State::state(). get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) -> {{sendfile, 0, Size, Path}, Req, State}; get_file(Req, State={Path, {archive, _}, _}) -> PathS = binary_to_list(Path), {ok, Bin, _} = erl_prim_loader:get_file(PathS), {Bin, Req, State}. ================================================ FILE: src/cowboy_stream.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_stream). -type state() :: any(). -type human_reason() :: atom(). -type streamid() :: any(). -export_type([streamid/0]). -type fin() :: fin | nofin. -export_type([fin/0]). %% @todo Perhaps it makes more sense to have resp_body in this module? -type resp_command() :: {response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()}. -export_type([resp_command/0]). -type commands() :: [{inform, cowboy:http_status(), cowboy:http_headers()} | resp_command() | {headers, cowboy:http_status(), cowboy:http_headers()} | {data, fin(), cowboy_req:resp_body()} | {trailers, cowboy:http_headers()} | {push, binary(), binary(), binary(), inet:port_number(), binary(), binary(), cowboy:http_headers()} | {flow, pos_integer()} | {spawn, pid(), timeout()} | {error_response, cowboy:http_status(), cowboy:http_headers(), iodata()} | {switch_protocol, cowboy:http_headers(), module(), state()} | {internal_error, any(), human_reason()} | {set_options, map()} | {log, logger:level(), io:format(), list()} | stop]. -export_type([commands/0]). -type reason() :: normal | switch_protocol | {internal_error, timeout | {error | exit | throw, any()}, human_reason()} | {socket_error, closed | atom(), human_reason()} %% @todo Or cow_http3:error(). | {stream_error, cow_http2:error(), human_reason()} | {connection_error, cow_http2:error(), human_reason()} | {stop, cow_http2:frame() | {exit, any()}, human_reason()}. -export_type([reason/0]). -type partial_req() :: map(). %% @todo Take what's in cowboy_req with everything? optional. -export_type([partial_req/0]). -callback init(streamid(), cowboy_req:req(), cowboy:opts()) -> {commands(), state()}. -callback data(streamid(), fin(), binary(), State) -> {commands(), State} when State::state(). -callback info(streamid(), any(), State) -> {commands(), State} when State::state(). -callback terminate(streamid(), reason(), state()) -> any(). -callback early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts()) -> Resp when Resp::resp_command(). %% @todo To optimize the number of active timers we could have a command %% that enables a timeout that is called in the absence of any other call, %% similar to what gen_server does. However the nice thing about this is %% that the connection process can keep a single timer around (the same %% one that would be used to detect half-closed sockets) and use this %% timer and other events to trigger the timeout in streams at their %% intended time. %% %% This same timer can be used to try and send PING frames to help detect %% that the connection is indeed unresponsive. -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). -export([make_error_log/5]). %% Note that this and other functions in this module do NOT catch %% exceptions. We want the exception to go all the way down to the %% protocol code. %% %% OK the failure scenario is not so clear. The problem is %% that the failure at any point in init/3 will result in the %% corresponding state being lost. I am unfortunately not %% confident we can do anything about this. If the crashing %% handler just created a process, we'll never know about it. %% Therefore at this time I choose to leave all failure handling %% to the protocol process. %% %% Note that a failure in init/3 will result in terminate/3 %% NOT being called. This is because the state is not available. -spec init(streamid(), cowboy_req:req(), cowboy:opts()) -> {commands(), {module(), state()} | undefined}. init(StreamID, Req, Opts) -> case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of [] -> {[], undefined}; [Handler|Tail] -> %% We call the next handler and remove it from the list of %% stream handlers. This means that handlers that run after %% it have no knowledge it exists. Should user require this %% knowledge they can just define a separate option that will %% be left untouched. {Commands, State} = Handler:init(StreamID, Req, Opts#{stream_handlers => Tail}), {Commands, {Handler, State}} end. -spec data(streamid(), fin(), binary(), {Handler, State} | undefined) -> {commands(), {Handler, State} | undefined} when Handler::module(), State::state(). data(_, _, _, undefined) -> {[], undefined}; data(StreamID, IsFin, Data, {Handler, State0}) -> {Commands, State} = Handler:data(StreamID, IsFin, Data, State0), {Commands, {Handler, State}}. -spec info(streamid(), any(), {Handler, State} | undefined) -> {commands(), {Handler, State} | undefined} when Handler::module(), State::state(). info(_, _, undefined) -> {[], undefined}; info(StreamID, Info, {Handler, State0}) -> {Commands, State} = Handler:info(StreamID, Info, State0), {Commands, {Handler, State}}. -spec terminate(streamid(), reason(), {module(), state()} | undefined) -> ok. terminate(_, _, undefined) -> ok; terminate(StreamID, Reason, {Handler, State}) -> _ = Handler:terminate(StreamID, Reason, State), ok. -spec early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts()) -> Resp when Resp::resp_command(). early_error(StreamID, Reason, PartialReq, Resp, Opts) -> case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of [] -> Resp; [Handler|Tail] -> %% This is the same behavior as in init/3. Handler:early_error(StreamID, Reason, PartialReq, Resp, Opts#{stream_handlers => Tail}) end. -spec make_error_log(init | data | info | terminate | early_error, list(), error | exit | throw, any(), list()) -> {log, error, string(), list()}. make_error_log(init, [StreamID, Req, Opts], Class, Exception, Stacktrace) -> {log, error, "Unhandled exception ~p:~p in cowboy_stream:init(~p, Req, Opts)~n" "Stacktrace: ~p~n" "Req: ~p~n" "Opts: ~p~n", [Class, Exception, StreamID, Stacktrace, Req, Opts]}; make_error_log(data, [StreamID, IsFin, Data, State], Class, Exception, Stacktrace) -> {log, error, "Unhandled exception ~p:~p in cowboy_stream:data(~p, ~p, Data, State)~n" "Stacktrace: ~p~n" "Data: ~p~n" "State: ~p~n", [Class, Exception, StreamID, IsFin, Stacktrace, Data, State]}; make_error_log(info, [StreamID, Msg, State], Class, Exception, Stacktrace) -> {log, error, "Unhandled exception ~p:~p in cowboy_stream:info(~p, Msg, State)~n" "Stacktrace: ~p~n" "Msg: ~p~n" "State: ~p~n", [Class, Exception, StreamID, Stacktrace, Msg, State]}; make_error_log(terminate, [StreamID, Reason, State], Class, Exception, Stacktrace) -> {log, error, "Unhandled exception ~p:~p in cowboy_stream:terminate(~p, Reason, State)~n" "Stacktrace: ~p~n" "Reason: ~p~n" "State: ~p~n", [Class, Exception, StreamID, Stacktrace, Reason, State]}; make_error_log(early_error, [StreamID, Reason, PartialReq, Resp, Opts], Class, Exception, Stacktrace) -> {log, error, "Unhandled exception ~p:~p in cowboy_stream:early_error(~p, Reason, PartialReq, Resp, Opts)~n" "Stacktrace: ~p~n" "Reason: ~p~n" "PartialReq: ~p~n" "Resp: ~p~n" "Opts: ~p~n", [Class, Exception, StreamID, Stacktrace, Reason, PartialReq, Resp, Opts]}. ================================================ FILE: src/cowboy_stream_h.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_stream_h). -behavior(cowboy_stream). -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). -export([request_process/3]). -export([resume/5]). -record(state, { next :: any(), ref = undefined :: ranch:ref(), pid = undefined :: pid(), expect = undefined :: undefined | continue, read_body_pid = undefined :: pid() | undefined, read_body_ref = undefined :: reference() | undefined, read_body_timer_ref = undefined :: reference() | undefined, read_body_length = 0 :: non_neg_integer() | infinity | auto, read_body_is_fin = nofin :: nofin | {fin, non_neg_integer()}, read_body_buffer = <<>> :: binary(), body_length = 0 :: non_neg_integer(), stream_body_pid = undefined :: pid() | undefined, stream_body_status = normal :: normal | blocking | blocked }). -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> {[{spawn, pid(), timeout()}], #state{}}. init(StreamID, Req=#{ref := Ref}, Opts) -> Env = maps:get(env, Opts, #{}), Middlewares = maps:get(middlewares, Opts, [cowboy_router, cowboy_handler]), Shutdown = maps:get(shutdown_timeout, Opts, 5000), Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env, Middlewares]), Expect = expect(Req), {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), {[{spawn, Pid, Shutdown}|Commands], #state{next=Next, ref=Ref, pid=Pid, expect=Expect}}. %% Ignore the expect header in HTTP/1.0. expect(#{version := 'HTTP/1.0'}) -> undefined; expect(Req) -> try cowboy_req:parse_header(<<"expect">>, Req) of Expect -> Expect catch _:_ -> undefined end. %% If we receive data and stream is waiting for data: %% If we accumulated enough data or IsFin=fin, send it. %% If we are in auto mode, send it and update flow control. %% If not, buffer it. %% If not, buffer it. %% %% We always reset the expect field when we receive data, %% since the client started sending the request body before %% we could send a 100 continue response. -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) -> {cowboy_stream:commands(), State} when State::#state{}. %% Stream isn't waiting for data. data(StreamID, IsFin, Data, State=#state{ read_body_ref=undefined, read_body_buffer=Buffer, body_length=BodyLen}) -> do_data(StreamID, IsFin, Data, [], State#state{ expect=undefined, read_body_is_fin=IsFin, read_body_buffer= << Buffer/binary, Data/binary >>, body_length=BodyLen + byte_size(Data) }); %% Stream is waiting for data using auto mode. %% %% There is no buffering done in auto mode. data(StreamID, IsFin, Data, State=#state{read_body_pid=Pid, read_body_ref=Ref, read_body_length=auto, body_length=BodyLen}) -> send_request_body(Pid, Ref, IsFin, BodyLen, Data), do_data(StreamID, IsFin, Data, [{flow, byte_size(Data)}], State#state{ read_body_ref=undefined, %% @todo This is wrong, it's missing byte_size(Data). body_length=BodyLen }); %% Stream is waiting for data but we didn't receive enough to send yet. data(StreamID, IsFin=nofin, Data, State=#state{ read_body_length=ReadLen, read_body_buffer=Buffer, body_length=BodyLen}) when byte_size(Data) + byte_size(Buffer) < ReadLen -> do_data(StreamID, IsFin, Data, [], State#state{ expect=undefined, read_body_buffer= << Buffer/binary, Data/binary >>, body_length=BodyLen + byte_size(Data) }); %% Stream is waiting for data and we received enough to send. data(StreamID, IsFin, Data, State=#state{read_body_pid=Pid, read_body_ref=Ref, read_body_timer_ref=TRef, read_body_buffer=Buffer, body_length=BodyLen0}) -> BodyLen = BodyLen0 + byte_size(Data), ok = erlang:cancel_timer(TRef, [{async, true}, {info, false}]), send_request_body(Pid, Ref, IsFin, BodyLen, <>), do_data(StreamID, IsFin, Data, [], State#state{ expect=undefined, read_body_ref=undefined, read_body_timer_ref=undefined, read_body_buffer= <<>>, body_length=BodyLen }). do_data(StreamID, IsFin, Data, Commands1, State=#state{next=Next0}) -> {Commands2, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), {Commands1 ++ Commands2, State#state{next=Next}}. -spec info(cowboy_stream:streamid(), any(), State) -> {cowboy_stream:commands(), State} when State::#state{}. info(StreamID, Info={'EXIT', Pid, normal}, State=#state{pid=Pid}) -> do_info(StreamID, Info, [stop], State); info(StreamID, Info={'EXIT', Pid, {{request_error, Reason, _HumanReadable}, _}}, State=#state{pid=Pid}) -> Status = case Reason of timeout -> 408; payload_too_large -> 413; _ -> 400 end, %% @todo Headers? Details in body? Log the crash? More stuff in debug only? do_info(StreamID, Info, [ {error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>}, stop ], State); info(StreamID, Exit={'EXIT', Pid, Reason}, State=#state{ref=Ref, pid=Pid}) -> Commands0 = [{internal_error, Exit, 'Stream process crashed.'}], Commands = case Reason of normal -> Commands0; shutdown -> Commands0; {shutdown, _} -> Commands0; _ -> [{log, error, "Ranch listener ~p, connection process ~p, stream ~p " "had its request process ~p exit with reason ~0p~n", [Ref, self(), StreamID, Pid, Reason]} |Commands0] end, %% @todo We are trying to send a 500 response before resetting %% the stream. But due to the way the RESET_STREAM frame %% works in QUIC the data may be lost. The problem is %% known and a draft RFC exists at %% https://www.ietf.org/id/draft-ietf-quic-reliable-stream-reset-03.html do_info(StreamID, Exit, [ {error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>} |Commands], State); %% Request body, auto mode, no body buffered. info(StreamID, Info={read_body, Pid, Ref, auto, infinity}, State=#state{read_body_buffer= <<>>}) -> do_info(StreamID, Info, [], State#state{ read_body_pid=Pid, read_body_ref=Ref, read_body_length=auto }); %% Request body, auto mode, body buffered or complete. info(StreamID, Info={read_body, Pid, Ref, auto, infinity}, State=#state{ read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) -> send_request_body(Pid, Ref, IsFin, BodyLen, Buffer), do_info(StreamID, Info, [{flow, byte_size(Buffer)}], State#state{read_body_buffer= <<>>}); %% Request body, body buffered large enough or complete. %% %% We do not send a 100 continue response if the client %% already started sending the body. info(StreamID, Info={read_body, Pid, Ref, Length, _}, State=#state{ read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) when IsFin =:= fin; byte_size(Buffer) >= Length -> send_request_body(Pid, Ref, IsFin, BodyLen, Buffer), do_info(StreamID, Info, [], State#state{read_body_buffer= <<>>}); %% Request body, not enough to send yet. info(StreamID, Info={read_body, Pid, Ref, Length, Period}, State=#state{expect=Expect}) -> Commands = case Expect of continue -> [{inform, 100, #{}}, {flow, Length}]; undefined -> [{flow, Length}] end, TRef = erlang:send_after(Period, self(), {{self(), StreamID}, {read_body_timeout, Ref}}), do_info(StreamID, Info, Commands, State#state{ read_body_pid=Pid, read_body_ref=Ref, read_body_timer_ref=TRef, read_body_length=Length }); %% Request body reading timeout; send what we got. info(StreamID, Info={read_body_timeout, Ref}, State=#state{read_body_pid=Pid, read_body_ref=Ref, read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) -> send_request_body(Pid, Ref, IsFin, BodyLen, Buffer), do_info(StreamID, Info, [], State#state{ read_body_ref=undefined, read_body_timer_ref=undefined, read_body_buffer= <<>> }); info(StreamID, Info={read_body_timeout, _}, State) -> do_info(StreamID, Info, [], State); %% Response. %% %% We reset the expect field when a 100 continue response %% is sent or when any final response is sent. info(StreamID, Inform={inform, Status, _}, State0) -> State = case cow_http:status_to_integer(Status) of 100 -> State0#state{expect=undefined}; _ -> State0 end, do_info(StreamID, Inform, [Inform], State); info(StreamID, Response={response, _, _, _}, State) -> do_info(StreamID, Response, [Response], State#state{expect=undefined}); info(StreamID, Headers={headers, _, _}, State) -> do_info(StreamID, Headers, [Headers], State#state{expect=undefined}); %% Sending data involves the data message, the stream_buffer_full alarm %% and the connection_buffer_full alarm. We stop sending acks when an alarm is on. %% %% We only apply backpressure when the message includes a pid. Otherwise %% it is a message from Cowboy, or the user circumventing the backpressure. %% %% We currently do not support sending data from multiple processes concurrently. info(StreamID, Data={data, _, _}, State) -> do_info(StreamID, Data, [Data], State); info(StreamID, Data0={data, Pid, _, _}, State0=#state{stream_body_status=Status}) -> State = case Status of normal -> Pid ! {data_ack, self()}, State0; blocking -> State0#state{stream_body_pid=Pid, stream_body_status=blocked}; blocked -> State0 end, Data = erlang:delete_element(2, Data0), do_info(StreamID, Data, [Data], State); info(StreamID, Alarm={alarm, Name, on}, State0=#state{stream_body_status=Status}) when Name =:= connection_buffer_full; Name =:= stream_buffer_full -> State = case Status of normal -> State0#state{stream_body_status=blocking}; _ -> State0 end, do_info(StreamID, Alarm, [], State); info(StreamID, Alarm={alarm, Name, off}, State=#state{stream_body_pid=Pid, stream_body_status=Status}) when Name =:= connection_buffer_full; Name =:= stream_buffer_full -> _ = case Status of normal -> ok; blocking -> ok; blocked -> Pid ! {data_ack, self()} end, do_info(StreamID, Alarm, [], State#state{stream_body_pid=undefined, stream_body_status=normal}); info(StreamID, Trailers={trailers, _}, State) -> do_info(StreamID, Trailers, [Trailers], State); info(StreamID, Push={push, _, _, _, _, _, _, _}, State) -> do_info(StreamID, Push, [Push], State); info(StreamID, SwitchProtocol={switch_protocol, _, _, _}, State) -> do_info(StreamID, SwitchProtocol, [SwitchProtocol], State#state{expect=undefined}); %% Convert the set_options message to a command. info(StreamID, SetOptions={set_options, _}, State) -> do_info(StreamID, SetOptions, [SetOptions], State); %% Unknown message, either stray or meant for a handler down the line. info(StreamID, Info, State) -> do_info(StreamID, Info, [], State). do_info(StreamID, Info, Commands1, State0=#state{next=Next0}) -> {Commands2, Next} = cowboy_stream:info(StreamID, Info, Next0), {Commands1 ++ Commands2, State0#state{next=Next}}. -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> ok. terminate(StreamID, Reason, #state{next=Next}) -> cowboy_stream:terminate(StreamID, Reason, Next). -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp when Resp::cowboy_stream:resp_command(). early_error(StreamID, Reason, PartialReq, Resp, Opts) -> cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). send_request_body(Pid, Ref, nofin, _, Data) -> Pid ! {request_body, Ref, nofin, Data}, ok; send_request_body(Pid, Ref, fin, BodyLen, Data) -> Pid ! {request_body, Ref, fin, BodyLen, Data}, ok. %% Request process. %% We add the stacktrace to exit exceptions here in order %% to simplify the debugging of errors. The proc_lib library %% already adds the stacktrace to other types of exceptions. -spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok. request_process(Req, Env, Middlewares) -> try execute(Req, Env, Middlewares) catch exit:Reason={shutdown, _}:Stacktrace -> erlang:raise(exit, Reason, Stacktrace); exit:Reason:Stacktrace when Reason =/= normal, Reason =/= shutdown -> erlang:raise(exit, {Reason, Stacktrace}, Stacktrace) end. execute(_, _, []) -> ok; execute(Req, Env, [Middleware|Tail]) -> case Middleware:execute(Req, Env) of {ok, Req2, Env2} -> execute(Req2, Env2, Tail); {suspend, Module, Function, Args} -> proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module, Function, Args]); {stop, _Req2} -> ok end. -spec resume(cowboy_middleware:env(), [module()], module(), atom(), [any()]) -> ok. resume(Env, Tail, Module, Function, Args) -> case apply(Module, Function, Args) of {ok, Req2, Env2} -> execute(Req2, Env2, Tail); {suspend, Module2, Function2, Args2} -> proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module2, Function2, Args2]); {stop, _Req2} -> ok end. ================================================ FILE: src/cowboy_sub_protocol.erl ================================================ %% Copyright (c) Loïc Hoguin %% Copyright (c) James Fish %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_sub_protocol). -callback upgrade(Req, Env, module(), any()) -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). -callback upgrade(Req, Env, module(), any(), any()) -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). ================================================ FILE: src/cowboy_sup.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_sup). -behaviour(supervisor). -export([start_link/0]). -export([init/1]). -spec start_link() -> {ok, pid()}. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). -spec init([]) -> {ok, {{supervisor:strategy(), 10, 10}, [supervisor:child_spec()]}}. init([]) -> Procs = [{cowboy_clock, {cowboy_clock, start_link, []}, permanent, 5000, worker, [cowboy_clock]}], {ok, {{one_for_one, 10, 10}, Procs}}. ================================================ FILE: src/cowboy_tls.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_tls). -behavior(ranch_protocol). -export([start_link/3]). -export([start_link/4]). -export([connection_process/4]). %% Ranch 1. -spec start_link(ranch:ref(), ssl:sslsocket(), module(), cowboy:opts()) -> {ok, pid()}. start_link(Ref, _Socket, Transport, Opts) -> start_link(Ref, Transport, Opts). %% Ranch 2. -spec start_link(ranch:ref(), module(), cowboy:opts()) -> {ok, pid()}. start_link(Ref, Transport, Opts) -> Pid = proc_lib:spawn_link(?MODULE, connection_process, [self(), Ref, Transport, Opts]), {ok, Pid}. -spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok. connection_process(Parent, Ref, Transport, Opts) -> ProxyInfo = get_proxy_info(Ref, Opts), {ok, Socket} = ranch:handshake(Ref), case ssl:negotiated_protocol(Socket) of {ok, <<"h2">>} -> init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http2); _ -> %% http/1.1 or no protocol negotiated. Protocol = case maps:get(alpn_default_protocol, Opts, http) of http -> cowboy_http; http2 -> cowboy_http2 end, init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) end. init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) -> _ = case maps:get(connection_type, Opts, supervisor) of worker -> ok; supervisor -> process_flag(trap_exit, true) end, Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts). get_proxy_info(Ref, #{proxy_header := true}) -> case ranch:recv_proxy_header(Ref, 1000) of {ok, ProxyInfo} -> ProxyInfo; {error, closed} -> exit({shutdown, closed}) end; get_proxy_info(_, _) -> undefined. ================================================ FILE: src/cowboy_tracer_h.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_tracer_h). -behavior(cowboy_stream). -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). -export([set_trace_patterns/0]). -export([tracer_process/3]). -export([system_continue/3]). -export([system_terminate/4]). -export([system_code_change/4]). -type match_predicate() :: fun((cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> boolean()). -type tracer_match_specs() :: [match_predicate() | {method, binary()} | {host, binary()} | {path, binary()} | {path_start, binary()} | {header, binary()} | {header, binary(), binary()} | {peer_ip, inet:ip_address()} ]. -export_type([tracer_match_specs/0]). -type tracer_callback() :: fun((init | terminate | tuple(), any()) -> any()). -export_type([tracer_callback/0]). -spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> {cowboy_stream:commands(), any()}. init(StreamID, Req, Opts) -> init_tracer(StreamID, Req, Opts), cowboy_stream:init(StreamID, Req, Opts). -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) -> {cowboy_stream:commands(), State} when State::any(). data(StreamID, IsFin, Data, Next) -> cowboy_stream:data(StreamID, IsFin, Data, Next). -spec info(cowboy_stream:streamid(), any(), State) -> {cowboy_stream:commands(), State} when State::any(). info(StreamID, Info, Next) -> cowboy_stream:info(StreamID, Info, Next). -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), any()) -> any(). terminate(StreamID, Reason, Next) -> cowboy_stream:terminate(StreamID, Reason, Next). -spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp when Resp::cowboy_stream:resp_command(). early_error(StreamID, Reason, PartialReq, Resp, Opts) -> cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). %% API. %% These trace patterns are most likely not suitable for production. -spec set_trace_patterns() -> ok. set_trace_patterns() -> erlang:trace_pattern({'_', '_', '_'}, [{'_', [], [{return_trace}]}], [local]), erlang:trace_pattern(on_load, [{'_', [], [{return_trace}]}], [local]), ok. %% Internal. init_tracer(StreamID, Req, Opts=#{tracer_match_specs := List, tracer_callback := _}) -> case match(List, StreamID, Req, Opts) of false -> ok; true -> start_tracer(StreamID, Req, Opts) end; %% When the options tracer_match_specs or tracer_callback %% are not provided we do not enable tracing. init_tracer(_, _, _) -> ok. match([], _, _, _) -> true; match([Predicate|Tail], StreamID, Req, Opts) when is_function(Predicate) -> case Predicate(StreamID, Req, Opts) of true -> match(Tail, StreamID, Req, Opts); false -> false end; match([{method, Value}|Tail], StreamID, Req=#{method := Value}, Opts) -> match(Tail, StreamID, Req, Opts); match([{host, Value}|Tail], StreamID, Req=#{host := Value}, Opts) -> match(Tail, StreamID, Req, Opts); match([{path, Value}|Tail], StreamID, Req=#{path := Value}, Opts) -> match(Tail, StreamID, Req, Opts); match([{path_start, PathStart}|Tail], StreamID, Req=#{path := Path}, Opts) -> Len = byte_size(PathStart), case Path of <> -> match(Tail, StreamID, Req, Opts); _ -> false end; match([{header, Name}|Tail], StreamID, Req=#{headers := Headers}, Opts) -> case Headers of #{Name := _} -> match(Tail, StreamID, Req, Opts); _ -> false end; match([{header, Name, Value}|Tail], StreamID, Req=#{headers := Headers}, Opts) -> case Headers of #{Name := Value} -> match(Tail, StreamID, Req, Opts); _ -> false end; match([{peer_ip, IP}|Tail], StreamID, Req=#{peer := {IP, _}}, Opts) -> match(Tail, StreamID, Req, Opts); match(_, _, _, _) -> false. %% We only start the tracer if one wasn't started before. start_tracer(StreamID, Req, Opts) -> case erlang:trace_info(self(), tracer) of {tracer, []} -> TracerPid = proc_lib:spawn_link(?MODULE, tracer_process, [StreamID, Req, Opts]), %% The default flags are probably not suitable for production. Flags = maps:get(tracer_flags, Opts, [ send, 'receive', call, return_to, procs, ports, monotonic_timestamp, %% The set_on_spawn flag is necessary to catch events %% from request processes. set_on_spawn ]), erlang:trace(self(), true, [{tracer, TracerPid}|Flags]), ok; _ -> ok end. %% Tracer process. -spec tracer_process(_, _, _) -> no_return(). tracer_process(StreamID, Req=#{pid := Parent}, Opts=#{tracer_callback := Fun}) -> %% This is necessary because otherwise the tracer could stop %% before it has finished processing the events in its queue. process_flag(trap_exit, true), State = Fun(init, {StreamID, Req, Opts}), tracer_loop(Parent, Opts, State). tracer_loop(Parent, Opts=#{tracer_callback := Fun}, State0) -> receive Msg when element(1, Msg) =:= trace; element(1, Msg) =:= trace_ts -> State = Fun(Msg, State0), tracer_loop(Parent, Opts, State); {'EXIT', Parent, Reason} -> tracer_terminate(Reason, Opts, State0); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Opts, State0}); Msg -> cowboy:log(warning, "~p: Tracer process received stray message ~9999p~n", [?MODULE, Msg], Opts), tracer_loop(Parent, Opts, State0) end. -spec tracer_terminate(_, _, _) -> no_return(). tracer_terminate(Reason, #{tracer_callback := Fun}, State) -> _ = Fun(terminate, State), exit(Reason). %% System callbacks. -spec system_continue(pid(), _, {cowboy:opts(), any()}) -> no_return(). system_continue(Parent, _, {Opts, State}) -> tracer_loop(Parent, Opts, State). -spec system_terminate(any(), _, _, _) -> no_return(). system_terminate(Reason, _, _, {Opts, State}) -> tracer_terminate(Reason, Opts, State). -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::any(). system_code_change(Misc, _, _, _) -> {ok, Misc}. ================================================ FILE: src/cowboy_websocket.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% Cowboy supports versions 7 through 17 of the Websocket drafts. %% It also supports RFC6455, the proposed standard for Websocket. -module(cowboy_websocket). -behaviour(cowboy_sub_protocol). -export([is_upgrade_request/1]). -export([upgrade/4]). -export([upgrade/5]). -export([takeover/7]). -export([loop/3]). -export([system_continue/3]). -export([system_terminate/4]). -export([system_code_change/4]). -type commands() :: [cow_ws:frame() | {active, boolean()} | {deflate, boolean()} | {set_options, map()} | {shutdown_reason, any()} ]. -export_type([commands/0]). -type call_result(State) :: {commands(), State} | {commands(), State, hibernate}. -type deprecated_call_result(State) :: {ok, State} | {ok, State, hibernate} | {reply, cow_ws:frame() | [cow_ws:frame()], State} | {reply, cow_ws:frame() | [cow_ws:frame()], State, hibernate} | {stop, State}. -type terminate_reason() :: normal | stop | timeout | remote | {remote, cow_ws:close_code(), binary()} | {error, badencoding | badframe | closed | atom()} | {crash, error | exit | throw, any()}. -callback init(Req, any()) -> {ok | module(), Req, any()} | {module(), Req, any(), any()} when Req::cowboy_req:req(). -callback websocket_init(State) -> call_result(State) | deprecated_call_result(State) when State::any(). -optional_callbacks([websocket_init/1]). -callback websocket_handle(ping | pong | {text | binary | ping | pong, binary()}, State) -> call_result(State) | deprecated_call_result(State) when State::any(). -callback websocket_info(any(), State) -> call_result(State) | deprecated_call_result(State) when State::any(). -callback terminate(any(), cowboy_req:req(), any()) -> ok. -optional_callbacks([terminate/3]). -type opts() :: #{ active_n => pos_integer(), compress => boolean(), data_delivery => stream_handlers | relay, data_delivery_flow => pos_integer(), deflate_opts => cow_ws:deflate_opts(), dynamic_buffer => false | {pos_integer(), pos_integer()}, dynamic_buffer_initial_average => non_neg_integer(), dynamic_buffer_initial_size => pos_integer(), idle_timeout => timeout(), max_frame_size => non_neg_integer() | infinity, req_filter => fun((cowboy_req:req()) -> map()), validate_utf8 => boolean() }. -export_type([opts/0]). %% We don't want to reset the idle timeout too often, %% so we don't reset it on data. Instead we reset the %% number of ticks we have observed. We divide the %% timeout value by a value and that value becomes %% the number of ticks at which point we can drop %% the connection. This value is the number of ticks. -define(IDLE_TIMEOUT_TICKS, 10). -record(state, { parent :: undefined | pid(), ref :: ranch:ref(), socket = undefined :: inet:socket() | {pid(), cowboy_stream:streamid()} | undefined, transport :: module() | {data_delivery, stream_handlers | relay}, opts = #{} :: opts(), active = true :: boolean(), handler :: module(), key = undefined :: undefined | binary(), timeout_ref = undefined :: undefined | reference(), timeout_num = 0 :: 0..?IDLE_TIMEOUT_TICKS, messages = undefined :: undefined | {atom(), atom(), atom()} | {atom(), atom(), atom(), atom()}, %% Dynamic buffer moving average and current buffer size. dynamic_buffer_size = false :: pos_integer() | false, dynamic_buffer_moving_average = 0.0 :: float(), hibernate = false :: boolean(), frag_state = undefined :: cow_ws:frag_state(), frag_buffer = <<>> :: binary(), utf8_state :: cow_ws:utf8_state(), deflate = true :: boolean(), extensions = #{} :: map(), req = #{} :: map(), shutdown_reason = normal :: any() }). %% Because the HTTP/1.1 and HTTP/2 handshakes are so different, %% this function is necessary to figure out whether a request %% is trying to upgrade to the Websocket protocol. -spec is_upgrade_request(cowboy_req:req()) -> boolean(). is_upgrade_request(#{version := Version, method := <<"CONNECT">>, protocol := Protocol}) when Version =:= 'HTTP/2'; Version =:= 'HTTP/3' -> <<"websocket">> =:= cowboy_bstr:to_lower(Protocol); is_upgrade_request(Req=#{version := 'HTTP/1.1', method := <<"GET">>}) -> ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []), case lists:member(<<"upgrade">>, ConnTokens) of false -> false; true -> UpgradeTokens = cowboy_req:parse_header(<<"upgrade">>, Req), lists:member(<<"websocket">>, UpgradeTokens) end; is_upgrade_request(_) -> false. %% Stream process. -spec upgrade(Req, Env, module(), any()) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade(Req, Env, Handler, HandlerState) -> upgrade(Req, Env, Handler, HandlerState, #{}). -spec upgrade(Req, Env, module(), any(), opts()) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). %% @todo Immediately crash if a response has already been sent. upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) -> FilteredReq = case maps:get(req_filter, Opts, undefined) of undefined -> maps:with([method, version, scheme, host, port, path, qs, peer, streamid], Req0); FilterFun -> FilterFun(Req0) end, Utf8State = case maps:get(validate_utf8, Opts, true) of true -> 0; false -> undefined end, State0 = #state{opts=Opts, handler=Handler, utf8_state=Utf8State, req=FilteredReq}, try websocket_upgrade(State0, Req0) of {ok, State, Req} -> websocket_handshake(State, Req, HandlerState, Env); %% The status code 426 is specific to HTTP/1.1 connections. {error, upgrade_required} when Version =:= 'HTTP/1.1' -> {ok, cowboy_req:reply(426, #{ <<"connection">> => <<"upgrade">>, <<"upgrade">> => <<"websocket">> }, Req0), Env}; %% Use 501 Not Implemented for HTTP/2 and HTTP/3 as recommended %% by RFC9220 3 (WebSockets Upgrade over HTTP/3). {error, upgrade_required} -> {ok, cowboy_req:reply(501, Req0), Env} catch _:_ -> %% @todo Probably log something here? %% @todo Test that we can have 2 /ws 400 status code in a row on the same connection. {ok, cowboy_req:reply(400, Req0), Env} end. websocket_upgrade(State, Req=#{version := Version}) -> case is_upgrade_request(Req) of false -> {error, upgrade_required}; true when Version =:= 'HTTP/1.1' -> Key = cowboy_req:header(<<"sec-websocket-key">>, Req), false = Key =:= undefined, websocket_version(State#state{key=Key}, Req); true -> websocket_version(State, Req) end. websocket_version(State, Req) -> WsVersion = cowboy_req:parse_header(<<"sec-websocket-version">>, Req), case WsVersion of 7 -> ok; 8 -> ok; 13 -> ok end, websocket_extensions(State, Req#{websocket_version => WsVersion}). websocket_extensions(State=#state{opts=Opts}, Req) -> %% @todo We want different options for this. For example %% * compress everything auto %% * compress only text auto %% * compress only binary auto %% * compress nothing auto (but still enabled it) %% * disable compression Compress = maps:get(compress, Opts, false), case {Compress, cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req)} of {true, Extensions} when Extensions =/= undefined -> websocket_extensions(State, Req, Extensions, []); _ -> {ok, State, Req} end. websocket_extensions(State, Req, [], []) -> {ok, State, Req}; websocket_extensions(State, Req, [], [<<", ">>|RespHeader]) -> {ok, State, cowboy_req:set_resp_header(<<"sec-websocket-extensions">>, lists:reverse(RespHeader), Req)}; %% For HTTP/2 we ARE on the controlling process and do NOT want to update the owner. websocket_extensions(State=#state{opts=Opts, extensions=Extensions}, Req=#{pid := Pid, version := Version}, [{<<"permessage-deflate">>, Params}|Tail], RespHeader) -> DeflateOpts0 = maps:get(deflate_opts, Opts, #{}), DeflateOpts = case Version of 'HTTP/1.1' -> DeflateOpts0#{owner => Pid}; _ -> DeflateOpts0 end, try cow_ws:negotiate_permessage_deflate(Params, Extensions, DeflateOpts) of {ok, RespExt, Extensions2} -> websocket_extensions(State#state{extensions=Extensions2}, Req, Tail, [<<", ">>, RespExt|RespHeader]); ignore -> websocket_extensions(State, Req, Tail, RespHeader) catch exit:{error, incompatible_zlib_version, _} -> websocket_extensions(State, Req, Tail, RespHeader) end; websocket_extensions(State=#state{opts=Opts, extensions=Extensions}, Req=#{pid := Pid, version := Version}, [{<<"x-webkit-deflate-frame">>, Params}|Tail], RespHeader) -> DeflateOpts0 = maps:get(deflate_opts, Opts, #{}), DeflateOpts = case Version of 'HTTP/1.1' -> DeflateOpts0#{owner => Pid}; _ -> DeflateOpts0 end, try cow_ws:negotiate_x_webkit_deflate_frame(Params, Extensions, DeflateOpts) of {ok, RespExt, Extensions2} -> websocket_extensions(State#state{extensions=Extensions2}, Req, Tail, [<<", ">>, RespExt|RespHeader]); ignore -> websocket_extensions(State, Req, Tail, RespHeader) catch exit:{error, incompatible_zlib_version, _} -> websocket_extensions(State, Req, Tail, RespHeader) end; websocket_extensions(State, Req, [_|Tail], RespHeader) -> websocket_extensions(State, Req, Tail, RespHeader). -spec websocket_handshake(#state{}, Req, any(), Env) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). websocket_handshake(State=#state{key=Key}, Req=#{version := 'HTTP/1.1', pid := Pid, streamid := StreamID}, HandlerState, Env) -> Challenge = base64:encode(crypto:hash(sha, << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)), %% @todo We don't want date and server headers. Headers = cowboy_req:response_headers(#{ <<"connection">> => <<"Upgrade">>, <<"upgrade">> => <<"websocket">>, <<"sec-websocket-accept">> => Challenge }, Req), Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, {State, HandlerState}}}, {ok, Req, Env}; %% For HTTP/2 we do not let the process die, we instead keep it %% for the Websocket stream. This is because in HTTP/2 we only %% have a stream, it doesn't take over the whole connection. %% %% There are two methods of delivering data to the Websocket session: %% - 'stream_handlers' is the default and makes the data go %% through stream handlers just like when reading a request body; %% - 'relay' is a new method where data is sent as a message as %% soon as it is received from the socket in a DATA frame. websocket_handshake(State=#state{opts=Opts}, Req=#{ref := Ref, pid := Pid, streamid := StreamID}, HandlerState, _Env) -> %% @todo We don't want date and server headers. Headers = cowboy_req:response_headers(#{}, Req), DataDelivery = maps:get(data_delivery, Opts, stream_handlers), ModState = #{ data_delivery => DataDelivery, %% For relay data_delivery. The flow is a hint and may %% not be used by the underlying protocol. data_delivery_pid => self(), data_delivery_flow => maps:get(data_delivery_flow, Opts, 131072) }, Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, ModState}}, takeover(Pid, Ref, {Pid, StreamID}, {data_delivery, DataDelivery}, #{}, <<>>, {State, HandlerState}). %% Connection process. -record(ps_header, { buffer = <<>> :: binary() }). -record(ps_payload, { type :: cow_ws:frame_type(), len :: non_neg_integer(), mask_key :: cow_ws:mask_key(), rsv :: cow_ws:rsv(), close_code = undefined :: undefined | cow_ws:close_code(), unmasked = <<>> :: binary(), unmasked_len = 0 :: non_neg_integer(), buffer = <<>> :: binary() }). -type parse_state() :: #ps_header{} | #ps_payload{}. -spec takeover(pid(), ranch:ref(), inet:socket() | {pid(), cowboy_stream:streamid()}, module() | {data_delivery, stream_handlers | relay}, any(), binary(), {#state{}, any()}) -> no_return(). takeover(Parent, Ref, Socket, Transport, Opts, Buffer, {State0=#state{opts=WsOpts, handler=Handler, req=Req}, HandlerState}) -> case Req of #{version := 'HTTP/3'} -> ok; %% @todo We should have an option to disable this behavior. _ -> ranch:remove_connection(Ref) end, Messages = case Transport of {data_delivery, _} -> undefined; _ -> Transport:messages() end, State = set_idle_timeout(State0#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, opts=WsOpts#{dynamic_buffer => maps:get(dynamic_buffer, Opts, false)}, key=undefined, messages=Messages, %% Dynamic buffer only applies to HTTP/1.1 Websocket. dynamic_buffer_size=init_dynamic_buffer_size(Opts), dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0.0)}, 0), %% We call parse_header/3 immediately because there might be %% some data in the buffer that was sent along with the handshake. %% While it is not allowed by the protocol to send frames immediately, %% we still want to process that data if any. case erlang:function_exported(Handler, websocket_init, 1) of true -> handler_call(State, HandlerState, #ps_header{buffer=Buffer}, websocket_init, undefined, fun after_init/3); false -> after_init(State, HandlerState, #ps_header{buffer=Buffer}) end. -include("cowboy_dynamic_buffer.hrl"). %% @todo Implement early socket error detection. maybe_socket_error(_, _) -> ok. after_init(State=#state{active=true}, HandlerState, ParseState) -> %% Enable active,N for HTTP/1.1, and auto read_body for HTTP/2. %% We must do this only after calling websocket_init/1 (if any) %% to give the handler a chance to disable active mode immediately. setopts_active(State), maybe_read_body(State), parse_header(State, HandlerState, ParseState); after_init(State, HandlerState, ParseState) -> parse_header(State, HandlerState, ParseState). %% We have two ways of reading the body for Websocket. For HTTP/1.1 %% we have full control of the socket and can therefore use active,N. %% For HTTP/2 we are just a stream, and are instead using read_body %% (automatic mode). Technically HTTP/2 will only go passive after %% receiving the next data message, while HTTP/1.1 goes passive %% immediately but there might still be data to be processed in %% the message queue. setopts_active(#state{transport={data_delivery, _}}) -> ok; setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) -> N = maps:get(active_n, Opts, 1), Transport:setopts(Socket, [{active, N}]). maybe_read_body(#state{transport={data_delivery, stream_handlers}, socket=Stream={Pid, _}, active=true}) -> %% @todo Keep Ref around. ReadBodyRef = make_ref(), Pid ! {Stream, {read_body, self(), ReadBodyRef, auto, infinity}}, ok; maybe_read_body(_) -> ok. active(State=#state{transport={data_delivery, relay}, socket=Stream={Pid, _}}) -> Pid ! {'$cowboy_relay_command', Stream, active}, State#state{active=true}; active(State0) -> State = State0#state{active=true}, setopts_active(State), maybe_read_body(State), State. passive(State=#state{transport={data_delivery, stream_handlers}}) -> %% Unfortunately we cannot currently cancel read_body. %% But that's OK, we will just stop reading the body %% after the next message. State#state{active=false}; passive(State=#state{transport={data_delivery, relay}, socket=Stream={Pid, _}}) -> Pid ! {'$cowboy_relay_command', Stream, passive}, State#state{active=false}; passive(State=#state{socket=Socket, transport=Transport, messages=Messages}) -> Transport:setopts(Socket, [{active, false}]), flush_passive(Socket, Messages), State#state{active=false}. flush_passive(Socket, Messages) -> receive {Passive, Socket} when Passive =:= element(4, Messages); %% Hardcoded for compatibility with Ranch 1.x. Passive =:= tcp_passive; Passive =:= ssl_passive -> flush_passive(Socket, Messages) after 0 -> ok end. before_loop(State=#state{hibernate=true}, HandlerState, ParseState) -> proc_lib:hibernate(?MODULE, loop, [State#state{hibernate=false}, HandlerState, ParseState]); before_loop(State, HandlerState, ParseState) -> loop(State, HandlerState, ParseState). -spec set_idle_timeout(#state{}, 0..?IDLE_TIMEOUT_TICKS) -> #state{}. %% @todo Do we really need this for HTTP/2? set_idle_timeout(State=#state{opts=Opts, timeout_ref=PrevRef}, TimeoutNum) -> %% Most of the time we don't need to cancel the timer since it %% will have triggered already. But this call is harmless so %% it is kept to simplify the code as we do need to cancel when %% options are changed dynamically. _ = case PrevRef of undefined -> ignore; PrevRef -> erlang:cancel_timer(PrevRef, [{async, true}, {info, false}]) end, case maps:get(idle_timeout, Opts, 60000) of infinity -> State#state{timeout_ref=undefined, timeout_num=TimeoutNum}; Timeout -> TRef = erlang:start_timer(Timeout div ?IDLE_TIMEOUT_TICKS, self(), ?MODULE), State#state{timeout_ref=TRef, timeout_num=TimeoutNum} end. -define(reset_idle_timeout(State), State#state{timeout_num=0}). tick_idle_timeout(State=#state{timeout_num=?IDLE_TIMEOUT_TICKS}, HandlerState, _) -> websocket_close(State, HandlerState, timeout); tick_idle_timeout(State=#state{timeout_num=TimeoutNum}, HandlerState, ParseState) -> before_loop(set_idle_timeout(State, TimeoutNum + 1), HandlerState, ParseState). -spec loop(#state{}, any(), parse_state()) -> no_return(). loop(State=#state{parent=Parent, socket=Socket, messages=Messages, timeout_ref=TRef}, HandlerState, ParseState) -> receive %% Socket messages. (HTTP/1.1) {OK, Socket, Data} when OK =:= element(1, Messages) -> State1 = maybe_resize_buffer(State, Data), parse(?reset_idle_timeout(State1), HandlerState, ParseState, Data); {Closed, Socket} when Closed =:= element(2, Messages) -> terminate(State, HandlerState, {error, closed}); {Error, Socket, Reason} when Error =:= element(3, Messages) -> terminate(State, HandlerState, {error, Reason}); {Passive, Socket} when Passive =:= element(4, Messages); %% Hardcoded for compatibility with Ranch 1.x. Passive =:= tcp_passive; Passive =:= ssl_passive -> setopts_active(State), loop(State, HandlerState, ParseState); %% Body reading messages. (HTTP/2) {request_body, _Ref, nofin, Data} -> maybe_read_body(State), parse(?reset_idle_timeout(State), HandlerState, ParseState, Data); %% @todo We need to handle this case as if it was an {error, closed} %% but not before we finish processing frames. We probably should have %% a check in before_loop to let us stop looping if a flag is set. {request_body, _Ref, fin, _, Data} -> maybe_read_body(State), parse(?reset_idle_timeout(State), HandlerState, ParseState, Data); %% @todo It would be better to check StreamID. %% @todo We must ensure that IsFin=fin is handled like a socket close? {'$cowboy_relay_data', {Pid, _StreamID}, _IsFin, Data} when Pid =:= Parent -> parse(?reset_idle_timeout(State), HandlerState, ParseState, Data); %% Timeouts. {timeout, TRef, ?MODULE} -> tick_idle_timeout(State, HandlerState, ParseState); {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> before_loop(State, HandlerState, ParseState); %% System messages. {'EXIT', Parent, Reason} -> %% The terminate reason will differ with HTTP/1.1 %% since we don't have direct access to the socket. %% @todo Perhaps we can make cowboy_children:terminate %% receive the shutdown Reason and send {shutdown, Reason} %% instead of just 'shutdown' in this scenario. terminate(State, HandlerState, Reason); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, HandlerState, ParseState}); %% Calls from supervisor module. {'$gen_call', From, Call} -> cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE), before_loop(State, HandlerState, ParseState); Message -> handler_call(State, HandlerState, ParseState, websocket_info, Message, fun before_loop/3) end. parse(State, HandlerState, PS=#ps_header{buffer=Buffer}, Data) -> parse_header(State, HandlerState, PS#ps_header{ buffer= <>}); parse(State, HandlerState, PS=#ps_payload{buffer=Buffer}, Data) -> parse_payload(State, HandlerState, PS#ps_payload{buffer= <<>>}, <>). parse_header(State=#state{opts=Opts, frag_state=FragState, extensions=Extensions}, HandlerState, ParseState=#ps_header{buffer=Data}) -> MaxFrameSize = maps:get(max_frame_size, Opts, infinity), case cow_ws:parse_header(Data, Extensions, FragState) of %% All frames sent from the client to the server are masked. {_, _, _, _, undefined, _} -> websocket_close(State, HandlerState, {error, badframe}); {_, _, _, Len, _, _} when Len > MaxFrameSize -> websocket_close(State, HandlerState, {error, badsize}); {Type, FragState2, Rsv, Len, MaskKey, Rest} -> parse_payload(State#state{frag_state=FragState2}, HandlerState, #ps_payload{type=Type, len=Len, mask_key=MaskKey, rsv=Rsv}, Rest); more -> before_loop(State, HandlerState, ParseState); error -> websocket_close(State, HandlerState, {error, badframe}) end. parse_payload(State=#state{opts=Opts, frag_state=FragState, utf8_state=Incomplete, extensions=Extensions}, HandlerState, ParseState=#ps_payload{ type=Type, len=Len, mask_key=MaskKey, rsv=Rsv, unmasked=Unmasked, unmasked_len=UnmaskedLen}, Data) -> MaxFrameSize = case maps:get(max_frame_size, Opts, infinity) of infinity -> infinity; MaxFrameSize0 -> MaxFrameSize0 - UnmaskedLen end, case cow_ws:parse_payload(Data, MaskKey, Incomplete, UnmaskedLen, Type, Len, FragState, Extensions#{max_inflate_size => MaxFrameSize}, Rsv) of {ok, CloseCode, Payload, Utf8State, Rest} -> dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState, ParseState#ps_payload{unmasked= <>, close_code=CloseCode}, Rest); {ok, Payload, Utf8State, Rest} -> dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState, ParseState#ps_payload{unmasked= <>}, Rest); {more, CloseCode, Payload, Utf8State} -> before_loop(State#state{utf8_state=Utf8State}, HandlerState, ParseState#ps_payload{len=Len - byte_size(Data), close_code=CloseCode, unmasked= <>, unmasked_len=UnmaskedLen + byte_size(Data)}); {more, Payload, Utf8State} -> before_loop(State#state{utf8_state=Utf8State}, HandlerState, ParseState#ps_payload{len=Len - byte_size(Data), unmasked= <>, unmasked_len=UnmaskedLen + byte_size(Data)}); Error = {error, _Reason} -> websocket_close(State, HandlerState, Error) end. dispatch_frame(State=#state{opts=Opts, frag_state=FragState, frag_buffer=SoFar}, HandlerState, #ps_payload{type=Type0, unmasked=Payload0, close_code=CloseCode0}, RemainingData) -> MaxFrameSize = maps:get(max_frame_size, Opts, infinity), case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of %% @todo Allow receiving fragments. {fragment, _, _, Payload} when byte_size(Payload) + byte_size(SoFar) > MaxFrameSize -> websocket_close(State, HandlerState, {error, badsize}); {fragment, nofin, _, Payload} -> parse_header(State#state{frag_buffer= << SoFar/binary, Payload/binary >>}, HandlerState, #ps_header{buffer=RemainingData}); {fragment, fin, Type, Payload} -> handler_call(State#state{frag_state=undefined, frag_buffer= <<>>}, HandlerState, #ps_header{buffer=RemainingData}, websocket_handle, {Type, << SoFar/binary, Payload/binary >>}, fun parse_header/3); close -> websocket_close(State, HandlerState, remote); {close, CloseCode, Payload} -> websocket_close(State, HandlerState, {remote, CloseCode, Payload}); Frame = ping -> transport_send(State, nofin, frame(pong, State)), handler_call(State, HandlerState, #ps_header{buffer=RemainingData}, websocket_handle, Frame, fun parse_header/3); Frame = {ping, Payload} -> transport_send(State, nofin, frame({pong, Payload}, State)), handler_call(State, HandlerState, #ps_header{buffer=RemainingData}, websocket_handle, Frame, fun parse_header/3); Frame -> handler_call(State, HandlerState, #ps_header{buffer=RemainingData}, websocket_handle, Frame, fun parse_header/3) end. handler_call(State=#state{handler=Handler}, HandlerState, ParseState, Callback, Message, NextState) -> try case Callback of websocket_init -> Handler:websocket_init(HandlerState); _ -> Handler:Callback(Message, HandlerState) end of {Commands, HandlerState2} when is_list(Commands) -> handler_call_result(State, HandlerState2, ParseState, NextState, Commands); {Commands, HandlerState2, hibernate} when is_list(Commands) -> handler_call_result(State#state{hibernate=true}, HandlerState2, ParseState, NextState, Commands); %% The following call results are deprecated. {ok, HandlerState2} -> NextState(State, HandlerState2, ParseState); {ok, HandlerState2, hibernate} -> NextState(State#state{hibernate=true}, HandlerState2, ParseState); {reply, Payload, HandlerState2} -> case websocket_send(Payload, State) of ok -> NextState(State, HandlerState2, ParseState); stop -> terminate(State, HandlerState2, stop); Error = {error, _} -> terminate(State, HandlerState2, Error) end; {reply, Payload, HandlerState2, hibernate} -> case websocket_send(Payload, State) of ok -> NextState(State#state{hibernate=true}, HandlerState2, ParseState); stop -> terminate(State, HandlerState2, stop); Error = {error, _} -> terminate(State, HandlerState2, Error) end; {stop, HandlerState2} -> websocket_close(State, HandlerState2, stop) catch Class:Reason:Stacktrace -> websocket_send_close(State, {crash, Class, Reason}), handler_terminate(State, HandlerState, {crash, Class, Reason}), erlang:raise(Class, Reason, Stacktrace) end. -spec handler_call_result(#state{}, any(), parse_state(), fun(), commands()) -> no_return(). handler_call_result(State0, HandlerState, ParseState, NextState, Commands) -> case commands(Commands, State0, []) of {ok, State} -> NextState(State, HandlerState, ParseState); {stop, State} -> terminate(State, HandlerState, stop); {Error = {error, _}, State} -> terminate(State, HandlerState, Error) end. commands([], State, []) -> {ok, State}; commands([], State, Data) -> Result = transport_send(State, nofin, lists:reverse(Data)), {Result, State}; commands([{active, Active}|Tail], State0=#state{active=Active0}, Data) when is_boolean(Active) -> State = if Active, not Active0 -> active(State0); Active0, not Active -> passive(State0); true -> State0 end, commands(Tail, State#state{active=Active}, Data); commands([{deflate, Deflate}|Tail], State, Data) when is_boolean(Deflate) -> commands(Tail, State#state{deflate=Deflate}, Data); commands([{set_options, SetOpts}|Tail], State0, Data) -> State = maps:fold(fun (idle_timeout, IdleTimeout, StateF=#state{opts=Opts}) -> %% We reset the number of ticks when changing the idle_timeout option. set_idle_timeout(StateF#state{opts=Opts#{idle_timeout => IdleTimeout}}, 0); (max_frame_size, MaxFrameSize, StateF=#state{opts=Opts}) -> StateF#state{opts=Opts#{max_frame_size => MaxFrameSize}}; (_, _, StateF) -> StateF end, State0, SetOpts), commands(Tail, State, Data); commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) -> commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data); commands([Frame|Tail], State, Data0) -> Data = [frame(Frame, State)|Data0], case is_close_frame(Frame) of true -> _ = transport_send(State, fin, lists:reverse(Data)), {stop, State}; false -> commands(Tail, State, Data) end. transport_send(#state{transport={data_delivery, stream_handlers}, socket=Stream={Pid, _}}, IsFin, Data) -> Pid ! {Stream, {data, IsFin, Data}}, ok; transport_send(#state{transport={data_delivery, relay}, socket=Stream={Pid, _}}, IsFin, Data) -> Pid ! {'$cowboy_relay_command', Stream, {data, IsFin, Data}}, ok; transport_send(#state{socket=Socket, transport=Transport}, _, Data) -> Transport:send(Socket, Data). -spec websocket_send(cow_ws:frame(), #state{}) -> ok | stop | {error, atom()}. websocket_send(Frames, State) when is_list(Frames) -> websocket_send_many(Frames, State, []); websocket_send(Frame, State) -> Data = frame(Frame, State), case is_close_frame(Frame) of true -> _ = transport_send(State, fin, Data), stop; false -> transport_send(State, nofin, Data) end. websocket_send_many([], State, Acc) -> transport_send(State, nofin, lists:reverse(Acc)); websocket_send_many([Frame|Tail], State, Acc0) -> Acc = [frame(Frame, State)|Acc0], case is_close_frame(Frame) of true -> _ = transport_send(State, fin, lists:reverse(Acc)), stop; false -> websocket_send_many(Tail, State, Acc) end. is_close_frame(close) -> true; is_close_frame({close, _}) -> true; is_close_frame({close, _, _}) -> true; is_close_frame(_) -> false. -spec websocket_close(#state{}, any(), terminate_reason()) -> no_return(). websocket_close(State, HandlerState, Reason) -> websocket_send_close(State, Reason), terminate(State, HandlerState, Reason). websocket_send_close(State, Reason) -> _ = case Reason of Normal when Normal =:= stop; Normal =:= timeout -> transport_send(State, fin, frame({close, 1000, <<>>}, State)); {error, badframe} -> transport_send(State, fin, frame({close, 1002, <<>>}, State)); {error, badencoding} -> transport_send(State, fin, frame({close, 1007, <<>>}, State)); {error, badsize} -> transport_send(State, fin, frame({close, 1009, <<>>}, State)); {crash, _, _} -> transport_send(State, fin, frame({close, 1011, <<>>}, State)); remote -> transport_send(State, fin, frame(close, State)); {remote, Code, _} -> transport_send(State, fin, frame({close, Code, <<>>}, State)) end, ok. %% Don't compress frames while deflate is disabled. frame(Frame, #state{deflate=false, extensions=Extensions}) -> cow_ws:frame(Frame, Extensions#{deflate => false}); frame(Frame, #state{extensions=Extensions}) -> cow_ws:frame(Frame, Extensions). -spec terminate(#state{}, any(), terminate_reason()) -> no_return(). terminate(State=#state{shutdown_reason=Shutdown}, HandlerState, Reason) -> handler_terminate(State, HandlerState, Reason), case Shutdown of normal -> exit(normal); _ -> exit({shutdown, Shutdown}) end. handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) -> cowboy_handler:terminate(Reason, Req, HandlerState, Handler). %% System callbacks. -spec system_continue(_, _, {#state{}, any(), parse_state()}) -> no_return(). system_continue(_, _, {State, HandlerState, ParseState}) -> loop(State, HandlerState, ParseState). -spec system_terminate(any(), _, _, {#state{}, any(), parse_state()}) -> no_return(). system_terminate(Reason, _, _, {State, HandlerState, _}) -> %% @todo We should exit gracefully, if possible. terminate(State, HandlerState, Reason). -spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, any(), parse_state()}. system_code_change(Misc, _, _, _) -> {ok, Misc}. ================================================ FILE: src/cowboy_webtransport.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% @todo To enable WebTransport the following options need to be set: %% %% QUIC: %% - max_datagram_frame_size > 0 %% %% HTTP/3: %% - SETTINGS_H3_DATAGRAM = 1 %% - SETTINGS_ENABLE_CONNECT_PROTOCOL = 1 %% - SETTINGS_WT_MAX_SESSIONS >= 1 %% Cowboy supports versions 07 through 13 of the WebTransport drafts. %% Cowboy also has some compatibility with version 02. %% %% WebTransport CONNECT requests go through cowboy_stream as normal %% and then an upgrade/switch_protocol is issued (just like Websocket). %% After that point none of the events go through cowboy_stream except %% the final terminate event. The request process becomes the process %% handling all events in the WebTransport session. %% %% WebTransport sessions can be ended via a command, via a crash or %% exit, via the closing of the connection (client or server inititated), %% via the client ending the session (mirroring the command) or via %% the client terminating the CONNECT stream. -module(cowboy_webtransport). -export([upgrade/4]). -export([upgrade/5]). %% cowboy_stream. -export([info/3]). -export([terminate/3]). -type stream_type() :: unidi | bidi. -type open_stream_ref() :: any(). -type event() :: {stream_open, cow_http3:stream_id(), stream_type()} | {opened_stream_id, open_stream_ref(), cow_http3:stream_id()} | {stream_data, cow_http3:stream_id(), cow_http:fin(), binary()} | {datagram, binary()} | close_initiated. -type commands() :: [ {open_stream, open_stream_ref(), stream_type(), iodata()} | {close_stream, cow_http3:stream_id(), cow_http3:wt_app_error_code()} | {send, cow_http3:stream_id() | datagram, iodata()} | initiate_close | close | {close, cow_http3:wt_app_error_code()} | {close, cow_http3:wt_app_error_code(), iodata()} ]. -export_type([commands/0]). -type call_result(State) :: {commands(), State} | {commands(), State, hibernate}. -callback init(Req, any()) -> {ok | module(), Req, any()} | {module(), Req, any(), any()} when Req::cowboy_req:req(). -callback webtransport_init(State) -> call_result(State) when State::any(). -optional_callbacks([webtransport_init/1]). -callback webtransport_handle(event(), State) -> call_result(State) when State::any(). -optional_callbacks([webtransport_handle/2]). -callback webtransport_info(any(), State) -> call_result(State) when State::any(). -optional_callbacks([webtransport_info/2]). -callback terminate(any(), cowboy_req:req(), any()) -> ok. -optional_callbacks([terminate/3]). -type opts() :: #{ req_filter => fun((cowboy_req:req()) -> map()) }. -export_type([opts/0]). -record(state, { id :: cow_http3:stream_id(), parent :: pid(), opts = #{} :: opts(), handler :: module(), hibernate = false :: boolean(), req = #{} :: map() }). %% This function mirrors a similar function for Websocket. -spec is_upgrade_request(cowboy_req:req()) -> boolean(). is_upgrade_request(#{version := Version, method := <<"CONNECT">>, protocol := Protocol}) when Version =:= 'HTTP/3' -> %% @todo scheme MUST BE "https" <<"webtransport">> =:= cowboy_bstr:to_lower(Protocol); is_upgrade_request(_) -> false. %% Stream process. -spec upgrade(Req, Env, module(), any()) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade(Req, Env, Handler, HandlerState) -> upgrade(Req, Env, Handler, HandlerState, #{}). -spec upgrade(Req, Env, module(), any(), opts()) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). %% @todo Immediately crash if a response has already been sent. upgrade(Req=#{version := 'HTTP/3', pid := Pid, streamid := StreamID}, Env, Handler, HandlerState, Opts) -> FilteredReq = case maps:get(req_filter, Opts, undefined) of undefined -> maps:with([method, version, scheme, host, port, path, qs, peer], Req); FilterFun -> FilterFun(Req) end, State = #state{id=StreamID, parent=Pid, opts=Opts, handler=Handler, req=FilteredReq}, %% @todo Must ensure the relevant settings are enabled (QUIC and H3). %% Either we check them BEFORE, or we check them when the handler %% is OK to initiate a webtransport session. Probably need to %% check them BEFORE as we need to become (takeover) the webtransport process %% after we are done with the upgrade. Maybe in cow_http3_machine but %% it doesn't have QUIC settings currently (max_datagram_size). case is_upgrade_request(Req) of true -> Headers = cowboy_req:response_headers(#{}, Req), Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, #{session_pid => self()}}}, webtransport_init(State, HandlerState); %% Use 501 Not Implemented to mirror the recommendation in %% by RFC9220 3 (WebSockets Upgrade over HTTP/3). false -> %% @todo I don't think terminate will be called. {ok, cowboy_req:reply(501, Req), Env} end. webtransport_init(State=#state{handler=Handler}, HandlerState) -> case erlang:function_exported(Handler, webtransport_init, 1) of true -> handler_call(State, HandlerState, webtransport_init, undefined); false -> before_loop(State, HandlerState) end. before_loop(State=#state{hibernate=true}, HandlerState) -> proc_lib:hibernate(?MODULE, loop, [State#state{hibernate=false}, HandlerState]); before_loop(State, HandlerState) -> loop(State, HandlerState). -spec loop(#state{}, any()) -> no_return(). loop(State=#state{id=SessionID, parent=Parent}, HandlerState) -> receive {'$webtransport_event', SessionID, Event={closed, _, _}} -> terminate_proc(State, HandlerState, Event); {'$webtransport_event', SessionID, Event=closed_abruptly} -> terminate_proc(State, HandlerState, Event); {'$webtransport_event', SessionID, Event} -> handler_call(State, HandlerState, webtransport_handle, Event); %% Timeouts. %% @todo idle_timeout % {timeout, TRef, ?MODULE} -> % tick_idle_timeout(State, HandlerState, ParseState); % {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> % before_loop(State, HandlerState, ParseState); %% System messages. {'EXIT', Parent, Reason} -> %% @todo We should exit gracefully. exit(Reason); {system, From, Request} -> sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, HandlerState}); %% Calls from supervisor module. {'$gen_call', From, Call} -> cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE), before_loop(State, HandlerState); Message -> handler_call(State, HandlerState, webtransport_info, Message) end. handler_call(State=#state{handler=Handler}, HandlerState, Callback, Message) -> try case Callback of webtransport_init -> Handler:webtransport_init(HandlerState); _ -> Handler:Callback(Message, HandlerState) end of {Commands, HandlerState2} when is_list(Commands) -> handler_call_result(State, HandlerState2, Commands); {Commands, HandlerState2, hibernate} when is_list(Commands) -> handler_call_result(State#state{hibernate=true}, HandlerState2, Commands) catch Class:Reason:Stacktrace -> %% @todo Do we need to send a close? Let cowboy_http3 detect and handle it? handler_terminate(State, HandlerState, {crash, Class, Reason}), erlang:raise(Class, Reason, Stacktrace) end. handler_call_result(State0, HandlerState, Commands) -> case commands(Commands, State0, ok, []) of {ok, State} -> before_loop(State, HandlerState); {stop, State} -> terminate_proc(State, HandlerState, stop) end. %% We accumulate the commands that must be sent to the connection process %% because we want to send everything into one message. Other commands are %% processed immediately. commands([], State, Res, []) -> {Res, State}; commands([], State=#state{id=SessionID, parent=Pid}, Res, Commands) -> Pid ! {'$webtransport_commands', SessionID, lists:reverse(Commands)}, {Res, State}; %% {open_stream, OpenStreamRef, StreamType, InitialData}. commands([Command={open_stream, _, _, _}|Tail], State, Res, Acc) -> commands(Tail, State, Res, [Command|Acc]); %% {close_stream, StreamID, Code}. commands([Command={close_stream, _, _}|Tail], State, Res, Acc) -> commands(Tail, State, Res, [Command|Acc]); %% @todo We must reject send to a remote unidi stream. %% {send, StreamID | datagram, Data}. commands([Command={send, _, _}|Tail], State, Res, Acc) -> commands(Tail, State, Res, [Command|Acc]); %% {send, StreamID, IsFin, Data}. commands([Command={send, _, _, _}|Tail], State, Res, Acc) -> commands(Tail, State, Res, [Command|Acc]); %% initiate_close - DRAIN_WT_SESSION commands([Command=initiate_close|Tail], State, Res, Acc) -> commands(Tail, State, Res, [Command|Acc]); %% close | {close, Code} | {close, Code, Msg} - CLOSE_WT_SESSION %% @todo At this point the handler must not issue stream or send commands. commands([Command=close|Tail], State, _, Acc) -> commands(Tail, State, stop, [Command|Acc]); commands([Command={close, _}|Tail], State, _, Acc) -> commands(Tail, State, stop, [Command|Acc]); commands([Command={close, _, _}|Tail], State, _, Acc) -> commands(Tail, State, stop, [Command|Acc]). %% @todo A set_options command could be useful to increase the number of allowed streams %% or other forms of flow control. Alternatively a flow command. Or both. %% @todo A shutdown_reason command could be useful for the same reasons as Websocekt. -spec terminate_proc(_, _, _) -> no_return(). terminate_proc(State, HandlerState, Reason) -> handler_terminate(State, HandlerState, Reason), %% @todo This is what should be done if shutdown_reason gets implemented. % case Shutdown of % normal -> exit(normal); % _ -> exit({shutdown, Shutdown}) % end. exit(normal). handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) -> cowboy_handler:terminate(Reason, Req, HandlerState, Handler). %% cowboy_stream callbacks. %% %% We shortcut stream handlers but still need to process some events %% such as process exiting or termination. We implement the relevant %% callbacks here. Note that as far as WebTransport is concerned, %% receiving stream data here would be an error therefore the data %% callback is not implemented. %% %% @todo Better type than map() for the cowboy_stream state. %% @todo Is this really useful? -spec info(cowboy_stream:streamid(), any(), State) -> {cowboy_stream:commands(), State} when State::map(). info(StreamID, Msg, WTState=#{stream_state := StreamState0}) -> {Commands, StreamState} = cowboy_stream:info(StreamID, Msg, StreamState0), {Commands, WTState#{stream_state => StreamState}}. -spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), map()) -> any(). terminate(StreamID, Reason, #{stream_state := StreamState}) -> cowboy_stream:terminate(StreamID, Reason, StreamState). ================================================ FILE: test/compress_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(compress_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> All = [ {group, http_compress}, {group, https_compress}, {group, h2_compress}, {group, h2c_compress}, {group, h3_compress} ], %% Don't run HTTP/3 tests on Windows for now. case os:type() of {win32, _} -> All -- [{group, h3_compress}]; _ -> All end. groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Routes. init_dispatch(_Config) -> cowboy_router:compile([{"[...]", [ {"/reply/:what", compress_h, reply}, {"/stream_reply/:what", compress_h, stream_reply} ]}]). %% Internal. do_get(Path, ReqHeaders, Config) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, ReqHeaders), {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref), {ok, Body} = case IsFin of nofin -> gun:await_body(ConnPid, Ref); fin -> {ok, <<>>} end, gun:close(ConnPid), {Status, RespHeaders, Body}. %% Tests. gzip_accept_encoding_malformed(Config) -> doc("Send malformed accept-encoding; get an uncompressed response."), {200, Headers, _} = do_get("/reply/large", [{<<"accept-encoding">>, <<";">>}], Config), false = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100000">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_accept_encoding_missing(Config) -> doc("Don't send accept-encoding; get an uncompressed response."), {200, Headers, _} = do_get("/reply/large", [], Config), false = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100000">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_accept_encoding_no_gzip(Config) -> doc("Send accept-encoding: compress (unsupported by Cowboy); get an uncompressed response."), {200, Headers, _} = do_get("/reply/large", [{<<"accept-encoding">>, <<"compress">>}], Config), false = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100000">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_accept_encoding_not_supported(Config) -> doc("Send unsupported accept-encoding; get an uncompressed response."), {200, Headers, _} = do_get("/reply/large", [{<<"accept-encoding">>, <<"application/gzip">>}], Config), false = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100000">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_reply_content_encoding(Config) -> doc("Reply with content-encoding header; get an uncompressed response."), {200, Headers, _} = do_get("/reply/content-encoding", [{<<"accept-encoding">>, <<"gzip">>}], Config), %% We set the content-encoding to compress; without actually compressing. {_, <<"compress">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), %% The reply didn't include a vary header. {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100000">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_reply_etag(Config) -> doc("Reply with etag header; get an uncompressed response."), {200, Headers, _} = do_get("/reply/etag", [{<<"accept-encoding">>, <<"gzip">>}], Config), %% We set a strong etag. {_, <<"\"STRONK\"">>} = lists:keyfind(<<"etag">>, 1, Headers), %% The reply didn't include a vary header. {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100000">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_reply_large_body(Config) -> doc("Reply a large body; get a gzipped response."), {200, Headers, GzBody} = do_get("/reply/large", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, Length} = lists:keyfind(<<"content-length">>, 1, Headers), ct:log("Original length: 100000; compressed: ~s.", [Length]), _ = zlib:gunzip(GzBody), ok. gzip_reply_sendfile(Config) -> doc("Reply using sendfile; get an uncompressed response."), {200, Headers, Body} = do_get("/reply/sendfile", [{<<"accept-encoding">>, <<"gzip">>}], Config), false = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), ct:log("Body received:~n~p~n", [Body]), ok. gzip_reply_small_body(Config) -> doc("Reply a small body; get an uncompressed response."), {200, Headers, _} = do_get("/reply/small", [{<<"accept-encoding">>, <<"gzip">>}], Config), false = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), {_, <<"100">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. gzip_stream_reply(Config) -> doc("Stream reply; get a gzipped response."), {200, Headers, GzBody} = do_get("/stream_reply/large", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), _ = zlib:gunzip(GzBody), ok. gzip_stream_reply_sendfile(Config) -> doc("Stream reply using sendfile for some chunks; get a gzipped response."), {200, Headers, GzBody} = do_get("/stream_reply/sendfile", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), _ = zlib:gunzip(GzBody), ok. gzip_stream_reply_sendfile_fin(Config) -> doc("Stream reply using sendfile for some chunks; get a gzipped response."), {200, Headers, GzBody} = do_get("/stream_reply/sendfile_fin", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), _ = zlib:gunzip(GzBody), ok. gzip_stream_reply_content_encoding(Config) -> doc("Stream reply with content-encoding header; get an uncompressed response."), {200, Headers, Body} = do_get("/stream_reply/content-encoding", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"compress">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), 100000 = iolist_size(Body), ok. gzip_stream_reply_etag(Config) -> doc("Stream reply with etag header; get an uncompressed response."), {200, Headers, Body} = do_get("/stream_reply/etag", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"\"STRONK\"">>} = lists:keyfind(<<"etag">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), 100000 = iolist_size(Body), ok. opts_compress_buffering_false(Config0) -> doc("Confirm that the compress_buffering option can be set to false, " "which is the default."), Fun = case config(ref, Config0) of https_compress -> init_https; h2_compress -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => init_dispatch(Config0)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h], compress_buffering => false }, Config0), try ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/stream_reply/delayed", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), Z = zlib:open(), zlib:inflateInit(Z, 31), {data, nofin, Data1} = gun:await(ConnPid, Ref, 500), <<"data: Hello!\r\n\r\n">> = iolist_to_binary(zlib:inflate(Z, Data1)), timer:sleep(1000), {data, nofin, Data2} = gun:await(ConnPid, Ref, 500), <<"data: World!\r\n\r\n">> = iolist_to_binary(zlib:inflate(Z, Data2)), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. opts_compress_buffering_true(Config0) -> doc("Confirm that the compress_buffering option can be set to true, " "and that the data received is buffered."), Fun = case config(ref, Config0) of https_compress -> init_https; h2_compress -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => init_dispatch(Config0)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h], compress_buffering => true }, Config0), try ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/stream_reply/delayed", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), Z = zlib:open(), zlib:inflateInit(Z, 31), %% The data gets buffered because it is too small. %% In zlib versions before OTP 20.1 the gzip header was also buffered. <<>> = case gun:await(ConnPid, Ref, 500) of {data, nofin, Data1} -> iolist_to_binary(zlib:inflate(Z, Data1)); {error, timeout} -> <<>> end, gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_compress_buffering_false(Config0) -> doc("Confirm that the compress_buffering option can be dynamically " "set to false by a handler and that the data received is not buffered."), Fun = case config(ref, Config0) of https_compress -> init_https; h2_compress -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => init_dispatch(Config0)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h], compress_buffering => true }, Config0), try ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/stream_reply/set_options_buffering_false", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), Z = zlib:open(), zlib:inflateInit(Z, 31), {data, nofin, Data1} = gun:await(ConnPid, Ref, 500), <<"data: Hello!\r\n\r\n">> = iolist_to_binary(zlib:inflate(Z, Data1)), timer:sleep(1000), {data, nofin, Data2} = gun:await(ConnPid, Ref, 500), <<"data: World!\r\n\r\n">> = iolist_to_binary(zlib:inflate(Z, Data2)), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_compress_buffering_true(Config0) -> doc("Confirm that the compress_buffering option can be dynamically " "set to true by a handler and that the data received is buffered."), Fun = case config(ref, Config0) of https_compress -> init_https; h2_compress -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => init_dispatch(Config0)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h], compress_buffering => false }, Config0), try ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/stream_reply/set_options_buffering_true", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), Z = zlib:open(), zlib:inflateInit(Z, 31), %% The data gets buffered because it is too small. %% In zlib versions before OTP 20.1 the gzip header was also buffered. <<>> = case gun:await(ConnPid, Ref, 500) of {data, nofin, Data1} -> iolist_to_binary(zlib:inflate(Z, Data1)); {error, timeout} -> <<>> end, gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_compress_threshold_0(Config) -> doc("Confirm that the compress_threshold option can be dynamically " "set to change how large response bodies must be to be compressed."), {200, Headers, GzBody} = do_get("/reply/set_options_threshold0", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), _ = zlib:gunzip(GzBody), ok. vary_accept(Config) -> doc("Add accept-encoding to vary when the response has a 'vary: accept' header."), {200, Headers, _} = do_get("/reply/vary", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-vary">>, <<"accept">>} ], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept, accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), ok. vary_accept_accept_encoding(Config) -> doc("Don't change the vary value when the response has a 'vary: accept, accept-encoding' header."), {200, Headers, _} = do_get("/reply/vary", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-vary">>, <<"accept, accept-encoding">>} ], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept, accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), ok. vary_empty(Config) -> doc("Add accept-encoding to vary when the response has an empty vary header."), {200, Headers, _} = do_get("/reply/vary", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-vary">>, <<>>} ], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"accept-encoding">>} = lists:keyfind(<<"vary">>, 1, Headers), ok. vary_wildcard(Config) -> doc("Don't change the vary value when the response has a 'vary: *' header."), {200, Headers, _} = do_get("/reply/vary", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-vary">>, <<"*">>} ], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), {_, <<"*">>} = lists:keyfind(<<"vary">>, 1, Headers), ok. ================================================ FILE: test/cover.spec ================================================ {incl_app, cowboy, details}. ================================================ FILE: test/cowboy_ct_hook.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_ct_hook). -export([init/2]). init(_, _) -> ct_helper:start([cowboy, gun]), ct_helper:make_certs_in_ets(), error_logger:add_report_handler(ct_helper_error_h), {ok, undefined}. ================================================ FILE: test/cowboy_test.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(cowboy_test). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). %% Listeners initialization. init_http(Ref, ProtoOpts, Config) -> {ok, _} = cowboy:start_clear(Ref, [{port, 0}], ProtoOpts), Port = ranch:get_port(Ref), [{ref, Ref}, {type, tcp}, {protocol, http}, {port, Port}, {opts, []}|Config]. init_https(Ref, ProtoOpts, Config) -> Opts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(Ref, Opts ++ [{port, 0}, {verify, verify_none}], ProtoOpts), Port = ranch:get_port(Ref), [{ref, Ref}, {type, ssl}, {protocol, http}, {port, Port}, {opts, Opts}|Config]. init_http2(Ref, ProtoOpts, Config) -> Opts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(Ref, Opts ++ [{port, 0}, {verify, verify_none}], ProtoOpts), Port = ranch:get_port(Ref), [{ref, Ref}, {type, ssl}, {protocol, http2}, {port, Port}, {opts, Opts}|Config]. %% @todo This will probably require TransOpts as argument. init_http3(Ref, ProtoOpts, Config) -> %% @todo Quicer does not currently support non-file cert/key, %% so we use quicer test certificates for now. %% @todo Quicer also does not support cacerts which means %% we currently have no authentication based security. DataDir = filename:dirname(filename:dirname(config(data_dir, Config))) ++ "/rfc9114_SUITE_data", TransOpts = #{ socket_opts => [ {certfile, DataDir ++ "/server.pem"}, {keyfile, DataDir ++ "/server.key"} ] }, {ok, Listener} = cowboy:start_quic(Ref, TransOpts, ProtoOpts), {ok, {_, Port}} = quicer:sockname(Listener), %% @todo Keep listener information around in a better place. persistent_term:put({cowboy_test_quic, Ref}, Listener), [{ref, Ref}, {type, quic}, {protocol, http3}, {port, Port}, {opts, TransOpts}|Config]. stop_group(Ref) -> case persistent_term:get({cowboy_test_quic, Ref}, undefined) of undefined -> cowboy:stop_listener(Ref); Listener -> quicer:close_listener(Listener) end. %% Common group of listeners used by most suites. common_all() -> All = [ {group, http}, {group, https}, {group, h2}, {group, h2c}, {group, h3}, {group, http_compress}, {group, https_compress}, {group, h2_compress}, {group, h2c_compress}, {group, h3_compress} ], %% Don't run HTTP/3 tests on Windows for now. case os:type() of {win32, _} -> All -- [{group, h3}, {group, h3_compress}]; _ -> All end. common_groups(Tests) -> Parallel = case os:getenv("NO_PARALLEL") of false -> parallel; _ -> no_parallel end, common_groups(Tests, Parallel). common_groups(Tests, Parallel) -> Opts = case Parallel of parallel -> [parallel]; no_parallel -> [] end, Groups = [ {http, Opts, Tests}, {https, Opts, Tests}, {h2, Opts, Tests}, {h2c, Opts, Tests}, {h3, Opts, Tests}, {http_compress, Opts, Tests}, {https_compress, Opts, Tests}, {h2_compress, Opts, Tests}, {h2c_compress, Opts, Tests}, {h3_compress, Opts, Tests} ], %% Don't run HTTP/3 tests on Windows for now. case os:type() of {win32, _} -> Groups -- [{h3, Opts, Tests}, {h3_compress, Opts, Tests}]; _ -> Groups end. init_common_groups(Name, Config, Mod) -> init_common_groups(Name, Config, Mod, #{}). init_common_groups(Name = http, Config, Mod, ProtoOpts) -> init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); init_common_groups(Name = https, Config, Mod, ProtoOpts) -> init_https(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); init_common_groups(Name = h2, Config, Mod, ProtoOpts) -> init_http2(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); init_common_groups(Name = h2c, Config, Mod, ProtoOpts) -> Config1 = init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_common_groups(Name = h3, Config, Mod, ProtoOpts) -> init_http3(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)} }, [{flavor, vanilla}|Config]); init_common_groups(Name = http_compress, Config, Mod, ProtoOpts) -> init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]); init_common_groups(Name = https_compress, Config, Mod, ProtoOpts) -> init_https(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]); init_common_groups(Name = h2_compress, Config, Mod, ProtoOpts) -> init_http2(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]); init_common_groups(Name = h2c_compress, Config, Mod, ProtoOpts) -> Config1 = init_http(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_common_groups(Name = h3_compress, Config, Mod, ProtoOpts) -> init_http3(Name, ProtoOpts#{ env => #{dispatch => Mod:init_dispatch(Config)}, stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, compress}|Config]). %% Support functions for testing using Gun. gun_open(Config) -> gun_open(Config, #{}). gun_open(Config, Opts) -> TlsOpts = case proplists:get_value(no_cert, Config, false) of true -> [{verify, verify_none}]; false -> ct_helper:get_certs_from_ets() %% @todo Wrong in current quicer. end, {ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{ retry => 0, transport => config(type, Config), tls_opts => TlsOpts, protocols => [config(protocol, Config)] }), ConnPid. gun_down(ConnPid) -> receive {gun_down, ConnPid, _, _, _} -> ok after 500 -> error(timeout) end. %% Support functions for testing using a raw socket. raw_open(Config) -> Transport = case config(type, Config) of tcp -> gen_tcp; ssl -> ssl end, {_, Opts} = lists:keyfind(opts, 1, Config), {ok, Socket} = Transport:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}, {reuseaddr, true}, {nodelay, true}|Opts]), {raw_client, Socket, Transport}. raw_send({raw_client, Socket, Transport}, Data) -> Transport:send(Socket, Data). raw_recv_head({raw_client, Socket, Transport}) -> {ok, Data} = Transport:recv(Socket, 0, 10000), raw_recv_head(Socket, Transport, Data). raw_recv_head(Socket, Transport, Buffer) -> case binary:match(Buffer, <<"\r\n\r\n">>) of nomatch -> {ok, Data} = Transport:recv(Socket, 0, 10000), raw_recv_head(Socket, Transport, << Buffer/binary, Data/binary >>); {_, _} -> Buffer end. raw_recv_rest({raw_client, _, _}, Length, Buffer) when Length =:= byte_size(Buffer) -> Buffer; raw_recv_rest({raw_client, Socket, Transport}, Length, Buffer) when Length > byte_size(Buffer) -> {ok, Data} = Transport:recv(Socket, Length - byte_size(Buffer), 10000), << Buffer/binary, Data/binary >>. raw_recv({raw_client, Socket, Transport}, Length, Timeout) -> Transport:recv(Socket, Length, Timeout). raw_expect_recv({raw_client, _, _}, <<>>) -> ok; raw_expect_recv({raw_client, Socket, Transport}, Expect) -> {ok, Expect} = Transport:recv(Socket, iolist_size(Expect), 10000), ok. ================================================ FILE: test/decompress_SUITE.erl ================================================ %% Copyright (c) jdamanalo %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(decompress_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name = http, Config) -> cowboy_test:init_http(Name, init_plain_opts(Config), Config); init_per_group(Name = https, Config) -> cowboy_test:init_http(Name, init_plain_opts(Config), Config); init_per_group(Name = h2, Config) -> cowboy_test:init_http2(Name, init_plain_opts(Config), Config); init_per_group(Name = h2c, Config) -> Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = h3, Config) -> cowboy_test:init_http3(Name, init_plain_opts(Config), Config); init_per_group(Name = http_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(Config), Config); init_per_group(Name = https_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(Config), Config); init_per_group(Name = h2_compress, Config) -> cowboy_test:init_http2(Name, init_compress_opts(Config), Config); init_per_group(Name = h2c_compress, Config) -> Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = h3_compress, Config) -> cowboy_test:init_http3(Name, init_compress_opts(Config), Config). end_per_group(Name, _) -> cowboy:stop_listener(Name). init_plain_opts(Config) -> #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, stream_handlers => [cowboy_decompress_h, cowboy_stream_h] }. init_compress_opts(Config) -> #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, stream_handlers => [cowboy_decompress_h, cowboy_compress_h, cowboy_stream_h] }. init_routes(_) -> [{'_', [ {"/echo/:what", decompress_h, echo}, {"/test/:what", decompress_h, test} ]}]. %% Internal. do_post(Path, ReqHeaders, Body, Config) -> ConnPid = gun_open(Config), Ref = gun:post(ConnPid, Path, ReqHeaders, Body), {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref), {ok, ResponseBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref); fin -> {ok, <<>>} end, gun:close(ConnPid), {Status, RespHeaders, ResponseBody}. create_gzip_bomb() -> Z = zlib:open(), zlib:deflateInit(Z, 9, deflated, 31, 8, default), %% 1000 chunks of 100000 zeroes (100MB). Bomb = do_create_gzip_bomb(Z, 1000), zlib:deflateEnd(Z), zlib:close(Z), iolist_to_binary(Bomb). do_create_gzip_bomb(Z, 0) -> zlib:deflate(Z, << >>, finish); do_create_gzip_bomb(Z, N) -> Data = <<0:800000>>, Deflate = zlib:deflate(Z, Data), [Deflate | do_create_gzip_bomb(Z, N - 1)]. %% Tests. content_encoding_none(Config) -> doc("Requests without content-encoding are processed normally."), Body = <<"test">>, {200, _, Body} = do_post("/echo/normal", [], Body, Config), %% The content-encoding header would be propagated, %% but there was no content-encoding header to propagate. {200, _, <<"undefined">>} = do_post("/test/content-encoding", [], Body, Config), %% The content_decoded list is empty. {200, _, <<"[]">>} = do_post("/test/content-decoded", [], Body, Config), ok. content_encoding_malformed(Config) -> doc("Requests with a malformed content-encoding are processed " "as if no content-encoding was sent."), Body = <<"test">>, {200, _, Body} = do_post("/echo/normal", [{<<"content-encoding">>, <<";">>}], Body, Config), %% The content-encoding header is propagated. {200, _, <<";">>} = do_post("/test/content-encoding", [{<<"content-encoding">>, <<";">>}], Body, Config), %% The content_decoded list is empty. {200, _, <<"[]">>} = do_post("/test/content-decoded", [{<<"content-encoding">>, <<";">>}], Body, Config), ok. content_encoding_not_supported(Config) -> doc("Requests with an unsupported content-encoding are processed " "as if no content-encoding was sent."), Body = <<"test">>, {200, _, Body} = do_post("/echo/normal", [{<<"content-encoding">>, <<"compress">>}], Body, Config), %% The content-encoding header is propagated. {200, _, <<"compress">>} = do_post("/test/content-encoding", [{<<"content-encoding">>, <<"compress">>}], Body, Config), %% The content_decoded list is empty. {200, _, <<"[]">>} = do_post("/test/content-decoded", [{<<"content-encoding">>, <<"compress">>}], Body, Config), ok. content_encoding_multiple(Config) -> doc("Requests with multiple content-encoding values are processed " "as if no content-encoding was sent."), Body = <<"test">>, {200, _, Body} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip, compress">>}], Body, Config), %% The content-encoding header is propagated. {200, _, <<"gzip, compress">>} = do_post("/test/content-encoding", [{<<"content-encoding">>, <<"gzip, compress">>}], Body, Config), %% The content_decoded list is empty. {200, _, <<"[]">>} = do_post("/test/content-decoded", [{<<"content-encoding">>, <<"gzip, compress">>}], Body, Config), ok. decompress(Config) -> doc("Requests with content-encoding set to gzip and gzipped data " "are transparently decompressed."), Data = <<"test">>, Body = zlib:gzip(Data), {200, _, Data} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), %% The content-encoding header is NOT propagated. {200, _, <<"undefined">>} = do_post("/test/content-encoding", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), %% The content_decoded list contains <<"gzip">>. {200, _, <<"[<<\"gzip\">>]">>} = do_post("/test/content-decoded", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), ok. decompress_error(Config) -> doc("Requests with content-encoding set to gzip but the data " "cannot be decoded are rejected with a 400 Bad Request error."), Body = <<"test">>, {400, _, _} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), ok. decompress_stream(Config) -> doc("Requests with content-encoding set to gzip and gzipped data " "are transparently decompressed, even when the data is streamed."), %% Handler read length 1KB. Compressing 3KB should be enough to trigger more. Data = crypto:strong_rand_bytes(3000), Body = zlib:gzip(Data), Size = byte_size(Body), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/echo/normal", [{<<"content-encoding">>, <<"gzip">>}]), gun:data(ConnPid, Ref, nofin, binary:part(Body, 0, Size div 2)), timer:sleep(1000), gun:data(ConnPid, Ref, fin, binary:part(Body, Size div 2, Size div 2 + Size rem 2)), {response, IsFin, 200, _} = gun:await(ConnPid, Ref), {ok, Data} = case IsFin of nofin -> gun:await_body(ConnPid, Ref); fin -> {ok, <<>>} end, gun:close(ConnPid), %% The content-encoding header is NOT propagated. ConnPid2 = gun_open(Config), Ref2 = gun:post(ConnPid2, "/test/content-encoding", [{<<"content-encoding">>, <<"gzip">>}]), {response, nofin, 200, _} = gun:await(ConnPid2, Ref2), {ok, <<"undefined">>} = gun:await_body(ConnPid2, Ref2), gun:close(ConnPid2), %% The content_decoded list contains <<"gzip">>. ConnPid3 = gun_open(Config), Ref3 = gun:post(ConnPid3, "/test/content-decoded", [{<<"content-encoding">>, <<"gzip">>}]), {response, nofin, 200, _} = gun:await(ConnPid3, Ref3), {ok, <<"[<<\"gzip\">>]">>} = gun:await_body(ConnPid3, Ref3), gun:close(ConnPid3). opts_decompress_enabled_false(Config0) -> doc("Confirm that the decompress_enabled option can be set."), Fun = case config(ref, Config0) of HTTPS when HTTPS =:= https_compress; HTTPS =:= https -> init_https; H2 when H2 =:= h2_compress; H2 =:= h2 -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, stream_handlers => [cowboy_decompress_h, cowboy_stream_h], decompress_enabled => false }, Config0), Data = <<"test">>, Body = zlib:gzip(Data), try {200, Headers, Body} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), %% We do not set accept-encoding when we are disabled. false = lists:keyfind(<<"accept-encoding">>, 1, Headers) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_decompress_enabled_false(Config) -> doc("Confirm that the decompress_enabled option can be dynamically " "set to false and the data received is not decompressed."), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Body} = do_post("/echo/decompress_disable", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), %% We do not set accept-encoding when we are disabled. false = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. set_options_decompress_disable_in_the_middle(Config) -> doc("Confirm that setting the decompress_enabled option dynamically " "to false after starting to read the body does not disable decompression " "and the data received is decompressed."), Data = rand:bytes(1000000), Body = zlib:gzip(Data), %% Since we were not ignoring before starting to read, %% we receive the entire body decompressed. {200, Headers, Data} = do_post("/test/disable-in-the-middle", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), %% We do set accept-encoding when we are enabled, %% even if an attempt to disable in the middle is ignored. {_, _} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. set_options_decompress_enable_in_the_middle(Config0) -> doc("Confirm that setting the decompress_enabled option dynamically " "to true after starting to read the body does not enable decompression " "and the data received is not decompressed."), Fun = case config(ref, Config0) of HTTPS when HTTPS =:= https_compress; HTTPS =:= https -> init_https; H2 when H2 =:= h2_compress; H2 =:= h2 -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, stream_handlers => [cowboy_decompress_h, cowboy_stream_h], decompress_enabled => false }, Config0), Data = rand:bytes(1000000), Body = zlib:gzip(Data), try %% Since we were ignoring before starting to read, %% we receive the entire body compressed. {200, Headers, Body} = do_post("/test/enable-in-the-middle", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), %% We do not set accept-encoding when we are disabled, %% even if an attempt to enable in the middle is ignored. false = lists:keyfind(<<"accept-encoding">>, 1, Headers) after cowboy:stop_listener(?FUNCTION_NAME) end. opts_decompress_ratio_limit(Config0) -> doc("Confirm that the decompress_ratio_limit option can be set."), Fun = case config(ref, Config0) of HTTPS when HTTPS =:= https_compress; HTTPS =:= https -> init_https; H2 when H2 =:= h2_compress; H2 =:= h2 -> init_http2; _ -> init_http end, Config = cowboy_test:Fun(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, stream_handlers => [cowboy_decompress_h, cowboy_stream_h], decompress_ratio_limit => 1 }, Config0), %% Data must be big enough for compression to be effective, %% so that ratio_limit=1 will fail. Data = <<0:800>>, Body = zlib:gzip(Data), try {413, _, _} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip">>}], Body, Config) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_decompress_ratio_limit(Config) -> doc("Confirm that the decompress_ratio_limit option can be dynamically set."), %% Data must be big enough for compression to be effective, %% so that ratio_limit=1 will fail. Data = <<0:800>>, Body = zlib:gzip(Data), {413, _, _} = do_post("/echo/decompress_ratio_limit", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), ok. gzip_bomb(Config) -> doc("Confirm that requests are rejected with a 413 Payload Too Large " "error when the ratio limit is exceeded."), Body = create_gzip_bomb(), {413, _, _} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), ok. set_accept_encoding_response(Config) -> doc("Header accept-encoding must be set on valid response command. " "(RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/echo/normal", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"gzip">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. set_accept_encoding_header(Config) -> doc("Header accept-encoding must be set on valid header command. " "(RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/header-command", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"gzip">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. add_accept_encoding_header_valid(Config) -> doc("Supported content codings must be added to the accept-encoding " "header if it already exists. (RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/accept-identity", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"identity, gzip">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. override_accept_encoding_header_invalid(Config) -> doc("When the stream handler cannot parse the accept-encoding header " "found in the response, it overrides it."), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/invalid-header", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"gzip">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. override_accept_encoding_excluded(Config) -> doc("The stream handler must ensure that the content encodings " "it supports are not marked as unsupported in response headers. " "The stream handler enables gzip when explicitly excluded. " "(RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/reject-explicit-header", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"identity;q=1, gzip;q=1">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. %% *;q=0 will reject codings that are not listed. Supported codings %% must always be enabled when the handler is used. add_accept_encoding_excluded(Config) -> doc("The stream handler must ensure that the content encodings " "it supports are not marked as unsupported in response headers. " "The stream handler enables gzip when implicitly excluded (*;q=0). " "(RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/reject-implicit-header", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"gzip;q=1, identity;q=1, *;q=0">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. no_override_accept_coding_set_explicit(Config) -> doc("Confirm that accept-encoding is not overridden when the " "content encodings it supports are explicitly set. " "(RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/accept-explicit-header", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"identity, gzip;q=0.5">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. no_override_accept_coding_set_implicit(Config) -> doc("Confirm that accept-encoding is not overridden when the " "content encodings it supports are implicitly set. " "(RFC9110 12.5.3)"), Data = <<"test">>, Body = zlib:gzip(Data), {200, Headers, Data} = do_post("/test/accept-implicit-header", [{<<"content-encoding">>, <<"gzip">>}], Body, Config), {_, <<"identity, *;q=0.5">>} = lists:keyfind(<<"accept-encoding">>, 1, Headers), ok. ================================================ FILE: test/draft_h3_webtransport_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(draft_h3_webtransport_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(rfc9114_SUITE, [do_wait_stream_aborted/1]). -ifdef(COWBOY_QUICER). -include_lib("quicer/include/quicer.hrl"). all() -> [{group, enabled}]. groups() -> Tests = ct_helper:all(?MODULE), [{enabled, [], Tests}]. %% @todo Enable parallel when all is better. init_per_group(Name = enabled, Config) -> cowboy_test:init_http3(Name, #{ enable_connect_protocol => true, h3_datagram => true, enable_webtransport => true, %% For compatibility with draft-02. wt_max_sessions => 10, env => #{dispatch => cowboy_router:compile(init_routes(Config))} }, Config). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_routes(_) -> [ {"localhost", [ {"/wt", wt_echo_h, []} ]} ]. %% Temporary. %% To start Chromium the command line is roughly: %% chromium --ignore-certificate-errors-spki-list=LeLykt63i2FRAm+XO91yBoSjKfrXnAFygqe5xt0zgDA= --ignore-certificate-errors --user-data-dir=/tmp/chromium-wt --allow-insecure-localhost --webtransport-developer-mode --enable-quic https://googlechrome.github.io/samples/webtransport/client.html %% %% To find the SPKI the command is roughly: %% openssl x509 -in ~/ninenines/cowboy/test/rfc9114_SUITE_data/server.pem -pubkey -noout | \ %% openssl pkey -pubin -outform der | \ %% openssl dgst -sha256 -binary | \ %% openssl enc -base64 %run(Config) -> % ct:pal("port ~p", [config(port, Config)]), % timer:sleep(infinity). %% 3. Session Establishment %% 3.1. Establishing a WebTransport-Capable HTTP/3 Connection %% In order to indicate support for WebTransport, the server MUST send a SETTINGS_WT_MAX_SESSIONS value greater than "0" in its SETTINGS frame. (3.1) %% @todo reject_session_disabled %% @todo accept_session_below %% @todo accept_session_equal %% @todo reject_session_above %% The client MUST NOT send a WebTransport request until it has received the setting indicating WebTransport support from the server. (3.1) %% For draft verisons of WebTransport only, the server MUST NOT process any incoming WebTransport requests until the client settings have been received, as the client may be using a version of the WebTransport extension that is different from the one used by the server. (3.1) %% Because WebTransport over HTTP/3 requires support for HTTP/3 datagrams and the Capsule Protocol, both the client and the server MUST indicate support for HTTP/3 datagrams by sending a SETTINGS_H3_DATAGRAM value set to 1 in their SETTINGS frame (see Section 2.1.1 of [HTTP-DATAGRAM]). (3.1) %% @todo settings_h3_datagram_enabled %% WebTransport over HTTP/3 also requires support for QUIC datagrams. To indicate support, both the client and the server MUST send a max_datagram_frame_size transport parameter with a value greater than 0 (see Section 3 of [QUIC-DATAGRAM]). (3.1) %% @todo quic_datagram_enabled (if size is too low the CONNECT stream can be used for capsules) %% Any WebTransport requests sent by the client without enabling QUIC and HTTP datagrams MUST be treated as malformed by the server, as described in Section 4.1.2 of [HTTP3]. (3.1) %% @todo reject_h3_datagram_disabled %% @todo reject_quic_datagram_disabled %% WebTransport over HTTP/3 relies on the RESET_STREAM_AT frame defined in [RESET-STREAM-AT]. To indicate support, both the client and the server MUST enable the extension as described in Section 3 of [RESET-STREAM-AT]. (3.1) %% @todo reset_stream_at_enabled %% 3.2. Extended CONNECT in HTTP/3 %% [RFC8441] defines an extended CONNECT method in Section 4, enabled by the SETTINGS_ENABLE_CONNECT_PROTOCOL setting. That setting is defined for HTTP/3 by [RFC9220]. A server supporting WebTransport over HTTP/3 MUST send both the SETTINGS_WT_MAX_SESSIONS setting with a value greater than "0" and the SETTINGS_ENABLE_CONNECT_PROTOCOL setting with a value of "1". (3.2) %% @todo settings_enable_connect_protocol_enabled %% @todo reject_settings_enable_connect_protocol_disabled %% 3.3. Creating a New Session %% As WebTransport sessions are established over HTTP/3, they are identified using the https URI scheme ([HTTP], Section 4.2.2). (3.3) %% In order to create a new WebTransport session, a client can send an HTTP CONNECT request. The :protocol pseudo-header field ([RFC8441]) MUST be set to webtransport. The :scheme field MUST be https. Both the :authority and the :path value MUST be set; those fields indicate the desired WebTransport server. If the WebTransport session is coming from a browser client, an Origin header [RFC6454] MUST be provided within the request; otherwise, the header is OPTIONAL. (3.3) %% If it does not (have a WT server), it SHOULD reply with status code 404 (Section 15.5.5 of [HTTP]). (3.3) %% When the request contains the Origin header, the WebTransport server MUST verify the Origin header to ensure that the specified origin is allowed to access the server in question. If the verification fails, the WebTransport server SHOULD reply with status code 403 (Section 15.5.4 of [HTTP]). (3.3) accept_session_when_enabled(Config) -> doc("Confirm that a WebTransport session can be established over HTTP/3. " "(draft_webtrans_http3 3.3, RFC9220)"), %% Connect to the WebTransport server. #{ conn := Conn, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send Hello, get Hello back. {ok, BidiStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(BidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), {nofin, <<"Hello">>} = do_receive_data(BidiStreamRef), ok. %% If the server accepts 0-RTT, the server MUST NOT reduce the limit of maximum open WebTransport sessions from the one negotiated during the previous session; such change would be deemed incompatible, and MUST result in a H3_SETTINGS_ERROR connection error. (3.3) %% The capsule-protocol header field Section 3.4 of [HTTP-DATAGRAM] is not required by WebTransport and can safely be ignored by WebTransport endpoints. (3.3) %% 3.4. Application Protocol Negotiation application_protocol_negotiation(Config) -> doc("Applications can negotiate a protocol to use via WebTransport. " "(draft_webtrans_http3 3.4)"), %% Connect to the WebTransport server. WTAvailableProtocols = cow_http_hd:wt_available_protocols([<<"foo">>, <<"bar">>]), #{ resp_headers := RespHeaders } = do_webtransport_connect(Config, [{<<"wt-available-protocols">>, WTAvailableProtocols}]), {<<"wt-protocol">>, WTProtocol} = lists:keyfind(<<"wt-protocol">>, 1, RespHeaders), <<"foo">> = iolist_to_binary(cow_http_hd:parse_wt_protocol(WTProtocol)), ok. %% Both WT-Available-Protocols and WT-Protocol are Structured Fields [RFC8941]. WT-Available-Protocols is a List of Tokens, and WT-Protocol is a Token. The token in the WT-Protocol response header field MUST be one of the tokens listed in WT-Available-Protocols of the request. (3.4) %% @todo 3.5 Prioritization %% 4. WebTransport Features %% The client MAY optimistically open unidirectional and bidirectional streams, as well as send datagrams, for a session that it has sent the CONNECT request for, even if it has not yet received the server's response to the request. (4) %% If at any point a session ID is received that cannot be a valid ID for a client-initiated bidirectional stream, the recipient MUST close the connection with an H3_ID_ERROR error code. (4) %% @todo Open bidi with Session ID 0, then do the CONNECT request. %% 4.1. Unidirectional streams unidirectional_streams(Config) -> doc("Both endpoints can open and use unidirectional streams. " "(draft_webtrans_http3 4.1)"), %% Connect to the WebTransport server. #{ conn := Conn, session_id := SessionID } = do_webtransport_connect(Config), %% Create a unidi stream, send Hello with a Fin flag. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>, ?QUIC_SEND_FLAG_FIN), %% Accept an identical unidi stream. {unidi, RemoteStreamRef} = do_receive_new_stream(), {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef), {fin, <<"Hello">>} = do_receive_data(RemoteStreamRef), ok. %% 4.2. Bidirectional Streams bidirectional_streams_client(Config) -> doc("The WT client can open and use bidirectional streams. " "(draft_webtrans_http3 4.2)"), %% Connect to the WebTransport server. #{ conn := Conn, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send Hello, get Hello back. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), {nofin, <<"Hello">>} = do_receive_data(LocalStreamRef), ok. bidirectional_streams_server(Config) -> doc("The WT server can open and use bidirectional streams. " "(draft_webtrans_http3 4.2)"), %% Connect to the WebTransport server. #{ conn := Conn, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction %% to make the server create another bidi stream. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>), %% Accept the bidi stream and receive the data. {bidi, RemoteStreamRef} = do_receive_new_stream(), {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef), {ok, _} = quicer:send(RemoteStreamRef, <<"Hello">>, ?QUIC_SEND_FLAG_FIN), {fin, <<"Hello">>} = do_receive_data(RemoteStreamRef), ok. %% Endpoints MUST NOT send WT_STREAM as a frame type on HTTP/3 streams other than the very first bytes of a request stream. Receiving this frame type in any other circumstances MUST be treated as a connection error of type H3_FRAME_ERROR. (4.2) %% 4.3. Resetting Data Streams %% A WebTransport endpoint may send a RESET_STREAM or a STOP_SENDING frame for a WebTransport data stream. Those signals are propagated by the WebTransport implementation to the application. (4.3) %% A WebTransport application SHALL provide an error code for those operations. (4.3) %% WebTransport implementations MUST use the RESET_STREAM_AT frame [RESET-STREAM-AT] with a Reliable Size set to at least the size of the WebTransport header when resetting a WebTransport data stream. This ensures that the ID field associating the data stream with a WebTransport session is always delivered. (4.3) %% WebTransport implementations SHALL forward the error code for a stream associated with a known session to the application that owns that session (4.3) %% 4.4. Datagrams datagrams(Config) -> doc("Both endpoints can send and receive datagrams. (draft_webtrans_http3 4.4)"), %% Connect to the WebTransport server. #{ conn := Conn, session_id := SessionID } = do_webtransport_connect(Config), QuarterID = SessionID div 4, %% Send a Hello datagram. {ok, _} = quicer:send_dgram(Conn, <<0:2, QuarterID:6, "Hello">>), %% Receive a Hello datagram back. {datagram, SessionID, <<"Hello">>} = do_receive_datagram(Conn), ok. %% @todo datagrams_via_capsule? %% 4.5. Buffering Incoming Streams and Datagrams %% To handle this case (out of order stream_open/CONNECT), WebTransport endpoints SHOULD buffer streams and datagrams until those can be associated with an established session. (4.5) %% To avoid resource exhaustion, the endpoints MUST limit the number of buffered streams and datagrams. When the number of buffered streams is exceeded, a stream SHALL be closed by sending a RESET_STREAM and/or STOP_SENDING with the WT_BUFFERED_STREAM_REJECTED error code. When the number of buffered datagrams is exceeded, a datagram SHALL be dropped. It is up to an implementation to choose what stream or datagram to discard. (4.5) %% 4.6. Interaction with HTTP/3 GOAWAY frame %% A client receiving GOAWAY cannot initiate CONNECT requests for new WebTransport sessions on that HTTP/3 connection; it must open a new HTTP/3 connection to initiate new WebTransport sessions with the same peer. (4.6) %% An HTTP/3 GOAWAY frame is also a signal to applications to initiate shutdown for all WebTransport sessions. (4.6) %% @todo Currently receipt of a GOAWAY frame immediately ends the connection. %% We want to allow WT sessions to gracefully shut down before that. %goaway_client(Config) -> % doc("The HTTP/3 client can initiate the close of all WT sessions " % "by sending a GOAWAY frame. (draft_webtrans_http3 4.6)"), % %% Connect to the WebTransport server. % #{ % conn := Conn, % connect_stream_ref := ConnectStreamRef, % session_id := SessionID % } = do_webtransport_connect(Config), % %% Open a control stream and send a GOAWAY frame. % {ok, ControlRef} = quicer:start_stream(Conn, % #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), % {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), % {ok, _} = quicer:send(ControlRef, [ % <<0>>, %% CONTROL stream. % SettingsBin, % <<7>>, %% GOAWAY frame. % cow_http3:encode_int(1), % cow_http3:encode_int(0) % ]), % %% Receive a datagram indicating processing by the WT handler. % {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), % ok. wt_drain_session_client(Config) -> doc("The WT client can initiate the close of a single session. " "(draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Send the WT_DRAIN_SESSION capsule on the CONNECT stream. {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_drain_session()), %% Receive a datagram indicating processing by the WT handler. {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), ok. wt_drain_session_server(Config) -> doc("The WT server can initiate the close of a single session. " "(draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it initiate the close. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:initiate_close">>), %% Receive the WT_DRAIN_SESSION capsule on the CONNECT stream. DrainWTSessionCapsule = cow_capsule:wt_drain_session(), {nofin, DrainWTSessionCapsule} = do_receive_data(ConnectStreamRef), ok. wt_drain_session_continue_client(Config) -> doc("After the WT client has initiated the close of the session, " "both client and server can continue using the session and " "open new streams. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Send the WT_DRAIN_SESSION capsule on the CONNECT stream. {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_drain_session()), %% Receive a datagram indicating processing by the WT handler. {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), %% Create a new bidi stream, send Hello, get Hello back. {ok, ContinueStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(ContinueStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), {nofin, <<"Hello">>} = do_receive_data(ContinueStreamRef), ok. wt_drain_session_continue_server(Config) -> doc("After the WT server has initiated the close of the session, " "both client and server can continue using the session and " "open new streams. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it initiate the close. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:initiate_close">>), %% Receive the WT_DRAIN_SESSION capsule on the CONNECT stream. DrainWTSessionCapsule = cow_capsule:wt_drain_session(), {nofin, DrainWTSessionCapsule} = do_receive_data(ConnectStreamRef), %% Create a new bidi stream, send Hello, get Hello back. {ok, ContinueStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(ContinueStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>), {nofin, <<"Hello">>} = do_receive_data(ContinueStreamRef), ok. %% @todo 4.7. Use of Keying Material Exporters %% 5. Flow Control %% 5.1. Limiting the Number of Simultaneous Sessions %% This document defines a SETTINGS_WT_MAX_SESSIONS parameter that allows the server to limit the maximum number of concurrent WebTransport sessions on a single HTTP/3 connection. The client MUST NOT open more simultaneous sessions than indicated in the server SETTINGS parameter. The server MUST NOT close the connection if the client opens sessions exceeding this limit, as the client and the server do not have a consistent view of how many sessions are open due to the asynchronous nature of the protocol; instead, it MUST reset all of the CONNECT streams it is not willing to process with the H3_REQUEST_REJECTED status defined in [HTTP3]. (5.1) %% 5.2. Limiting the Number of Streams Within a Session %% The WT_MAX_STREAMS capsule (Section 5.6.1) establishes a limit on the number of streams within a WebTransport session. (5.2) %% Note that the CONNECT stream for the session is not included in either the bidirectional or the unidirectional stream limits (5.2) %% The session-level stream limit applies in addition to the QUIC MAX_STREAMS frame, which provides a connection-level stream limit. New streams can only be created within the session if both the stream- and the connection-level limit permit (5.2) %% The WT_STREAMS_BLOCKED capsule (Section 5.7) can be sent to indicate that an endpoint was unable to create a stream due to the session-level stream limit. (5.2) %% Note that enforcing this limit requires reliable resets for stream headers so that both endpoints can agree on the number of streams that are open. (5.2) %% 5.3. Data Limits %% The WT_MAX_DATA capsule (Section 5.8) establishes a limit on the amount of data that can be sent within a WebTransport session. This limit counts all data that is sent on streams of the corresponding type, excluding the stream header (see Section 4.1 and Section 4.2). (5.3) %% Implementing WT_MAX_DATA requires that the QUIC stack provide the WebTransport implementation with information about the final size of streams; see { {Section 4.5 of !RFC9000}}. This allows both endpoints to agree on how much data was consumed by that stream, although the stream header exclusion above applies. (5.3) %% The WT_DATA_BLOCKED capsule (Section 5.9) can be sent to indicate that an endpoint was unable to send data due to a limit set by the WT_MAX_DATA capsule. (5.3) %% The WT_MAX_STREAM_DATA and WT_STREAM_DATA_BLOCKED capsules (Part XX of [I-D.ietf-webtrans-http2]) are not used and so are prohibited. Endpoints MUST treat receipt of a WT_MAX_STREAM_DATA or a WT_STREAM_DATA_BLOCKED capsule as a session error. (5.3) %% 5.4. Flow Control and Intermediaries %% In practice, an intermediary that translates flow control signals between similar WebTransport protocols, such as between two HTTP/3 connections, can often simply reexpress the same limits received on one connection directly on the other connection. (5.4) %% 5.5. Flow Control SETTINGS %% WT_MAX_STREAMS via SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI (5.5) %% WT_MAX_DATA via SETTINGS_WT_INITIAL_MAX_DATA (5.5) %% 5.6. Flow Control Capsules %% 5.6.1. WT_MAX_STREAMS Capsule %% An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_STREAMS is introduced to inform the peer of the cumulative number of streams of a given type it is permitted to open. A WT_MAX_STREAMS capsule with a type of 0x190B4D3F applies to bidirectional streams, and a WT_MAX_STREAMS capsule with a type of 0x190B4D40 applies to unidirectional streams. (5.6.1) %% Note that, because Maximum Streams is a cumulative value representing the total allowed number of streams, including previously closed streams, endpoints repeatedly send new WT_MAX_STREAMS capsules with increasing Maximum Streams values as streams are opened. (5.6.1) %% Maximum Streams: A count of the cumulative number of streams of the corresponding type that can be opened over the lifetime of the session. This value cannot exceed 260, as it is not possible to encode stream IDs larger than 262-1. (5.6.1) %% An endpoint MUST NOT open more streams than permitted by the current stream limit set by its peer. (5.6.1) %% Note that this limit includes streams that have been closed as well as those that are open. (5.6.1) %% Initial values for these limits MAY be communicated by sending non-zero values for SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI. (5.6.1) %% 5.7. WT_STREAMS_BLOCKED Capsule %% A sender SHOULD send a WT_STREAMS_BLOCKED capsule (type=0x190B4D43 for bidi or 0x190B4D44 for unidi) when it wishes to open a stream but is unable to do so due to the maximum stream limit set by its peer. (5.7) %% 5.8. WT_MAX_DATA Capsule %% An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_DATA (type=0x190B4D3D) is introduced to inform the peer of the maximum amount of data that can be sent on the WebTransport session as a whole. (5.8) %% This limit counts all data that is sent on streams of the corresponding type, excluding the stream header (see Section 4.1 and Section 4.2). Implementing WT_MAX_DATA requires that the QUIC stack provide the WebTransport implementation with information about the final size of streams; see Section 4.5 of [RFC9000]. (5.8) %% All data sent in WT_STREAM capsules counts toward this limit. The sum of the lengths of Stream Data fields in WT_STREAM capsules MUST NOT exceed the value advertised by a receiver. (5.8) %% The initial value for this limit MAY be communicated by sending a non-zero value for SETTINGS_WT_INITIAL_MAX_DATA. (5.8) %% 5.9. WT_DATA_BLOCKED Capsule %% A sender SHOULD send a WT_DATA_BLOCKED capsule (type=0x190B4D41) when it wishes to send data but is unable to do so due to WebTransport session-level flow control. (5.9) %% WT_DATA_BLOCKED capsules can be used as input to tuning of flow control algorithms. (5.9) %% 6. Session Termination %% A WebTransport session over HTTP/3 is considered terminated when either of the following conditions is met: %% * the CONNECT stream is closed, either cleanly or abruptly, on either side; or %% * a WT_CLOSE_SESSION capsule is either sent or received. %% (6) wt_close_session_client(Config) -> doc("The WT client can close a single session. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ connect_stream_ref := ConnectStreamRef } = do_webtransport_connect(Config), %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream. {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_close_session(0, <<>>), ?QUIC_SEND_FLAG_FIN), %% Normally we should also stop reading but in order to detect %% that the server stops the stream we must not otherwise the %% stream will be de facto closed on our end. %% %% The recipient must close or reset the stream in response. receive {quic, stream_closed, ConnectStreamRef, _} -> ok after 1000 -> error({timeout, waiting_for_stream_closed}) end. wt_close_session_server(Config) -> doc("The WT server can close a single session. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it initiate the close. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:close">>), %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. CloseWTSessionCapsule = cow_capsule:wt_close_session(0, <<>>), {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), ok. wt_session_gone_client(Config) -> doc("Upon learning that the session has been terminated, " "the WT server must reset associated streams with the " "WEBTRANSPORT_SESSION_GONE error code. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a unidi stream. {ok, LocalUnidiStreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(LocalUnidiStreamRef, <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>), %% Accept an identical unidi stream. {unidi, RemoteUnidiStreamRef} = do_receive_new_stream(), {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteUnidiStreamRef), {nofin, <<"Hello">>} = do_receive_data(RemoteUnidiStreamRef), %% Create a bidi stream, send a special instruction %% to make the server create another bidi stream. {ok, LocalBidiStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalBidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>), %% Accept the bidi stream and receive the data. {bidi, RemoteBidiStreamRef} = do_receive_new_stream(), {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteBidiStreamRef), {ok, _} = quicer:send(RemoteBidiStreamRef, <<"Hello">>), {nofin, <<"Hello">>} = do_receive_data(RemoteBidiStreamRef), %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream. {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_close_session(0, <<>>), ?QUIC_SEND_FLAG_FIN), %% All streams from that WT session have been aborted. #{reason := wt_session_gone} = do_wait_stream_aborted(LocalUnidiStreamRef), #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteUnidiStreamRef), #{reason := wt_session_gone} = do_wait_stream_aborted(LocalBidiStreamRef), #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteBidiStreamRef), ok. wt_session_gone_server(Config) -> doc("After the session has been terminated by the WT server, " "the WT server must reset associated streams with the " "WT_SESSION_GONE error code. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a unidi stream. {ok, LocalUnidiStreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(LocalUnidiStreamRef, <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>), %% Accept an identical unidi stream. {unidi, RemoteUnidiStreamRef} = do_receive_new_stream(), {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteUnidiStreamRef), {nofin, <<"Hello">>} = do_receive_data(RemoteUnidiStreamRef), %% Create a bidi stream, send a special instruction %% to make the server create another bidi stream. {ok, LocalBidiStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalBidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>), %% Accept the bidi stream and receive the data. {bidi, RemoteBidiStreamRef} = do_receive_new_stream(), {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteBidiStreamRef), {ok, _} = quicer:send(RemoteBidiStreamRef, <<"Hello">>), {nofin, <<"Hello">>} = do_receive_data(RemoteBidiStreamRef), %% Send a special instruction to make the server initiate the close. {ok, _} = quicer:send(LocalBidiStreamRef, <<"TEST:close">>), %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. CloseWTSessionCapsule = cow_capsule:wt_close_session(0, <<>>), {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), %% All streams from that WT session have been aborted. #{reason := wt_session_gone} = do_wait_stream_aborted(LocalUnidiStreamRef), #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteUnidiStreamRef), #{reason := wt_session_gone} = do_wait_stream_aborted(LocalBidiStreamRef), #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteBidiStreamRef), ok. %% Application Error Message: A UTF-8 encoded error message string provided by the application closing the session. The message takes up the remainder of the capsule, and its length MUST NOT exceed 1024 bytes. (6) %% @todo What if it's larger? wt_close_session_app_code_msg_client(Config) -> doc("The WT client can close a single session with an application error code " "and an application error message. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it propagate events. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), EventPidBin = term_to_binary(self()), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:event_pid:", EventPidBin/binary>>), %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream. {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_close_session(17, <<"seventeen">>), ?QUIC_SEND_FLAG_FIN), %% @todo Stop reading from the CONNECt stream too. (STOP_SENDING) %% Receive the terminate event from the WT handler. receive {'$wt_echo_h', terminate, {closed, 17, <<"seventeen">>}, _, _} -> ok after 1000 -> error({timeout, waiting_for_terminate_event}) end. wt_close_session_app_code_server(Config) -> doc("The WT server can close a single session with an application error code. " "(draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it initiate the close. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:close_app_code">>), %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. CloseWTSessionCapsule = cow_capsule:wt_close_session(1234567890, <<>>), {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), ok. wt_close_session_app_code_msg_server(Config) -> doc("The WT server can close a single session with an application error code " "and an application error message. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it initiate the close. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:close_app_code_msg">>), %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream. CloseWTSessionCapsule = iolist_to_binary(cow_capsule:wt_close_session(1234567890, <<"onetwothreefourfivesixseveneightnineten">>)), {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), ok. %% An endpoint that sends a WT_CLOSE_SESSION capsule MUST immediately send a FIN. The endpoint MAY send a STOP_SENDING to indicate it is no longer reading from the CONNECT stream. The recipient MUST either close or reset the stream in response. (6) %% @todo wt_close_session_server_fin %% @todo The part about close/reset should be tested in wt_close_session_client. %% If any additional stream data is received on the CONNECT stream after receiving a WT_CLOSE_SESSION capsule, the stream MUST be reset with code H3_MESSAGE_ERROR. (6) %% @todo wt_close_session_followed_by_data connect_stream_closed_cleanly_fin(Config) -> doc("The WT client closing the CONNECT stream cleanly " "is equivalent to a capsule with an application error code of 0 " "and an empty error string. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it propagate events. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), EventPidBin = term_to_binary(self()), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:event_pid:", EventPidBin/binary>>), {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef), %% Cleanly terminate the CONNECT stream. {ok, _} = quicer:send(ConnectStreamRef, <<>>, ?QUIC_SEND_FLAG_FIN), %% Receive the terminate event from the WT handler. receive {'$wt_echo_h', terminate, {closed, 0, <<>>}, _, _} -> ok after 1000 -> error({timeout, waiting_for_terminate_event}) end. connect_stream_closed_cleanly_shutdown(Config) -> doc("The WT client closing the CONNECT stream cleanly " "is equivalent to a capsule with an application error code of 0 " "and an empty error string. (draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it propagate events. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), EventPidBin = term_to_binary(self()), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:event_pid:", EventPidBin/binary>>), {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef), %% Cleanly terminate the CONNECT stream. _ = quicer:shutdown_stream(ConnectStreamRef), %% Receive the terminate event from the WT handler. receive {'$wt_echo_h', terminate, {closed, 0, <<>>}, _, _} -> ok after 1000 -> error({timeout, waiting_for_terminate_event}) end. connect_stream_closed_abruptly(Config) -> doc("The WT client may close the CONNECT stream abruptly. " "(draft_webtrans_http3 4.6)"), %% Connect to the WebTransport server. #{ conn := Conn, connect_stream_ref := ConnectStreamRef, session_id := SessionID } = do_webtransport_connect(Config), %% Create a bidi stream, send a special instruction to make it propagate events. {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), EventPidBin = term_to_binary(self()), {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:event_pid:", EventPidBin/binary>>), {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef), %% Abruptly terminate the CONNECT stream. _ = quicer:shutdown_stream(ConnectStreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0, infinity), %% Receive the terminate event from the WT handler. receive %% @todo It would be good to forward a stream error as well %% so that a WT error can be sent, but I have been unsuccessful. {'$wt_echo_h', terminate, closed_abruptly, _, _} -> ok after 1000 -> error({timeout, waiting_for_terminate_event}) end. %% @todo This one is about gracefully closing HTTP/3 connection with WT sessions. %% the endpoint SHOULD wait until all CONNECT streams have been closed by the peer before sending the CONNECTION_CLOSE (6) %% Helpers. do_webtransport_connect(Config) -> do_webtransport_connect(Config, []). do_webtransport_connect(Config, ExtraHeaders) -> %% Connect to server. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config, #{ peer_unidi_stream_count => 100, datagram_send_enabled => 1, datagram_receive_enabled => 1 }), %% Confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{enable_connect_protocol := true} = Settings, %% Confirm that SETTINGS_WT_MAX_SESSIONS >= 1. #{wt_max_sessions := WTMaxSessions} = Settings, true = WTMaxSessions >= 1, %% Confirm that SETTINGS_H3_DATAGRAM = 1. #{h3_datagram := true} = Settings, %% Confirm that QUIC's max_datagram_size > 0. receive {quic, dgram_state_changed, Conn, DatagramState} -> #{ dgram_max_len := DatagramMaxLen, dgram_send_enabled := DatagramSendEnabled } = DatagramState, true = DatagramMaxLen > 0, true = DatagramSendEnabled, ok after 5000 -> error({timeout, waiting_for_datagram_state_change}) end, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ok, ConnectStreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"webtransport">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/wt">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"origin">>, <<"https://localhost">>} |ExtraHeaders], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(ConnectStreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% Receive a 200 response. {nofin, Data} = do_receive_data(ConnectStreamRef), {HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data), << 1, %% HEADERS frame. HLenEnc:2, HLen:HLenBits, EncodedResponse:HLen/bytes >> = Data, {ok, DecodedResponse, _DecData, _DecSt} = cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)), #{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse), %% Retrieve the Session ID. {ok, SessionID} = quicer:get_stream_id(ConnectStreamRef), %% Accept QPACK streams to avoid conflicts with unidi streams from tests. Unidi1 = rfc9114_SUITE:do_accept_qpack_stream(Conn), Unidi2 = rfc9114_SUITE:do_accept_qpack_stream(Conn), %% Done. #{ conn => Conn, connect_stream_ref => ConnectStreamRef, session_id => SessionID, resp_headers => DecodedResponse, enc_or_dec1 => Unidi1, enc_or_dec2 => Unidi2 }. do_receive_new_stream() -> receive {quic, new_stream, StreamRef, #{flags := Flags}} -> ok = quicer:setopt(StreamRef, active, true), case quicer:is_unidirectional(Flags) of true -> {unidi, StreamRef}; false -> {bidi, StreamRef} end after 5000 -> error({timeout, waiting_for_stream}) end. do_receive_data(StreamRef) -> receive {quic, Data, StreamRef, #{flags := Flags}} -> IsFin = case Flags band ?QUIC_RECEIVE_FLAG_FIN of ?QUIC_RECEIVE_FLAG_FIN -> fin; _ -> nofin end, {IsFin, Data} after 5000 -> error({timeout, waiting_for_data}) end. do_receive_datagram(Conn) -> receive {quic, <<0:2, QuarterID:6, Data/bits>>, Conn, Flags} when is_integer(Flags) -> {datagram, QuarterID * 4, Data} after 5000 -> ct:pal("~p", [process_info(self(), messages)]), error({timeout, waiting_for_datagram}) end. -endif. ================================================ FILE: test/examples_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(examples_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> ct_helper:all(?MODULE). init_per_suite(Config) -> %% Remove environment variables inherited from Erlang.mk. os:unsetenv("ERLANG_MK_TMP"), os:unsetenv("APPS_DIR"), os:unsetenv("DEPS_DIR"), os:unsetenv("ERL_LIBS"), %% Clone and build Cowboy, Cowlib and Ranch only once and %% reuse the same build across all tests. Make = do_find_make_cmd(), CommonDir = config(priv_dir, Config), ct:log("~ts~n", [os:cmd("git clone --depth 1 https://github.com/ninenines/cowboy " ++ CommonDir ++ "cowboy")]), ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy distclean")]), ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy DEPS_DIR=" ++ CommonDir)]), Config. end_per_suite(_) -> ok. %% Find GNU Make. do_find_make_cmd() -> case os:getenv("MAKE") of false -> case os:find_executable("gmake") of false -> "make"; Cmd -> Cmd end; Cmd -> Cmd end. %% Compile, start and stop releases. do_get_paths(Example0) -> Example = atom_to_list(Example0), {ok, CWD} = file:get_cwd(), Dir = CWD ++ "/../../examples/" ++ Example, Rel = Dir ++ "/_rel/" ++ Example ++ "_example/bin/" ++ Example ++ "_example", Log = Dir ++ "/_rel/" ++ Example ++ "_example/log/erlang.log.1", {Dir, Rel, Log}. do_compile_and_start(Example, Config) -> Make = do_find_make_cmd(), {Dir, Rel, _} = do_get_paths(Example), ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ Dir ++ " distclean")]), %% We use a common build for Cowboy, Cowlib and Ranch to speed things up. CommonDir = config(priv_dir, Config), ct:log("~ts~n", [os:cmd("mkdir " ++ Dir ++ "/deps")]), ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "cowboy " ++ Dir ++ "/deps/cowboy")]), ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "cowlib " ++ Dir ++ "/deps/cowlib")]), ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "ranch " ++ Dir ++ "/deps/ranch")]), %% TERM=dumb disables relx coloring. ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ Dir ++ " TERM=dumb")]), ct:log("~ts~n", [os:cmd(Rel ++ " stop")]), ct:log("~ts~n", [os:cmd(Rel ++ " daemon")]), timer:sleep(2000), ok. do_stop(Example) -> {_, Rel, Log} = do_get_paths(Example), ct:log("~ts~n", [os:cmd(Rel ++ " stop")]), ct:log("~ts~n", [element(2, file:read_file(Log))]), ok. %% Fetch a response. do_get(Transport, Protocol, Path, Config) -> do_get(Transport, Protocol, Path, [], Config). do_get(Transport, Protocol, Path, ReqHeaders, Config) -> Port = case Transport of tcp -> 8080; ssl -> 8443 end, ConnPid = gun_open([{port, Port}, {type, Transport}, {protocol, Protocol}|Config]), Ref = gun:get(ConnPid, Path, ReqHeaders), case gun:await(ConnPid, Ref) of {response, nofin, Status, RespHeaders} -> {ok, Body} = gun:await_body(ConnPid, Ref), {Status, RespHeaders, Body}; {response, fin, Status, RespHeaders} -> {Status, RespHeaders, <<>>} end. %% TCP and SSL Hello World. hello_world(Config) -> doc("Hello World example."), try do_compile_and_start(hello_world, Config), do_hello_world(tcp, http, Config), do_hello_world(tcp, http2, Config) after do_stop(hello_world) end. ssl_hello_world(Config) -> doc("SSL Hello World example."), try do_compile_and_start(ssl_hello_world, Config), do_hello_world(ssl, http, Config), do_hello_world(ssl, http2, Config) after do_stop(ssl_hello_world) end. do_hello_world(Transport, Protocol, Config) -> {200, _, <<"Hello world!">>} = do_get(Transport, Protocol, "/", Config), ok. %% Chunked Hello World. chunked_hello_world(Config) -> doc("Chunked Hello World example."), try do_compile_and_start(chunked_hello_world, Config), do_chunked_hello_world(tcp, http, Config), do_chunked_hello_world(tcp, http2, Config) after do_stop(chunked_hello_world) end. do_chunked_hello_world(Transport, Protocol, Config) -> ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]), Ref = gun:get(ConnPid, "/"), {response, nofin, 200, _} = gun:await(ConnPid, Ref), %% We expect to receive a chunk every second, three total. {data, nofin, <<"Hello\r\n">>} = gun:await(ConnPid, Ref, 2000), {data, nofin, <<"World\r\n">>} = gun:await(ConnPid, Ref, 2000), {data, IsFin, <<"Chunked!\r\n">>} = gun:await(ConnPid, Ref, 2000), %% We may get an extra empty chunk (last chunk for HTTP/1.1, %% empty DATA frame with the FIN bit set for HTTP/2). case IsFin of fin -> ok; nofin -> {data, fin, <<>>} = gun:await(ConnPid, Ref, 500), ok end. %% Compressed responses. compress_response(Config) -> doc("Compressed response example."), try do_compile_and_start(compress_response, Config), do_compress_response(tcp, http, Config), do_compress_response(tcp, http2, Config) after do_stop(compress_response) end. do_compress_response(Transport, Protocol, Config) -> {200, Headers, Body} = do_get(Transport, Protocol, "/", [{<<"accept-encoding">>, <<"gzip">>}], Config), {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers), _ = zlib:gunzip(Body), ok. %% Cookie. cookie(Config) -> doc("Cookie example."), try do_compile_and_start(cookie, Config), do_cookie(tcp, http, Config), do_cookie(tcp, http2, Config) after do_stop(cookie) end. do_cookie(Transport, Protocol, Config) -> {200, _, One} = do_get(Transport, Protocol, "/", Config), {200, _, Two} = do_get(Transport, Protocol, "/", [{<<"cookie">>, <<"server=abcdef">>}], Config), true = One =/= Two, ok. %% Echo GET. echo_get(Config) -> doc("GET parameter echo example."), try do_compile_and_start(echo_get, Config), do_echo_get(tcp, http, Config), do_echo_get(tcp, http2, Config) after do_stop(echo_get) end. do_echo_get(Transport, Protocol, Config) -> {200, _, <<"this is fun">>} = do_get(Transport, Protocol, "/?echo=this+is+fun", Config), {400, _, _} = do_get(Transport, Protocol, "/", Config), ok. %% Echo POST. echo_post(Config) -> doc("POST parameter echo example."), try do_compile_and_start(echo_post, Config), do_echo_post(tcp, http, Config), do_echo_post(tcp, http2, Config) after do_stop(echo_post) end. do_echo_post(Transport, Protocol, Config) -> ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]), Ref = gun:post(ConnPid, "/", [ {<<"content-type">>, <<"application/octet-stream">>} ], <<"echo=this+is+fun">>), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"this is fun">>} = gun:await_body(ConnPid, Ref), ok. %% Eventsource. eventsource(Config) -> doc("Eventsource example."), try do_compile_and_start(eventsource, Config), do_eventsource(tcp, http, Config), do_eventsource(tcp, http2, Config) after do_stop(eventsource) end. do_eventsource(Transport, Protocol, Config) -> ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]), Ref = gun:get(ConnPid, "/eventsource"), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), %% Receive a few events. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000), {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000), {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000), gun:close(ConnPid). %% REST Hello World. rest_hello_world(Config) -> doc("REST Hello World example."), try do_compile_and_start(rest_hello_world, Config), do_rest_hello_world(tcp, http, Config), do_rest_hello_world(tcp, http2, Config) after do_stop(rest_hello_world) end. do_rest_hello_world(Transport, Protocol, Config) -> << "", _/bits >> = do_rest_get(Transport, Protocol, "/", undefined, undefined, Config), << "REST Hello World as text!" >> = do_rest_get(Transport, Protocol, "/", <<"text/plain">>, undefined, Config), << "{\"rest\": \"Hello World!\"}" >> = do_rest_get(Transport, Protocol, "/", <<"application/json">>, undefined, Config), not_acceptable = do_rest_get(Transport, Protocol, "/", <<"text/css">>, undefined, Config), ok. do_rest_get(Transport, Protocol, Path, Accept, Auth, Config) -> ReqHeaders0 = case Accept of undefined -> []; _ -> [{<<"accept">>, Accept}] end, ReqHeaders = case Auth of undefined -> ReqHeaders0; _ -> [{<<"authorization">>, [<<"Basic ">>, base64:encode(Auth)]}|ReqHeaders0] end, case do_get(Transport, Protocol, Path, ReqHeaders, Config) of {200, RespHeaders, Body} -> Accept = case Accept of undefined -> undefined; _ -> {_, ContentType} = lists:keyfind(<<"content-type">>, 1, RespHeaders), ContentType end, Body; {401, _, _} -> unauthorized; {406, _, _} -> not_acceptable end. %% REST basic auth. rest_basic_auth(Config) -> doc("REST basic authorization example."), try do_compile_and_start(rest_basic_auth, Config), do_rest_basic_auth(tcp, http, Config), do_rest_basic_auth(tcp, http2, Config) after do_stop(rest_basic_auth) end. do_rest_basic_auth(Transport, Protocol, Config) -> unauthorized = do_rest_get(Transport, Protocol, "/", undefined, undefined, Config), <<"Hello, Alladin!\n">> = do_rest_get(Transport, Protocol, "/", undefined, "Alladin:open sesame", Config), ok. %% REST pastebin. rest_pastebin(Config) -> doc("REST pastebin example."), try do_compile_and_start(rest_pastebin, Config), do_rest_pastebin(tcp, http, Config), do_rest_pastebin(tcp, http2, Config) after do_stop(rest_pastebin) end. do_rest_pastebin(Transport, Protocol, Config) -> %% Existing files. _ = do_rest_get(Transport, Protocol, "/", <<"text/html">>, undefined, Config), _ = do_rest_get(Transport, Protocol, "/", <<"text/plain">>, undefined, Config), %% Use POST to upload a new file and download it back. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]), Ref = gun:post(ConnPid, "/", [ {<<"content-type">>, <<"application/x-www-form-urlencoded">>} ], <<"paste=this+is+fun">>), %% @todo Not too happy about 303 here, %% will need to revisit this example. {response, _, 303, Headers} = gun:await(ConnPid, Ref), {_, Location} = lists:keyfind(<<"location">>, 1, Headers), <<"this is fun">> = do_rest_get(Transport, Protocol, Location, <<"text/plain">>, undefined, Config), << "", _/bits >> = do_rest_get(Transport, Protocol, Location, <<"text/html">>, undefined, Config), ok. %% File server. file_server(Config) -> doc("File server example with directory listing."), try do_compile_and_start(file_server, Config), do_file_server(tcp, http, Config), do_file_server(tcp, http2, Config) after do_stop(file_server) end. do_file_server(Transport, Protocol, Config) -> %% Directory. {200, DirHeaders, <<"", _/bits >>} = do_get(Transport, Protocol, "/", Config), {_, <<"text/html; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, DirHeaders), _ = do_rest_get(Transport, Protocol, "/", <<"application/json">>, undefined, Config), %% Files. {200, _, _} = do_get(Transport, Protocol, "/small.mp4", Config), {200, _, _} = do_get(Transport, Protocol, "/small.ogv", Config), {200, _, _} = do_get(Transport, Protocol, "/test.txt", Config), {200, _, _} = do_get(Transport, Protocol, "/video.html", Config), {200, _, _} = do_get(Transport, Protocol, ["/", cow_uri:urlencode(<<"中文"/utf8>>), "/", cow_uri:urlencode(<<"中文.html"/utf8>>)], Config), ok. %% Markdown middleware. markdown_middleware(Config) -> doc("Markdown middleware example."), try do_compile_and_start(markdown_middleware, Config), do_markdown_middleware(tcp, http, Config), do_markdown_middleware(tcp, http2, Config) after do_stop(markdown_middleware) end. do_markdown_middleware(Transport, Protocol, Config) -> {200, Headers, <<"

", _/bits >>} = do_get(Transport, Protocol, "/video.html", Config), {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. %% Upload. upload(Config) -> doc("Upload example."), try do_compile_and_start(upload, Config), do_upload(tcp, http, Config), do_upload(tcp, http2, Config) after do_stop(upload) end. do_upload(Transport, Protocol, Config) -> {200, _, << "", _/bits >>} = do_get(Transport, Protocol, "/", Config), %% Use POST to upload a file using multipart. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]), Ref = gun:post(ConnPid, "/upload", [ {<<"content-type">>, <<"multipart/form-data;boundary=deadbeef">>} ], << "--deadbeef\r\n" "Content-Disposition: form-data; name=\"inputfile\"; filename=\"test.txt\"\r\n" "Content-Type: text/plain\r\n" "\r\n" "Cowboy upload example!\r\n" "--deadbeef--">>), {response, fin, 204, _} = gun:await(ConnPid, Ref), ok. %% Websocket. websocket(Config) -> doc("Websocket example."), try do_compile_and_start(websocket, Config), %% We can only initiate a Websocket connection from HTTP/1.1. {ok, Pid} = gun:open("127.0.0.1", 8080, #{protocols => [http], retry => 0}), {ok, http} = gun:await_up(Pid), _ = monitor(process, Pid), StreamRef = gun:ws_upgrade(Pid, "/websocket", [], #{compress => true}), receive {gun_upgrade, Pid, StreamRef, _, _} -> ok; Msg1 -> exit({connection_failed, Msg1}) end, %% Check that we receive the message sent on timer on init. receive {gun_ws, Pid, StreamRef, {text, <<"Hello!">>}} -> ok after 2000 -> exit(timeout) end, %% Check that we receive subsequent messages sent on timer. receive {gun_ws, Pid, StreamRef, {text, <<"How' you doin'?">>}} -> ok after 2000 -> exit(timeout) end, %% Check that we receive the echoed message. gun:ws_send(Pid, StreamRef, {text, <<"hello">>}), receive {gun_ws, Pid, StreamRef, {text, <<"That's what she said! hello">>}} -> ok after 500 -> exit(timeout) end, gun:ws_send(Pid, StreamRef, close) after do_stop(websocket) end. ================================================ FILE: test/h2spec_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(h2spec_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). %% ct. all() -> [h2spec]. init_per_suite(Config) -> case os:getenv("H2SPEC") of false -> {skip, "H2SPEC environment variable undefined."}; H2spec -> case filelib:is_file(H2spec) of false -> {skip, "H2SPEC executable not found."}; true -> cowboy_test:init_http(h2spec, #{ env => #{dispatch => init_dispatch()}, max_concurrent_streams => 100, %% This test suite expects an HTTP/2-only connection. protocols => [http2], %% Disable the DATA threshold for this test suite. stream_window_data_threshold => 0 }, Config) end end. end_per_suite(_Config) -> cowboy:stop_listener(h2spec). %% Dispatch configuration. init_dispatch() -> cowboy_router:compile([ {'_', [ {"/", delay_hello_h, 50} ]} ]). %% Tests. h2spec(Config) -> doc("h2spec test suite for the HTTP/2 protocol."), Self = self(), spawn_link(fun() -> start_port(Config, Self) end), receive {h2spec_exit, 0, Log} -> ct:log("~ts", [Log]), ok; {h2spec_exit, Status, Log} -> ct:log("~ts", [Log]), error({exit_status, Status}) end. start_port(Config, Pid) -> H2spec = os:getenv("H2SPEC"), ListenPort = config(port, Config), Port = open_port( {spawn, H2spec ++ " -S -p " ++ integer_to_list(ListenPort)}, [{line, 10000}, {cd, config(priv_dir, Config)}, binary, exit_status]), receive_infinity(Port, Pid, []). receive_infinity(Port, Pid, Acc) -> receive {Port, {data, {eol, Line}}} -> io:format(user, "~s~n", [Line]), receive_infinity(Port, Pid, [Line|Acc]); {Port, {exit_status, Status}} -> Pid ! {h2spec_exit, Status, [[L, $\n] || L <- lists:reverse(Acc)]} end. ================================================ FILE: test/handlers/accept_callback_h.erl ================================================ %% This module returns something different in %% AcceptCallback depending on the query string. -module(accept_callback_h). -export([init/2]). -export([allowed_methods/2]). -export([content_types_accepted/2]). -export([put_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. allowed_methods(Req, State) -> {[<<"PUT">>, <<"POST">>, <<"PATCH">>], Req, State}. content_types_accepted(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, put_text_plain}], Req, State}. put_text_plain(Req=#{qs := <<"false">>}, State) -> {false, Req, State}; put_text_plain(Req=#{qs := <<"true">>}, State) -> {true, Req, State}. ================================================ FILE: test/handlers/accept_callback_missing_h.erl ================================================ -module(accept_callback_missing_h). -export([init/2]). -export([allowed_methods/2]). -export([content_types_accepted/2]). init(Req, State) -> {cowboy_rest, Req, State}. allowed_methods(Req, State) -> {[<<"PUT">>], Req, State}. content_types_accepted(Req, State) -> ct_helper_error_h:ignore(cowboy_rest, process_content_type, 3), {[{<<"text/plain">>, accept}], Req, State}. ================================================ FILE: test/handlers/asterisk_h.erl ================================================ %% This module echoes back the value the test is interested in. -module(asterisk_h). -export([init/2]). init(Req, Opts) -> echo(cowboy_req:header(<<"x-echo">>, Req), Req, Opts). echo(undefined, Req, Opts) -> {ok, cowboy_req:reply(200, Req), Opts}; echo(What, Req, Opts) -> F = binary_to_atom(What, latin1), Value = case cowboy_req:F(Req) of V when is_integer(V) -> integer_to_binary(V); V -> V end, {ok, cowboy_req:reply(200, #{}, Value, Req), Opts}. ================================================ FILE: test/handlers/charset_in_content_types_provided_h.erl ================================================ %% This module has a media type provided with an explicit charset. -module(charset_in_content_types_provided_h). -export([init/2]). -export([content_types_provided/2]). -export([charsets_provided/2]). -export([get_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, [{<<"charset">>, <<"utf-8">>}]}, get_text_plain} ], Req, State}. charsets_provided(Req, State) -> {[<<"utf-16">>, <<"iso-8861-1">>], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. ================================================ FILE: test/handlers/charset_in_content_types_provided_implicit_h.erl ================================================ %% This module has a media type provided with a wildcard %% and a list of charsets that is limited. -module(charset_in_content_types_provided_implicit_h). -export([init/2]). -export([content_types_provided/2]). -export([charsets_provided/2]). -export([get_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, '*'}, get_text_plain} ], Req, State}. charsets_provided(Req, State) -> {[<<"utf-8">>, <<"utf-16">>], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. ================================================ FILE: test/handlers/charset_in_content_types_provided_implicit_no_callback_h.erl ================================================ %% This module has a media type provided with a wildcard %% and lacks a charsets_provided callback. -module(charset_in_content_types_provided_implicit_no_callback_h). -export([init/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, '*'}, get_text_plain} ], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. ================================================ FILE: test/handlers/charsets_provided_empty_h.erl ================================================ %% This module has a text and non-text media type, %% but provides no charset. All requests will result %% in a 406 not acceptable. -module(charsets_provided_empty_h). -export([init/2]). -export([content_types_provided/2]). -export([charsets_provided/2]). -export([get_text_plain/2]). -export([get_application_json/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, []}, get_text_plain}, {{<<"application">>, <<"json">>, []}, get_application_json} ], Req, State}. charsets_provided(Req, State) -> {[], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. get_application_json(Req, State) -> {<<"{\"hello\": \"rest\"}">>, Req, State}. ================================================ FILE: test/handlers/charsets_provided_h.erl ================================================ %% This module has a text and non-text media type, %% and provides two charsets. -module(charsets_provided_h). -export([init/2]). -export([content_types_provided/2]). -export([charsets_provided/2]). -export([get_text_plain/2]). -export([get_application_json/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, []}, get_text_plain}, {{<<"application">>, <<"json">>, []}, get_application_json} ], Req, State}. charsets_provided(Req, State) -> {[<<"utf-8">>, <<"utf-16">>], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. get_application_json(Req, State) -> {<<"{\"hello\": \"rest\"}">>, Req, State}. ================================================ FILE: test/handlers/compress_h.erl ================================================ %% This module sends a response body of varying sizes to test %% the cowboy_compress_h stream handler. -module(compress_h). -export([init/2]). init(Req0, State=reply) -> Req = case cowboy_req:binding(what, Req0) of <<"small">> -> cowboy_req:reply(200, #{}, lists:duplicate(100, $a), Req0); <<"large">> -> cowboy_req:reply(200, #{}, lists:duplicate(100000, $a), Req0); <<"vary">> -> Vary = cowboy_req:header(<<"x-test-vary">>, Req0), cowboy_req:reply(200, #{<<"vary">> => Vary}, lists:duplicate(100000, $a), Req0); <<"over-threshold">> -> cowboy_req:reply(200, #{}, lists:duplicate(200, $a), Req0); <<"content-encoding">> -> cowboy_req:reply(200, #{<<"content-encoding">> => <<"compress">>}, lists:duplicate(100000, $a), Req0); <<"etag">> -> cowboy_req:reply(200, #{<<"etag">> => <<"\"STRONK\"">>}, lists:duplicate(100000, $a), Req0); <<"sendfile">> -> AppFile = code:where_is_file("cowboy.app"), Size = filelib:file_size(AppFile), cowboy_req:reply(200, #{}, {sendfile, 0, Size, AppFile}, Req0); <<"set_options_threshold0">> -> cowboy_req:cast({set_options, #{compress_threshold => 0}}, Req0), cowboy_req:reply(200, #{}, lists:duplicate(100, $a), Req0) end, {ok, Req, State}; init(Req0, State=stream_reply) -> Req = case cowboy_req:binding(what, Req0) of <<"large">> -> stream_reply(#{}, Req0); <<"content-encoding">> -> stream_reply(#{<<"content-encoding">> => <<"compress">>}, Req0); <<"etag">> -> stream_reply(#{<<"etag">> => <<"\"STRONK\"">>}, Req0); <<"sendfile">> -> Data = lists:duplicate(10000, $a), AppFile = code:where_is_file("cowboy.app"), Size = filelib:file_size(AppFile), Req1 = cowboy_req:stream_reply(200, Req0), %% We send a few files interspersed into other data. cowboy_req:stream_body(Data, nofin, Req1), cowboy_req:stream_body({sendfile, 0, Size, AppFile}, nofin, Req1), cowboy_req:stream_body(Data, nofin, Req1), cowboy_req:stream_body({sendfile, 0, Size, AppFile}, nofin, Req1), cowboy_req:stream_body(Data, fin, Req1), Req1; <<"sendfile_fin">> -> Data = lists:duplicate(10000, $a), AppFile = code:where_is_file("cowboy.app"), Size = filelib:file_size(AppFile), Req1 = cowboy_req:stream_reply(200, Req0), %% We send a few files interspersed into other data. cowboy_req:stream_body(Data, nofin, Req1), cowboy_req:stream_body({sendfile, 0, Size, AppFile}, nofin, Req1), cowboy_req:stream_body(Data, nofin, Req1), cowboy_req:stream_body({sendfile, 0, Size, AppFile}, fin, Req1), Req1; <<"delayed">> -> stream_delayed(Req0); <<"set_options_buffering_false">> -> cowboy_req:cast({set_options, #{compress_buffering => false}}, Req0), stream_delayed(Req0); <<"set_options_buffering_true">> -> cowboy_req:cast({set_options, #{compress_buffering => true}}, Req0), stream_delayed(Req0) end, {ok, Req, State}. stream_reply(Headers, Req0) -> Data = lists:duplicate(10000, $a), Req = cowboy_req:stream_reply(200, Headers, Req0), _ = [cowboy_req:stream_body(Data, nofin, Req) || _ <- lists:seq(1,9)], cowboy_req:stream_body(Data, fin, Req), Req. stream_delayed(Req0) -> Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<"data: Hello!\r\n\r\n">>, nofin, Req), timer:sleep(1000), cowboy_req:stream_body(<<"data: World!\r\n\r\n">>, nofin, Req), timer:sleep(1000), cowboy_req:stream_body(<<"data: Closing!\r\n\r\n">>, fin, Req), Req. ================================================ FILE: test/handlers/content_types_accepted_h.erl ================================================ %% This module returns something different in %% content_types_accepted depending on the query string. -module(content_types_accepted_h). -export([init/2]). -export([allowed_methods/2]). -export([content_types_accepted/2]). -export([put_multipart_mixed/2]). -export([put_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. allowed_methods(Req, State) -> {[<<"PUT">>], Req, State}. content_types_accepted(Req=#{qs := <<"multipart">>}, State) -> {[ {{<<"multipart">>, <<"mixed">>, [{<<"v">>, <<"1">>}]}, put_multipart_mixed} ], Req, State}; content_types_accepted(Req=#{qs := <<"param">>}, State) -> {[{{<<"text">>, <<"plain">>, [{<<"charset">>, <<"utf-8">>}]}, put_text_plain}], Req, State}; content_types_accepted(Req=#{qs := <<"wildcard">>}, State) -> {[{'*', put_text_plain}], Req, State}; content_types_accepted(Req=#{qs := <<"wildcard-param">>}, State) -> {[{{<<"text">>, <<"plain">>, '*'}, put_text_plain}], Req, State}. put_multipart_mixed(Req, State) -> {true, Req, State}. put_text_plain(Req0, State) -> {ok, _, Req} = cowboy_req:read_body(Req0), {true, Req, State}. ================================================ FILE: test/handlers/content_types_provided_h.erl ================================================ %% This module has different content_types_provided values %% and/or sends a different response body depending on the %% query string. -module(content_types_provided_h). -export([init/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req=#{qs := <<"invalid-type">>}, State) -> ct_helper:ignore(cowboy_rest, normalize_content_types, 2), {[{{'*', '*', '*'}, get_text_plain}], Req, State}; content_types_provided(Req=#{qs := <<"wildcard-param">>}, State) -> {[{{<<"text">>, <<"plain">>, '*'}, get_text_plain}], Req, State}. get_text_plain(Req=#{qs := <<"invalid-type">>}, State) -> {<<"invalid-type">>, Req, State}; get_text_plain(Req=#{qs := <<"wildcard-param">>}, State) -> {_, _, Param} = maps:get(media_type, Req), Body = if Param =:= [] -> <<"[]">>; Param =/= [] -> iolist_to_binary([[Key, $=, Value] || {Key, Value} <- Param]) end, {Body, Req, State}. ================================================ FILE: test/handlers/crash_h.erl ================================================ %% This module crashes immediately. -module(crash_h). -behaviour(cowboy_handler). -export([init/2]). -spec init(_, _) -> no_return(). init(_, external_exit) -> ct_helper:ignore(?MODULE, init, 2), exit(self(), ct_helper_ignore); init(_, no_reply) -> ct_helper:ignore(?MODULE, init, 2), error(crash); init(Req, reply) -> _ = cowboy_req:reply(200, Req), ct_helper:ignore(?MODULE, init, 2), error(crash). ================================================ FILE: test/handlers/create_resource_h.erl ================================================ -module(create_resource_h). -export([init/2]). -export([allowed_methods/2]). -export([resource_exists/2]). -export([content_types_accepted/2]). -export([from_text/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. allowed_methods(Req, State) -> {[<<"POST">>], Req, State}. resource_exists(Req, State) -> {true, Req, State}. content_types_accepted(Req, State) -> {[{{<<"application">>, <<"text">>, []}, from_text}], Req, State}. from_text(Req=#{qs := Qs}, State) -> NewURI = [cowboy_req:uri(Req), "/foo"], case Qs of <<"created">> -> {{created, NewURI}, Req, State}; <<"see_other">> -> {{see_other, NewURI}, Req, State} end. ================================================ FILE: test/handlers/custom_req_fields_h.erl ================================================ %% This module adds custom fields to the Req object. %% It is only meant to be checked by Dialyzer. -module(custom_req_fields_h). -export([init/2]). -spec init(Req, Opts) -> {ok, Req, Opts} when Req::cowboy_req:req(). init(Req, Opts) -> {ok, Req#{'_myapp_auth_method' => pubkey}, Opts}. ================================================ FILE: test/handlers/decompress_h.erl ================================================ %% This module echoes a request body of to test %% the cowboy_decompress_h stream handler. -module(decompress_h). -export([init/2]). init(Req0, State=echo) -> case cowboy_req:binding(what, Req0) of <<"decompress_disable">> -> cowboy_req:cast({set_options, #{decompress_enabled => false}}, Req0); <<"decompress_ratio_limit">> -> cowboy_req:cast({set_options, #{decompress_ratio_limit => 0.5}}, Req0); <<"normal">> -> ok end, {ok, Body, Req1} = read_body(Req0), Req = cowboy_req:reply(200, #{}, Body, Req1), {ok, Req, State}; init(Req0, State=test) -> Req = test(Req0, cowboy_req:binding(what, Req0)), {ok, Req, State}. test(Req, <<"content-encoding">>) -> cowboy_req:reply(200, #{}, cowboy_req:header(<<"content-encoding">>, Req, <<"undefined">>), Req); test(Req, <<"content-decoded">>) -> cowboy_req:reply(200, #{}, io_lib:format("~0p", [maps:get(content_decoded, Req, undefined)]), Req); test(Req0, <<"disable-in-the-middle">>) -> {Status, Data, Req1} = cowboy_req:read_body(Req0, #{length => 1000}), cowboy_req:cast({set_options, #{decompress_enabled => false}}, Req1), {ok, Body, Req} = do_read_body(Status, Req1, Data), cowboy_req:reply(200, #{}, Body, Req); test(Req0, <<"enable-in-the-middle">>) -> {Status, Data, Req1} = cowboy_req:read_body(Req0, #{length => 1000}), cowboy_req:cast({set_options, #{decompress_enabled => true}}, Req1), {ok, Body, Req} = do_read_body(Status, Req1, Data), cowboy_req:reply(200, #{}, Body, Req); test(Req0, <<"header-command">>) -> {ok, Body, Req1} = read_body(Req0), Req = cowboy_req:stream_reply(200, #{}, Req1), cowboy_req:stream_body(Body, fin, Req); test(Req0, <<"accept-identity">>) -> {ok, Body, Req} = read_body(Req0), cowboy_req:reply(200, #{<<"accept-encoding">> => <<"identity">>}, Body, Req); test(Req0, <<"invalid-header">>) -> {ok, Body, Req} = read_body(Req0), cowboy_req:reply(200, #{<<"accept-encoding">> => <<";">>}, Body, Req); test(Req0, <<"reject-explicit-header">>) -> {ok, Body, Req} = read_body(Req0), cowboy_req:reply(200, #{<<"accept-encoding">> => <<"identity, gzip;q=0">>}, Body, Req); test(Req0, <<"reject-implicit-header">>) -> {ok, Body, Req} = read_body(Req0), cowboy_req:reply(200, #{<<"accept-encoding">> => <<"identity, *;q=0">>}, Body, Req); test(Req0, <<"accept-explicit-header">>) -> {ok, Body, Req} = read_body(Req0), cowboy_req:reply(200, #{<<"accept-encoding">> => <<"identity, gzip;q=0.5">>}, Body, Req); test(Req0, <<"accept-implicit-header">>) -> {ok, Body, Req} = read_body(Req0), cowboy_req:reply(200, #{<<"accept-encoding">> => <<"identity, *;q=0.5">>}, Body, Req). read_body(Req0) -> {Status, Data, Req} = cowboy_req:read_body(Req0, #{length => 1000}), do_read_body(Status, Req, Data). do_read_body(more, Req0, Acc) -> {Status, Data, Req} = cowboy_req:read_body(Req0), do_read_body(Status, Req, << Acc/binary, Data/binary >>); do_read_body(ok, Req, Acc) -> {ok, Acc, Req}. ================================================ FILE: test/handlers/default_h.erl ================================================ %% This module does not do anything. -module(default_h). -export([init/2]). init(Req, Opts) -> {ok, Req, Opts}. ================================================ FILE: test/handlers/delay_hello_h.erl ================================================ %% This module sends a hello world response after a delay. -module(delay_hello_h). -export([init/2]). init(Req, Delay) when is_integer(Delay) -> init(Req, #{delay => Delay}); init(Req, Opts=#{delay := Delay}) -> _ = case Opts of #{notify_received := Pid} -> Pid ! {request_received, maps:get(path, Req)}; _ -> ok end, timer:sleep(Delay), {ok, cowboy_req:reply(200, #{}, <<"Hello world!">>, Req), Delay}. ================================================ FILE: test/handlers/delete_resource_h.erl ================================================ %% This module accepts a multipart media type with parameters %% that do not include boundary. -module(delete_resource_h). -export([init/2]). -export([allowed_methods/2]). -export([delete_resource/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. allowed_methods(Req, State) -> {[<<"DELETE">>], Req, State}. delete_resource(#{qs := <<"missing">>}, _) -> no_call. ================================================ FILE: test/handlers/echo_h.erl ================================================ %% This module echoes back the value the test is interested in. -module(echo_h). -export([init/2]). init(Req, Opts) -> case cowboy_req:binding(arg, Req) of undefined -> echo(cowboy_req:binding(key, Req), Req, Opts); Arg -> echo_arg(Arg, Req, Opts) end. echo(<<"read_body">>, Req0, Opts) -> case Opts of #{crash := true} -> ct_helper:ignore(cowboy_req, read_body, 2); _ -> ok end, {_, Body, Req} = case cowboy_req:path(Req0) of <<"/100-continue", _/bits>> -> cowboy_req:inform(100, Req0), cowboy_req:read_body(Req0); <<"/delay", _/bits>> -> timer:sleep(500), cowboy_req:read_body(Req0); <<"/full", _/bits>> -> read_body(Req0, <<>>); <<"/auto-sync", _/bits>> -> read_body_auto_sync(Req0, <<>>); <<"/auto-async", _/bits>> -> read_body_auto_async(Req0, <<>>); <<"/length", _/bits>> -> {_, _, Req1} = read_body(Req0, <<>>), Length = cowboy_req:body_length(Req1), {ok, integer_to_binary(Length), Req1}; <<"/opts", _/bits>> -> cowboy_req:read_body(Req0, Opts); <<"/spawn", _/bits>> -> Parent = self(), Pid = spawn_link(fun() -> Parent ! {self(), cowboy_req:read_body(Req0)} end), receive {Pid, Msg} -> Msg after 5000 -> error(timeout) end; _ -> cowboy_req:read_body(Req0) end, {ok, cowboy_req:reply(200, #{}, Body, Req), Opts}; echo(<<"read_urlencoded_body">>, Req0, Opts) -> Path = cowboy_req:path(Req0), case {Path, Opts} of {<<"/opts", _/bits>>, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_body, 2); {_, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_urlencoded_body, 2); _ -> ok end, {ok, Body, Req} = case Path of <<"/opts", _/bits>> -> cowboy_req:read_urlencoded_body(Req0, Opts); <<"/crash", _/bits>> -> cowboy_req:read_urlencoded_body(Req0, Opts); _ -> cowboy_req:read_urlencoded_body(Req0) end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts}; echo(<<"read_and_match_urlencoded_body">>, Req0, Opts) -> Path = cowboy_req:path(Req0), case {Path, Opts} of {<<"/opts", _/bits>>, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_body, 2); {_, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_urlencoded_body, 2); _ -> ok end, {ok, Body, Req} = case Path of <<"/opts", _/bits>> -> cowboy_req:read_and_match_urlencoded_body([], Req0, Opts); <<"/crash", _/bits>> -> cowboy_req:read_and_match_urlencoded_body([], Req0, Opts); _ -> cowboy_req:read_and_match_urlencoded_body([], Req0) end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts}; echo(<<"uri">>, Req, Opts) -> Value = case cowboy_req:path_info(Req) of [<<"origin">>] -> cowboy_req:uri(Req, #{host => undefined}); [<<"protocol-relative">>] -> cowboy_req:uri(Req, #{scheme => undefined}); [<<"no-qs">>] -> cowboy_req:uri(Req, #{qs => undefined}); [<<"no-path">>] -> cowboy_req:uri(Req, #{path => undefined, qs => undefined}); [<<"set-port">>] -> cowboy_req:uri(Req, #{port => 123}); _ -> cowboy_req:uri(Req) end, {ok, cowboy_req:reply(200, #{}, Value, Req), Opts}; echo(<<"match">>, Req, Opts) -> [Type|Fields0] = cowboy_req:path_info(Req), Fields = [binary_to_atom(F, latin1) || F <- Fields0], Value = case Type of <<"qs">> -> cowboy_req:match_qs(Fields, Req); <<"qs_with_constraints">> -> cowboy_req:match_qs([{id, integer}], Req); <<"cookies">> -> cowboy_req:match_cookies(Fields, Req); <<"body_qs">> -> %% Note that the Req should not be discarded but for the %% purpose of this test this has no ill impacts. {ok, Match, _} = cowboy_req:read_and_match_urlencoded_body(Fields, Req), Match end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}; echo(<<"filter_then_parse_cookies">>, Req0, Opts) -> Req = cowboy_req:filter_cookies([cake, color], Req0), Value = cowboy_req:parse_cookies(Req), {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}; echo(What, Req, Opts) -> Key = binary_to_atom(What, latin1), Value = case cowboy_req:path(Req) of <<"/direct/",_/bits>> -> maps:get(Key, Req); _ -> cowboy_req:Key(Req) end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}. echo_arg(Arg0, Req, Opts) -> F = binary_to_atom(cowboy_req:binding(key, Req), latin1), Arg = case F of binding -> binary_to_atom(Arg0, latin1); _ -> Arg0 end, Value = case cowboy_req:binding(default, Req) of undefined -> cowboy_req:F(Arg, Req); Default -> cowboy_req:F(Arg, Req, Default) end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}. read_body(Req0, Acc) -> case cowboy_req:read_body(Req0) of {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req}; {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>) end. read_body_auto_sync(Req0, Acc) -> Opts = #{length => auto, period => infinity}, case cowboy_req:read_body(Req0, Opts) of {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req}; {more, Data, Req} -> read_body_auto_sync(Req, << Acc/binary, Data/binary >>) end. read_body_auto_async(Req, Acc) -> read_body_auto_async(Req, make_ref(), Acc). read_body_auto_async(Req, ReadBodyRef, Acc) -> cowboy_req:cast({read_body, self(), ReadBodyRef, auto, infinity}, Req), receive {request_body, ReadBodyRef, nofin, Data} -> read_body_auto_async(Req, ReadBodyRef, <>); {request_body, ReadBodyRef, fin, _, Data} -> {ok, <>, Req} end. value_to_iodata(V) when is_integer(V) -> integer_to_binary(V); value_to_iodata(V) when is_atom(V) -> atom_to_binary(V, latin1); value_to_iodata(V) when is_list(V); is_tuple(V); is_map(V) -> io_lib:format("~999999p", [V]); value_to_iodata(V) -> V. ================================================ FILE: test/handlers/expires_h.erl ================================================ %% This module sends a different expires value %% depending on the query string. -module(expires_h). -export([init/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). -export([expires/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. expires(Req=#{qs := <<"tuple">>}, State) -> {{{2012, 9, 21}, {22, 36, 14}}, Req, State}; expires(Req=#{qs := <<"binary">>}, State) -> {<<"0">>, Req, State}; expires(Req=#{qs := <<"undefined">>}, State) -> {undefined, Req, State}; %% Simulate the callback being missing in other cases. expires(#{qs := <<"missing">>}, _) -> no_call. ================================================ FILE: test/handlers/generate_etag_h.erl ================================================ %% This module sends a different etag value %% depending on the query string. -module(generate_etag_h). -export([init/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). -export([generate_etag/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. %% Correct return values from generate_etag/2. generate_etag(Req=#{qs := <<"tuple-weak">>}, State) -> {{weak, <<"etag-header-value">>}, Req, State}; generate_etag(Req=#{qs := <<"tuple-strong">>}, State) -> {{strong, <<"etag-header-value">>}, Req, State}; %% Backwards compatible return values from generate_etag/2. generate_etag(Req=#{qs := <<"binary-weak-quoted">>}, State) -> {<<"W/\"etag-header-value\"">>, Req, State}; generate_etag(Req=#{qs := <<"binary-strong-quoted">>}, State) -> {<<"\"etag-header-value\"">>, Req, State}; %% Invalid return values from generate_etag/2. generate_etag(Req=#{qs := <<"binary-weak-unquoted">>}, State) -> ct_helper_error_h:ignore(cow_http_hd, parse_etag, 1), {<<"W/etag-header-value">>, Req, State}; generate_etag(Req=#{qs := <<"binary-strong-unquoted">>}, State) -> ct_helper_error_h:ignore(cow_http_hd, parse_etag, 1), {<<"etag-header-value">>, Req, State}; %% Returning 'undefined' to indicate no etag. generate_etag(Req=#{qs := <<"undefined">>}, State) -> {undefined, Req, State}; %% Simulate the callback being missing in other cases. generate_etag(#{qs := <<"missing">>}, _) -> no_call. ================================================ FILE: test/handlers/hello_h.erl ================================================ %% This module sends a hello world response. -module(hello_h). -export([init/2]). init(Req, Opts) -> {ok, cowboy_req:reply(200, #{}, <<"Hello world!">>, Req), Opts}. ================================================ FILE: test/handlers/if_range_h.erl ================================================ %% This module defines the ranges_provided callback %% and a generate_etag callback that returns something %% different depending on query string. It also defines %% a last_modified callback that must be ignored when a %% date is provided in if_range. -module(if_range_h). -export([init/2]). -export([content_types_provided/2]). -export([ranges_provided/2]). -export([generate_etag/2]). -export([last_modified/2]). -export([get_text_plain/2]). -export([get_text_plain_bytes/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. %% Simulate the callback being missing. ranges_provided(#{qs := <<"missing-ranges_provided">>}, _) -> no_call; ranges_provided(Req=#{qs := <<"empty-ranges_provided">>}, State) -> {[], Req, State}; ranges_provided(Req, State) -> {[{<<"bytes">>, get_text_plain_bytes}], Req, State}. generate_etag(Req=#{qs := <<"weak-etag">>}, State) -> {{weak, <<"weak-no-match">>}, Req, State}; generate_etag(Req, State) -> {{strong, <<"strong-and-match">>}, Req, State}. last_modified(Req, State) -> {{{2222, 2, 22}, {11, 11, 11}}, Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. get_text_plain_bytes(Req, State) -> %% We send everything in one part, since we are not testing %% this callback specifically. Body = <<"This is ranged REST!">>, {[{{0, byte_size(Body) - 1, byte_size(Body)}, Body}], Req, State}. ================================================ FILE: test/handlers/last_modified_h.erl ================================================ %% This module sends a different last-modified value %% depending on the query string. -module(last_modified_h). -export([init/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). -export([last_modified/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. last_modified(Req=#{qs := <<"tuple">>}, State) -> {{{2012, 9, 21}, {22, 36, 14}}, Req, State}; last_modified(Req=#{qs := <<"undefined">>}, State) -> {undefined, Req, State}; %% Simulate the callback being missing in other cases. last_modified(#{qs := <<"missing">>}, _) -> no_call. ================================================ FILE: test/handlers/long_polling_h.erl ================================================ %% This module implements a loop handler for long-polling. %% It starts by sending itself a message after 200ms, %% then sends another after that for a total of 3 messages. %% When it receives the last message, it sends a 102 reply back. -module(long_polling_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> erlang:send_after(200, self(), timeout), {cowboy_loop, Req, 2, hibernate}. info(timeout, Req, 0) -> %% Send an unused status code to make sure there's no %% conflict with whatever Cowboy may send itself. {stop, cowboy_req:reply(<<"299 OK!">>, Req), 0}; info(timeout, Req, Count) -> erlang:send_after(200, self(), timeout), {ok, Req, Count - 1, hibernate}. terminate(stop, _, 0) -> ok; terminate({error, overflow}, _, _) -> ok. ================================================ FILE: test/handlers/long_polling_sys_h.erl ================================================ %% This module implements a loop handler that does nothing %% and expects a crash to happen. -module(long_polling_sys_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> process_flag(trap_exit, true), erlang:send_after(500, self(), timeout), {cowboy_loop, Req, undefined}. info(timeout, Req, State) -> %% Send an unused status code to make sure there's no %% conflict with whatever Cowboy may send itself. {ok, cowboy_req:reply(<<"299 OK!">>, Req), State}; info(_, Req, State) -> {ok, Req, State}. terminate(_, _, _) -> ok. ================================================ FILE: test/handlers/loop_handler_abort_h.erl ================================================ %% This module implements a loop handler that reads %% 1000 bytes of the request body after sending itself %% a message, then terminates the stream. -module(loop_handler_abort_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> self() ! timeout, {cowboy_loop, Req, undefined, hibernate}. info(timeout, Req0, State) -> {_Status, Body, Req} = cowboy_req:read_body(Req0, #{length => 1000}), 1000 = byte_size(Body), {stop, cowboy_req:reply(200, Req), State}. terminate(stop, _, _) -> ok. ================================================ FILE: test/handlers/loop_handler_body_h.erl ================================================ %% This module implements a loop handler that reads %% the request body after sending itself a message, %% checks that its size is exactly 100000 bytes, %% then sends a 200 reply back. -module(loop_handler_body_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> self() ! timeout, {cowboy_loop, Req, undefined, hibernate}. info(timeout, Req0, State) -> {ok, Body, Req} = cowboy_req:read_body(Req0), 100000 = byte_size(Body), {stop, cowboy_req:reply(200, Req), State}. terminate(stop, _, _) -> ok. ================================================ FILE: test/handlers/loop_handler_endless_h.erl ================================================ %% This module implements a loop handler that streams endless data. -module(loop_handler_endless_h). -export([init/2]). -export([info/3]). init(Req0, #{delay := Delay} = Opts) -> case cowboy_req:header(<<"x-test-pid">>, Req0) of BinPid when is_binary(BinPid) -> Pid = list_to_pid(binary_to_list(BinPid)), Pid ! {Pid, self(), init}, ok; _ -> ok end, erlang:send_after(Delay, self(), timeout), Req = cowboy_req:stream_reply(200, Req0), {cowboy_loop, Req, Opts}. info(timeout, Req, State) -> cowboy_req:stream_body(<<0:10000/unit:8>>, nofin, Req), %% Equivalent to a 0 timeout. self() ! timeout, {ok, Req, State}. ================================================ FILE: test/handlers/loop_handler_timeout_h.erl ================================================ %% This module implements a loop handler that sends %% itself a timeout that will intentionally arrive %% after the HTTP/1.1 request_timeout. The protocol %% is not supposed to close the connection when a %% request is ongoing, and therefore this handler %% will eventually send a 200 reply. -module(loop_handler_timeout_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> erlang:send_after(6000, self(), timeout), {cowboy_loop, Req, #{hibernate => true}}. info(timeout, Req, State) -> {stop, cowboy_req:reply(200, #{}, <<"Good!">>, Req), State}. terminate(stop, _, _) -> ok. ================================================ FILE: test/handlers/loop_handler_timeout_hibernate_h.erl ================================================ %% This module implements a loop handler that first %% sets a timeout, then hibernates, then ensures %% that the timeout initially set no longer triggers. %% If everything goes fine a 200 is returned. If the %% timeout triggers again a 299 is. -module(loop_handler_timeout_hibernate_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> self() ! message1, {cowboy_loop, Req, undefined, 100}. info(message1, Req, State) -> erlang:send_after(200, self(), message2), {ok, Req, State, hibernate}; info(message2, Req, State) -> erlang:send_after(200, self(), message3), %% Don't set a timeout now. {ok, Req, State}; info(message3, Req, State) -> {stop, cowboy_req:reply(200, Req), State}; info(timeout, Req, State) -> {stop, cowboy_req:reply(<<"299 OK!">>, Req), State}. terminate(stop, _, _) -> ok. ================================================ FILE: test/handlers/loop_handler_timeout_info_h.erl ================================================ %% This module implements a loop handler that changes %% the timeout value to 500ms after the first message %% then sends itself another message after 1000ms. %% It is expected to timeout, that is, reply a 299. -module(loop_handler_timeout_info_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> self() ! message, {cowboy_loop, Req, undefined}. info(message, Req, State) -> erlang:send_after(500, self(), message), {ok, Req, State, 100}; info(timeout, Req, State) -> {stop, cowboy_req:reply(<<"299 OK!">>, Req), State}. terminate(stop, _, _) -> ok. ================================================ FILE: test/handlers/loop_handler_timeout_init_h.erl ================================================ %% This module implements a loop handler that reads %% the request query for a timeout value, then sends %% itself a message after 1000ms. It replies a 200 when %% the message does not timeout and a 299 otherwise. -module(loop_handler_timeout_init_h). -export([init/2]). -export([info/3]). -export([terminate/3]). init(Req, _) -> #{timeout := Timeout} = cowboy_req:match_qs([{timeout, int}], Req), erlang:send_after(500, self(), message), {cowboy_loop, Req, undefined, Timeout}. info(message, Req, State) -> {stop, cowboy_req:reply(200, Req), State}; info(timeout, Req, State) -> {stop, cowboy_req:reply(<<"299 OK!">>, Req), State}. terminate(stop, _, _) -> ok. ================================================ FILE: test/handlers/multipart_h.erl ================================================ %% This module reads a multipart body and echoes it back as an Erlang term. -module(multipart_h). -export([init/2]). init(Req0, State) -> {Result, Req} = case cowboy_req:binding(key, Req0) of undefined -> acc_multipart(Req0, []); <<"skip_body">> -> skip_body_multipart(Req0, []); <<"read_part2">> -> read_part2_multipart(Req0, []); <<"read_part_body2">> -> read_part_body2_multipart(Req0, []) end, {ok, cowboy_req:reply(200, #{}, term_to_binary(Result), Req), State}. acc_multipart(Req0, Acc) -> case cowboy_req:read_part(Req0) of {ok, Headers, Req1} -> {ok, Body, Req} = stream_body(Req1, <<>>), acc_multipart(Req, [{Headers, Body}|Acc]); {done, Req} -> {lists:reverse(Acc), Req} end. stream_body(Req0, Acc) -> case cowboy_req:read_part_body(Req0) of {more, Data, Req} -> stream_body(Req, << Acc/binary, Data/binary >>); {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req} end. skip_body_multipart(Req0, Acc) -> case cowboy_req:read_part(Req0) of {ok, Headers, Req} -> skip_body_multipart(Req, [Headers|Acc]); {done, Req} -> {lists:reverse(Acc), Req} end. read_part2_multipart(Req0, Acc) -> case cowboy_req:read_part(Req0, #{length => 1, period => 1}) of {ok, Headers, Req1} -> {ok, Body, Req} = stream_body(Req1, <<>>), acc_multipart(Req, [{Headers, Body}|Acc]); {done, Req} -> {lists:reverse(Acc), Req} end. read_part_body2_multipart(Req0, Acc) -> case cowboy_req:read_part(Req0) of {ok, Headers, Req1} -> {ok, Body, Req} = stream_body2(Req1, <<>>), acc_multipart(Req, [{Headers, Body}|Acc]); {done, Req} -> {lists:reverse(Acc), Req} end. stream_body2(Req0, Acc) -> case cowboy_req:read_part_body(Req0, #{length => 1, period => 1}) of {more, Data, Req} -> stream_body(Req, << Acc/binary, Data/binary >>); {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req} end. ================================================ FILE: test/handlers/provide_callback_missing_h.erl ================================================ -module(provide_callback_missing_h). -export([init/2]). -export([content_types_provided/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> ct_helper_error_h:ignore(cowboy_rest, set_resp_body, 2), {[{<<"text/plain">>, provide}], Req, State}. ================================================ FILE: test/handlers/provide_range_callback_h.erl ================================================ %% This module defines many callbacks relevant to range requests %% and return something different depending on query string. -module(provide_range_callback_h). -export([init/2]). -export([content_types_provided/2]). -export([ranges_provided/2]). -export([expires/2]). -export([generate_etag/2]). -export([last_modified/2]). -export([get_text_plain/2]). -export([get_text_plain_bytes/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> {[ {{<<"text">>, <<"plain">>, []}, get_text_plain}, %% This one only exists so we generate a vary header. {{<<"text">>, <<"html">>, []}, get_text_html} ], Req, State}. ranges_provided(Req, State) -> {[{<<"bytes">>, get_text_plain_bytes}], Req, State}. generate_etag(Req=#{qs := <<"weak-etag">>}, State) -> {{weak, <<"weak-no-match">>}, Req, State}; generate_etag(Req, State) -> {{strong, <<"strong-and-match">>}, Req, State}. last_modified(Req, State) -> {{{2222, 2, 22}, {11, 11, 11}}, Req, State}. expires(Req, State) -> {{{3333, 3, 3}, {11, 11, 11}}, Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. %% Simulate the callback being missing, otherwise expect true/false. get_text_plain_bytes(#{qs := <<"missing">>}, _) -> ct_helper_error_h:ignore(cowboy_rest, set_ranged_body_callback, 3), no_call; get_text_plain_bytes(Req=#{qs := <<"sendfile">>, range := {_, [{From=0, infinity}]}}, State) -> Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), {[{{From, Size - 1, Size}, {sendfile, From, Size, Path}}], Req, State}; get_text_plain_bytes(Req=#{range := {_, [{From=0, infinity}]}}, State) -> %% We send everything in one part. Body = <<"This is ranged REST!">>, Total = byte_size(Body), {[{{From, Total - 1, Total}, Body}], Req, State}; get_text_plain_bytes(Req=#{qs := <<"sendfile">>, range := {_, Range}}, State) -> %% We check the range header we get and send everything hardcoded. [ {50, 99}, {150, 199}, {250, 299}, -99 ] = Range, Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), {[ {{50, 99, Size}, {sendfile, 50, 50, Path}}, {{150, 199, Size}, {sendfile, 150, 50, Path}}, {{250, 299, Size}, {sendfile, 250, 50, Path}}, {{Size - 99, Size - 1, Size}, {sendfile, Size - 99, 99, Path}} ], Req, State}; get_text_plain_bytes(Req=#{range := {_, Range}}, State) -> %% We check the range header we get and send everything hardcoded. [ {0, 3}, {5, 6}, {8, 13}, {15, infinity} ] = Range, Body = <<"This is ranged REST!">>, Total = byte_size(Body), {[ {{0, 3, Total}, <<"This">>}, {{5, 6, Total}, <<"is">>}, {{8, 13, Total}, <<"ranged">>}, {{15, 19, Total}, <<"REST!">>} ], Req, State}. ================================================ FILE: test/handlers/range_satisfiable_h.erl ================================================ %% This module defines the range_satisfiable callback %% and return something different depending on query string. -module(range_satisfiable_h). -export([init/2]). -export([content_types_provided/2]). -export([ranges_provided/2]). -export([range_satisfiable/2]). -export([get_text_plain/2]). -export([get_text_plain_bytes/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. ranges_provided(Req, State) -> {[{<<"bytes">>, get_text_plain_bytes}], Req, State}. %% Simulate the callback being missing, otherwise expect true/false. range_satisfiable(#{qs := <<"missing">>}, _) -> no_call; range_satisfiable(Req=#{qs := <<"false-int">>}, State) -> {{false, 123}, Req, State}; range_satisfiable(Req=#{qs := <<"false-bin">>}, State) -> {{false, <<"*/456">>}, Req, State}; range_satisfiable(Req=#{qs := Qs}, State) -> {Qs =:= <<"true">>, Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. get_text_plain_bytes(Req, State) -> %% We send everything in one part, since we are not testing %% this callback specifically. Body = <<"This is ranged REST!">>, {[{{0, byte_size(Body) - 1, byte_size(Body)}, Body}], Req, State}. ================================================ FILE: test/handlers/ranges_provided_auto_h.erl ================================================ %% This module defines the ranges_provided callback %% which returns the auto option for bytes ranges %% and the normal ProvideCallback that returns %% something different depending on query string. -module(ranges_provided_auto_h). -export([init/2]). -export([content_types_provided/2]). -export([ranges_provided/2]). -export([get_text_plain/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. ranges_provided(Req, State) -> {[{<<"bytes">>, auto}], Req, State}. get_text_plain(Req=#{qs := <<"data">>}, State) -> {<<"This is ranged REST!">>, Req, State}; get_text_plain(Req=#{qs := <<"sendfile">>}, State) -> Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), {{sendfile, 0, Size, Path}, Req, State}. ================================================ FILE: test/handlers/ranges_provided_h.erl ================================================ %% This module defines the ranges_provided callback %% and return something different depending on query string. -module(ranges_provided_h). -export([init/2]). -export([content_types_provided/2]). -export([ranges_provided/2]). -export([get_text_plain/2]). init(Req, State) -> {cowboy_rest, Req, State}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. ranges_provided(Req=#{qs := <<"list">>}, State) -> {[ {<<"bytes">>, get_text_plain_bytes}, {<<"pages">>, get_text_plain_pages}, {<<"chapters">>, get_text_plain_chapters} ], Req, State}; ranges_provided(Req=#{qs := <<"none">>}, State) -> {[], Req, State}; %% Simulate the callback being missing in other cases. ranges_provided(_, _) -> no_call. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. ================================================ FILE: test/handlers/rate_limited_h.erl ================================================ %% This module does rate limiting based on the query string value. -module(rate_limited_h). -export([init/2]). -export([rate_limited/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). init(Req, State) -> {cowboy_rest, Req, State}. rate_limited(Req=#{qs := <<"false">>}, State) -> {false, Req, State}; rate_limited(Req=#{qs := <<"true-date">>}, State) -> {{true, {{2222, 2, 22}, {11, 11, 11}}}, Req, State}; rate_limited(Req=#{qs := <<"true">>}, State) -> {{true, 3600}, Req, State}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. ================================================ FILE: test/handlers/read_body_h.erl ================================================ %% This module reads the request body fully and send a 204 response. -module(read_body_h). -export([init/2]). init(Req0, Opts) -> {ok, Req} = read_body(Req0), {ok, cowboy_req:reply(200, #{}, Req), Opts}. read_body(Req0) -> case cowboy_req:read_body(Req0) of {ok, _, Req} -> {ok, Req}; {more, _, Req} -> read_body(Req) end. ================================================ FILE: test/handlers/resp_h.erl ================================================ %% This module echoes back the value the test is interested in. -module(resp_h). %% @todo Probably should have a separate handler for errors, %% so that we can dialyze all the other correct calls. -dialyzer({nowarn_function, do/3}). -export([init/2]). init(Req, Opts) -> do(cowboy_req:binding(key, Req), Req, Opts). do(<<"set_resp_cookie3">>, Req0, Opts) -> Req = case cowboy_req:binding(arg, Req0) of undefined -> cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0); <<"multiple">> -> Req1 = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0), cowboy_req:set_resp_cookie(<<"yourcookie">>, <<"yourvalue">>, Req1); <<"overwrite">> -> Req1 = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0), cowboy_req:set_resp_cookie(<<"mycookie">>, <<"overwrite">>, Req1) end, {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_cookie4">>, Req0, Opts) -> Req = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0, #{path => cowboy_req:path(Req0)}), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_header">>, Req0, Opts) -> Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_header_cookie">>, Req0, Opts) -> ct_helper:ignore(cowboy_req, set_resp_header, 3), Req = cowboy_req:set_resp_header(<<"set-cookie">>, <<"name=value">>, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_header_server">>, Req0, Opts) -> Req = cowboy_req:set_resp_header(<<"server">>, <<"nginx">>, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers">>, Req0, Opts) -> Req = cowboy_req:set_resp_headers(#{ <<"content-type">> => <<"text/plain">>, <<"content-encoding">> => <<"compress">> }, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers_list">>, Req0, Opts) -> Req = cowboy_req:set_resp_headers([ {<<"content-type">>, <<"text/plain">>}, {<<"test-header">>, <<"one">>}, {<<"content-encoding">>, <<"compress">>}, {<<"test-header">>, <<"two">>} ], Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers_cookie">>, Req0, Opts) -> ct_helper:ignore(cowboy_req, set_resp_headers, 2), Req = cowboy_req:set_resp_headers(#{ <<"set-cookie">> => <<"name=value">> }, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers_list_cookie">>, Req0, Opts) -> ct_helper:ignore(cowboy_req, set_resp_headers_list, 3), Req = cowboy_req:set_resp_headers([ {<<"set-cookie">>, <<"name=value">>}, {<<"set-cookie">>, <<"name2=value2">>} ], Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_headers_http11">>, Req0, Opts) -> Req = cowboy_req:set_resp_headers(#{ <<"connection">> => <<"custom-header, close">>, <<"custom-header">> => <<"value">>, <<"keep-alive">> => <<"timeout=5, max=1000">>, <<"proxy-connection">> => <<"close">>, <<"transfer-encoding">> => <<"chunked">>, <<"upgrade">> => <<"HTTP/1.1">> }, Req0), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"resp_header_defined">>, Req0, Opts) -> Req1 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), <<"text/plain">> = cowboy_req:resp_header(<<"content-type">>, Req1), <<"text/plain">> = cowboy_req:resp_header(<<"content-type">>, Req1, default), {ok, cowboy_req:reply(200, #{}, "OK", Req0), Opts}; do(<<"resp_header_default">>, Req, Opts) -> undefined = cowboy_req:resp_header(<<"content-type">>, Req), default = cowboy_req:resp_header(<<"content-type">>, Req, default), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"resp_headers">>, Req0, Opts) -> Req1 = cowboy_req:set_resp_header(<<"server">>, <<"nginx">>, Req0), Req = cowboy_req:set_resp_headers(#{ <<"content-type">> => <<"text/plain">>, <<"content-encoding">> => <<"compress">> }, Req1), Headers = cowboy_req:resp_headers(Req), true = maps:is_key(<<"server">>, Headers), true = maps:is_key(<<"content-type">>, Headers), true = maps:is_key(<<"content-encoding">>, Headers), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"resp_headers_empty">>, Req, Opts) -> #{} = cowboy_req:resp_headers(Req), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"set_resp_body">>, Req0, Opts) -> Arg = cowboy_req:binding(arg, Req0), Req1 = case Arg of <<"sendfile0">> -> AppFile = code:where_is_file("cowboy.app"), cowboy_req:set_resp_body({sendfile, 0, 0, AppFile}, Req0); <<"sendfile">> -> AppFile = code:where_is_file("cowboy.app"), cowboy_req:set_resp_body({sendfile, 0, filelib:file_size(AppFile), AppFile}, Req0); _ -> cowboy_req:set_resp_body(<<"OK">>, Req0) end, Req = case Arg of <<"override">> -> cowboy_req:reply(200, #{}, <<"OVERRIDE">>, Req1); _ -> cowboy_req:reply(200, Req1) end, {ok, Req, Opts}; do(<<"has_resp_header">>, Req0, Opts) -> false = cowboy_req:has_resp_header(<<"content-type">>, Req0), Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), true = cowboy_req:has_resp_header(<<"content-type">>, Req), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"has_resp_body">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of <<"sendfile">> -> %% @todo Cases for sendfile. Note that sendfile 0 is unallowed. false = cowboy_req:has_resp_body(Req0), Req = cowboy_req:set_resp_body({sendfile, 0, 10, code:where_is_file("cowboy.app")}, Req0), true = cowboy_req:has_resp_body(Req), {ok, cowboy_req:reply(200, #{}, <<"OK">>, Req), Opts}; undefined -> false = cowboy_req:has_resp_body(Req0), Req = cowboy_req:set_resp_body(<<"OK">>, Req0), true = cowboy_req:has_resp_body(Req), {ok, cowboy_req:reply(200, #{}, Req), Opts} end; do(<<"delete_resp_header">>, Req0, Opts) -> %% We try to delete first even though it hasn't been set to %% make sure this noop is possible. Req1 = cowboy_req:delete_resp_header(<<"content-type">>, Req0), false = cowboy_req:has_resp_header(<<"content-type">>, Req1), Req2 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req1), true = cowboy_req:has_resp_header(<<"content-type">>, Req2), Req = cowboy_req:delete_resp_header(<<"content-type">>, Req2), false = cowboy_req:has_resp_header(<<"content-type">>, Req), {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; do(<<"inform2">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of <<"binary">> -> cowboy_req:inform(<<"102 On my way">>, Req0); <<"error">> -> ct_helper:ignore(cowboy_req, inform, 3), cowboy_req:inform(ok, Req0); <<"twice">> -> cowboy_req:inform(102, Req0), cowboy_req:inform(102, Req0); <<"after_reply">> -> ct_helper:ignore(cowboy_req, inform, 3), Req1 = cowboy_req:reply(200, Req0), cowboy_req:inform(102, Req1); Status -> cowboy_req:inform(binary_to_integer(Status), Req0) end, Req = cowboy_req:reply(200, Req0), {ok, Req, Opts}; do(<<"inform3">>, Req0, Opts) -> Headers = #{<<"ext-header">> => <<"ext-value">>}, case cowboy_req:binding(arg, Req0) of <<"binary">> -> cowboy_req:inform(<<"102 On my way">>, Headers, Req0); <<"error">> -> ct_helper:ignore(cowboy_req, inform, 3), cowboy_req:inform(ok, Headers, Req0); <<"set_cookie">> -> ct_helper:ignore(cowboy_req, inform, 3), cowboy_req:inform(102, #{<<"set-cookie">> => <<"name=value">>}, Req0); <<"twice">> -> cowboy_req:inform(102, Headers, Req0), cowboy_req:inform(102, Headers, Req0); <<"after_reply">> -> ct_helper:ignore(cowboy_req, inform, 3), Req1 = cowboy_req:reply(200, Req0), cowboy_req:inform(102, Headers, Req1); Status -> cowboy_req:inform(binary_to_integer(Status), Headers, Req0) end, Req = cowboy_req:reply(200, Req0), {ok, Req, Opts}; do(<<"reply2">>, Req0, Opts) -> Req = case cowboy_req:binding(arg, Req0) of <<"binary">> -> cowboy_req:reply(<<"200 GOOD">>, Req0); <<"error">> -> ct_helper:ignore(cowboy_req, reply, 4), cowboy_req:reply(ok, Req0); <<"twice">> -> ct_helper:ignore(cowboy_req, reply, 4), Req1 = cowboy_req:reply(200, Req0), timer:sleep(100), cowboy_req:reply(200, Req1); Status -> cowboy_req:reply(binary_to_integer(Status), Req0) end, {ok, Req, Opts}; do(<<"reply3">>, Req0, Opts) -> Req = case cowboy_req:binding(arg, Req0) of <<"error">> -> ct_helper:ignore(cowboy_req, reply, 4), cowboy_req:reply(200, ok, Req0); <<"set_cookie">> -> ct_helper:ignore(cowboy_req, reply, 4), cowboy_req:reply(200, #{<<"set-cookie">> => <<"name=value">>}, Req0); Status -> cowboy_req:reply(binary_to_integer(Status), #{<<"content-type">> => <<"text/plain">>}, Req0) end, {ok, Req, Opts}; do(<<"reply4">>, Req0, Opts) -> Req = case cowboy_req:binding(arg, Req0) of <<"error">> -> ct_helper:ignore(erlang, iolist_size, 1), cowboy_req:reply(200, #{}, ok, Req0); <<"set_cookie">> -> ct_helper:ignore(cowboy_req, reply, 4), cowboy_req:reply(200, #{<<"set-cookie">> => <<"name=value">>}, <<"OK">>, Req0); <<"204body">> -> ct_helper:ignore(cowboy_req, do_reply_ensure_no_body, 4), cowboy_req:reply(204, #{}, <<"OK">>, Req0); <<"304body">> -> ct_helper:ignore(cowboy_req, do_reply_ensure_no_body, 4), cowboy_req:reply(304, #{}, <<"OK">>, Req0); Status -> cowboy_req:reply(binary_to_integer(Status), #{}, <<"OK">>, Req0) end, {ok, Req, Opts}; do(<<"stream_reply2">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of <<"binary">> -> Req = cowboy_req:stream_reply(<<"200 GOOD">>, Req0), stream_body(Req), {ok, Req, Opts}; <<"error">> -> ct_helper:ignore(cowboy_req, stream_reply, 3), Req = cowboy_req:stream_reply(ok, Req0), stream_body(Req), {ok, Req, Opts}; <<"204">> -> Req = cowboy_req:stream_reply(204, Req0), {ok, Req, Opts}; <<"204body">> -> ct_helper:ignore(cowboy_req, stream_body, 3), Req = cowboy_req:stream_reply(204, Req0), stream_body(Req), {ok, Req, Opts}; <<"304body">> -> ct_helper:ignore(cowboy_req, stream_body, 3), Req = cowboy_req:stream_reply(304, Req0), stream_body(Req), {ok, Req, Opts}; <<"twice">> -> ct_helper:ignore(cowboy_req, stream_reply, 3), Req1 = cowboy_req:stream_reply(200, Req0), timer:sleep(100), %% We will crash here so the body shouldn't be sent. Req = cowboy_req:stream_reply(200, Req1), stream_body(Req), {ok, Req, Opts}; Status -> Req = cowboy_req:stream_reply(binary_to_integer(Status), Req0), stream_body(Req), {ok, Req, Opts} end; do(<<"stream_reply3">>, Req0, Opts) -> Req = case cowboy_req:binding(arg, Req0) of <<"error">> -> ct_helper:ignore(cowboy_req, stream_reply, 3), cowboy_req:stream_reply(200, ok, Req0); <<"set_cookie">> -> ct_helper:ignore(cowboy_req, stream_reply, 3), cowboy_req:stream_reply(200, #{<<"set-cookie">> => <<"name=value">>}, Req0); Status -> cowboy_req:stream_reply(binary_to_integer(Status), #{<<"content-type">> => <<"text/plain">>}, Req0) end, stream_body(Req), {ok, Req, Opts}; do(<<"stream_body">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of <<"fin0">> -> Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<"Hello world!">>, nofin, Req), cowboy_req:stream_body(<<>>, fin, Req), {ok, Req, Opts}; <<"multiple">> -> Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<"Hello ">>, nofin, Req), cowboy_req:stream_body(<<"world">>, nofin, Req), cowboy_req:stream_body(<<"!">>, fin, Req), {ok, Req, Opts}; <<"loop">> -> Req = cowboy_req:stream_reply(200, Req0), _ = [cowboy_req:stream_body(<<0:1000000/unit:8>>, nofin, Req) || _ <- lists:seq(1, 32)], {ok, Req, Opts}; <<"nofin">> -> Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<"Hello world!">>, nofin, Req), {ok, Req, Opts}; <<"sendfile">> -> AppFile = code:where_is_file("cowboy.app"), AppSize = filelib:file_size(AppFile), Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<"Hello ">>, nofin, Req), cowboy_req:stream_body({sendfile, 0, AppSize, AppFile}, nofin, Req), cowboy_req:stream_body(<<" interspersed ">>, nofin, Req), cowboy_req:stream_body({sendfile, 0, AppSize, AppFile}, nofin, Req), cowboy_req:stream_body(<<" world!">>, fin, Req), {ok, Req, Opts}; <<"sendfile_fin">> -> AppFile = code:where_is_file("cowboy.app"), AppSize = filelib:file_size(AppFile), Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<"Hello! ">>, nofin, Req), cowboy_req:stream_body({sendfile, 0, AppSize, AppFile}, fin, Req), {ok, Req, Opts}; <<"spawn">> -> Req = cowboy_req:stream_reply(200, Req0), Parent = self(), Pid = spawn(fun() -> cowboy_req:stream_body(<<"Hello ">>, nofin, Req), cowboy_req:stream_body(<<"world">>, nofin, Req), cowboy_req:stream_body(<<"!">>, fin, Req), Parent ! {self(), ok} end), receive {Pid, ok} -> ok after 5000 -> error(timeout) end, {ok, Req, Opts}; _ -> %% Call stream_body without initiating streaming. cowboy_req:stream_body(<<0:800000>>, fin, Req0), {ok, Req0, Opts} end; do(<<"stream_body_content_length">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of <<"fin0">> -> Req1 = cowboy_req:set_resp_header(<<"content-length">>, <<"12">>, Req0), Req = cowboy_req:stream_reply(200, Req1), cowboy_req:stream_body(<<"Hello world!">>, nofin, Req), cowboy_req:stream_body(<<>>, fin, Req), {ok, Req, Opts}; <<"multiple">> -> Req1 = cowboy_req:set_resp_header(<<"content-length">>, <<"12">>, Req0), Req = cowboy_req:stream_reply(200, Req1), cowboy_req:stream_body(<<"Hello ">>, nofin, Req), cowboy_req:stream_body(<<"world">>, nofin, Req), cowboy_req:stream_body(<<"!">>, fin, Req), {ok, Req, Opts}; <<"nofin">> -> Req1 = cowboy_req:set_resp_header(<<"content-length">>, <<"12">>, Req0), Req = cowboy_req:stream_reply(200, Req1), cowboy_req:stream_body(<<"Hello world!">>, nofin, Req), {ok, Req, Opts}; <<"nofin-error">> -> Req1 = cowboy_req:set_resp_header(<<"content-length">>, <<"12">>, Req0), Req = cowboy_req:stream_reply(200, Req1), cowboy_req:stream_body(<<"Hello">>, nofin, Req), {ok, Req, Opts} end; do(<<"stream_events">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of %%<<"single">> %%<<"list">> <<"single">> -> Req = cowboy_req:stream_reply(200, #{<<"content-type">> => <<"text/event-stream">>}, Req0), cowboy_req:stream_events(#{ event => <<"add_comment">>, data => <<"Comment text.\nWith many lines.">> }, fin, Req), {ok, Req, Opts}; <<"list">> -> Req = cowboy_req:stream_reply(200, #{<<"content-type">> => <<"text/event-stream">>}, Req0), cowboy_req:stream_events([ #{ event => <<"add_comment">>, data => <<"Comment text.\nWith many lines.">> }, #{ comment => <<"Set retry higher\nwith many lines also.">>, retry => 10000 }, #{ id => <<"123">>, event => <<"add_comment">>, data => <<"Closing!">> } ], fin, Req), {ok, Req, Opts}; <<"multiple">> -> Req = cowboy_req:stream_reply(200, #{<<"content-type">> => <<"text/event-stream">>}, Req0), cowboy_req:stream_events(#{ event => <<"add_comment">>, data => <<"Comment text.\nWith many lines.">> }, nofin, Req), cowboy_req:stream_events(#{ comment => <<"Set retry higher\nwith many lines also.">>, retry => 10000 }, nofin, Req), cowboy_req:stream_events(#{ id => <<"123">>, event => <<"add_comment">>, data => <<"Closing!">> }, fin, Req), {ok, Req, Opts} end; do(<<"stream_trailers">>, Req0, Opts) -> case cowboy_req:binding(arg, Req0) of <<"large">> -> Req = cowboy_req:stream_reply(200, #{ <<"trailer">> => <<"grpc-status">> }, Req0), %% The size should be larger than StreamSize and ConnSize cowboy_req:stream_body(<<0:80000000>>, nofin, Req), cowboy_req:stream_trailers(#{ <<"grpc-status">> => <<"0">> }, Req), {ok, Req, Opts}; <<"set_cookie">> -> ct_helper:ignore(cowboy_req, stream_trailers, 2), Req = cowboy_req:stream_reply(200, #{ <<"trailer">> => <<"set-cookie">> }, Req0), cowboy_req:stream_body(<<"Hello world!">>, nofin, Req), cowboy_req:stream_trailers(#{ <<"set-cookie">> => <<"name=value">> }, Req), {ok, Req, Opts}; _ -> Req = cowboy_req:stream_reply(200, #{ <<"trailer">> => <<"grpc-status">> }, Req0), cowboy_req:stream_body(<<"Hello world!">>, nofin, Req), cowboy_req:stream_trailers(#{ <<"grpc-status">> => <<"0">> }, Req), {ok, Req, Opts} end; do(<<"push">>, Req, Opts) -> case cowboy_req:binding(arg, Req) of <<"read_body">> -> cowboy_req:push("/echo/read_body", #{}, Req, #{}); <<"method">> -> cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, #{method => <<"HEAD">>}); <<"origin">> -> cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, #{scheme => <<"ftp">>, host => <<"127.0.0.1">>, port => 21}); <<"qs">> -> cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, #{qs => <<"server=cowboy&version=2.0">>}); <<"after_reply">> -> ct_helper:ignore(cowboy_req, push, 4), Req1 = cowboy_req:reply(200, Req), %% We will crash here so no need to worry about propagating Req1. cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req1); _ -> cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req), %% The text/plain mime is not defined by default, so a 406 will be returned. cowboy_req:push("/static/plain.txt", #{<<"accept">> => <<"text/plain">>}, Req) end, {ok, cowboy_req:reply(200, Req), Opts}. stream_body(Req) -> _ = [cowboy_req:stream_body(<<0:800000>>, nofin, Req) || _ <- lists:seq(1,9)], cowboy_req:stream_body(<<0:800000>>, fin, Req). ================================================ FILE: test/handlers/resp_iolist_body_h.erl ================================================ %% This module sends an iolist with various odd elements in it as a response. -module(resp_iolist_body_h). -dialyzer(no_improper_lists). -export([init/2]). init(Req0, State) -> Req = cowboy_req:reply(200, #{ <<"content-type">> => <<"text/html">> }, [[[[<<>> | <<>> ] | <<>>] | [ <<>> ]], <<"

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

Ut eam dicunt voluptua principes, dicit perfecto mediocrem ad eam. Te suas integre quo. Nec posse atqui omittantur no, ad sea sumo veritus mandamus. Qui facer viris latine et. Cu tation altera quo, illud oporteat est ne.

His in quod noluisse vivendum. Modus etiam alterum et pri. Id quo dolorem indoctum. Elitr signiferumque at cum. Id habeo graeci consetetur qui. Nam ut sumo epicuri.

Assentior voluptatum ex eum. Ea est nemore democritum, ei odio iriure accumsan nam, no veniam voluptua perpetua vim. Vim et volumus denique, ad verear argumentum vim. At idque velit cum, quis illum ponderum eos te. Integre labitur disputando et pri. Te atqui legere adipisci has, no eum erant verear appellantur.

Quod audire abhorreant in est, pro novum partiendo ei, et quot porro pericula cum. Quaestio interesset scribentur cu nec, usu ei tritani eligendi adipiscing. Mea at antiopam dissentias constituam, an eam illud graece, probo habeo minim eam no. Sit aliquam interesset et.

An purto tota equidem his, et nec aliquid splendide, has ut ridens deserunt. Has at omittam appellantur, ei lorem audire gubergren vis. Ei sumo erat comprehensam nam, eam an enim ceteros corpora. Mea ut eirmod eripuit ornatus ceteros.

Ne per causae definitiones, ut veniam vocent cum. Eu torquatos expetendis eam. Volumus delicata neglegentur ne eam. Ut mel ubique facilis fastidii, cum no temporibus adversarium. Mucius scribentur intellegebat quo eu, id luptatum inciderint scribentur nam. Duis propriae in eam, an cum forensibus temporibus. Magna animal necessitatibus et sed, erroribus evertitur an est.

Posse ipsum sapientem at pri, eam ut option vocibus. Cu nullam corpora ius, ne stet splendide est. Meliore ponderum nec ea, quo ea suscipit phaedrum. Per wisi elaboraret ut.

Agam dicta sensibus quo an, vel ipsum veniam graeco cu. An viris aeterno dolorem est, novum diceret gubergren cum ad. Usu menandri patrioque scripserit te, usu an fugit molestiae. Qui tollit appellantur ut, pri solet aperiam facilis te. Integre electram quo et, persequeris consectetuer ne nam.

">>, <<"THIS WILL NEVER EVER LOAD">>], Req0), {ok, Req, State}. ================================================ FILE: test/handlers/rest_hello_h.erl ================================================ %% This module sends a hello world response via a REST handler. -module(rest_hello_h). -export([init/2]). -export([content_types_provided/2]). -export([get_text_plain/2]). init(Req, Opts) -> {cowboy_rest, Req, Opts}. content_types_provided(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. get_text_plain(Req, State) -> {<<"This is REST!">>, Req, State}. ================================================ FILE: test/handlers/send_message_h.erl ================================================ %% This module sends a message to the pid passed in a header. -module(send_message_h). -export([init/2]). init(Req, State) -> Pid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))), Pid ! {Pid, self(), init, Req, State}, {ok, cowboy_req:reply(200, Req), State}. ================================================ FILE: test/handlers/set_options_h.erl ================================================ %% This module sets options dynamically and performs %% some related relevant operation for testing the change. -module(set_options_h). -export([init/2]). init(Req, State) -> set_options(cowboy_req:binding(key, Req), Req, State). set_options(<<"chunked_false">>, Req0, State) -> cowboy_req:cast({set_options, #{chunked => false}}, Req0), Req = cowboy_req:stream_reply(200, Req0), cowboy_req:stream_body(<<0:8000000>>, fin, Req), {ok, Req, State}; set_options(<<"chunked_false_ignored">>, Req0, State) -> cowboy_req:cast({set_options, #{chunked => false}}, Req0), Req = cowboy_req:reply(200, #{}, <<"Hello world!">>, Req0), {ok, Req, State}; set_options(<<"idle_timeout_short">>, Req0, State) -> cowboy_req:cast({set_options, #{idle_timeout => 500}}, Req0), {_, Body, Req} = cowboy_req:read_body(Req0), {ok, cowboy_req:reply(200, #{}, Body, Req), State}; set_options(<<"idle_timeout_long">>, Req0, State) -> cowboy_req:cast({set_options, #{idle_timeout => 60000}}, Req0), {_, Body, Req} = cowboy_req:read_body(Req0), {ok, cowboy_req:reply(200, #{}, Body, Req), State}; set_options(<<"metrics_user_data">>, Req, State) -> cowboy_req:cast({set_options, #{metrics_user_data => #{handler => ?MODULE}}}, Req), {ok, cowboy_req:reply(200, #{}, <<"Hello world!">>, Req), State}. ================================================ FILE: test/handlers/stop_handler_h.erl ================================================ %% This module returns stop based on the query string. %% Success is indicated via a 248 status code in the response. -module(stop_handler_h). -export([init/2]). -export([allowed_methods/2]). -export([allow_missing_post/2]). -export([charsets_provided/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_completed/2]). -export([delete_resource/2]). -export([forbidden/2]). -export([is_authorized/2]). -export([is_conflict/2]). -export([known_methods/2]). -export([languages_provided/2]). -export([malformed_request/2]). -export([moved_permanently/2]). -export([moved_temporarily/2]). -export([multiple_choices/2]). -export([options/2]). -export([previously_existed/2]). -export([range_satisfiable/2]). -export([ranges_provided/2]). -export([rate_limited/2]). -export([resource_exists/2]). -export([service_available/2]). -export([uri_too_long/2]). -export([valid_content_headers/2]). -export([valid_entity_length/2]). -export([accept/2]). -export([provide/2]). -export([provide_range/2]). init(Req, State) -> {cowboy_rest, Req, State}. allowed_methods(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). allow_missing_post(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). charsets_provided(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). content_types_accepted(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). content_types_provided(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). delete_completed(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). delete_resource(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). forbidden(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). is_authorized(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). is_conflict(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). known_methods(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). languages_provided(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). malformed_request(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). moved_permanently(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). moved_temporarily(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). multiple_choices(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). options(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). previously_existed(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). range_satisfiable(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). ranges_provided(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). rate_limited(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). resource_exists(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). service_available(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). uri_too_long(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). valid_content_headers(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). valid_entity_length(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). accept(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). provide(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). provide_range(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). maybe_stop_handler(Req=#{qs := Qs}, State, StateName) -> case atom_to_binary(StateName, latin1) of Qs -> do_stop_handler(Req, State); _ -> do_default(Req, State, StateName) end. %% These are all the methods necessary to reach all callbacks. do_default(Req, State, allowed_methods) -> {[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}; %% We need to accept/provide media types to reach these callbacks. do_default(Req, State, content_types_accepted) -> {[{<<"text/plain">>, accept}], Req, State}; do_default(Req, State, content_types_provided) -> {[{<<"text/plain">>, provide}], Req, State}; %% We need to accept ranges to reach these callbacks. do_default(Req=#{qs := <<"range_satisfiable">>}, State, ranges_provided) -> {[{<<"bytes">>, provide_range}], Req, State}; do_default(Req=#{qs := <<"provide_range">>}, State, ranges_provided) -> {[{<<"bytes">>, provide_range}], Req, State}; %% We need resource_exists to return false to reach these callbacks. do_default(Req=#{qs := <<"allow_missing_post">>}, State, resource_exists) -> {false, Req, State}; do_default(Req=#{qs := <<"moved_permanently">>}, State, resource_exists) -> {false, Req, State}; do_default(Req=#{qs := <<"moved_temporarily">>}, State, resource_exists) -> {false, Req, State}; do_default(Req=#{qs := <<"previously_existed">>}, State, resource_exists) -> {false, Req, State}; %% We need previously_existed to return true to reach these callbacks. do_default(Req=#{qs := <<"moved_permanently">>}, State, previously_existed) -> {true, Req, State}; do_default(Req=#{qs := <<"moved_temporarily">>}, State, previously_existed) -> {true, Req, State}; %% We need the DELETE to suceed to reach this callback. do_default(Req=#{qs := <<"delete_completed">>}, State, delete_resource) -> {true, Req, State}; %% We should never reach these callbacks. do_default(Req, State, accept) -> {false, Req, State}; do_default(Req, State, provide) -> {<<"This is REST!">>, Req, State}; do_default(Req, State, provide_range) -> {<<"This is ranged REST!">>, Req, State}; %% Simulate the callback being missing in any other cases. do_default(_, _, _) -> no_call. do_stop_handler(Req0, State) -> Req = cowboy_req:reply(<<"248 REST handler stopped!">>, #{}, <<>>, Req0), {stop, Req, State}. ================================================ FILE: test/handlers/stream_handler_h.erl ================================================ %% This module behaves differently depending on a specific header. -module(stream_handler_h). -behavior(cowboy_stream). -export([init/3]). -export([data/4]). -export([info/3]). -export([terminate/3]). -export([early_error/5]). %% For switch_protocol. -export([takeover/7]). -record(state, { pid, test }). init(StreamID, Req, Opts) -> Pid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))), Test = binary_to_atom(cowboy_req:header(<<"x-test-case">>, Req), latin1), State = #state{pid=Pid, test=Test}, Pid ! {Pid, self(), init, StreamID, Req, Opts}, {init_commands(StreamID, Req, State), State}. init_commands(_, _, #state{test=crash_in_init}) -> error(crash); init_commands(_, _, #state{test=crash_in_data}) -> []; init_commands(_, _, #state{test=crash_in_info}) -> []; init_commands(_, _, #state{test=crash_in_terminate}) -> [{response, 200, #{<<"content-length">> => <<"12">>}, <<"Hello world!">>}, stop]; init_commands(_, _, #state{test=crash_in_early_error}) -> error(crash); init_commands(_, _, #state{test=flow_after_body_fully_read}) -> []; init_commands(_, _, #state{test=set_options_ignore_unknown}) -> [ {set_options, #{unknown_options => true}}, {response, 200, #{<<"content-length">> => <<"12">>}, <<"Hello world!">>}, stop ]; init_commands(_, _, State=#state{test=shutdown_on_stream_stop}) -> Spawn = init_process(false, State), [{spawn, Spawn, 5000}, {headers, 200, #{}}, stop]; init_commands(_, _, State=#state{test=shutdown_on_socket_close}) -> Spawn = init_process(false, State), [{spawn, Spawn, 5000}, {headers, 200, #{}}]; init_commands(_, _, State=#state{test=shutdown_timeout_on_stream_stop}) -> Spawn = init_process(true, State), [{spawn, Spawn, 2000}, {headers, 200, #{}}, stop]; init_commands(_, _, State=#state{test=shutdown_timeout_on_socket_close}) -> Spawn = init_process(true, State), [{spawn, Spawn, 2000}, {headers, 200, #{}}]; init_commands(_, _, State=#state{test=switch_protocol_after_headers}) -> [{headers, 200, #{}}, {switch_protocol, #{}, ?MODULE, State}]; init_commands(_, _, State=#state{test=switch_protocol_after_headers_data}) -> [{headers, 200, #{}}, {data, fin, <<"{}">>}, {switch_protocol, #{}, ?MODULE, State}]; init_commands(_, _, State=#state{test=switch_protocol_after_response}) -> [{response, 200, #{}, <<"{}">>}, {switch_protocol, #{}, ?MODULE, State}]; init_commands(_, _, State=#state{test=terminate_on_switch_protocol}) -> [{switch_protocol, #{}, ?MODULE, State}]; init_commands(_, _, #state{test=terminate_on_stop}) -> [{response, 204, #{}, <<>>}]; init_commands(_, _, _) -> [{headers, 200, #{}}]. init_process(TrapExit, #state{pid=Pid}) -> Self = self(), Spawn = spawn_link(fun() -> process_flag(trap_exit, TrapExit), Pid ! {Pid, Self, spawned, self()}, receive {Pid, ready} -> ok after 1000 -> error(timeout) end, Self ! {self(), ready}, receive after 5000 -> Pid ! {Pid, Self, still_alive, self()} end end), receive {Spawn, ready} -> ok after 1000 -> error(timeout) end, Spawn. data(_, _, _, #state{test=crash_in_data}) -> error(crash); data(_, fin, <<"Hello world!">>, State=#state{test=flow_after_body_fully_read}) -> {[{flow, 12}, {response, 200, #{}, <<"{}">>}], State}; data(StreamID, IsFin, Data, State=#state{pid=Pid}) -> Pid ! {Pid, self(), data, StreamID, IsFin, Data, State}, {[], State}. info(_, Resp={response, _, _, _}, State) -> {[Resp], State}; info(_, crash, #state{test=crash_in_info}) -> error(crash); info(StreamID, Info, State=#state{pid=Pid}) -> Pid ! {Pid, self(), info, StreamID, Info, State}, case Info of please_stop -> {[stop], State}; _ -> {[Info], State} end. terminate(StreamID, Reason, State=#state{pid=Pid, test=crash_in_terminate}) -> Pid ! {Pid, self(), terminate, StreamID, Reason, State}, error(crash); terminate(StreamID, Reason, State=#state{pid=Pid}) -> Pid ! {Pid, self(), terminate, StreamID, Reason, State}, ok. %% This clause can only test for early errors that reached the required headers. early_error(StreamID, Reason, PartialReq, Resp, Opts) -> Pid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, PartialReq))), Pid ! {Pid, self(), early_error, StreamID, Reason, PartialReq, Resp, Opts}, case cowboy_req:header(<<"x-test-case">>, PartialReq) of <<"crash_in_early_error",_/bits>> -> error(crash); _ -> Resp end. %% @todo It would be good if we could allow this function to return normally. -spec takeover(_, _, _, _, _, _, _) -> no_return(). takeover(Parent, Ref, Socket, Transport, Opts, Buffer, State=#state{pid=Pid}) -> Pid ! {Pid, self(), takeover, Parent, Ref, Socket, Transport, Opts, Buffer, State}, exit(normal). ================================================ FILE: test/handlers/stream_hello_h.erl ================================================ %% This module is the fastest way of producing a Hello world! -module(stream_hello_h). -export([init/3]). -export([terminate/3]). init(_, _, State) -> {[ {response, 200, #{<<"content-length">> => <<"12">>}, <<"Hello world!">>}, stop ], State}. terminate(_, _, _) -> ok. ================================================ FILE: test/handlers/streamed_result_h.erl ================================================ -module(streamed_result_h). -export([init/2]). init(Req, Opts) -> N = list_to_integer(binary_to_list(cowboy_req:binding(n, Req))), Interval = list_to_integer(binary_to_list(cowboy_req:binding(interval, Req))), chunked(N, Interval, Req, Opts). chunked(N, Interval, Req0, Opts) -> Req = cowboy_req:stream_reply(200, Req0), {ok, loop(N, Interval, Req), Opts}. loop(0, _Interval, Req) -> ok = cowboy_req:stream_body("Finished!\n", fin, Req), Req; loop(N, Interval, Req) -> ok = cowboy_req:stream_body(iolist_to_binary([integer_to_list(N), <<"\n">>]), nofin, Req), timer:sleep(Interval), loop(N-1, Interval, Req). ================================================ FILE: test/handlers/switch_handler_h.erl ================================================ %% This module returns switch_handler based on the query string. -module(switch_handler_h). -export([init/2]). -export([allowed_methods/2]). -export([allow_missing_post/2]). -export([charsets_provided/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_completed/2]). -export([delete_resource/2]). -export([forbidden/2]). -export([is_authorized/2]). -export([is_conflict/2]). -export([known_methods/2]). -export([languages_provided/2]). -export([malformed_request/2]). -export([moved_permanently/2]). -export([moved_temporarily/2]). -export([multiple_choices/2]). -export([options/2]). -export([previously_existed/2]). -export([range_satisfiable/2]). -export([ranges_provided/2]). -export([rate_limited/2]). -export([resource_exists/2]). -export([service_available/2]). -export([uri_too_long/2]). -export([valid_content_headers/2]). -export([valid_entity_length/2]). -export([accept/2]). -export([provide/2]). -export([provide_range/2]). -export([info/3]). init(Req, State) -> {cowboy_rest, Req, State}. allowed_methods(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). allow_missing_post(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). charsets_provided(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). content_types_accepted(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). content_types_provided(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). delete_completed(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). delete_resource(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). forbidden(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). is_authorized(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). is_conflict(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). known_methods(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). languages_provided(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). malformed_request(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). moved_permanently(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). moved_temporarily(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). multiple_choices(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). options(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). previously_existed(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). range_satisfiable(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). ranges_provided(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). rate_limited(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). resource_exists(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). service_available(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). uri_too_long(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). valid_content_headers(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). valid_entity_length(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). accept(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). provide(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). provide_range(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). maybe_switch_handler(Req=#{qs := Qs}, State, StateName) -> case atom_to_binary(StateName, latin1) of Qs -> do_switch_handler(Req, State); _ -> do_default(Req, State, StateName) end. %% These are all the methods necessary to reach all callbacks. do_default(Req, State, allowed_methods) -> {[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}; %% We need to accept/provide media types to reach these callbacks. do_default(Req, State, content_types_accepted) -> {[{<<"text/plain">>, accept}], Req, State}; do_default(Req, State, content_types_provided) -> {[{<<"text/plain">>, provide}], Req, State}; %% We need to accept ranges to reach these callbacks. do_default(Req=#{qs := <<"range_satisfiable">>}, State, ranges_provided) -> {[{<<"bytes">>, provide_range}], Req, State}; do_default(Req=#{qs := <<"provide_range">>}, State, ranges_provided) -> {[{<<"bytes">>, provide_range}], Req, State}; %% We need resource_exists to return false to reach these callbacks. do_default(Req=#{qs := <<"allow_missing_post">>}, State, resource_exists) -> {false, Req, State}; do_default(Req=#{qs := <<"moved_permanently">>}, State, resource_exists) -> {false, Req, State}; do_default(Req=#{qs := <<"moved_temporarily">>}, State, resource_exists) -> {false, Req, State}; do_default(Req=#{qs := <<"previously_existed">>}, State, resource_exists) -> {false, Req, State}; %% We need previously_existed to return true to reach these callbacks. do_default(Req=#{qs := <<"moved_permanently">>}, State, previously_existed) -> {true, Req, State}; do_default(Req=#{qs := <<"moved_temporarily">>}, State, previously_existed) -> {true, Req, State}; %% We need the DELETE to suceed to reach this callback. do_default(Req=#{qs := <<"delete_completed">>}, State, delete_resource) -> {true, Req, State}; %% We should never reach these callbacks. do_default(Req, State, accept) -> {false, Req, State}; do_default(Req, State, provide) -> {<<"This is REST!">>, Req, State}; do_default(Req, State, provide_range) -> {<<"This is ranged REST!">>, Req, State}; %% Simulate the callback being missing in any other cases. do_default(_, _, _) -> no_call. do_switch_handler(Req0, run) -> Req = cowboy_req:stream_reply(200, Req0), send_after(0), {{switch_handler, cowboy_loop}, Req, 0}; do_switch_handler(Req0, hibernate) -> Req = cowboy_req:stream_reply(200, Req0), send_after(0), {{switch_handler, cowboy_loop, hibernate}, Req, 0}. send_after(N) -> erlang:send_after(100, self(), {stream, msg(N)}). msg(0) -> <<"Hello\n">>; msg(1) -> <<"streamed\n">>; msg(2) -> <<"world!\n">>; msg(3) -> stop. info({stream, stop}, Req, State) -> {stop, Req, State}; info({stream, What}, Req, State) -> cowboy_req:stream_body(What, nofin, Req), send_after(State + 1), {ok, Req, State + 1}. ================================================ FILE: test/handlers/switch_protocol_flush_h.erl ================================================ %% This module is used to test the flushing of messages when %% switch_protocol is executed by cowboy_http. -module(switch_protocol_flush_h). -export([init/3]). -export([info/3]). -export([terminate/3]). -export([takeover/7]). -export([validate/1]). init(StreamID, Req, _) -> Pid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))), %% Send ourselves a few messages that may or may not be flushed. self() ! good, self() ! {'EXIT', Pid, normal}, self() ! {system, a, b}, self() ! {{self(), StreamID}, hello}, self() ! {'$gen_call', a, b}, self() ! {timeout, make_ref(), ?MODULE}, self() ! {ranch_tcp, socket, <<"123">>}, {[{switch_protocol, #{}, ?MODULE, Pid}], undefined}. info(_, _, State) -> {[], State}. terminate(_, _, _) -> ok. %% @todo It would be good if we could allow this function to return normally. -spec takeover(_, _, _, _, _, _, _) -> no_return(). takeover(_, _, _, _, _, _, Pid) -> Msgs = receive_all([]), Pid ! {Pid, Msgs}, exit(normal). receive_all(Acc) -> receive Msg -> receive_all([Msg|Acc]) after 0 -> Acc end. validate(Msgs) -> [ {ranch_tcp, socket, <<"123">>}, {'$gen_call', a, b}, {system, a, b}, good ] = Msgs, ok. ================================================ FILE: test/handlers/ws_active_commands_h.erl ================================================ %% This module starts with active mode disabled %% and enables it again once a timeout is triggered. -module(ws_active_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> {cowboy_websocket, Req, maps:get(run_or_hibernate, Opts), Opts}. websocket_init(State=run) -> erlang:send_after(1500, self(), active_true), {[{active, false}], State}; websocket_init(State=hibernate) -> erlang:send_after(1500, self(), active_true), {[{active, false}], State, hibernate}. websocket_handle(Frame, State=run) -> {[Frame], State}; websocket_handle(Frame, State=hibernate) -> {[Frame], State, hibernate}. websocket_info(active_true, State=run) -> {[{active, true}], State}; websocket_info(active_true, State=hibernate) -> {[{active, true}], State, hibernate}. ================================================ FILE: test/handlers/ws_deflate_commands_h.erl ================================================ %% This module enables/disables compression %% every time it echoes a frame. -module(ws_deflate_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> DataDelivery = maps:get(data_delivery, Opts, stream_handlers), {cowboy_websocket, Req, #{deflate => true, hibernate => maps:get(run_or_hibernate, Opts)}, #{compress => true, data_delivery => DataDelivery}}. websocket_handle(Frame, State=#{deflate := Deflate0, hibernate := run}) -> Deflate = not Deflate0, {[Frame, {deflate, Deflate}], State#{deflate => Deflate}}; websocket_handle(Frame, State=#{deflate := Deflate0, hibernate := hibernate}) -> Deflate = not Deflate0, {[Frame, {deflate, Deflate}], State#{deflate => Deflate}, hibernate}. websocket_info(_Info, State) -> {[], State}. ================================================ FILE: test/handlers/ws_deflate_opts_h.erl ================================================ %% This module enables compression and returns deflate %% options depending on the query string. -module(ws_deflate_opts_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req=#{qs := Qs}, State) -> {Name, Value} = case Qs of <<"server_context_takeover">> -> {server_context_takeover, takeover}; <<"server_no_context_takeover">> -> {server_context_takeover, no_takeover}; <<"client_context_takeover">> -> {client_context_takeover, takeover}; <<"client_no_context_takeover">> -> {client_context_takeover, no_takeover}; <<"server_max_window_bits">> -> {server_max_window_bits, 9}; <<"client_max_window_bits">> -> {client_max_window_bits, 9}; <<"level">> -> {level, best_speed}; <<"mem_level">> -> {mem_level, 1}; <<"strategy">> -> {strategy, rle} end, {cowboy_websocket, Req, State, #{ compress => true, deflate_opts => #{Name => Value} }}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}; websocket_handle(_, State) -> {[], State}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_dont_validate_utf8_h.erl ================================================ %% This module disables UTF-8 validation. -module(ws_dont_validate_utf8_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, State) -> {cowboy_websocket, Req, State, #{ validate_utf8 => false }}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}; websocket_handle(_, State) -> {[], State}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_handle_commands_h.erl ================================================ %% This module takes commands from the x-commands header %% and returns them in the websocket_handle/2 callback. -module(ws_handle_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> Commands0 = cowboy_req:header(<<"x-commands">>, Req), Commands = binary_to_term(base64:decode(Commands0)), case Commands of bad -> Pid = case Req of #{version := 'HTTP/2'} -> self(); #{pid := Pid0} -> Pid0 end, ct_helper_error_h:ignore(Pid, cowboy_websocket, handler_call, 6); _ -> ok end, {cowboy_websocket, Req, {Commands, maps:get(run_or_hibernate, Opts)}, Opts}. websocket_init(State) -> {[], State}. websocket_handle(_, State={Commands, run}) -> {Commands, State}; websocket_handle(_, State={Commands, hibernate}) -> {Commands, State, hibernate}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_ignore.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_ignore). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, _) -> {cowboy_websocket, Req, undefined, #{ data_delivery => relay, compress => true }}. websocket_handle({text, <<"CHECK">>}, State) -> {[{text, <<"CHECK">>}], State}; websocket_handle(_Frame, State) -> {[], State}. websocket_info(_Info, State) -> {[], State}. ================================================ FILE: test/handlers/ws_info_commands_h.erl ================================================ %% This module takes commands from the x-commands header %% and returns them in the websocket_info/2 callback. %% This callback is triggered via a message. -module(ws_info_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> Commands0 = cowboy_req:header(<<"x-commands">>, Req), Commands = binary_to_term(base64:decode(Commands0)), case Commands of bad -> Pid = case Req of #{version := 'HTTP/2'} -> self(); #{pid := Pid0} -> Pid0 end, ct_helper_error_h:ignore(Pid, cowboy_websocket, handler_call, 6); _ -> ok end, {cowboy_websocket, Req, {Commands, maps:get(run_or_hibernate, Opts)}, Opts}. websocket_init(State) -> self() ! shoot, {[], State}. websocket_handle(_, State) -> {[], State}. websocket_info(_, State={Commands, run}) -> {Commands, State}; websocket_info(_, State={Commands, hibernate}) -> {Commands, State, hibernate}. ================================================ FILE: test/handlers/ws_init_commands_h.erl ================================================ %% This module takes commands from the x-commands header %% and returns them in the websocket_init/1 callback. -module(ws_init_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> Commands0 = cowboy_req:header(<<"x-commands">>, Req), Commands = binary_to_term(base64:decode(Commands0)), case Commands of bad -> Pid = case Req of #{version := 'HTTP/2'} -> self(); #{pid := Pid0} -> Pid0 end, ct_helper_error_h:ignore(Pid, cowboy_websocket, handler_call, 6); _ -> ok end, {cowboy_websocket, Req, {Commands, maps:get(run_or_hibernate, Opts)}, Opts}. websocket_init(State={Commands, run}) -> {Commands, State}; websocket_init(State={Commands, hibernate}) -> {Commands, State, hibernate}. websocket_handle(_, State) -> {[], State}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_init_h.erl ================================================ %% This module returns a different value in websocket_init/1 depending on the query string. -module(ws_init_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> State = binary_to_atom(cowboy_req:qs(Req), latin1), {cowboy_websocket, Req, State, Opts}. %% Sleep to make sure the HTTP response was sent. websocket_init(State) -> timer:sleep(100), do_websocket_init(State). do_websocket_init(State=ok) -> {[], State}; do_websocket_init(State=ok_hibernate) -> {[], State, hibernate}; do_websocket_init(State=reply) -> {[{text, "Hello"}], State}; do_websocket_init(State=reply_hibernate) -> {[{text, "Hello"}], State, hibernate}; do_websocket_init(State=reply_close) -> {[close], State}; do_websocket_init(State=reply_close_hibernate) -> {[close], State, hibernate}; do_websocket_init(State=reply_many) -> {[{text, "Hello"}, {binary, "World"}], State}; do_websocket_init(State=reply_many_hibernate) -> {[{text, "Hello"}, {binary, "World"}], State, hibernate}; do_websocket_init(State=reply_many_close) -> {[{text, "Hello"}, close], State}; do_websocket_init(State=reply_many_close_hibernate) -> {[{text, "Hello"}, close], State, hibernate}; do_websocket_init(State=reply_trap_exit) -> Text = "trap_exit: " ++ atom_to_list(element(2, process_info(self(), trap_exit))), {[{text, Text}, close], State, hibernate}. websocket_handle(_, State) -> {[], State}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_ping_h.erl ================================================ %% This module sends an empty ping to the client and %% waits for a pong before sending a text frame. It %% is used to confirm server-initiated pings work. -module(ws_ping_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, _) -> {cowboy_websocket, Req, undefined}. websocket_init(State) -> {[{ping, <<>>}], State}. websocket_handle(pong, State) -> {[{text, <<"OK!!">>}], State}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_set_options_commands_h.erl ================================================ %% This module sets options based on the frame received. -module(ws_set_options_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> DataDelivery = maps:get(data_delivery, Opts, stream_handlers), {cowboy_websocket, Req, maps:get(run_or_hibernate, Opts), #{idle_timeout => infinity, data_delivery => DataDelivery}}. %% Set the idle_timeout option dynamically. websocket_handle({text, <<"idle_timeout_short">>}, State=run) -> {[{set_options, #{idle_timeout => 500}}], State}; websocket_handle({text, <<"idle_timeout_short">>}, State=hibernate) -> {[{set_options, #{idle_timeout => 500}}], State, hibernate}; %% Set the max_frame_size option dynamically. websocket_handle({text, <<"max_frame_size_small">>}, State=run) -> {[{set_options, #{max_frame_size => 1000}}], State}; websocket_handle({text, <<"max_frame_size_small">>}, State=hibernate) -> {[{set_options, #{max_frame_size => 1000}}], State, hibernate}; %% We just echo binary frames. websocket_handle(Frame={binary, _}, State=run) -> {[Frame], State}; websocket_handle(Frame={binary, _}, State=hibernate) -> {[Frame], State, hibernate}. websocket_info(_Info, State) -> {[], State}. ================================================ FILE: test/handlers/ws_shutdown_reason_commands_h.erl ================================================ %% This module sends the process pid to the test pid %% found in the x-test-pid header, then changes the %% shutdown reason and closes the connection normally. -module(ws_shutdown_reason_commands_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> TestPid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))), {cowboy_websocket, Req, {TestPid, maps:get(run_or_hibernate, Opts)}, Opts}. websocket_init(State={TestPid, RunOrHibernate}) -> TestPid ! {ws_pid, self()}, ShutdownReason = receive {TestPid, SR} -> SR after 1000 -> error(timeout) end, Commands = [ {shutdown_reason, ShutdownReason}, close ], case RunOrHibernate of run -> {Commands, State}; hibernate -> {Commands, State, hibernate} end. websocket_handle(_, State) -> {[], State}. websocket_info(_, State) -> {[], State}. ================================================ FILE: test/handlers/ws_terminate_h.erl ================================================ %% This module sends a message with terminate arguments to the test case process. -module(ws_terminate_h). -behavior(cowboy_websocket). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). -export([terminate/3]). -record(state, { pid }). init(Req, _) -> Pid = list_to_pid(binary_to_list(cowboy_req:header(<<"x-test-pid">>, Req))), Opts = case cowboy_req:qs(Req) of <<"req_filter">> -> #{req_filter => fun(_) -> filtered end}; _ -> #{} end, {cowboy_websocket, Req, #state{pid=Pid}, Opts}. websocket_init(State=#state{pid=Pid}) -> Pid ! {ws_pid, self()}, %% We must trap 'EXIT' signals for HTTP/2 to call terminate/3. process_flag(trap_exit, true), {ok, State}. websocket_handle(_, State) -> {ok, State}. websocket_info(_, State) -> {ok, State}. terminate(Reason, Req, #state{pid=Pid}) -> Pid ! {terminate, Reason, Req}, ok. ================================================ FILE: test/handlers/wt_echo_h.erl ================================================ %% This module echoes client events back, %% including creating new streams. -module(wt_echo_h). -behavior(cowboy_webtransport). -export([init/2]). -export([webtransport_handle/2]). -export([webtransport_info/2]). -export([terminate/3]). %% -define(DEBUG, 1). -ifdef(DEBUG). -define(LOG(Fmt, Args), ct:pal(Fmt, Args)). -else. -define(LOG(Fmt, Args), _ = Fmt, _ = Args, ok). -endif. init(Req0, _) -> ?LOG("WT init ~p~n", [Req0]), Req = case cowboy_req:parse_header(<<"wt-available-protocols">>, Req0) of undefined -> Req0; [Protocol|_] -> cowboy_req:set_resp_header(<<"wt-protocol">>, cow_http_hd:wt_protocol(Protocol), Req0) end, {cowboy_webtransport, Req, #{}}. webtransport_handle(Event = {stream_open, StreamID, bidi}, Streams) -> ?LOG("WT handle ~p~n", [Event]), {[], Streams#{StreamID => bidi}}; webtransport_handle(Event = {stream_open, StreamID, unidi}, Streams) -> ?LOG("WT handle ~p~n", [Event]), OpenStreamRef = make_ref(), {[{open_stream, OpenStreamRef, unidi, <<>>}], Streams#{ StreamID => {unidi_remote, OpenStreamRef}, OpenStreamRef => {unidi_local, StreamID}}}; webtransport_handle(Event = {opened_stream_id, OpenStreamRef, OpenStreamID}, Streams) -> ?LOG("WT handle ~p~n", [Event]), case Streams of #{OpenStreamRef := bidi} -> {[], maps:remove(OpenStreamRef, Streams#{ OpenStreamID => bidi })}; #{OpenStreamRef := {unidi_local, RemoteStreamID}} -> #{RemoteStreamID := {unidi_remote, OpenStreamRef}} = Streams, {[], maps:remove(OpenStreamRef, Streams#{ RemoteStreamID => {unidi_remote, OpenStreamID}, OpenStreamID => {unidi_local, RemoteStreamID} })} end; webtransport_handle(Event = {stream_data, StreamID, _IsFin, <<"TEST:", Test/bits>>}, Streams) -> ?LOG("WT handle ~p~n", [Event]), case Test of <<"open_bidi">> -> OpenStreamRef = make_ref(), {[{open_stream, OpenStreamRef, bidi, <<>>}], Streams#{OpenStreamRef => bidi}}; <<"initiate_close">> -> {[initiate_close], Streams}; <<"close">> -> {[close], Streams}; <<"close_app_code">> -> {[{close, 1234567890}], Streams}; <<"close_app_code_msg">> -> {[{close, 1234567890, <<"onetwothreefourfivesixseveneightnineten">>}], Streams}; <<"event_pid:", EventPidBin/bits>> -> {[{send, StreamID, nofin, <<"event_pid_received">>}], Streams#{event_pid => binary_to_term(EventPidBin)}} end; webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) -> ?LOG("WT handle ~p~n", [Event]), case Streams of #{StreamID := bidi} -> {[{send, StreamID, IsFin, Data}], Streams}; #{StreamID := {unidi_remote, Ref}} when is_reference(Ref) -> %% The stream isn't ready. We try again later. erlang:send_after(100, self(), {try_again, Event}), {[], Streams}; #{StreamID := {unidi_remote, LocalStreamID}} -> {[{send, LocalStreamID, IsFin, Data}], Streams} end; webtransport_handle(Event = {datagram, Data}, Streams) -> ?LOG("WT handle ~p~n", [Event]), {[{send, datagram, Data}], Streams}; webtransport_handle(Event = close_initiated, Streams) -> ?LOG("WT handle ~p~n", [Event]), {[{send, datagram, <<"TEST:close_initiated">>}], Streams}; webtransport_handle(Event, Streams) -> ?LOG("WT handle ignore ~p~n", [Event]), {[], Streams}. webtransport_info({try_again, Event}, Streams) -> ?LOG("WT try_again ~p", [Event]), webtransport_handle(Event, Streams). terminate(Reason, Req, State=#{event_pid := EventPid}) -> ?LOG("WT terminate ~0p~n~0p~n~0p", [Reason, Req, State]), EventPid ! {'$wt_echo_h', terminate, Reason, Req, State}, ok; terminate(Reason, Req, State) -> ?LOG("WT terminate ~0p~n~0p~n~0p", [Reason, Req, State]), ok. ================================================ FILE: test/http2_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(http2_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(ct_helper, [get_remote_pid_tcp/1]). -import(cowboy_test, [gun_open/1]). all() -> [{group, clear}]. groups() -> [{clear, [parallel], ct_helper:all(?MODULE)}]. init_dispatch(_) -> cowboy_router:compile([{"localhost", [ {"/", hello_h, []}, {"/echo/:key", echo_h, []}, {"/resp_iolist_body", resp_iolist_body_h, []}, {"/streamed_result/:n/:interval", streamed_result_h, []} ]}]). %% Do a prior knowledge handshake (function originally copied from rfc7540_SUITE). do_handshake(Config) -> do_handshake(#{}, Config). do_handshake(Settings, Config) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}|proplists:get_value(tcp_opts, Config, [])]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(Settings)]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, Socket}. hibernate(Config) -> doc("Ensure that we can enable hibernation for HTTP/1.1 connections."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, hibernate => true }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), {ok, http2} = gun:await_up(ConnPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), StreamRef3 = gun:get(ConnPid, "/"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout(Config) -> doc("Terminate when the idle timeout is reached."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => 1000 }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake([{port, Port}|Config]), timer:sleep(1000), %% Receive a GOAWAY frame back with NO_ERROR. {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_infinity(Config) -> doc("Ensure the idle_timeout option accepts the infinity value."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => infinity }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake([{port, Port}|Config]), timer:sleep(1000), %% Don't receive a GOAWAY frame. {error, timeout} = gen_tcp:recv(Socket, 17, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_reset_on_data(Config) -> doc("Terminate when the idle timeout is reached."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => 1000 }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake([{port, Port}|Config]), %% We wait a little, send a PING, receive a PING ack. {error, timeout} = gen_tcp:recv(Socket, 17, 500), ok = gen_tcp:send(Socket, cow_http2:ping(0)), {ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000), %% Again. {error, timeout} = gen_tcp:recv(Socket, 17, 500), ok = gen_tcp:send(Socket, cow_http2:ping(0)), {ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000), %% And one more time. {error, timeout} = gen_tcp:recv(Socket, 17, 500), ok = gen_tcp:send(Socket, cow_http2:ping(0)), {ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000), %% The connection goes away soon after we stop sending data. timer:sleep(1000), {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_on_send(Config) -> doc("Ensure the idle timeout is not reset when sending (by default)."), http_SUITE:do_idle_timeout_on_send(Config, http2). idle_timeout_reset_on_send(Config) -> doc("Ensure the reset_idle_timeout_on_send results in the " "idle timeout resetting when sending ."), http_SUITE:do_idle_timeout_reset_on_send(Config, http2). inactivity_timeout(Config) -> doc("Terminate when the inactivity timeout is reached."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, inactivity_timeout => 1000 }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake([{port, Port}|Config]), receive after 1000 -> ok end, %% Receive a GOAWAY frame back with an INTERNAL_ERROR. {ok, << _:24, 7:8, _:72, 2:32 >>} = gen_tcp:recv(Socket, 17, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. initial_connection_window_size(Config) -> doc("Confirm a WINDOW_UPDATE frame is sent when the configured " "connection window is larger than the default."), ConfiguredSize = 100000, ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, initial_connection_window_size => ConfiguredSize }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Receive a WINDOW_UPDATE frame incrementing the connection window to 100000. {ok, <<4:24, 8:8, 0:41, Size:31>>} = gen_tcp:recv(Socket, 13, 1000), ConfiguredSize = Size + 65535, gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. max_frame_size_sent(Config) -> doc("Confirm that frames sent by Cowboy are limited in size " "by the max_frame_size_sent configuration value."), MaxFrameSize = 20000, ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, max_frame_size_sent => MaxFrameSize }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake(#{max_frame_size => MaxFrameSize + 10000}, [{port, Port}|Config]), %% Send a request with a 30000 bytes body. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:16384/unit:8>>), cow_http2:data(1, fin, <<0:13616/unit:8>>) ]), %% Receive a HEADERS frame as a response. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = case gen_tcp:recv(Socket, 9, 1000) of %% We received a WINDOW_UPDATE first. Skip it and the next. {ok, <<4:24, 8:8, 0:40>>} -> {ok, _} = gen_tcp:recv(Socket, 4 + 13, 1000), gen_tcp:recv(Socket, 9, 1000); Res -> Res end, {ok, _} = gen_tcp:recv(Socket, SkipLen, 6000), %% The DATA frames following must have lengths of 20000 %% and then 10000 due to the limit. {ok, <<20000:24, 0:8, _:40, _:20000/unit:8>>} = gen_tcp:recv(Socket, 20009, 6000), {ok, <<10000:24, 0:8, _:40, _:10000/unit:8>>} = gen_tcp:recv(Socket, 10009, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. persistent_term_router(Config) -> doc("The router can retrieve the routes from persistent_term storage."), case erlang:function_exported(persistent_term, get, 1) of true -> do_persistent_term_router(Config); false -> {skip, "This test uses the persistent_term functionality added in Erlang/OTP 21.2."} end. do_persistent_term_router(Config) -> persistent_term:put(?FUNCTION_NAME, init_dispatch(Config)), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => {persistent_term, ?FUNCTION_NAME}} }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), {ok, http2} = gun:await_up(ConnPid), StreamRef = gun:get(ConnPid, "/"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. preface_timeout_infinity(Config) -> doc("Ensure infinity for preface_timeout is accepted."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, preface_timeout => infinity }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake([{port, Port}|Config]), Pid = get_remote_pid_tcp(Socket), Ref = erlang:monitor(process, Pid), receive {'DOWN', Ref, process, Pid, Reason} -> error(Reason) after 1000 -> gen_tcp:close(Socket) end after cowboy:stop_listener(?FUNCTION_NAME) end. resp_iolist_body(Config) -> doc("Regression test when response bodies are iolists that " "include improper lists, empty lists and empty binaries. " "The original issue failed to split the body into frames properly."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)} }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), Ref = gun:get(ConnPid, "/resp_iolist_body"), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref), {_, BinLen} = lists:keyfind(<<"content-length">>, 1, RespHeaders), Len = binary_to_integer(BinLen), {ok, RespBody} = gun:await_body(ConnPid, Ref), Len = iolist_size(RespBody), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_timeout_infinity(Config) -> doc("Ensure infinity for settings_timeout is accepted."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, settings_timeout => infinity }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = do_handshake([{port, Port}|Config]), Pid = get_remote_pid_tcp(Socket), Ref = erlang:monitor(process, Pid), receive {'DOWN', Ref, process, Pid, Reason} -> error(Reason) after 1000 -> gen_tcp:close(Socket) end after cowboy:stop_listener(?FUNCTION_NAME) end. graceful_shutdown_connection(Config) -> doc("Check that ongoing requests are handled before gracefully shutting down a connection."), Dispatch = cowboy_router:compile([{"localhost", [ {"/delay_hello", delay_hello_h, #{delay => 500, notify_received => self()}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch} }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), Ref = gun:get(ConnPid, "/delay_hello"), %% Make sure the request is received. receive {request_received, <<"/delay_hello">>} -> ok end, %% Tell the connection to shutdown while the handler is working. [CowboyConnPid] = ranch:procs(?FUNCTION_NAME, connections), monitor(process, CowboyConnPid), ok = sys:terminate(CowboyConnPid, goaway), %% Check that the response is sent to the client before the %% connection goes down. {response, nofin, 200, _RespHeaders} = gun:await(ConnPid, Ref), {ok, RespBody} = gun:await_body(ConnPid, Ref), <<"Hello world!">> = iolist_to_binary(RespBody), %% Check that the connection is gone soon afterwards. (The exit %% reason is supposed to be 'goaway' as passed to %% sys:terminate/2, but it is {shutdown, closed}.) receive {'DOWN', _, process, CowboyConnPid, _Reason} -> ok end, [] = ranch:procs(?FUNCTION_NAME, connections), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. graceful_shutdown_timeout(Config) -> doc("Check that a connection is closed when gracefully shutting down times out."), Dispatch = cowboy_router:compile([{"localhost", [ {"/long_delay_hello", delay_hello_h, #{delay => 10000, notify_received => self()}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch}, goaway_initial_timeout => 200, goaway_complete_timeout => 500 }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), Ref = gun:get(ConnPid, "/long_delay_hello"), %% Make sure the request is received. receive {request_received, <<"/long_delay_hello">>} -> ok end, %% Tell the connection to shutdown while the handler is working. [CowboyConnPid] = ranch:procs(?FUNCTION_NAME, connections), monitor(process, CowboyConnPid), ok = sys:terminate(CowboyConnPid, goaway), %% Check that connection didn't wait for the slow handler. {error, {stream_error, closed}} = gun:await(ConnPid, Ref), %% Check that the connection is gone. (The exit reason is %% supposed to be 'goaway' as passed to sys:terminate/2, but it %% is {shutdown, {stop, {exit, goaway}, 'Graceful shutdown timed %% out.'}}.) receive {'DOWN', _, process, CowboyConnPid, _Reason} -> ok after 100 -> error(still_alive) end, [] = ranch:procs(?FUNCTION_NAME, connections), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. graceful_shutdown_listener(Config) -> doc("Check that connections are shut down gracefully when stopping a listener."), TransOpts = #{ socket_opts => [{port, 0}], shutdown => 1000 %% Shorter timeout to make the test case faster. }, Dispatch = cowboy_router:compile([{"localhost", [ {"/delay_hello", delay_hello_h, #{delay => 500, notify_received => self()}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch} }, {ok, Listener} = cowboy:start_clear(?FUNCTION_NAME, TransOpts, ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), Ref = gun:get(ConnPid, "/delay_hello"), %% Shutdown listener while the handlers are working. receive {request_received, <<"/delay_hello">>} -> ok end, ListenerMonitorRef = monitor(process, Listener), %% Note: This call does not complete quickly and will %% prevent other cowboy:stop_listener/1 calls to complete. ok = cowboy:stop_listener(?FUNCTION_NAME), receive {'DOWN', ListenerMonitorRef, process, Listener, _Reason} -> ok end, %% Check that the request is handled before shutting down. {response, nofin, 200, _RespHeaders} = gun:await(ConnPid, Ref), {ok, RespBody} = gun:await_body(ConnPid, Ref), <<"Hello world!">> = iolist_to_binary(RespBody), gun:close(ConnPid). graceful_shutdown_listener_timeout(Config) -> doc("Check that connections are shut down when gracefully stopping a listener times out."), TransOpts = #{ socket_opts => [{port, 0}], shutdown => 1000 %% Shorter timeout to make the test case faster. }, Dispatch = cowboy_router:compile([{"localhost", [ {"/long_delay_hello", delay_hello_h, #{delay => 10000, notify_received => self()}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch}, goaway_initial_timeout => 200, goaway_complete_timeout => 500 }, {ok, Listener} = cowboy:start_clear(?FUNCTION_NAME, TransOpts, ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]), Ref = gun:get(ConnPid, "/long_delay_hello"), %% Shutdown listener while the handlers are working. receive {request_received, <<"/long_delay_hello">>} -> ok end, ListenerMonitorRef = monitor(process, Listener), %% Note: This call does not complete quickly and will %% prevent other cowboy:stop_listener/1 calls to complete. ok = cowboy:stop_listener(?FUNCTION_NAME), receive {'DOWN', ListenerMonitorRef, process, Listener, _Reason} -> ok end, %% Check that the slow request is aborted. {error, {stream_error, closed}} = gun:await(ConnPid, Ref), gun:close(ConnPid). send_timeout_close(Config) -> doc("Check that connections are closed on send timeout."), TransOpts = #{ socket_opts => [ {port, 0}, {send_timeout, 100}, {send_timeout_close, true}, {sndbuf, 10} ] }, Dispatch = cowboy_router:compile([{"localhost", [ {"/endless", loop_handler_endless_h, #{delay => 100}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch}, idle_timeout => infinity }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, TransOpts, ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try %% Connect a client that sends a request and waits indefinitely. {ok, ClientSocket} = do_handshake([{port, Port}, {tcp_opts, [{recbuf, 10}, {buffer, 10}, {active, false}]}|Config]), {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/endless">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), ok = gen_tcp:send(ClientSocket, [ cow_http2:headers(1, fin, HeadersBlock), %% Greatly increase the window to make sure we don't run %% out of space before we get send timeouts. cow_http2:window_update(10000000), cow_http2:window_update(1, 10000000) ]), %% Wait for the handler to start then get its pid, %% the remote connection's pid and socket. StreamPid = receive {Self, StreamPid0, init} when Self =:= self() -> StreamPid0 after 1000 -> error(timeout) end, ServerPid = ct_helper:get_remote_pid_tcp(ClientSocket), {links, ServerLinks} = process_info(ServerPid, links), [ServerSocket] = [PidOrPort || PidOrPort <- ServerLinks, is_port(PidOrPort)], %% Poll the socket repeatedly until it is closed by the server. WaitClosedFun = fun F(T) when T =< 0 -> error({status, prim_inet:getstatus(ServerSocket)}); F(T) -> Snooze = 100, case inet:sockname(ServerSocket) of {error, _} -> timer:sleep(Snooze); {ok, _} -> timer:sleep(Snooze), F(T - Snooze) end end, ok = WaitClosedFun(2000), false = erlang:is_process_alive(StreamPid), false = erlang:is_process_alive(ServerPid), gen_tcp:close(ClientSocket) after cowboy:stop_listener(?FUNCTION_NAME) end. ================================================ FILE: test/http_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(http_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(ct_helper, [get_remote_pid_tcp/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_down/1]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv_rest/3]). -import(cowboy_test, [raw_recv/3]). -import(cowboy_test, [raw_expect_recv/2]). all() -> [{group, clear_no_parallel}, {group, clear}]. groups() -> [ %% cowboy:stop_listener can be slow when called many times %% in parallel so we must run this test separately from the others. {clear_no_parallel, [], [graceful_shutdown_listener]}, {clear, [parallel], ct_helper:all(?MODULE) -- [graceful_shutdown_listener]} ]. init_per_group(Name, Config) -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch(Config)} }, Config). end_per_group(Name, _) -> cowboy:stop_listener(Name). init_dispatch(_) -> cowboy_router:compile([{"localhost", [ {"/", hello_h, []}, {"/delay_hello", delay_hello_h, #{delay => 1000, notify_received => self()}}, {"/echo/:key", echo_h, []}, {"/resp/:key[/:arg]", resp_h, []}, {"/set_options/:key", set_options_h, []}, {"/streamed_result/:n/:interval", streamed_result_h, []} ]}]). chunked_false(Config) -> doc("Confirm the option chunked => false disables chunked " "transfer-encoding for HTTP/1.1 connections."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, chunked => false }), Port = ranch:get_port(?FUNCTION_NAME), try Request = "GET /resp/stream_reply2/200 HTTP/1.1\r\nhost: localhost\r\n\r\n", Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]), ok = raw_send(Client, Request), Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> %% Cowboy always advertises itself as HTTP/1.1. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {Headers, Rest1} = cow_http:parse_headers(Rest0), false = lists:keyfind(<<"content-length">>, 1, Headers), false = lists:keyfind(<<"transfer-encoding">>, 1, Headers), Rest1 end, Bits = 8000000 - bit_size(Rest), raw_expect_recv(Client, <<0:Bits>>), {error, closed} = raw_recv(Client, 1, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. chunked_one_byte_at_a_time(Config) -> doc("Confirm that chunked transfer-encoding works when " "the body is received one byte at a time."), Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])), ChunkedBody = iolist_to_binary(do_chunked_body(50, Body, [])), Client = raw_open(Config), ok = raw_send(Client, "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n\r\n"), _ = [begin raw_send(Client, <>), timer:sleep(1) end || <> <= ChunkedBody], Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {_, Rest1} = cow_http:parse_headers(Rest0), Rest1 end, RestSize = byte_size(Rest), <> = Body, raw_expect_recv(Client, Expect). chunked_one_chunk_at_a_time(Config) -> doc("Confirm that chunked transfer-encoding works when " "the body is received one chunk at a time."), Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])), Chunks = do_chunked_body(50, Body, []), Client = raw_open(Config), ok = raw_send(Client, "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n\r\n"), _ = [begin raw_send(Client, Chunk), timer:sleep(10) end || Chunk <- Chunks], Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {_, Rest1} = cow_http:parse_headers(Rest0), Rest1 end, RestSize = byte_size(Rest), <> = Body, raw_expect_recv(Client, Expect). chunked_split_delay_in_chunk_body(Config) -> doc("Confirm that chunked transfer-encoding works when " "the body is received with a delay inside the chunks."), Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])), Chunks = do_chunked_body(50, Body, []), Client = raw_open(Config), ok = raw_send(Client, "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n\r\n"), _ = [begin case Chunk of <<"0\r\n\r\n">> -> raw_send(Client, Chunk); _ -> [Size, ChunkBody, <<>>] = binary:split(Chunk, <<"\r\n">>, [global]), PartASize = rand:uniform(byte_size(ChunkBody)), <> = ChunkBody, raw_send(Client, [Size, <<"\r\n">>, PartA]), timer:sleep(10), raw_send(Client, [PartB, <<"\r\n">>]) end end || Chunk <- Chunks], Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {_, Rest1} = cow_http:parse_headers(Rest0), Rest1 end, RestSize = byte_size(Rest), <> = Body, raw_expect_recv(Client, Expect). chunked_split_delay_in_chunk_crlf(Config) -> doc("Confirm that chunked transfer-encoding works when " "the body is received with a delay inside the chunks end CRLF."), Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])), Chunks = do_chunked_body(50, Body, []), Client = raw_open(Config), ok = raw_send(Client, "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n\r\n"), _ = [begin Len = byte_size(Chunk) - (rand:uniform(2) - 1), <> = Chunk, raw_send(Client, Begin), timer:sleep(10), raw_send(Client, End) end || Chunk <- Chunks], Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {_, Rest1} = cow_http:parse_headers(Rest0), Rest1 end, RestSize = byte_size(Rest), <> = Body, raw_expect_recv(Client, Expect). do_chunked_body(_, <<>>, Acc) -> lists:reverse([cow_http_te:last_chunk()|Acc]); do_chunked_body(ChunkSize0, Data, Acc) -> ChunkSize = min(byte_size(Data), ChunkSize0), <> = Data, do_chunked_body(ChunkSize, Rest, [iolist_to_binary(cow_http_te:chunk(Chunk))|Acc]). disable_http1_tls(Config) -> doc("Ensure that we can disable HTTP/1.1 over TLS (force HTTP/2)."), TlsOpts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(?FUNCTION_NAME, TlsOpts ++ [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, alpn_default_protocol => http2 }), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}|TlsOpts]), %% ALPN was not negotiated but we're still over HTTP/2. {error, protocol_not_negotiated} = ssl:negotiated_protocol(Socket), %% Send a valid preface. ok = ssl:send(Socket, [ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), ok after cowboy:stop_listener(?FUNCTION_NAME) end. disable_http2_prior_knowledge(Config) -> doc("Ensure that we can disable prior knowledge HTTP/2 upgrade."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, protocols => [http] }), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, [ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), {ok, <<"HTTP/1.1 501">>} = gen_tcp:recv(Socket, 12, 1000), ok after cowboy:stop_listener(?FUNCTION_NAME) end. disable_http2_upgrade(Config) -> doc("Ensure that we can disable HTTP/1.1 upgrade to HTTP/2."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, protocols => [http] }), Port = ranch:get_port(?FUNCTION_NAME), try {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok after cowboy:stop_listener(?FUNCTION_NAME) end. hibernate(Config) -> doc("Ensure that we can enable hibernation for HTTP/1.1 connections."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, hibernate => true }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), StreamRef3 = gun:get(ConnPid, "/"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. http10_keepalive_false(Config) -> doc("Confirm the option http10_keepalive => false disables keep-alive " "completely for HTTP/1.0 connections."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, http10_keepalive => false }), Port = ranch:get_port(?FUNCTION_NAME), try Keepalive = "GET / HTTP/1.0\r\nhost: localhost\r\nConnection: keep-alive\r\n\r\n", Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]), ok = raw_send(Client, Keepalive), _ = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> %% Cowboy always advertises itself as HTTP/1.1. {'HTTP/1.1', 200, _, Rest} = cow_http:parse_status_line(Data), {Headers, _} = cow_http:parse_headers(Rest), {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, Headers) end, ok = raw_send(Client, Keepalive), case catch raw_recv_head(Client) of {'EXIT', _} -> closed; _ -> error(not_closed) end after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_read_body(Config) -> doc("Ensure the idle_timeout drops connections when the " "connection is idle too long reading the request body."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 60000, idle_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), _StreamRef = gun:post(ConnPid, "/echo/read_body", #{<<"content-length">> => <<"12">>}), {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_read_body_pipeline(Config) -> doc("Ensure the idle_timeout drops connections when the " "connection is idle too long reading the request body."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 60000, idle_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), _StreamRef3 = gun:post(ConnPid, "/echo/read_body", #{<<"content-length">> => <<"12">>}), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_skip_body(Config) -> doc("Ensure the idle_timeout drops connections when the " "connection is idle too long skipping the request body."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 60000, idle_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef = gun:post(ConnPid, "/", #{<<"content-length">> => <<"12">>}), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_infinity(Config) -> doc("Ensure the idle_timeout option accepts the infinity value."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => infinity }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), timer:sleep(500), #{socket := Socket} = gun:info(ConnPid), Pid = get_remote_pid_tcp(Socket), _ = gun:post(ConnPid, "/echo/read_body", [{<<"content-type">>, <<"text/plain">>}]), Ref = erlang:monitor(process, Pid), receive {'DOWN', Ref, process, Pid, Reason} -> error(Reason) after 1000 -> gun:close(ConnPid) end after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_on_send(Config) -> doc("Ensure the idle timeout is not reset when sending (by default)."), do_idle_timeout_on_send(Config, http). %% Also used by http2_SUITE. do_idle_timeout_on_send(Config, Protocol) -> {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => 1000 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]), {ok, Protocol} = gun:await_up(ConnPid), timer:sleep(500), #{socket := Socket} = gun:info(ConnPid), Pid = get_remote_pid_tcp(Socket), StreamRef = gun:get(ConnPid, "/streamed_result/10/250"), Ref = erlang:monitor(process, Pid), receive {gun_response, ConnPid, StreamRef, nofin, _Status, _Headers} -> do_idle_timeout_recv_loop(Ref, Pid, ConnPid, StreamRef, false) after 2000 -> error(timeout) end after cowboy:stop_listener(?FUNCTION_NAME) end. idle_timeout_reset_on_send(Config) -> doc("Ensure the reset_idle_timeout_on_send results in the " "idle timeout resetting when sending ."), do_idle_timeout_reset_on_send(Config, http). %% Also used by http2_SUITE. do_idle_timeout_reset_on_send(Config, Protocol) -> {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => 1000, reset_idle_timeout_on_send => true }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]), {ok, Protocol} = gun:await_up(ConnPid), timer:sleep(500), #{socket := Socket} = gun:info(ConnPid), Pid = get_remote_pid_tcp(Socket), StreamRef = gun:get(ConnPid, "/streamed_result/10/250"), Ref = erlang:monitor(process, Pid), receive {gun_response, ConnPid, StreamRef, nofin, _Status, _Headers} -> do_idle_timeout_recv_loop(Ref, Pid, ConnPid, StreamRef, true) after 2000 -> error(timeout) end after cowboy:stop_listener(?FUNCTION_NAME) end. do_idle_timeout_recv_loop(Ref, Pid, ConnPid, StreamRef, ExpectCompletion) -> receive {gun_data, ConnPid, StreamRef, nofin, _Data} -> do_idle_timeout_recv_loop(Ref, Pid, ConnPid, StreamRef, ExpectCompletion); {gun_data, ConnPid, StreamRef, fin, _Data} when ExpectCompletion -> gun:close(ConnPid); {gun_data, ConnPid, StreamRef, fin, _Data} -> gun:close(ConnPid), error(completed); {'DOWN', Ref, process, Pid, _} when ExpectCompletion -> gun:close(ConnPid), error(exited); {'DOWN', Ref, process, Pid, _} -> ok after 2000 -> error(timeout) end. max_authorization_header_value_length(Config) -> doc("Confirm the max_authorization_header_value_length option " "correctly limits the length of authorization header values."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, max_authorization_header_value_length => 2048 }), Port = ranch:get_port(?FUNCTION_NAME), try do_max_header_value_length(Config, Port, <<"authorization">>, 2048), %% Confirm that other headers still use the default limit. do_max_header_value_length(Config, Port, <<"my-header">>, 4096) after cowboy:stop_listener(?FUNCTION_NAME) end. max_cookie_header_value_length(Config) -> doc("Confirm the max_cookie_header_value_length option " "correctly limits the length of cookie header values."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, max_cookie_header_value_length => 2048 }), Port = ranch:get_port(?FUNCTION_NAME), try do_max_header_value_length(Config, Port, <<"cookie">>, 2048), %% Confirm that other headers still use the default limit. do_max_header_value_length(Config, Port, <<"my-header">>, 4096) after cowboy:stop_listener(?FUNCTION_NAME) end. max_header_value_length(Config) -> doc("Confirm the max_header_value_length option " "correctly limits the length of header values."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, max_header_value_length => 2048 }), Port = ranch:get_port(?FUNCTION_NAME), try do_max_header_value_length(Config, Port, <<"my-header">>, 2048) after cowboy:stop_listener(?FUNCTION_NAME) end. max_header_value_length_default(Config) -> doc("Confirm the max_header_value_length option " "correctly limits the length of header values."), do_max_header_value_length(Config, config(port, Config), <<"my-header">>, 4096). do_max_header_value_length(Config, Port, Name, MaxLen) -> ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef1 = gun:get(ConnPid, "/", #{Name => lists:duplicate(MaxLen, $a)}), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), %% We * 2 because this is a soft limit. StreamRef2 = gun:get(ConnPid, "/", #{Name => lists:duplicate(MaxLen * 2, $a)}), {response, fin, 431, _} = gun:await(ConnPid, StreamRef2), gun:close(ConnPid). persistent_term_router(Config) -> doc("The router can retrieve the routes from persistent_term storage."), case erlang:function_exported(persistent_term, get, 1) of true -> do_persistent_term_router(Config); false -> {skip, "This test uses the persistent_term functionality added in Erlang/OTP 21.2."} end. do_persistent_term_router(Config) -> persistent_term:put(?FUNCTION_NAME, init_dispatch(Config)), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => {persistent_term, ?FUNCTION_NAME}} }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef = gun:get(ConnPid, "/"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. request_timeout(Config) -> doc("Ensure the request_timeout drops connections when requests " "fail to come in fast enough."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. request_timeout_pipeline(Config) -> doc("Ensure the request_timeout drops connections when requests " "fail to come in fast enough after pipelined requests went through."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), StreamRef3 = gun:get(ConnPid, "/"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. request_timeout_pipeline_delay(Config) -> doc("Ensure the request_timeout does not trigger on requests " "coming in after a large request body."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), StreamRef1 = gun:post(ConnPid, "/", #{}, <<0:8000000>>), StreamRef2 = gun:get(ConnPid, "/delay_hello"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. request_timeout_skip_body(Config) -> doc("Ensure the request_timeout drops connections when requests " "fail to come in fast enough after skipping a request body."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]), ok = raw_send(Client, << "POST / HTTP/1.1\r\n" "host: localhost\r\n" "content-length: 12\r\n\r\n" >>), Data = raw_recv_head(Client), {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {Headers, Rest} = cow_http:parse_headers(Rest0), {_, Len} = lists:keyfind(<<"content-length">>, 1, Headers), <<"Hello world!">> = raw_recv_rest(Client, binary_to_integer(Len), Rest), %% We then send the request data that should be skipped by Cowboy. timer:sleep(100), raw_send(Client, <<"Hello world!">>), %% Connection should be closed by the request_timeout after that. {error, closed} = raw_recv(Client, 1, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. request_timeout_skip_body_more(Config) -> doc("Ensure the request_timeout drops connections when requests " "fail to come in fast enough after skipping a request body."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]), ok = raw_send(Client, << "POST / HTTP/1.1\r\n" "host: localhost\r\n" "content-length: 12\r\n\r\n" >>), Data = raw_recv_head(Client), {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {Headers, Rest} = cow_http:parse_headers(Rest0), {_, Len} = lists:keyfind(<<"content-length">>, 1, Headers), <<"Hello world!">> = raw_recv_rest(Client, binary_to_integer(Len), Rest), %% We then send the request data that should be skipped by Cowboy. timer:sleep(100), raw_send(Client, <<"Hello world!">>), %% Send the start of another request. ok = raw_send(Client, << "GET / HTTP/1.1\r\n" "host: localhost\r\n" %% Missing final \r\n on purpose. >>), %% Connection should be closed by the request_timeout after that. %% We attempt to send a 408 response on a best effort basis so %% that is accepted as well. case raw_recv(Client, 13, 1000) of {error, closed} -> ok; {ok, <<"HTTP/1.1 408 ", _/bits>>} -> ok end after cowboy:stop_listener(?FUNCTION_NAME) end. request_timeout_infinity(Config) -> doc("Ensure the request_timeout option accepts the infinity value."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, request_timeout => infinity }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), timer:sleep(500), #{socket := Socket} = gun:info(ConnPid), Pid = get_remote_pid_tcp(Socket), Ref = erlang:monitor(process, Pid), receive {'DOWN', Ref, process, Pid, Reason} -> error(Reason) after 1000 -> gun:close(ConnPid) end after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_chunked_false(Config) -> doc("Confirm the option chunked can be dynamically set to disable " "chunked transfer-encoding. This results in the closing of the " "connection after the current request."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, chunked => true }), Port = ranch:get_port(?FUNCTION_NAME), try Request = "GET /set_options/chunked_false HTTP/1.1\r\nhost: localhost\r\n\r\n", Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]), ok = raw_send(Client, Request), Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> %% Cowboy always advertises itself as HTTP/1.1. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {Headers, Rest1} = cow_http:parse_headers(Rest0), false = lists:keyfind(<<"content-length">>, 1, Headers), false = lists:keyfind(<<"transfer-encoding">>, 1, Headers), Rest1 end, Bits = 8000000 - bit_size(Rest), raw_expect_recv(Client, <<0:Bits>>), {error, closed} = raw_recv(Client, 1, 1000) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_chunked_false_ignored(Config) -> doc("Confirm the option chunked can be dynamically set to disable " "chunked transfer-encoding, and that it is ignored if the " "response is not streamed."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, chunked => true }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), %% We do a first request setting the option but not %% using chunked transfer-encoding in the response. StreamRef1 = gun:get(ConnPid, "/set_options/chunked_false_ignored"), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, StreamRef1), %% We then do a second request to confirm that chunked %% is not disabled for that second request. StreamRef2 = gun:get(ConnPid, "/resp/stream_reply2/200"), {response, nofin, 200, Headers} = gun:await(ConnPid, StreamRef2), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, Headers), gun:close(ConnPid) after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_idle_timeout(Config) -> doc("Confirm that the idle_timeout option can be dynamically " "set to change how long Cowboy will wait before it closes the connection."), %% We start with a long timeout and then cut it short. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => 60000 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), timer:sleep(500), #{socket := Socket} = gun:info(ConnPid), Pid = get_remote_pid_tcp(Socket), _ = gun:post(ConnPid, "/set_options/idle_timeout_short", [{<<"content-type">>, <<"text/plain">>}]), Ref = erlang:monitor(process, Pid), receive {'DOWN', Ref, process, Pid, _} -> ok after 2000 -> error(timeout) end after cowboy:stop_listener(?FUNCTION_NAME) end. set_options_idle_timeout_only_applies_to_current_request(Config) -> doc("Confirm that changes to the idle_timeout option only apply to the current stream."), %% We start with a long timeout and then cut it short. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{ env => #{dispatch => init_dispatch(Config)}, idle_timeout => 500 }), Port = ranch:get_port(?FUNCTION_NAME), try ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), {ok, http} = gun:await_up(ConnPid), timer:sleep(500), #{socket := Socket} = gun:info(ConnPid), Pid = get_remote_pid_tcp(Socket), StreamRef = gun:post(ConnPid, "/set_options/idle_timeout_long", [{<<"content-type">>, <<"text/plain">>}]), Ref = erlang:monitor(process, Pid), receive {'DOWN', Ref, process, Pid, Reason} -> error(Reason) after 2000 -> ok end, %% Finish the first request and start a second one to confirm %% the idle_timeout option is back to normal. gun:data(ConnPid, StreamRef, fin, <<"Hello!">>), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {ok, <<"Hello!">>} = gun:await_body(ConnPid, StreamRef), _ = gun:post(ConnPid, "/echo/read_body", [{<<"content-type">>, <<"text/plain">>}]), receive {'DOWN', Ref, process, Pid, _} -> ok after 2000 -> error(timeout) end after cowboy:stop_listener(?FUNCTION_NAME) end. switch_protocol_flush(Config) -> doc("Confirm that switch_protocol does not flush unrelated messages."), ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, stream_handlers => [switch_protocol_flush_h] }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try Self = self(), ConnPid = gun_open([{port, Port}, {type, tcp}, {protocol, http}|Config]), _ = gun:get(ConnPid, "/", [ {<<"x-test-pid">>, pid_to_list(Self)} ]), receive {Self, Events} -> switch_protocol_flush_h:validate(Events) after 5000 -> error(timeout) end after cowboy:stop_listener(?FUNCTION_NAME) end. graceful_shutdown_connection(Config) -> doc("Check that the current request is handled before gracefully " "shutting down a connection."), Dispatch = cowboy_router:compile([{"localhost", [ {"/hello", delay_hello_h, #{delay => 0, notify_received => self()}}, {"/delay_hello", delay_hello_h, #{delay => 1000, notify_received => self()}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch} }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]), ok = raw_send(Client, "GET /delay_hello HTTP/1.1\r\n" "Host: localhost\r\n\r\n" "GET /hello HTTP/1.1\r\n" "Host: localhost\r\n\r\n"), receive {request_received, <<"/delay_hello">>} -> ok end, receive {request_received, <<"/hello">>} -> ok end, CowboyConnPid = get_remote_pid_tcp(element(2, Client)), CowboyConnRef = erlang:monitor(process, CowboyConnPid), ok = sys:terminate(CowboyConnPid, system_is_going_down), Rest = case catch raw_recv_head(Client) of {'EXIT', _} -> error(closed); Data -> {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data), {Headers, Rest1} = cow_http:parse_headers(Rest0), <<"close">> = proplists:get_value(<<"connection">>, Headers), Rest1 end, <<"Hello world!">> = raw_recv_rest(Client, byte_size(<<"Hello world!">>), Rest), {error, closed} = raw_recv(Client, 0, 1000), receive {'DOWN', CowboyConnRef, process, CowboyConnPid, _Reason} -> ok end after cowboy:stop_listener(?FUNCTION_NAME) end. graceful_shutdown_listener(Config) -> doc("Check that connections are shut down gracefully when stopping a listener."), TransOpts = #{ socket_opts => [{port, 0}], shutdown => 1000 %% Shorter timeout to make the test case faster. }, Dispatch = cowboy_router:compile([{"localhost", [ {"/delay_hello", delay_hello_h, #{delay => 500, notify_received => self()}}, {"/long_delay_hello", delay_hello_h, #{delay => 10000, notify_received => self()}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch} }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, TransOpts, ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), ConnPid1 = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), Ref1 = gun:get(ConnPid1, "/delay_hello"), ConnPid2 = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]), Ref2 = gun:get(ConnPid2, "/long_delay_hello"), %% Shutdown listener while the handlers are working. receive {request_received, <<"/delay_hello">>} -> ok end, receive {request_received, <<"/long_delay_hello">>} -> ok end, %% Note: This call does not complete quickly and will %% prevent other cowboy:stop_listener/1 calls to complete. ok = cowboy:stop_listener(?FUNCTION_NAME), %% Check that the 1st request is handled before shutting down. {response, nofin, 200, RespHeaders} = gun:await(ConnPid1, Ref1), <<"close">> = proplists:get_value(<<"connection">>, RespHeaders), {ok, RespBody} = gun:await_body(ConnPid1, Ref1), <<"Hello world!">> = iolist_to_binary(RespBody), gun:close(ConnPid1), %% Check that the 2nd (very slow) request is not handled. {error, {stream_error, closed}} = gun:await(ConnPid2, Ref2), gun:close(ConnPid2). send_timeout_close(_Config) -> doc("Check that connections are closed on send timeout."), TransOpts = #{ socket_opts => [ {port, 0}, {send_timeout, 100}, {send_timeout_close, true}, {sndbuf, 10} ] }, Dispatch = cowboy_router:compile([{"localhost", [ {"/endless", loop_handler_endless_h, #{delay => 100}} ]}]), ProtoOpts = #{ env => #{dispatch => Dispatch}, idle_timeout => infinity }, {ok, _} = cowboy:start_clear(?FUNCTION_NAME, TransOpts, ProtoOpts), Port = ranch:get_port(?FUNCTION_NAME), try %% Connect a client that sends a request and waits indefinitely. {ok, ClientSocket} = gen_tcp:connect("localhost", Port, [{recbuf, 10}, {buffer, 10}, {active, false}, {packet, 0}]), ok = gen_tcp:send(ClientSocket, [ "GET /endless HTTP/1.1\r\n", "Host: localhost:", integer_to_list(Port), "\r\n", "x-test-pid: ", pid_to_list(self()), "\r\n\r\n" ]), %% Wait for the handler to start then get its pid, %% the remote connection's pid and socket. StreamPid = receive {Self, StreamPid0, init} when Self =:= self() -> StreamPid0 after 1000 -> error(timeout) end, ServerPid = ct_helper:get_remote_pid_tcp(ClientSocket), {links, ServerLinks} = process_info(ServerPid, links), [ServerSocket] = [PidOrPort || PidOrPort <- ServerLinks, is_port(PidOrPort)], %% Poll the socket repeatedly until it is closed by the server. WaitClosedFun = fun F(T) when T =< 0 -> error({status, prim_inet:getstatus(ServerSocket)}); F(T) -> Snooze = 100, case inet:sockname(ServerSocket) of {error, _} -> timer:sleep(Snooze); {ok, _} -> timer:sleep(Snooze), F(T - Snooze) end end, ok = WaitClosedFun(2000), false = erlang:is_process_alive(StreamPid), false = erlang:is_process_alive(ServerPid) after cowboy:stop_listener(?FUNCTION_NAME) end. ================================================ FILE: test/http_perf_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(http_perf_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> %% @todo Enable HTTP/3 for this test suite. cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}]. groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel). init_per_suite(Config) -> do_log("", []), %% Optionally enable `perf` for the current node. % spawn(fun() -> ct:pal(os:cmd("perf record -g -F 9999 -o /tmp/http_perf.data -p " ++ os:getpid() ++ " -- sleep 60")) end), Config. end_per_suite(_) -> ok. init_per_group(Name, Config) -> [{group, Name}|cowboy_test:init_common_groups(Name, Config, ?MODULE, #{ %% HTTP/1.1 max_keepalive => infinity, %% HTTP/2 %% @todo Must configure Gun for performance too. connection_window_margin_size => 64*1024, enable_connect_protocol => true, env => #{dispatch => init_dispatch(Config)}, max_frame_size_sent => 64*1024, max_frame_size_received => 16384 * 1024 - 1, max_received_frame_rate => {10_000_000, 1}, stream_window_data_threshold => 1024, stream_window_margin_size => 64*1024 })]. end_per_group(Name, _) -> do_log("", []), cowboy_test:stop_group(Name). %% Routes. init_dispatch(_) -> cowboy_router:compile([{'_', [ {"/", hello_h, []}, {"/read_body", read_body_h, []} ]}]). %% Tests: Hello world. plain_h_hello_1(Config) -> doc("Plain HTTP handler Hello World; 10K requests per 1 client."), do_bench_get(?FUNCTION_NAME, "/", #{}, 1, 10000, Config). plain_h_hello_10(Config) -> doc("Plain HTTP handler Hello World; 10K requests per 10 clients."), do_bench_get(?FUNCTION_NAME, "/", #{}, 10, 10000, Config). stream_h_hello_1(Config) -> doc("Stream handler Hello World; 10K requests per 1 client."), do_stream_h_hello(Config, 1). stream_h_hello_10(Config) -> doc("Stream handler Hello World; 10K requests per 10 clients."), do_stream_h_hello(Config, 10). do_stream_h_hello(Config, NumClients) -> Ref = config(ref, Config), ProtoOpts = ranch:get_protocol_options(Ref), StreamHandlers = case ProtoOpts of #{stream_handlers := _} -> [cowboy_compress_h, stream_hello_h]; _ -> [stream_hello_h] end, ranch:set_protocol_options(Ref, ProtoOpts#{ env => #{}, stream_handlers => StreamHandlers }), do_bench_get(?FUNCTION_NAME, "/", #{}, NumClients, 10000, Config), ranch:set_protocol_options(Ref, ProtoOpts). %% Tests: Large body upload. plain_h_1M_post_1(Config) -> doc("Plain HTTP handler body reading; 10K requests per 1 client."), do_bench_post(?FUNCTION_NAME, "/read_body", #{}, <<0:8_000_000>>, 1, 10000, Config). plain_h_1M_post_10(Config) -> doc("Plain HTTP handler body reading; 10K requests per 10 clients."), do_bench_post(?FUNCTION_NAME, "/read_body", #{}, <<0:8_000_000>>, 10, 10000, Config). plain_h_10G_post(Config) -> doc("Plain HTTP handler body reading; 1 request with a 10GB body."), do_bench_post_one_large(?FUNCTION_NAME, "/read_body", #{}, 10_000, <<0:8_000_000>>, Config). %% Internal. do_bench_get(What, Path, Headers, NumClients, NumRuns, Config) -> Clients = [spawn_link(?MODULE, do_bench_get_proc, [self(), What, Path, Headers, NumRuns, Config]) || _ <- lists:seq(1, NumClients)], {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, Clients]), do_log("~32s: ~8bµs ~8.1freqs/s", [ [atom_to_list(config(group, Config)), $., atom_to_list(What)], Time, (NumClients * NumRuns) / Time * 1_000_000]), ok. do_bench_get_proc(Parent, What, Path, Headers0, NumRuns, Config) -> ConnPid = gun_open(Config), Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>}, Parent ! {What, ready}, receive {What, go} -> ok end, do_bench_get_run(ConnPid, Path, Headers, NumRuns), Parent ! {What, done}, gun:close(ConnPid). do_bench_get_run(_, _, _, 0) -> ok; do_bench_get_run(ConnPid, Path, Headers, Num) -> Ref = gun:request(ConnPid, <<"GET">>, Path, Headers, <<>>), {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, _} = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, do_bench_get_run(ConnPid, Path, Headers, Num - 1). do_bench_post(What, Path, Headers, Body, NumClients, NumRuns, Config) -> Clients = [spawn_link(?MODULE, do_bench_post_proc, [self(), What, Path, Headers, Body, NumRuns, Config]) || _ <- lists:seq(1, NumClients)], {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, Clients]), do_log("~32s: ~8bµs ~8.1freqs/s", [ [atom_to_list(config(group, Config)), $., atom_to_list(What)], Time, (NumClients * NumRuns) / Time * 1_000_000]), ok. do_bench_post_proc(Parent, What, Path, Headers0, Body, NumRuns, Config) -> ConnPid = gun_open(Config), Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>}, Parent ! {What, ready}, receive {What, go} -> ok end, do_bench_post_run(ConnPid, Path, Headers, Body, NumRuns), Parent ! {What, done}, gun:close(ConnPid). do_bench_post_run(_, _, _, _, 0) -> ok; do_bench_post_run(ConnPid, Path, Headers, Body, Num) -> Ref = gun:request(ConnPid, <<"POST">>, Path, Headers, Body), {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, _} = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, do_bench_post_run(ConnPid, Path, Headers, Body, Num - 1). do_bench_post_one_large(What, Path, Headers, NumChunks, BodyChunk, Config) -> Client = spawn_link(?MODULE, do_bench_post_one_large_proc, [self(), What, Path, Headers, NumChunks, BodyChunk, Config]), {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, [Client]]), do_log("~32s: ~8bµs ~8.1freqs/s", [ [atom_to_list(config(group, Config)), $., atom_to_list(What)], Time, 1 / Time * 1_000_000]), ok. do_bench_post_one_large_proc(Parent, What, Path, Headers0, NumChunks, BodyChunk, Config) -> ConnPid = gun_open(Config), Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>}, Parent ! {What, ready}, receive {What, go} -> ok end, StreamRef = gun:headers(ConnPid, <<"POST">>, Path, Headers#{ <<"content-length">> => integer_to_binary(NumChunks * byte_size(BodyChunk)) }), do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks - 1, BodyChunk), {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, StreamRef, infinity), {ok, _} = case IsFin of nofin -> gun:await_body(ConnPid, StreamRef, infinity); fin -> {ok, <<>>} end, Parent ! {What, done}, gun:close(ConnPid). do_bench_post_one_large_run(ConnPid, StreamRef, 0, BodyChunk) -> gun:data(ConnPid, StreamRef, fin, BodyChunk); do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks, BodyChunk) -> gun:data(ConnPid, StreamRef, nofin, BodyChunk), do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks - 1, BodyChunk). do_bench_wait(What, Clients) -> _ = [receive {What, ready} -> ok end || _ <- Clients], _ = [ClientPid ! {What, go} || ClientPid <- Clients], _ = [receive {What, done} -> ok end || _ <- Clients], ok. do_log(Str, Args) -> ct:log(Str, Args), io:format(ct_default_gl, Str ++ "~n", Args). ================================================ FILE: test/loop_handler_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(loop_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Dispatch configuration. init_dispatch(_) -> cowboy_router:compile([{'_', [ {"/long_polling", long_polling_h, []}, {"/loop_body", loop_handler_body_h, []}, {"/loop_request_timeout", loop_handler_timeout_h, []}, {"/loop_timeout_init", loop_handler_timeout_init_h, []}, {"/loop_timeout_info", loop_handler_timeout_info_h, []}, {"/loop_timeout_hibernate", loop_handler_timeout_hibernate_h, []} ]}]). %% Tests. info_read_body(Config) -> doc("Check that a loop handler can read the request body in info/3."), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/loop_body", [{<<"accept-encoding">>, <<"gzip">>}], << 0:100000/unit:8 >>), {response, fin, 200, _} = gun:await(ConnPid, Ref), ok. long_polling(Config) -> doc("Simple long-polling."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 299, _} = gun:await(ConnPid, Ref), ok. long_polling_unread_body(Config) -> doc("Long-polling with a body that is not read by the handler."), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/long_polling", [{<<"accept-encoding">>, <<"gzip">>}], << 0:100000/unit:8 >>), {response, fin, 299, _} = gun:await(ConnPid, Ref), ok. long_polling_pipeline(Config) -> doc("Pipeline of long-polling calls."), ConnPid = gun_open(Config), Refs = [gun:get(ConnPid, "/long_polling", [{<<"accept-encoding">>, <<"gzip">>}]) || _ <- lists:seq(1, 2)], _ = [{response, fin, 299, _} = gun:await(ConnPid, Ref) || Ref <- Refs], ok. request_timeout(Config) -> doc("Ensure that the request_timeout isn't applied when a request is ongoing."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/loop_request_timeout", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, _} = gun:await(ConnPid, Ref, 10000), ok. timeout_hibernate(Config) -> doc("Ensure that loop handler idle timeouts don't trigger after hibernate is returned."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/loop_timeout_hibernate", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 200, _} = gun:await(ConnPid, Ref), ok. timeout_info(Config) -> doc("Ensure that loop handler idle timeouts trigger on time when set in info/3."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/loop_timeout_info", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 299, _} = gun:await(ConnPid, Ref), ok. timeout_init(Config) -> doc("Ensure that loop handler idle timeouts trigger on time when set in init/2."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/loop_timeout_init?timeout=1000", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 200, _} = gun:await(ConnPid, Ref), Ref2 = gun:get(ConnPid, "/loop_timeout_init?timeout=100", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 299, _} = gun:await(ConnPid, Ref2), ok. ================================================ FILE: test/metrics_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(metrics_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_down/1]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). %% ct. suite() -> [{timetrap, 30000}]. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name = http, Config) -> cowboy_test:init_http(Name, init_plain_opts(Config), Config); init_per_group(Name = https, Config) -> cowboy_test:init_http(Name, init_plain_opts(Config), Config); init_per_group(Name = h2, Config) -> cowboy_test:init_http2(Name, init_plain_opts(Config), Config); init_per_group(Name = h2c, Config) -> Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = h3, Config) -> cowboy_test:init_http3(Name, init_plain_opts(Config), Config); init_per_group(Name = http_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(Config), Config); init_per_group(Name = https_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(Config), Config); init_per_group(Name = h2_compress, Config) -> cowboy_test:init_http2(Name, init_compress_opts(Config), Config); init_per_group(Name = h2c_compress, Config) -> Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = h3_compress, Config) -> cowboy_test:init_http3(Name, init_compress_opts(Config), Config). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_plain_opts(Config) -> #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, metrics_callback => do_metrics_callback(), stream_handlers => [cowboy_metrics_h, cowboy_stream_h] }. init_compress_opts(Config) -> #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, metrics_callback => do_metrics_callback(), stream_handlers => [cowboy_metrics_h, cowboy_compress_h, cowboy_stream_h] }. init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, {"/crash/no_reply", crash_h, no_reply}, {"/crash/reply", crash_h, reply}, {"/default", default_h, []}, {"/full/:key", echo_h, []}, {"/resp/:key[/:arg]", resp_h, []}, {"/set_options/:key", set_options_h, []}, {"/ws_echo", ws_echo, []} ]} ]. do_metrics_callback() -> fun(Metrics) -> Pid = case Metrics of #{req := #{headers := #{<<"x-test-pid">> := P}}} -> list_to_pid(binary_to_list(P)); #{partial_req := #{headers := #{<<"x-test-pid">> := P}}} -> list_to_pid(binary_to_list(P)); _ -> whereis(early_error_metrics) end, Pid ! {metrics, self(), Metrics}, ok end. %% Tests. hello_world(Config) -> doc("Confirm metrics are correct for a normal GET request."), do_get("/", #{}, Config). user_data(Config) -> doc("Confirm user data can be attached to metrics."), do_get("/set_options/metrics_user_data", #{handler => set_options_h}, Config). do_get(Path, UserData, Config) -> %% Perform a GET request. ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, RespBody} = gun:await_body(ConnPid, Ref, infinity), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Ensure the timestamps are in the expected order. #{ req_start := ReqStart, req_end := ReqEnd, resp_start := RespStart, resp_end := RespEnd } = Metrics, true = (ReqStart =< RespStart) and (RespStart =< RespEnd) and (RespEnd =< ReqEnd), %% We didn't send a body. #{ req_body_start := undefined, req_body_end := undefined, req_body_length := 0 } = Metrics, %% We got a 200 response with a body. #{ resp_status := 200, resp_headers := ExpectedRespHeaders, resp_body_length := RespBodyLen } = Metrics, %% The transfer-encoding header is hidden from stream handlers. ExpectedRespHeaders = maps:remove(<<"transfer-encoding">>, maps:from_list(RespHeaders)), true = byte_size(RespBody) > 0, true = RespBodyLen > 0, %% The request process executed normally. #{procs := Procs} = Metrics, [{_, #{ spawn := ProcSpawn, exit := ProcExit, reason := normal }}] = maps:to_list(Procs), true = ProcSpawn =< ProcExit, %% Confirm other metadata are as expected. #{ ref := _, pid := From, streamid := StreamID, reason := normal, %% @todo Getting h3_no_error here. req := #{}, informational := [], user_data := UserData } = Metrics, do_check_streamid(StreamID, Config), %% All good! gun:close(ConnPid) end. do_check_streamid(StreamID, Config) -> case config(protocol, Config) of http -> 1 = StreamID; http2 -> 1 = StreamID; http3 -> 0 = StreamID end. post_body(Config) -> doc("Confirm metrics are correct for a normal POST request."), %% Perform a POST request. ConnPid = gun_open(Config), Body = <<0:8000000>>, Ref = gun:post(ConnPid, "/full/read_body", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ], Body), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, RespBody} = gun:await_body(ConnPid, Ref, infinity), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Ensure the timestamps are in the expected order. #{ req_start := ReqStart, req_end := ReqEnd, resp_start := RespStart, resp_end := RespEnd } = Metrics, true = (ReqStart =< RespStart) and (RespStart =< RespEnd) and (RespEnd =< ReqEnd), %% We didn't send a body. #{ req_body_start := ReqBodyStart, req_body_end := ReqBodyEnd, req_body_length := ReqBodyLen } = Metrics, true = ReqBodyStart =< ReqBodyEnd, ReqBodyLen = byte_size(Body), %% We got a 200 response with a body. #{ resp_status := 200, resp_headers := ExpectedRespHeaders, resp_body_length := RespBodyLen } = Metrics, ExpectedRespHeaders = maps:from_list(RespHeaders), true = byte_size(RespBody) > 0, true = RespBodyLen > 0, %% The request process executed normally. #{procs := Procs} = Metrics, [{_, #{ spawn := ProcSpawn, exit := ProcExit, reason := normal }}] = maps:to_list(Procs), true = ProcSpawn =< ProcExit, %% Confirm other metadata are as expected. #{ ref := _, pid := From, streamid := StreamID, reason := normal, req := #{}, informational := [], user_data := #{} } = Metrics, do_check_streamid(StreamID, Config), %% All good! gun:close(ConnPid) end. no_resp_body(Config) -> doc("Confirm metrics are correct for a default 204 response to a GET request."), %% Perform a GET request. ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/default", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), {response, fin, 204, RespHeaders} = gun:await(ConnPid, Ref, infinity), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Ensure the timestamps are in the expected order. #{ req_start := ReqStart, req_end := ReqEnd, resp_start := RespStart, resp_end := RespEnd } = Metrics, true = (ReqStart =< RespStart) and (RespStart =< RespEnd) and (RespEnd =< ReqEnd), %% We didn't send a body. #{ req_body_start := undefined, req_body_end := undefined, req_body_length := 0 } = Metrics, %% We got a 200 response with a body. #{ resp_status := 204, resp_headers := ExpectedRespHeaders, resp_body_length := 0 } = Metrics, ExpectedRespHeaders = maps:from_list(RespHeaders), %% The request process executed normally. #{procs := Procs} = Metrics, [{_, #{ spawn := ProcSpawn, exit := ProcExit, reason := normal }}] = maps:to_list(Procs), true = ProcSpawn =< ProcExit, %% Confirm other metadata are as expected. #{ ref := _, pid := From, streamid := StreamID, reason := normal, req := #{}, informational := [], user_data := #{} } = Metrics, do_check_streamid(StreamID, Config), %% All good! gun:close(ConnPid) end. early_error(Config) -> doc("Confirm metrics are correct for an early_error response."), %% Perform a malformed GET request. ConnPid = gun_open(Config), %% We must use different solutions to hit early_error with a stream_error %% reason in both protocols. {Method, Headers, Status, Error} = case config(protocol, Config) of http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error}; http2 -> {<<"TRACE">>, [], 501, no_error}; http3 -> {<<"TRACE">>, [], 501, h3_no_error} end, Ref = gun:request(ConnPid, Method, "/", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} |Headers], <<>>), {response, fin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Confirm the metadata is there as expected. #{ ref := _, pid := From, streamid := StreamID, reason := {stream_error, Error, _}, partial_req := #{}, resp_status := Status, resp_headers := ExpectedRespHeaders, early_error_time := _, resp_body_length := 0 } = Metrics, do_check_streamid(StreamID, Config), ExpectedRespHeaders = maps:from_list(RespHeaders), %% All good! gun:close(ConnPid) end. early_error_request_line(Config) -> case config(protocol, Config) of http -> do_early_error_request_line(Config); http2 -> doc("There are no request lines in HTTP/2."); http3 -> doc("There are no request lines in HTTP/3.") end. do_early_error_request_line(Config) -> doc("Confirm metrics are correct for an early_error response " "that occurred on the request-line."), %% Register the process in order to receive the metrics event. register(early_error_metrics, self()), %% Send a malformed request-line. Client = raw_open(Config), ok = raw_send(Client, <<"FOO bar\r\n">>), {'HTTP/1.1', 400, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {RespHeaders, _} = cow_http:parse_headers(Rest), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Confirm the metadata is there as expected. #{ ref := _, pid := From, streamid := StreamID, reason := {connection_error, protocol_error, _}, partial_req := #{}, resp_status := 400, resp_headers := ExpectedRespHeaders, early_error_time := _, resp_body_length := 0 } = Metrics, do_check_streamid(StreamID, Config), ExpectedRespHeaders = maps:from_list(RespHeaders), %% All good! ok end. %% This test is identical to normal GET except for the handler. stream_reply(Config) -> doc("Confirm metrics are correct for long polling."), do_get("/resp/stream_reply2/200", #{}, Config). ws(Config) -> case config(protocol, Config) of http -> do_ws(Config); %% @todo The test can be implemented for HTTP/2. http2 -> doc("It is not currently possible to switch to Websocket over HTTP/2."); http3 -> {skip, "Gun does not currently support Websocket over HTTP/3."} end. do_ws(Config) -> doc("Confirm metrics are correct when switching to Websocket."), ConnPid = gun_open(Config), {ok, http} = gun:await_up(ConnPid, infinity), StreamRef = gun:ws_upgrade(ConnPid, "/ws_echo", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), receive {metrics, From, Metrics} -> %% Ensure the timestamps are in the expected order. #{ req_start := ReqStart, req_end := ReqEnd } = Metrics, true = ReqStart =< ReqEnd, %% We didn't send a body. #{ req_body_start := undefined, req_body_end := undefined, req_body_length := 0 } = Metrics, %% We didn't send a response. #{ resp_start := undefined, resp_end := undefined, resp_status := undefined, resp_headers := undefined, resp_body_length := 0 } = Metrics, %% The request process may not have terminated before terminate %% is called. We therefore only check when it spawned. #{procs := Procs} = Metrics, [{_, #{ spawn := _ }}] = maps:to_list(Procs), %% Confirm other metadata are as expected. #{ ref := _, pid := From, streamid := StreamID, reason := switch_protocol, req := #{}, %% A 101 upgrade response was sent. informational := [#{ status := 101, headers := #{ <<"connection">> := <<"Upgrade">>, <<"upgrade">> := <<"websocket">>, <<"sec-websocket-accept">> := _ }, time := _ }], user_data := #{} } = Metrics, do_check_streamid(StreamID, Config), %% All good! ok end, %% And of course the upgrade completed successfully after that. receive {gun_upgrade, ConnPid, StreamRef, _, _} -> ok end, gun:close(ConnPid). error_response(Config) -> doc("Confirm metrics are correct when an error_response command is returned."), %% Perform a GET request. ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/crash/no_reply", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), Protocol = config(protocol, Config), RespHeaders = case gun:await(ConnPid, Ref, infinity) of {response, fin, 500, RespHeaders0} -> RespHeaders0; %% The RST_STREAM arrived before the start of the response. %% See maybe_h3_error comment for details. {error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 -> unknown end, timer:sleep(100), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Ensure the timestamps are in the expected order. #{ req_start := ReqStart, req_end := ReqEnd, resp_start := RespStart, resp_end := RespEnd } = Metrics, true = (ReqStart =< RespStart) and (RespStart =< RespEnd) and (RespEnd =< ReqEnd), %% We didn't send a body. #{ req_body_start := undefined, req_body_end := undefined, req_body_length := 0 } = Metrics, %% We got a 500 response without a body. #{ resp_status := 500, resp_headers := ExpectedRespHeaders, resp_body_length := 0 } = Metrics, case RespHeaders of %% The HTTP/3 stream has reset too early so we can't %% verify the response headers. unknown -> ok; _ -> ExpectedRespHeaders = maps:from_list(RespHeaders) end, %% The request process executed normally. #{procs := Procs} = Metrics, [{_, #{ spawn := ProcSpawn, exit := ProcExit, reason := {crash, StackTrace} }}] = maps:to_list(Procs), true = ProcSpawn =< ProcExit, %% Confirm other metadata are as expected. #{ ref := _, pid := From, streamid := StreamID, reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'}, req := #{}, informational := [], user_data := #{} } = Metrics, do_check_streamid(StreamID, Config), %% All good! gun:close(ConnPid) end. error_response_after_reply(Config) -> doc("Confirm metrics are correct when an error_response command is returned " "after a response was sent."), %% Perform a GET request. ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/crash/reply", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), Protocol = config(protocol, Config), RespHeaders = case gun:await(ConnPid, Ref, infinity) of {response, fin, 200, RespHeaders0} -> RespHeaders0; %% The RST_STREAM arrived before the start of the response. %% See maybe_h3_error comment for details. {error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 -> unknown end, timer:sleep(100), %% Receive the metrics and validate them. receive {metrics, From, Metrics} -> %% Ensure the timestamps are in the expected order. #{ req_start := ReqStart, req_end := ReqEnd, resp_start := RespStart, resp_end := RespEnd } = Metrics, true = (ReqStart =< RespStart) and (RespStart =< RespEnd) and (RespEnd =< ReqEnd), %% We didn't send a body. #{ req_body_start := undefined, req_body_end := undefined, req_body_length := 0 } = Metrics, %% We got a 200 response without a body. #{ resp_status := 200, resp_headers := ExpectedRespHeaders, resp_body_length := 0 } = Metrics, case RespHeaders of %% The HTTP/3 stream has reset too early so we can't %% verify the response headers. unknown -> ok; _ -> ExpectedRespHeaders = maps:from_list(RespHeaders) end, %% The request process executed normally. #{procs := Procs} = Metrics, [{_, #{ spawn := ProcSpawn, exit := ProcExit, reason := {crash, StackTrace} }}] = maps:to_list(Procs), true = ProcSpawn =< ProcExit, %% Confirm other metadata are as expected. #{ ref := _, pid := From, streamid := StreamID, reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'}, req := #{}, informational := [], user_data := #{} } = Metrics, do_check_streamid(StreamID, Config), %% All good! gun:close(ConnPid) end. ================================================ FILE: test/misc_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(misc_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). all() -> [{group, app}, {group, env}|cowboy_test:common_all()]. groups() -> Common = ct_helper:all(?MODULE) -- [restart_gracefully, get_env, set_env, set_env_missing], [ {app, [], [restart_gracefully]}, {env, [parallel], [get_env, set_env, set_env_missing]} |cowboy_test:common_groups(Common)]. init_per_group(Name=app, Config) -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch(Config)} }, Config); init_per_group(env, Config) -> Config; init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(env, _) -> ok; end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_dispatch(_) -> cowboy_router:compile([{"localhost", [ {"/", hello_h, []} ]}]). %% Logger function silencing the expected crash. error("Ranch listener " ++ _, [set_env_missing|_]) -> ok; error(Format, Args) -> error_logger:error_msg(Format, Args). %% Tests. restart_gracefully(Config) -> doc("Ensure we can process request when the cowboy application is being restarted."), ConnPid = gun_open(Config), %% We can do a request before stopping cowboy. Ref1 = gun:get(ConnPid, "/"), {response, _, 200, _} = gun:await(ConnPid, Ref1), %% Stop the cowboy application. ok = application:stop(cowboy), %% We can still do a request even though cowboy is stopped. Ref2 = gun:get(ConnPid, "/"), {response, _, 200, _} = gun:await(ConnPid, Ref2), %% Start the cowboy application again. ok = application:start(cowboy), %% Even after restarting there are no issues. Ref3 = gun:get(ConnPid, "/"), {response, _, 200, _} = gun:await(ConnPid, Ref3), ok. router_invalid_path(Config) -> doc("Ensure a path with invalid percent-encoded characters results in a 400."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/version/path/%\\u0016\\u0016/path"), {response, _, 400, _} = gun:await(ConnPid, Ref), ok. get_env(Config0) -> doc("Ensure we can retrieve middleware environment values."), Dispatch = init_dispatch(Config0), _Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{ dispatch => Dispatch, the_key => the_value } }, Config0), try Dispatch = cowboy:get_env(?FUNCTION_NAME, dispatch), Dispatch = cowboy:get_env(?FUNCTION_NAME, dispatch, the_default), the_value = cowboy:get_env(?FUNCTION_NAME, the_key), the_value = cowboy:get_env(?FUNCTION_NAME, the_key, the_default), {'EXIT', _} = (catch cowboy:get_env(?FUNCTION_NAME, missing_key)), the_default = cowboy:get_env(?FUNCTION_NAME, missing_key, the_default) after cowboy:stop_listener(?FUNCTION_NAME) end. set_env(Config0) -> doc("Live replace a middleware environment value."), Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => []} }, Config0), try ConnPid1 = gun_open(Config), Ref1 = gun:get(ConnPid1, "/"), {response, _, 400, _} = gun:await(ConnPid1, Ref1), cowboy:set_env(?FUNCTION_NAME, dispatch, init_dispatch(Config)), %% Only new connections get the updated environment. ConnPid2 = gun_open(Config), Ref2 = gun:get(ConnPid2, "/"), {response, _, 200, _} = gun:await(ConnPid2, Ref2) after cowboy:stop_listener(?FUNCTION_NAME) end. set_env_missing(Config0) -> doc("Live replace a middleware environment value when env was not provided."), Config = cowboy_test:init_http(?FUNCTION_NAME, #{ logger => ?MODULE }, Config0), try ConnPid1 = gun_open(Config), Ref1 = gun:get(ConnPid1, "/"), {response, _, 500, _} = gun:await(ConnPid1, Ref1), cowboy:set_env(?FUNCTION_NAME, dispatch, []), %% Only new connections get the updated environment. ConnPid2 = gun_open(Config), Ref2 = gun:get(ConnPid2, "/"), {response, _, 400, _} = gun:await(ConnPid2, Ref2) after cowboy:stop_listener(?FUNCTION_NAME) end. ================================================ FILE: test/plain_handler_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(plain_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_suite(Config) -> ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"), Config. end_per_suite(Config) -> ct_helper:delete_static_dir(config(priv_dir, Config) ++ "/static"). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Routes. init_dispatch(_) -> cowboy_router:compile([{"localhost", [ {"/crash/external_exit", crash_h, external_exit}, {"/crash/no_reply", crash_h, no_reply}, {"/crash/reply", crash_h, reply} ]}]). %% Tests. crash_after_reply(Config) -> doc("A plain handler crash after a response was sent " "results in no 500 response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/crash/reply", [ {<<"accept-encoding">>, <<"gzip">>} ]), Protocol = config(protocol, Config), _ = case gun:await(ConnPid, Ref) of {response, fin, 200, _} -> {error, timeout} = gun:await(ConnPid, Ref, 1000); %% See maybe_h3_error comment for details. {error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 -> ok end, gun:close(ConnPid). crash_before_reply(Config) -> doc("A plain handler crash before a response was sent " "results in a 500 response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/crash/no_reply", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 500, _} = gun:await(ConnPid, Ref), gun:close(ConnPid). external_exit_before_reply(Config) -> doc("A plain handler exits externally before a response was sent " "results in a 500 response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/crash/external_exit", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 500, _} = gun:await(ConnPid, Ref), gun:close(ConnPid). ================================================ FILE: test/proxy_header_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(proxy_header_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv/3]). %% ct. all() -> [ {group, http}, {group, https}, {group, h2}, {group, h2c}, {group, h2c_upgrade} ]. groups() -> Tests = ct_helper:all(?MODULE), [{h2c_upgrade, [parallel], Tests}|cowboy_test:common_groups(Tests)]. init_per_group(Name=http, Config) -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch()}, proxy_header => true }, Config); init_per_group(Name=https, Config) -> cowboy_test:init_https(Name, #{ env => #{dispatch => init_dispatch()}, proxy_header => true }, Config); init_per_group(Name=h2, Config) -> cowboy_test:init_http2(Name, #{ env => #{dispatch => init_dispatch()}, proxy_header => true }, Config); init_per_group(Name, Config) -> Config1 = cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch()}, proxy_header => true }, Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}). end_per_group(Name, _) -> cowboy:stop_listener(Name). %% Routes. init_dispatch() -> cowboy_router:compile([{"[...]", [ {"/direct/:key/[...]", echo_h, []} ]}]). %% Tests. fail_gracefully_on_disconnect(Config) -> doc("Probing a port must not generate a crash"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}]), timer:sleep(50), Pid = case config(type, Config) of tcp -> ct_helper:get_remote_pid_tcp(Socket); %% We connect to a TLS port using a TCP socket so we need %% to first obtain the remote pid of the TCP socket, which %% is a TLS socket on the server, and then get the real %% remote pid from its state. ssl -> ct_helper:get_remote_pid_tls_state(ct_helper:get_remote_pid_tcp(Socket)) end, Ref = erlang:monitor(process, Pid), gen_tcp:close(Socket), receive {'DOWN', Ref, process, Pid, {shutdown, closed}} -> ok; {'DOWN', Ref, process, Pid, Reason} -> error(Reason) after 500 -> error(timeout) end. v1_proxy_header(Config) -> doc("Confirm we can read the proxy header at the start of the connection."), ProxyInfo = #{ version => 1, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 444, dest_address => {192, 168, 0, 1}, dest_port => 443 }, do_proxy_header(Config, ProxyInfo). v2_proxy_header(Config) -> doc("Confirm we can read the proxy header at the start of the connection."), ProxyInfo = #{ version => 2, command => proxy, transport_family => ipv4, transport_protocol => stream, src_address => {127, 0, 0, 1}, src_port => 444, dest_address => {192, 168, 0, 1}, dest_port => 443 }, do_proxy_header(Config, ProxyInfo). v2_local_header(Config) -> doc("Confirm we can read the proxy header at the start of the connection."), ProxyInfo = #{ version => 2, command => local }, do_proxy_header(Config, ProxyInfo). do_proxy_header(Config, ProxyInfo) -> case config(ref, Config) of http -> do_proxy_header_http(Config, ProxyInfo); https -> do_proxy_header_https(Config, ProxyInfo); h2 -> do_proxy_header_h2(Config, ProxyInfo); h2c -> do_proxy_header_h2c(Config, ProxyInfo); h2c_upgrade -> do_proxy_header_h2c_upgrade(Config, ProxyInfo) end. do_proxy_header_http(Config, ProxyInfo) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}]), ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)), do_proxy_header_http_common({raw_client, Socket, gen_tcp}, ProxyInfo). do_proxy_header_https(Config, ProxyInfo) -> {ok, Socket0} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}]), ok = gen_tcp:send(Socket0, ranch_proxy_header:header(ProxyInfo)), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect(Socket0, TlsOpts, 1000), do_proxy_header_http_common({raw_client, Socket, ssl}, ProxyInfo). do_proxy_header_http_common(Client, ProxyInfo) -> ok = raw_send(Client, "GET /direct/proxy_header HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {_, 200, _, Rest0} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, Body0} = cow_http:parse_headers(Rest0), {_, LenBin} = lists:keyfind(<<"content-length">>, 1, Headers), Len = binary_to_integer(LenBin), Body = if byte_size(Body0) =:= Len -> Body0; true -> {ok, Body1} = raw_recv(Client, Len - byte_size(Body0), 5000), <> end, ProxyInfo = do_parse_term(Body), ok. do_proxy_header_h2(Config, ProxyInfo) -> {ok, Socket0} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}]), ok = gen_tcp:send(Socket0, ranch_proxy_header:header(ProxyInfo)), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect(Socket0, [{alpn_advertised_protocols, [<<"h2">>]}|TlsOpts], 1000), do_proxy_header_h2_common({raw_client, Socket, ssl}, ProxyInfo). do_proxy_header_h2c(Config, ProxyInfo) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}]), ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)), do_proxy_header_h2_common({raw_client, Socket, gen_tcp}, ProxyInfo). do_proxy_header_h2c_upgrade(Config, ProxyInfo) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}, {packet, raw}]), ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)), Client = {raw_client, Socket, gen_tcp}, ok = raw_send(Client, [ "GET /direct/proxy_header HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(iolist_to_binary(cow_http2:settings_payload(#{}))), "\r\n" "\r\n"]), ok = do_recv_101(Client), %% Receive the server preface. {ok, <>} = raw_recv(Client, 3, 1000), {ok, <<4:8, 0:40, _:PrefaceLen/binary>>} = raw_recv(Client, 6 + PrefaceLen, 1000), do_proxy_header_h2_response_common(Client, ProxyInfo), ok. do_proxy_header_h2_common(Client, ProxyInfo) -> %% Send a valid preface. ok = raw_send(Client, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, <>} = raw_recv(Client, 3, 1000), {ok, <<4:8, 0:40, _:PrefaceLen/binary>>} = raw_recv(Client, 6 + PrefaceLen, 1000), %% Send the SETTINGS ack. ok = raw_send(Client, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, <<0:24, 4:8, 1:8, 0:32>>} = raw_recv(Client, 9, 1000), %% Send a GET request. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/direct/proxy_header">>} ]), Len = iolist_size(HeadersBlock), ok = raw_send(Client, [ <>, HeadersBlock ]), do_proxy_header_h2_response_common(Client, ProxyInfo). do_proxy_header_h2_response_common(Client, ProxyInfo) -> %% Receive a response with the proxy header data. {ok, <>} = raw_recv(Client, 9, 1000), {ok, _} = raw_recv(Client, SkipLen, 1000), {ok, <>} = raw_recv(Client, 9, 1000), {ok, Body} = raw_recv(Client, BodyLen, 1000), ProxyInfo = do_parse_term(Body), ok. do_parse_term(Body) -> {ok, Tokens, _} = erl_scan:string(binary_to_list(Body) ++ "."), {ok, Exprs} = erl_parse:parse_exprs(Tokens), {value, Term, _} = erl_eval:exprs(Exprs, erl_eval:new_bindings()), Term. %% Match directly for now. do_recv_101(Client) -> {ok, << "HTTP/1.1 101 Switching Protocols\r\n" "connection: Upgrade\r\n" "upgrade: h2c\r\n" "\r\n" >>} = raw_recv(Client, 71, 1000), ok. ================================================ FILE: test/req_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(req_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. suite() -> Timeout = case os:type() of {win32, _} -> 120000; _ -> 30000 end, [{timetrap, Timeout}]. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_suite(Config) -> ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"), Config. end_per_suite(Config) -> ct_helper:delete_static_dir(config(priv_dir, Config) ++ "/static"). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Routes. init_dispatch(Config) -> cowboy_router:compile([{"[...]", [ {"/static/[...]", cowboy_static, {dir, config(priv_dir, Config) ++ "/static"}}, %% @todo Seriously InitialState should be optional. {"/resp/:key[/:arg]", resp_h, []}, {"/multipart[/:key]", multipart_h, []}, {"/args/:key/:arg[/:default]", echo_h, []}, {"/crash/:key/period", echo_h, #{length => 999999999, period => 1000, timeout => 5000, crash => true}}, {"/no-opts/:key", echo_h, #{crash => true}}, {"/opts/:key/length", echo_h, #{length => 1000}}, {"/opts/:key/period", echo_h, #{length => 999999999, period => 2000}}, {"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}}, {"/100-continue/:key", echo_h, []}, {"/full/:key", echo_h, []}, {"/auto-sync/:key", echo_h, []}, {"/auto-async/:key", echo_h, []}, {"/spawn/:key", echo_h, []}, {"/no/:key", echo_h, []}, {"/direct/:key/[...]", echo_h, []}, {"/:key/[...]", echo_h, []} ]}]). %% Internal. do_body(Method, Path, Config) -> do_body(Method, Path, [], Config). do_body(Method, Path, Headers, Config) -> do_body(Method, Path, Headers, <<>>, Config). do_body(Method, Path, Headers0, Body, Config) -> ConnPid = gun_open(Config), Headers = [{<<"accept-encoding">>, <<"gzip">>}|Headers0], Ref = gun:request(ConnPid, Method, Path, Headers, Body), {response, IsFin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, RespBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, gun:close(ConnPid), do_decode(RespHeaders, RespBody). do_body_error(Method, Path, Headers0, Body, Config) -> ConnPid = gun_open(Config), Headers = [{<<"accept-encoding">>, <<"gzip">>}|Headers0], Ref = gun:request(ConnPid, Method, Path, Headers, Body), {response, _, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid), {Status, RespHeaders}. do_get(Path, Config) -> do_get(Path, [], Config). do_get(Path, Headers, Config) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]), case gun:await(ConnPid, Ref, infinity) of {response, IsFin, Status, RespHeaders} -> {ok, RespBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, gun:close(ConnPid), {Status, RespHeaders, do_decode(RespHeaders, RespBody)}; {error, {stream_error, Error}} -> Error end. do_get_body(Path, Config) -> do_get_body(Path, [], Config). do_get_body(Path, Headers, Config) -> do_body("GET", Path, Headers, Config). do_get_inform(Path, Config) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]), case gun:await(ConnPid, Ref, infinity) of {response, _, RespStatus, RespHeaders} -> %% We don't care about the body. gun:close(ConnPid), {RespStatus, RespHeaders}; {inform, InfoStatus, InfoHeaders} -> {response, IsFin, RespStatus, RespHeaders} = case gun:await(ConnPid, Ref, infinity) of {inform, InfoStatus, InfoHeaders} -> gun:await(ConnPid, Ref, infinity); Response -> Response end, {ok, RespBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, gun:close(ConnPid), {InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)}; {error, {stream_error, Error}} -> Error end. do_decode(Headers, Body) -> case lists:keyfind(<<"content-encoding">>, 1, Headers) of {_, <<"gzip">>} -> zlib:gunzip(Body); _ -> Body end. do_get_error(Path, Config) -> do_get_error(Path, [], Config). do_get_error(Path, Headers, Config) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]), {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity), Result = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, case Result of {ok, RespBody} -> {Status, RespHeaders, do_decode(RespHeaders, RespBody)}; _ -> Result end. %% Tests: Request. binding(Config) -> doc("Value bound from request URI path with/without default."), <<"binding">> = do_get_body("/args/binding/key", Config), <<"binding">> = do_get_body("/args/binding/key/default", Config), <<"default">> = do_get_body("/args/binding/undefined/default", Config), ok. bindings(Config) -> doc("Values bound from request URI path."), <<"#{key => <<\"bindings\">>}">> = do_get_body("/bindings", Config), ok. cert(Config) -> case config(type, Config) of tcp -> doc("TLS certificates can only be provided over TLS."); ssl -> do_cert(Config); quic -> do_cert(Config) end. do_cert(Config) -> doc("A client TLS certificate was provided."), Cert = do_get_body("/cert", Config), Cert = do_get_body("/direct/cert", Config), ok. cert_undefined(Config) -> doc("No client TLS certificate was provided."), <<"undefined">> = do_get_body("/cert", [{no_cert, true}|Config]), <<"undefined">> = do_get_body("/direct/cert", [{no_cert, true}|Config]), ok. header(Config) -> doc("Request header with/without default."), <<"value">> = do_get_body("/args/header/defined", [{<<"defined">>, "value"}], Config), <<"value">> = do_get_body("/args/header/defined/default", [{<<"defined">>, "value"}], Config), <<"default">> = do_get_body("/args/header/undefined/default", [{<<"defined">>, "value"}], Config), ok. headers(Config) -> doc("Request headers."), do_headers("/headers", Config), do_headers("/direct/headers", Config). do_headers(Path, Config) -> %% We always send accept-encoding with this test suite's requests. <<"#{<<\"accept-encoding\">> => <<\"gzip\">>," "<<\"content-length\">> => <<\"0\">>," "<<\"header\">> => <<\"value\">>", _/bits>> = do_get_body(Path, [{<<"header">>, "value"}], Config), ok. host(Config) -> doc("Request URI host."), <<"localhost">> = do_get_body("/host", Config), <<"localhost">> = do_get_body("/direct/host", Config), ok. host_info(Config) -> doc("Request host_info."), <<"[<<\"localhost\">>]">> = do_get_body("/host_info", Config), ok. %% @todo Actually write the related unit tests. match_cookies(Config) -> doc("Matched request cookies."), <<"#{}">> = do_get_body("/match/cookies", [{<<"cookie">>, "a=b; c=d"}], Config), <<"#{a => <<\"b\">>}">> = do_get_body("/match/cookies/a", [{<<"cookie">>, "a=b; c=d"}], Config), <<"#{c => <<\"d\">>}">> = do_get_body("/match/cookies/c", [{<<"cookie">>, "a=b; c=d"}], Config), case do_get_body("/match/cookies/a/c", [{<<"cookie">>, "a=b; c=d"}], Config) of <<"#{a => <<\"b\">>,c => <<\"d\">>}">> -> ok; <<"#{c => <<\"d\">>,a => <<\"b\">>}">> -> ok end, %% Ensure match errors result in a 400 response. {400, _, _} = do_get("/match/cookies/a/c", [{<<"cookie">>, "a=b"}], Config), %% This function is tested more extensively through unit tests. ok. %% @todo Actually write the related unit tests. match_qs(Config) -> doc("Matched request URI query string."), <<"#{}">> = do_get_body("/match/qs?a=b&c=d", Config), <<"#{a => <<\"b\">>}">> = do_get_body("/match/qs/a?a=b&c=d", Config), <<"#{c => <<\"d\">>}">> = do_get_body("/match/qs/c?a=b&c=d", Config), case do_get_body("/match/qs/a/c?a=b&c=d", Config) of <<"#{a => <<\"b\">>,c => <<\"d\">>}">> -> ok; <<"#{c => <<\"d\">>,a => <<\"b\">>}">> -> ok end, case do_get_body("/match/qs/a/c?a=b&c", Config) of <<"#{a => <<\"b\">>,c => true}">> -> ok; <<"#{c => true,a => <<\"b\">>}">> -> ok end, case do_get_body("/match/qs/a/c?a&c=d", Config) of <<"#{a => true,c => <<\"d\">>}">> -> ok; <<"#{c => <<\"d\">>,a => true}">> -> ok end, %% Ensure match errors result in a 400 response. {400, _, _} = do_get("/match/qs/a/c?a=b", [], Config), {400, _, _} = do_get("/match/qs_with_constraints", [], Config), %% This function is tested more extensively through unit tests. ok. method(Config) -> doc("Request method."), do_method("/method", Config), do_method("/direct/method", Config). do_method(Path, Config) -> <<"GET">> = do_body("GET", Path, Config), <<>> = do_body("HEAD", Path, Config), <<"OPTIONS">> = do_body("OPTIONS", Path, Config), <<"PATCH">> = do_body("PATCH", Path, Config), <<"POST">> = do_body("POST", Path, Config), <<"PUT">> = do_body("PUT", Path, Config), <<"ZZZZZZZZ">> = do_body("ZZZZZZZZ", Path, Config), ok. parse_cookies(Config) -> doc("Request cookies."), <<"[]">> = do_get_body("/parse_cookies", Config), <<"[{<<\"cake\">>,<<\"strawberry\">>}]">> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config), %% Ensure parse errors result in a 400 response. {400, _, _} = do_get("/parse_cookies", [{<<"cookie">>, "bad\tname=strawberry"}], Config), {400, _, _} = do_get("/parse_cookies", [{<<"cookie">>, "goodname=strawberry\tmilkshake"}], Config), ok. filter_then_parse_cookies(Config) -> doc("Filter cookies then parse them."), <<"[]">> = do_get_body("/filter_then_parse_cookies", Config), <<"[{<<\"cake\">>,<<\"strawberry\">>}]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config), <<"[]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "bad name=strawberry"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>}]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "bad name=strawberry; another bad name=strawberry; cake=strawberry"}], Config), <<"[]">> = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "Blocked by http://www.example.com/upgrade-to-remove"}], Config), ok. parse_header(Config) -> doc("Parsed request header with/without default."), <<"[{{<<\"text\">>,<<\"html\">>,[]},1000,[]}]">> = do_get_body("/args/parse_header/accept", [{<<"accept">>, "text/html"}], Config), <<"[{{<<\"text\">>,<<\"html\">>,[]},1000,[]}]">> = do_get_body("/args/parse_header/accept/default", [{<<"accept">>, "text/html"}], Config), %% Header not in request but with default defined by Cowboy. <<"0">> = do_get_body("/args/parse_header/content-length", Config), %% Header not in request and no default from Cowboy. <<"undefined">> = do_get_body("/args/parse_header/upgrade", Config), %% Header in request and with default provided. <<"100-continue">> = do_get_body("/args/parse_header/expect/100-continue", Config), %% Ensure parse errors result in a 400 response. {400, _, _} = do_get("/args/parse_header/accept", [{<<"accept">>, "bad media type"}], Config), ok. parse_qs(Config) -> doc("Parsed request URI query string."), <<"[]">> = do_get_body("/parse_qs", Config), <<"[{<<\"abc\">>,true}]">> = do_get_body("/parse_qs?abc", Config), <<"[{<<\"a\">>,<<\"b\">>},{<<\"c\">>,<<\"d e\">>}]">> = do_get_body("/parse_qs?a=b&c=d+e", Config), %% Ensure parse errors result in a 400 response. {400, _, _} = do_get("/parse_qs?%%%%%%%", Config), ok. path(Config) -> doc("Request URI path."), do_path("/path", Config), do_path("/direct/path", Config). do_path(Path0, Config) -> Path = list_to_binary(Path0 ++ "/to/the/resource"), Path = do_get_body(Path, Config), Path = do_get_body([Path, "?query"], Config), Path = do_get_body([Path, "?query#fragment"], Config), Path = do_get_body([Path, "#fragment"], Config), ok. path_info(Config) -> doc("Request path_info."), <<"undefined">> = do_get_body("/no/path_info", Config), <<"[]">> = do_get_body("/path_info", Config), <<"[]">> = do_get_body("/path_info/", Config), <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource", Config), <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource?query", Config), <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource?query#fragment", Config), <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource#fragment", Config), ok. peer(Config) -> doc("Remote socket address."), <<"{{127,0,0,1},", _/bits >> = do_get_body("/peer", Config), <<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/peer", Config), ok. port(Config) -> doc("Request URI port."), Port = integer_to_binary(config(port, Config)), Port = do_get_body("/port", Config), Port = do_get_body("/direct/port", Config), ExpectedPort = case config(type, Config) of tcp -> <<"80">>; ssl -> <<"443">>; quic -> <<"443">> end, ExpectedPort = do_get_body("/port", [{<<"host">>, <<"localhost">>}], Config), ExpectedPort = do_get_body("/direct/port", [{<<"host">>, <<"localhost">>}], Config), ok. qs(Config) -> doc("Request URI query string."), do_qs("/qs", Config), do_qs("/direct/qs", Config). do_qs(Path, Config) -> <<>> = do_get_body(Path, Config), <<"abc">> = do_get_body(Path ++ "?abc", Config), <<"a=b&c=d+e">> = do_get_body(Path ++ "?a=b&c=d+e", Config), ok. scheme(Config) -> doc("Request URI scheme."), do_scheme("/scheme", Config), do_scheme("/direct/scheme", Config). do_scheme(Path, Config) -> Transport = config(type, Config), case do_get_body(Path, Config) of <<"http">> when Transport =:= tcp -> ok; <<"https">> when Transport =:= ssl -> ok; <<"https">> when Transport =:= quic -> ok end. sock(Config) -> doc("Local socket address."), <<"{{127,0,0,1},", _/bits >> = do_get_body("/sock", Config), <<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/sock", Config), ok. uri(Config) -> doc("Request URI building/modification."), Scheme = case config(type, Config) of tcp -> <<"http">>; ssl -> <<"https">>; quic -> <<"https">> end, SLen = byte_size(Scheme), Port = integer_to_binary(config(port, Config)), PLen = byte_size(Port), %% Absolute form. << Scheme:SLen/binary, "://localhost:", Port:PLen/binary, "/uri?qs" >> = do_get_body("/uri?qs", Config), %% Origin form. << "/uri/origin?qs" >> = do_get_body("/uri/origin?qs", Config), %% Protocol relative. << "//localhost:", Port:PLen/binary, "/uri/protocol-relative?qs" >> = do_get_body("/uri/protocol-relative?qs", Config), %% No query string. << Scheme:SLen/binary, "://localhost:", Port:PLen/binary, "/uri/no-qs" >> = do_get_body("/uri/no-qs?qs", Config), %% No path or query string. << Scheme:SLen/binary, "://localhost:", Port:PLen/binary >> = do_get_body("/uri/no-path?qs", Config), %% Changed port. << Scheme:SLen/binary, "://localhost:123/uri/set-port?qs" >> = do_get_body("/uri/set-port?qs", Config), %% This function is tested more extensively through unit tests. ok. version(Config) -> doc("Request HTTP version."), do_version("/version", Config), do_version("/direct/version", Config). do_version(Path, Config) -> Protocol = config(protocol, Config), case do_get_body(Path, Config) of <<"HTTP/1.1">> when Protocol =:= http -> ok; <<"HTTP/2">> when Protocol =:= http2 -> ok; <<"HTTP/3">> when Protocol =:= http3 -> ok end. %% Tests: Request body. body_length(Config) -> doc("Request body length."), <<"0">> = do_get_body("/body_length", Config), <<"12">> = do_body("POST", "/body_length", [], "hello world!", Config), ok. has_body(Config) -> doc("Has a request body?"), <<"false">> = do_get_body("/has_body", Config), <<"true">> = do_body("POST", "/has_body", [], "hello world!", Config), ok. read_body(Config) -> doc("Request body."), <<>> = do_get_body("/read_body", Config), <<"hello world!">> = do_body("POST", "/read_body", [], "hello world!", Config), %% We expect to have read *at least* 1000 bytes. <<0:8000, _/bits>> = do_body("POST", "/opts/read_body/length", [], <<0:8000000>>, Config), %% The timeout value is set too low on purpose to ensure a crash occurs. ok = do_read_body_timeout("/opts/read_body/timeout", <<0:8000000>>, Config), %% 10MB body larger than default length. <<0:80000000>> = do_body("POST", "/full/read_body", [], <<0:80000000>>, Config), ok. read_body_mtu(Config) -> case os:type() of {win32, _} -> {skip, "Loopback MTU size is 0xFFFFFFFF on Windows."}; {unix, _} -> doc("Request body whose sizes are around the MTU."), MTU = ct_helper:get_loopback_mtu(), _ = [begin Body = <<0:Size/unit:8>>, Body = do_body("POST", "/full/read_body", [], Body, Config) end || Size <- lists:seq(MTU - 10, MTU + 10)], ok end. read_body_period(Config) -> doc("Read the request body for at most 2 seconds."), ConnPid = gun_open(Config), Body = <<0:8000000>>, Ref = gun:headers(ConnPid, "POST", "/opts/read_body/period", [ {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} ]), %% The body is sent without fin. The server will read what it can %% for 2 seconds. The test succeeds if we get some of the data back %% (meaning the function will have returned after the period ends). gun:data(ConnPid, Ref, nofin, Body), Response = gun:await(ConnPid, Ref, infinity), case Response of {response, nofin, 200, _} -> {data, _, Data} = gun:await(ConnPid, Ref, infinity), %% We expect to read at least some data. true = Data =/= <<>>, gun:close(ConnPid); %% We got a crash, likely because the environment %% was overloaded and the timeout triggered. Try again. {response, _, 500, _} -> gun:close(ConnPid), read_body_period(Config) end. %% We expect a crash. do_read_body_timeout(Path, Body, Config) -> ConnPid = gun_open(Config), Ref = gun:headers(ConnPid, "POST", Path, [ {<<"content-length">>, integer_to_binary(byte_size(Body))} ]), case gun:await(ConnPid, Ref, infinity) of {response, _, 500, _} -> ok; %% See do_maybe_h3_error comment for details. {error, {stream_error, {stream_error, h3_internal_error, _}}} -> ok end, gun:close(ConnPid). read_body_auto(Config) -> doc("Read the request body using auto mode."), <<0:80000000>> = do_body("POST", "/auto-sync/read_body", [], <<0:80000000>>, Config), <<0:80000000>> = do_body("POST", "/auto-async/read_body", [], <<0:80000000>>, Config), ok. read_body_spawn(Config) -> doc("Confirm we can use cowboy_req:read_body/1,2 from another process."), <<"hello world!">> = do_body("POST", "/spawn/read_body", [], "hello world!", Config), ok. read_body_expect_100_continue(Config) -> doc("Request body with a 100-continue expect header."), do_read_body_expect_100_continue("/read_body", Config). read_body_expect_100_continue_user_sent(Config) -> doc("Request body with a 100-continue expect header, 100 response sent by handler."), do_read_body_expect_100_continue("/100-continue/read_body", Config). do_read_body_expect_100_continue(Path, Config) -> ConnPid = gun_open(Config), Body = <<0:8000000>>, Headers = [ {<<"accept-encoding">>, <<"gzip">>}, {<<"expect">>, <<"100-continue">>}, {<<"content-length">>, integer_to_binary(byte_size(Body))} ], Ref = gun:post(ConnPid, Path, Headers), {inform, 100, []} = gun:await(ConnPid, Ref, infinity), gun:data(ConnPid, Ref, fin, Body), {response, IsFin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, RespBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref, infinity); fin -> {ok, <<>>} end, gun:close(ConnPid), do_decode(RespHeaders, RespBody), ok. read_urlencoded_body(Config) -> doc("application/x-www-form-urlencoded request body."), <<"[]">> = do_body("POST", "/read_urlencoded_body", [], <<>>, Config), <<"[{<<\"abc\">>,true}]">> = do_body("POST", "/read_urlencoded_body", [], "abc", Config), <<"[{<<\"a\">>,<<\"b\">>},{<<\"c\">>,<<\"d e\">>}]">> = do_body("POST", "/read_urlencoded_body", [], "a=b&c=d+e", Config), %% The timeout value is set too low on purpose to ensure a crash occurs. ok = do_read_body_timeout("/opts/read_urlencoded_body/timeout", <<"abc">>, Config), %% Ensure parse errors result in a 400 response. {400, _} = do_body_error("POST", "/read_urlencoded_body", [], "%%%%%", Config), ok. read_urlencoded_body_too_large(Config) -> doc("application/x-www-form-urlencoded request body too large. " "Send a 10MB body, larger than the default length, to ensure a crash occurs."), do_read_urlencoded_body_too_large("/no-opts/read_urlencoded_body", string:chars($a, 10000000), Config). %% We expect a crash. do_read_urlencoded_body_too_large(Path, Body, Config) -> ConnPid = gun_open(Config), Ref = gun:headers(ConnPid, "POST", Path, [ {<<"content-length">>, integer_to_binary(iolist_size(Body))} ]), gun:data(ConnPid, Ref, fin, Body), Response = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid), case Response of {response, _, 413, _} -> ok; %% We got the wrong crash, likely because the environment %% was overloaded and the timeout triggered. Try again. {response, _, 408, _} -> do_read_urlencoded_body_too_large(Path, Body, Config); %% Timing issues make it possible for the connection to be %% closed before the data went through. We retry. {error, {stream_error, {closed, {error,closed}}}} -> do_read_urlencoded_body_too_large(Path, Body, Config) end. read_urlencoded_body_too_long(Config) -> doc("application/x-www-form-urlencoded request body sent too slow. " "The body is simply not being sent fully. It is read by the handler " "for at most 1 second. A crash occurs because we don't have the full body."), do_read_urlencoded_body_too_long("/crash/read_urlencoded_body/period", <<"abc">>, Config). %% We expect a crash. do_read_urlencoded_body_too_long(Path, Body, Config) -> ConnPid = gun_open(Config), Ref = gun:headers(ConnPid, "POST", Path, [ {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} ]), gun:data(ConnPid, Ref, nofin, Body), Protocol = config(protocol, Config), case gun:await(ConnPid, Ref, infinity) of {response, _, 408, RespHeaders} when Protocol =:= http -> %% 408 error responses should close HTTP/1.1 connections. {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders), gun:close(ConnPid); {response, _, 408, _} when Protocol =:= http2; Protocol =:= http3 -> gun:close(ConnPid); %% We must have hit the timeout due to busy CI environment. Retry. {response, _, 500, _} -> gun:close(ConnPid), do_read_urlencoded_body_too_long(Path, Body, Config) end. read_and_match_urlencoded_body(Config) -> doc("Read and match an application/x-www-form-urlencoded request body."), <<"#{}">> = do_body("POST", "/match/body_qs", [], "a=b&c=d", Config), <<"#{a => <<\"b\">>}">> = do_body("POST", "/match/body_qs/a", [], "a=b&c=d", Config), <<"#{c => <<\"d\">>}">> = do_body("POST", "/match/body_qs/c", [], "a=b&c=d", Config), case do_body("POST", "/match/body_qs/a/c", [], "a=b&c=d", Config) of <<"#{a => <<\"b\">>,c => <<\"d\">>}">> -> ok; <<"#{c => <<\"d\">>,a => <<\"b\">>}">> -> ok end, case do_body("POST", "/match/body_qs/a/c", [], "a=b&c", Config) of <<"#{a => <<\"b\">>,c => true}">> -> ok; <<"#{c => true,a => <<\"b\">>}">> -> ok end, case do_body("POST", "/match/body_qs/a/c", [], "a&c=d", Config) of <<"#{a => true,c => <<\"d\">>}">> -> ok; <<"#{c => <<\"d\">>,a => true}">> -> ok end, %% Ensure match errors result in a 400 response. {400, _} = do_body_error("POST", "/match/body_qs/a/c", [], "a=b", Config), %% Ensure parse errors result in a 400 response. {400, _} = do_body_error("POST", "/match/body_qs", [], "%%%%%", Config), %% The timeout value is set too low on purpose to ensure a crash occurs. ok = do_read_body_timeout("/opts/read_and_match_urlencoded_body/timeout", <<"abc">>, Config), ok. read_and_match_urlencoded_body_too_large(Config) -> doc("Read and match an application/x-www-form-urlencoded request body too large. " "Send a 10MB body, larger than the default length, to ensure a crash occurs."), do_read_urlencoded_body_too_large( "/no-opts/read_and_match_urlencoded_body", string:chars($a, 10000000), Config). read_and_match_urlencoded_body_too_long(Config) -> doc("Read and match an application/x-www-form-urlencoded request body sent too slow. " "The body is simply not being sent fully. It is read by the handler " "for at most 1 second. A crash occurs because we don't have the full body."), do_read_urlencoded_body_too_long( "/crash/read_and_match_urlencoded_body/period", <<"abc">>, Config). multipart(Config) -> doc("Multipart request body."), do_multipart("/multipart", Config). do_multipart(Path, Config) -> LargeBody = iolist_to_binary(string:chars($a, 10000000)), ReqBody = [ "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n" "--deadbeef--" ], RespBody = do_body("POST", Path, [ {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} ], ReqBody, Config), [ {#{<<"content-type">> := <<"text/plain">>}, <<"Cowboy is an HTTP server.">>}, {LargeHeaders, LargeBody} ] = binary_to_term(RespBody), #{ <<"content-type">> := <<"application/octet-stream">>, <<"x-custom">> := <<"value">> } = LargeHeaders, ok. multipart_error_empty(Config) -> doc("Multipart request body is empty."), %% We use an empty list as a body to make sure Gun knows %% we want to send an empty body. %% @todo This is a terrible hack. Improve Gun! Body = [], %% Ensure an empty body results in a 400 error. {400, _} = do_body_error("POST", "/multipart", [ {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} ], Body, Config), ok. multipart_error_preamble_only(Config) -> doc("Multipart request body only contains a preamble."), %% Ensure an empty body results in a 400 error. {400, _} = do_body_error("POST", "/multipart", [ {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} ], <<"Preamble.">>, Config), ok. multipart_error_headers(Config) -> doc("Multipart request body with invalid part headers."), ReqBody = [ "--deadbeef\r\nbad-header text/plain\r\n\r\nCowboy is an HTTP server.\r\n" "--deadbeef--" ], %% Ensure parse errors result in a 400 response. {400, _} = do_body_error("POST", "/multipart", [ {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} ], ReqBody, Config), ok. %% The function to parse the multipart body currently does not crash, %% as far as I can tell. There is therefore no test for it. multipart_error_no_final_boundary(Config) -> doc("Multipart request body with no final boundary."), ReqBody = [ "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" ], %% Ensure parse errors result in a 400 response. {400, _} = do_body_error("POST", "/multipart", [ {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} ], ReqBody, Config), ok. multipart_missing_boundary(Config) -> doc("Multipart request body without a boundary in the media type."), ReqBody = [ "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" "--deadbeef--" ], %% Ensure parse errors result in a 400 response. {400, _} = do_body_error("POST", "/multipart", [ {<<"content-type">>, <<"multipart/mixed">>} ], ReqBody, Config), ok. read_part_skip_body(Config) -> doc("Multipart request body skipping part bodies."), LargeBody = iolist_to_binary(string:chars($a, 10000000)), ReqBody = [ "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n" "--deadbeef--" ], RespBody = do_body("POST", "/multipart/skip_body", [ {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} ], ReqBody, Config), [ #{<<"content-type">> := <<"text/plain">>}, LargeHeaders ] = binary_to_term(RespBody), #{ <<"content-type">> := <<"application/octet-stream">>, <<"x-custom">> := <<"value">> } = LargeHeaders, ok. %% @todo When reading a multipart body, length and period %% only apply to a single read_body call. We may want a %% separate option to know how many reads we want to do %% before we give up. read_part2(Config) -> doc("Multipart request body using read_part/2."), %% Override the length and period values only, making %% the request process use more read_body calls. %% %% We do not try a custom timeout value since this would %% be the same test as read_body/2. do_multipart("/multipart/read_part2", Config). read_part_body2(Config) -> doc("Multipart request body using read_part_body/2."), %% Override the length and period values only, making %% the request process use more read_body calls. %% %% We do not try a custom timeout value since this would %% be the same test as read_body/2. do_multipart("/multipart/read_part_body2", Config). %% Tests: Response. %% @todo We want to crash when calling set_resp_* or related %% functions after the reply has been sent. set_resp_cookie(Config) -> doc("Response using set_resp_cookie."), %% Single cookie, no options. {200, Headers1, _} = do_get("/resp/set_resp_cookie3", Config), {_, <<"mycookie=myvalue">>} = lists:keyfind(<<"set-cookie">>, 1, Headers1), %% Single cookie, with options. {200, Headers2, _} = do_get("/resp/set_resp_cookie4", Config), {_, <<"mycookie=myvalue; Path=/resp/set_resp_cookie4">>} = lists:keyfind(<<"set-cookie">>, 1, Headers2), %% Multiple cookies. {200, Headers3, _} = do_get("/resp/set_resp_cookie3/multiple", Config), [_, _] = [H || H={<<"set-cookie">>, _} <- Headers3], %% Overwrite previously set cookie. {200, Headers4, _} = do_get("/resp/set_resp_cookie3/overwrite", Config), {_, <<"mycookie=overwrite">>} = lists:keyfind(<<"set-cookie">>, 1, Headers4), ok. set_resp_header(Config) -> doc("Response using set_resp_header."), {200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config), true = lists:keymember(<<"content-type">>, 1, Headers), %% The set-cookie header is special. set_resp_cookie must be used. {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_header_cookie", Config)), ok. set_resp_headers(Config) -> doc("Response using set_resp_headers."), {200, Headers1, <<"OK">>} = do_get("/resp/set_resp_headers", Config), true = lists:keymember(<<"content-type">>, 1, Headers1), true = lists:keymember(<<"content-encoding">>, 1, Headers1), {200, Headers2, <<"OK">>} = do_get("/resp/set_resp_headers_list", Config), true = lists:keymember(<<"content-type">>, 1, Headers2), true = lists:keymember(<<"content-encoding">>, 1, Headers2), {_, <<"one, two">>} = lists:keyfind(<<"test-header">>, 1, Headers2), %% The set-cookie header is special. set_resp_cookie must be used. {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)), {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_list_cookie", Config)), ok. resp_header(Config) -> doc("Response header with/without default."), {200, _, <<"OK">>} = do_get("/resp/resp_header_defined", Config), {200, _, <<"OK">>} = do_get("/resp/resp_header_default", Config), ok. resp_headers(Config) -> doc("Get all response headers."), {200, _, <<"OK">>} = do_get("/resp/resp_headers", Config), {200, _, <<"OK">>} = do_get("/resp/resp_headers_empty", Config), ok. set_resp_body(Config) -> doc("Response using set_resp_body."), {200, _, <<"OK">>} = do_get("/resp/set_resp_body", Config), {200, _, <<"OVERRIDE">>} = do_get("/resp/set_resp_body/override", Config), {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")), {200, _, AppFile} = do_get("/resp/set_resp_body/sendfile", Config), ok. set_resp_body_sendfile0(Config) -> doc("Response using set_resp_body with a sendfile of length 0."), Path = "/resp/set_resp_body/sendfile0", ConnPid = gun_open(Config), %% First request. Ref1 = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]), {response, IsFin, 200, _} = gun:await(ConnPid, Ref1, infinity), {ok, <<>>} = case IsFin of nofin -> gun:await_body(ConnPid, Ref1, infinity); fin -> {ok, <<>>} end, %% Second request will confirm everything works as intended. Ref2 = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]), {response, IsFin, 200, _} = gun:await(ConnPid, Ref2, infinity), {ok, <<>>} = case IsFin of nofin -> gun:await_body(ConnPid, Ref2, infinity); fin -> {ok, <<>>} end, gun:close(ConnPid), ok. has_resp_header(Config) -> doc("Has response header?"), {200, Headers, <<"OK">>} = do_get("/resp/has_resp_header", Config), true = lists:keymember(<<"content-type">>, 1, Headers), ok. has_resp_body(Config) -> doc("Has response body?"), {200, _, <<"OK">>} = do_get("/resp/has_resp_body", Config), {200, _, <<"OK">>} = do_get("/resp/has_resp_body/sendfile", Config), ok. delete_resp_header(Config) -> doc("Delete response header."), {200, Headers, <<"OK">>} = do_get("/resp/delete_resp_header", Config), false = lists:keymember(<<"content-type">>, 1, Headers), ok. %% Data may be lost due to how RESET_STREAM QUIC frame works. %% Because there is ongoing work for a better way to reset streams %% (https://www.ietf.org/archive/id/draft-ietf-quic-reliable-stream-reset-03.html) %% we convert the error to a 500 to keep the tests more explicit %% at what we expect. %% @todo When RESET_STREAM_AT gets added we can remove this function. do_maybe_h3_error2({stream_error, h3_internal_error, _}) -> {500, []}; do_maybe_h3_error2(Result) -> Result. do_maybe_h3_error3({stream_error, h3_internal_error, _}) -> {500, [], <<>>}; do_maybe_h3_error3(Result) -> Result. inform2(Config) -> doc("Informational response(s) without headers, followed by the real response."), {102, [], 200, _, _} = do_get_inform("/resp/inform2/102", Config), {102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config), {500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform2/error", Config)), {102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config), %% With HTTP/1.1 and HTTP/2 we will not get an error. %% With HTTP/3 however the stream will occasionally %% be reset before Gun receives the response. case do_get_inform("/resp/inform2/after_reply", Config) of {200, _} -> ok; {stream_error, h3_internal_error, _} -> ok end. inform3(Config) -> doc("Informational response(s) with headers, followed by the real response."), Headers = [{<<"ext-header">>, <<"ext-value">>}], {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config), {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config), {500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/error", Config)), %% The set-cookie header is special. set_resp_cookie must be used. {500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/set_cookie", Config)), {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config), %% With HTTP/1.1 and HTTP/2 we will not get an error. %% With HTTP/3 however the stream will occasionally %% be reset before Gun receives the response. case do_get_inform("/resp/inform3/after_reply", Config) of {200, _} -> ok; {stream_error, h3_internal_error, _} -> ok end. reply2(Config) -> doc("Response with default headers and no body."), {200, _, _} = do_get("/resp/reply2/200", Config), {201, _, _} = do_get("/resp/reply2/201", Config), {404, _, _} = do_get("/resp/reply2/404", Config), {200, _, _} = do_get("/resp/reply2/binary", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply2/error", Config)), %% @todo How to test this properly? This isn't enough. {200, _, _} = do_get("/resp/reply2/twice", Config), ok. reply3(Config) -> doc("Response with additional headers and no body."), {200, Headers1, _} = do_get("/resp/reply3/200", Config), true = lists:keymember(<<"content-type">>, 1, Headers1), {201, Headers2, _} = do_get("/resp/reply3/201", Config), true = lists:keymember(<<"content-type">>, 1, Headers2), {404, Headers3, _} = do_get("/resp/reply3/404", Config), true = lists:keymember(<<"content-type">>, 1, Headers3), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/error", Config)), %% The set-cookie header is special. set_resp_cookie must be used. {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/set_cookie", Config)), ok. reply4(Config) -> doc("Response with additional headers and body."), {200, _, <<"OK">>} = do_get("/resp/reply4/200", Config), {201, _, <<"OK">>} = do_get("/resp/reply4/201", Config), {404, _, <<"OK">>} = do_get("/resp/reply4/404", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/error", Config)), %% The set-cookie header is special. set_resp_cookie must be used. {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/set_cookie", Config)), ok. stream_reply2(Config) -> doc("Response with default headers and streamed body."), Body = <<0:8000000>>, {200, _, Body} = do_get("/resp/stream_reply2/200", Config), {201, _, Body} = do_get("/resp/stream_reply2/201", Config), {404, _, Body} = do_get("/resp/stream_reply2/404", Config), {200, _, Body} = do_get("/resp/stream_reply2/binary", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply2/error", Config)), ok. stream_reply2_twice(Config) -> doc("Attempting to stream a response twice results in a crash."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/twice", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity), Protocol = config(protocol, Config), Flavor = config(flavor, Config), case {Protocol, Flavor, gun:await_body(ConnPid, Ref, infinity)} of %% In HTTP/1.1 we cannot propagate an error at that point. %% The response will simply not have a body. {http, vanilla, {ok, <<>>}} -> ok; %% When compression was used we do get gzip headers. But %% we do not have any data in the zlib stream. {http, compress, {ok, Data}} -> Z = zlib:open(), zlib:inflateInit(Z, 31), 0 = iolist_size(zlib:inflate(Z, Data)), ok; %% In HTTP/2 and HTTP/3 the stream gets reset with an appropriate error. {http2, _, {error, {stream_error, {stream_error, internal_error, _}}}} -> ok; {http3, _, {error, {stream_error, {stream_error, h3_internal_error, _}}}} -> ok end, gun:close(ConnPid). stream_reply3(Config) -> doc("Response with additional headers and streamed body."), Body = <<0:8000000>>, {200, Headers1, Body} = do_get("/resp/stream_reply3/200", Config), true = lists:keymember(<<"content-type">>, 1, Headers1), {201, Headers2, Body} = do_get("/resp/stream_reply3/201", Config), true = lists:keymember(<<"content-type">>, 1, Headers2), {404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config), true = lists:keymember(<<"content-type">>, 1, Headers3), {500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/error", Config)), %% The set-cookie header is special. set_resp_cookie must be used. {500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/set_cookie", Config)), ok. stream_body_fin0(Config) -> doc("Streamed body with last chunk of size 0."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/fin0", Config), ok. stream_body_multiple(Config) -> doc("Streamed body via multiple calls."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/multiple", Config), ok. stream_body_loop(Config) -> doc("Streamed body via a fast loop."), {200, _, <<0:32000000/unit:8>>} = do_get("/resp/stream_body/loop", Config), ok. stream_body_nofin(Config) -> doc("Unfinished streamed body."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/nofin", Config), ok. stream_body_sendfile(Config) -> doc("Streamed body via multiple calls, including sendfile calls."), {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")), ExpectedBody = iolist_to_binary([ <<"Hello ">>, AppFile, <<" interspersed ">>, AppFile, <<" world!">> ]), {200, _, ExpectedBody} = do_get("/resp/stream_body/sendfile", Config), ok. stream_body_sendfile_fin(Config) -> doc("Streamed body via multiple calls, including a sendfile final call."), {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")), ExpectedBody = iolist_to_binary([ <<"Hello! ">>, AppFile ]), {200, _, ExpectedBody} = do_get("/resp/stream_body/sendfile_fin", Config), ok. stream_body_spawn(Config) -> doc("Confirm we can use cowboy_req:stream_body/3 from another process."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/spawn", Config), ok. stream_body_content_length_multiple(Config) -> doc("Streamed body via multiple calls."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body_content_length/multiple", Config), ok. stream_body_content_length_fin0(Config) -> doc("Streamed body with last chunk of size 0."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body_content_length/fin0", Config), ok. stream_body_content_length_nofin(Config) -> doc("Unfinished streamed body."), {200, _, <<"Hello world!">>} = do_get("/resp/stream_body_content_length/nofin", Config), ok. stream_body_content_length_nofin_error(Config) -> doc("Not all of the response body sent."), case config(protocol, Config) of http -> case do_get_error("/resp/stream_body_content_length/nofin-error", Config) of %% When compression is used content-length is not sent. {200, Headers, <<"Hello">>} -> {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers); %% The server closes the connection when the body couldn't be sent fully. {error, {stream_error, closed}} -> receive {gun_down, ConnPid, _, _, _} -> gun:close(ConnPid) after 1000 -> error(timeout) end end; http2 -> %% @todo HTTP/2 should have the same content-length checks. {skip, "Implement the test for HTTP/2."}; http3 -> %% @todo HTTP/3 should have the same content-length checks. {skip, "Implement the test for HTTP/3."} end. stream_body_concurrent(Config) -> ConnPid = gun_open(Config), Ref1 = gun:get(ConnPid, "/resp/stream_body/loop", [{<<"accept-encoding">>, <<"gzip">>}]), Ref2 = gun:get(ConnPid, "/resp/stream_body/loop", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, _} = gun:await(ConnPid, Ref1, infinity), {ok, _} = gun:await_body(ConnPid, Ref1, infinity), {response, nofin, 200, _} = gun:await(ConnPid, Ref2, infinity), {ok, _} = gun:await_body(ConnPid, Ref2, infinity), gun:close(ConnPid). %% @todo Crash when calling stream_body after the fin flag has been set. %% @todo Crash when calling stream_body after calling reply. %% @todo Crash when calling stream_body before calling stream_reply. stream_events_single(Config) -> doc("Streamed event."), {200, Headers, << "event: add_comment\n" "data: Comment text.\n" "data: With many lines.\n" "\n" >>} = do_get("/resp/stream_events/single", Config), {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. stream_events_list(Config) -> doc("Streamed list of events."), {200, Headers, << "event: add_comment\n" "data: Comment text.\n" "data: With many lines.\n" "\n" ": Set retry higher\n" ": with many lines also.\n" "retry: 10000\n" "\n" "id: 123\n" "event: add_comment\n" "data: Closing!\n" "\n" >>} = do_get("/resp/stream_events/list", Config), {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. stream_events_multiple(Config) -> doc("Streamed events via multiple calls."), {200, Headers, << "event: add_comment\n" "data: Comment text.\n" "data: With many lines.\n" "\n" ": Set retry higher\n" ": with many lines also.\n" "retry: 10000\n" "\n" "id: 123\n" "event: add_comment\n" "data: Closing!\n" "\n" >>} = do_get("/resp/stream_events/multiple", Config), {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. stream_trailers(Config) -> doc("Stream body followed by trailer headers."), {200, RespHeaders, <<"Hello world!">>, [ {<<"grpc-status">>, <<"0">>} ]} = do_trailers("/resp/stream_trailers", Config), {_, <<"grpc-status">>} = lists:keyfind(<<"trailer">>, 1, RespHeaders), ok. stream_trailers_large(Config) -> doc("Stream large body followed by trailer headers."), {200, RespHeaders, <<0:80000000>>, [ {<<"grpc-status">>, <<"0">>} ]} = do_trailers("/resp/stream_trailers/large", Config), {_, <<"grpc-status">>} = lists:keyfind(<<"trailer">>, 1, RespHeaders), ok. stream_trailers_no_te(Config) -> doc("Stream body followed by trailer headers without a te header in the request."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_trailers", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity), %% @todo Do we want to remove the trailer header automatically? % false = lists:keyfind(<<"trailer">>, 1, RespHeaders), {ok, RespBody} = gun:await_body(ConnPid, Ref, infinity), <<"Hello world!">> = do_decode(RespHeaders, RespBody), gun:close(ConnPid). stream_trailers_set_cookie(Config) -> doc("Trying to send set-cookie in trailers should result in a crash."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_trailers/set_cookie", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"te">>, <<"trailers">>} ]), Protocol = config(protocol, Config), case gun:await(ConnPid, Ref, infinity) of {response, nofin, 200, _} when Protocol =:= http -> %% Trailers are not sent because of the stream error. {ok, _Body} = gun:await_body(ConnPid, Ref, infinity), {error, timeout} = gun:await_body(ConnPid, Ref, 1000), ok; {response, nofin, 200, _} when Protocol =:= http2 -> {error, {stream_error, {stream_error, internal_error, _}}} = gun:await_body(ConnPid, Ref, infinity), ok; {response, nofin, 200, _} when Protocol =:= http3 -> {error, {stream_error, {stream_error, h3_internal_error, _}}} = gun:await_body(ConnPid, Ref, infinity), ok; %% The RST_STREAM arrived before the start of the response. %% See maybe_h3_error comment for details. {error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 -> ok end, gun:close(ConnPid). do_trailers(Path, Config) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"te">>, <<"trailers">>} ]), {response, nofin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity), {ok, RespBody, Trailers} = gun:await_body(ConnPid, Ref, infinity), gun:close(ConnPid), {Status, RespHeaders, do_decode(RespHeaders, RespBody), Trailers}. %% @todo Crash when calling stream_trailers twice. %% @todo Crash when calling stream_trailers after the fin flag has been set. %% @todo Crash when calling stream_trailers after calling reply. %% @todo Crash when calling stream_trailers before calling stream_reply. %% Tests: Push. %% @todo We want to crash when push is called after reply has been initiated. push(Config) -> case config(protocol, Config) of http -> do_push_http("/resp/push", Config); http2 -> do_push_http2(Config); http3 -> {skip, "Implement server push for HTTP/3."} end. push_after_reply(Config) -> doc("Trying to push a response after the final response results in a crash."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/push/after_reply", []), %% With HTTP/1.1 and HTTP/2 we will not get an error. %% With HTTP/3 however the stream will occasionally %% be reset before Gun receives the response. case gun:await(ConnPid, Ref, infinity) of {response, fin, 200, _} -> ok; {error, {stream_error, {stream_error, h3_internal_error, _}}} -> ok end, gun:close(ConnPid). push_method(Config) -> case config(protocol, Config) of http -> do_push_http("/resp/push/method", Config); http2 -> do_push_http2_method(Config); http3 -> {skip, "Implement server push for HTTP/3."} end. push_origin(Config) -> case config(protocol, Config) of http -> do_push_http("/resp/push/origin", Config); http2 -> do_push_http2_origin(Config); http3 -> {skip, "Implement server push for HTTP/3."} end. push_qs(Config) -> case config(protocol, Config) of http -> do_push_http("/resp/push/qs", Config); http2 -> do_push_http2_qs(Config); http3 -> {skip, "Implement server push for HTTP/3."} end. do_push_http(Path, Config) -> doc("Ignore pushed responses when protocol is HTTP/1.1."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, []), {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid). do_push_http2(Config) -> doc("Pushed responses."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/push", []), %% We expect two pushed resources. Origin = iolist_to_binary([ case config(type, Config) of tcp -> "http"; ssl -> "https" end, "://localhost:", integer_to_binary(config(port, Config)) ]), OriginLen = byte_size(Origin), {push, PushCSS, <<"GET">>, <>, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity), {push, PushTXT, <<"GET">>, <>, [{<<"accept">>,<<"text/plain">>}]} = gun:await(ConnPid, Ref, infinity), %% Pushed CSS. {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS, infinity), %% Pushed TXT is 406 because the pushed accept header uses an undefined type. {response, fin, 406, _} = gun:await(ConnPid, PushTXT, infinity), %% Let's not forget about the response to the client's request. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid). do_push_http2_method(Config) -> doc("Pushed response with non-GET method."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/push/method", []), %% Pushed CSS. {push, PushCSS, <<"HEAD">>, _, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity), {response, fin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), %% Let's not forget about the response to the client's request. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid). do_push_http2_origin(Config) -> doc("Pushed response with custom scheme/host/port."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/push/origin", []), %% Pushed CSS. {push, PushCSS, <<"GET">>, <<"ftp://127.0.0.1:21/static/style.css">>, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity), {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS, infinity), %% Let's not forget about the response to the client's request. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid). do_push_http2_qs(Config) -> doc("Pushed response with query string."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/push/qs", []), %% Pushed CSS. Origin = iolist_to_binary([ case config(type, Config) of tcp -> "http"; ssl -> "https" end, "://localhost:", integer_to_binary(config(port, Config)) ]), OriginLen = byte_size(Origin), {push, PushCSS, <<"GET">>, <>, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity), {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS, infinity), %% Let's not forget about the response to the client's request. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity), gun:close(ConnPid). ================================================ FILE: test/rest_handler_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rest_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% ct. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Dispatch configuration. init_dispatch(_) -> cowboy_router:compile([{'_', [ {"/", rest_hello_h, []}, {"/accept_callback", accept_callback_h, []}, {"/accept_callback_missing", accept_callback_missing_h, []}, {"/charsets_provided", charsets_provided_h, []}, {"/charsets_provided_empty", charsets_provided_empty_h, []}, {"/charset_in_content_types_provided", charset_in_content_types_provided_h, []}, {"/charset_in_content_types_provided_implicit", charset_in_content_types_provided_implicit_h, []}, {"/charset_in_content_types_provided_implicit_no_callback", charset_in_content_types_provided_implicit_no_callback_h, []}, {"/content_types_accepted", content_types_accepted_h, []}, {"/content_types_provided", content_types_provided_h, []}, {"/delete_resource", delete_resource_h, []}, {"/create_resource", create_resource_h, []}, {"/expires", expires_h, []}, {"/generate_etag", generate_etag_h, []}, {"/if_range", if_range_h, []}, {"/last_modified", last_modified_h, []}, {"/provide_callback_missing", provide_callback_missing_h, []}, {"/provide_range_callback", provide_range_callback_h, []}, {"/range_satisfiable", range_satisfiable_h, []}, {"/ranges_provided", ranges_provided_h, []}, {"/ranges_provided_auto", ranges_provided_auto_h, []}, {"/rate_limited", rate_limited_h, []}, {"/stop_handler", stop_handler_h, []}, {"/switch_handler", switch_handler_h, run}, {"/switch_handler_opts", switch_handler_h, hibernate} ]}]). %% Internal. do_decode(Headers, Body) -> case lists:keyfind(<<"content-encoding">>, 1, Headers) of {_, <<"gzip">>} -> zlib:gunzip(Body); _ -> Body end. %% Tests. accept_callback_missing(Config) -> doc("A 500 response must be sent when the AcceptCallback can't be called."), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/accept_callback_missing", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ], <<"Missing!">>), {response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)), ok. accept_callback_patch_false(Config) -> do_accept_callback_false(Config, patch). accept_callback_patch_true(Config) -> do_accept_callback_true(Config, patch). accept_callback_post_false(Config) -> do_accept_callback_false(Config, post). accept_callback_post_true(Config) -> do_accept_callback_true(Config, post). accept_callback_put_false(Config) -> do_accept_callback_false(Config, put). accept_callback_put_true(Config) -> do_accept_callback_true(Config, put). do_accept_callback_false(Config, Fun) -> doc("When AcceptCallback returns false a 400 response must be returned."), ConnPid = gun_open(Config), Ref = gun:Fun(ConnPid, "/accept_callback?false", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ], <<"Request body.">>), {response, _, 400, _} = gun:await(ConnPid, Ref), ok. do_accept_callback_true(Config, Fun) -> doc("When AcceptCallback returns true a 204 response must be returned."), ConnPid = gun_open(Config), Ref = gun:Fun(ConnPid, "/accept_callback?true", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ], <<"Request body.">>), {response, _, 204, _} = gun:await(ConnPid, Ref), ok. charset_in_content_types_provided(Config) -> doc("When a charset is matched explicitly in content_types_provided, " "that charset is used and the charsets_provided callback is ignored."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charset_in_content_types_provided", [ {<<"accept">>, <<"text/plain;charset=utf-8">>}, {<<"accept-charset">>, <<"utf-16, utf-8;q=0.5">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_in_content_types_provided_implicit_match(Config) -> doc("When a charset is matched implicitly in content_types_provided, " "the charsets_provided callback is used to determine if the media " "type will match."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit", [ {<<"accept">>, <<"text/plain;charset=utf-16">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_in_content_types_provided_implicit_nomatch(Config) -> doc("When a charset is matched implicitly in content_types_provided, " "the charsets_provided callback is used to determine if the media " "type will match. If it doesn't, try the next media type."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit", [ {<<"accept">>, <<"text/plain;charset=utf-32, text/plain">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), %% We end up with the first charset listed in charsets_provided. {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_in_content_types_provided_implicit_nomatch_error(Config) -> doc("When a charset is matched implicitly in content_types_provided, " "the charsets_provided callback is used to determine if the media " "type will match. If it doesn't, and there's no other media type, " "a 406 is returned."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit", [ {<<"accept">>, <<"text/plain;charset=utf-32">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 406, _} = gun:await(ConnPid, Ref), ok. charset_in_content_types_provided_implicit_no_callback(Config) -> doc("When a charset is matched implicitly in content_types_provided, " "and the charsets_provided callback is not exported, the media " "type will match but the charset will be ignored like all other " "parameters."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit_no_callback", [ {<<"accept">>, <<"text/plain;charset=utf-32">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), %% The charset is ignored as if it was any other parameter. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_match_text(Config) -> doc("When the media type is text and the charsets_provided callback exists " "and the accept-charset header was sent, the selected charset is sent " "back in the content-type of the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-charset">>, <<"utf-8;q=0.5, utf-16">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_match_other(Config) -> doc("When the media type is not text and the charsets_provided callback exists " "and the accept-charset header was sent, the selected charset is not sent " "back in the content-type of the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"application/json">>}, {<<"accept-charset">>, <<"utf-8;q=0.5, utf-16">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"application/json">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_wildcard_text(Config) -> doc("When the media type is text and the charsets_provided callback exists " "and a wildcard accept-charset header was sent, the selected charset is sent " "back in the content-type of the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-charset">>, <<"*">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_wildcard_other(Config) -> doc("When the media type is not text and the charsets_provided callback exists " "and a wildcard accept-charset header was sent, the selected charset is not sent " "back in the content-type of the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"application/json">>}, {<<"accept-charset">>, <<"*">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"application/json">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_nomatch(Config) -> doc("Regardless of the media type negotiated, if no charset is found in the " "accept-charset header match a charset configured in charsets_provided, " "then a 406 not acceptable response is sent back."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-charset">>, <<"utf-8;q=0, iso-8859-1">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 406, _} = gun:await(ConnPid, Ref), ok. charsets_provided_noheader_text(Config) -> doc("When the media type is text and the charsets_provided callback exists " "but the accept-charset header was not sent, the first charset in the " "list is selected and sent back in the content-type of the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_noheader_other(Config) -> doc("When the media type is not text and the charsets_provided callback exists " "but the accept-charset header was not sent, the first charset in the " "list is selected but is not sent back in the content-type of the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided", [ {<<"accept">>, <<"application/json">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"application/json">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charsets_provided_empty(Config) -> doc("Regardless of the media type negotiated, if the charsets_provided " "callback returns an empty list a 406 not acceptable response is sent back."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided_empty", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-charset">>, <<"utf-8q=0.5, utf-16">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 406, _} = gun:await(ConnPid, Ref), ok. charsets_provided_empty_wildcard(Config) -> doc("Regardless of the media type negotiated, if the charsets_provided " "callback returns an empty list a 406 not acceptable response is sent back."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided_empty", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-charset">>, <<"*">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 406, _} = gun:await(ConnPid, Ref), ok. charsets_provided_empty_noheader(Config) -> doc("Regardless of the media type negotiated, if the charsets_provided " "callback returns an empty list a 406 not acceptable response is sent back."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/charsets_provided_empty", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 406, _} = gun:await(ConnPid, Ref), ok. content_type_invalid(Config) -> doc("An invalid content-type in a POST/PATCH/PUT request " "must be rejected with a 415 unsupported media type response. (RFC7231 6.5.13)"), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/content_types_accepted?wildcard-param", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain, text/html">>} ]), {response, fin, 415, _} = gun:await(ConnPid, Ref), ok. content_types_accepted_ignore_multipart_boundary(Config) -> doc("When a multipart content-type is provided for the request " "body, the boundary parameter is not expected to be returned " "from the content_types_accepted callback and will be " "automatically ignored."), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/content_types_accepted?multipart", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"multipart/mixed; boundary=abcdef; v=1">>} ], <<"Not really multipart!">>), {response, _, 204, _} = gun:await(ConnPid, Ref), ok. content_types_accepted_param(Config) -> doc("When a parameter is returned from the content_types_accepted " "callback, and the same parameter is found in the content-type " "header, the negotiation succeeds and the request is processed."), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/content_types_accepted?param", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain;charset=UTF-8">>} ], "12345"), {response, fin, 204, _} = gun:await(ConnPid, Ref), ok. content_types_accepted_wildcard(Config) -> doc("When a wildcard is returned from the content_types_accepted " "callback, any content-type must be accepted."), ConnPid = gun_open(Config), Ref1 = gun:put(ConnPid, "/content_types_accepted?wildcard", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ]), gun:data(ConnPid, Ref1, fin, "Hello world!"), {response, fin, 204, _} = gun:await(ConnPid, Ref1), Ref2 = gun:put(ConnPid, "/content_types_accepted?wildcard", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/vnd.plain;charset=UTF-8">>} ]), gun:data(ConnPid, Ref2, fin, "Hello world!"), {response, fin, 204, _} = gun:await(ConnPid, Ref2), ok. content_types_accepted_wildcard_param_no_content_type_param(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_accepted callback, a content-type header " "with no parameters must be accepted."), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/content_types_accepted?wildcard-param", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ]), gun:data(ConnPid, Ref, fin, "Hello world!"), {response, fin, 204, _} = gun:await(ConnPid, Ref), ok. content_types_accepted_wildcard_param_content_type_with_param(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_accepted callback, a content-type header " "with a parameter must be accepted."), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/content_types_accepted?wildcard-param", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain; charset=utf-8">>} ]), gun:data(ConnPid, Ref, fin, "Hello world!"), {response, fin, 204, _} = gun:await(ConnPid, Ref), ok. content_types_provided_invalid_type(Config) -> doc("When an invalid type is returned from the " "content_types_provided callback, the " "resource is incorrect and a 500 response is expected."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/content_types_provided?invalid-type", [ {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)), ok. content_types_provided_wildcard_param_no_accept_param(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_provided callback, an accept header " "with no parameters must be accepted."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/content_types_provided?wildcard-param", [ {<<"accept">>, <<"text/plain">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"[]">>} = gun:await_body(ConnPid, Ref), ok. content_types_provided_wildcard_param_accept_with_param(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_provided callback, an accept header " "with a parameter must be accepted."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/content_types_provided?wildcard-param", [ {<<"accept">>, <<"text/plain;level=1">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"level=1">>} = gun:await_body(ConnPid, Ref), ok. content_types_provided_wildcard_param_accept_with_param_and_qvalue(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_provided callback, an accept header " "with two media types containing parameters including a " "q-value must be accepted. The q-value determines which."), ConnPid = gun_open(Config), Ref1 = gun:get(ConnPid, "/content_types_provided?wildcard-param", [ {<<"accept">>, <<"text/plain;level=1;q=0.8, text/plain;level=2;q=0.5">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref1), {ok, <<"level=1">>} = gun:await_body(ConnPid, Ref1), Ref2 = gun:get(ConnPid, "/content_types_provided?wildcard-param", [ {<<"accept">>, <<"text/plain;level=1;q=0.5, text/plain;level=2;q=0.8">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref2), {ok, <<"level=2">>} = gun:await_body(ConnPid, Ref2), ok. content_types_provided_wildcard_param_no_accept_header(Config) -> doc("When a wildcard is returned for parameters from the " "content_types_provided callback, the lack of accept header " "results in the first media type returned being accepted. " "The wildcard must however not be present in the media_type " "value added to the Req object."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/content_types_provided?wildcard-param", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"[]">>} = gun:await_body(ConnPid, Ref), ok. delete_resource_missing(Config) -> doc("When a resource accepts the DELETE method and the " "delete_resource callback is not exported, the " "resource is incorrect and a 500 response is expected."), ConnPid = gun_open(Config), Ref = gun:delete(ConnPid, "/delete_resource?missing", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)), ok. create_resource_created(Config) -> doc("POST to an existing resource to create a new resource. " "When the accept callback returns {created, NewURI}, " "the expected reply is 201 Created."), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/create_resource?created", [ {<<"content-type">>, <<"application/text">>} ], <<"hello">>, #{}), {response, _, 201, _} = gun:await(ConnPid, Ref), ok. create_resource_see_other(Config) -> doc("POST to an existing resource to create a new resource. " "When the accept callback returns {see_other, NewURI}, " "the expected reply is 303 See Other with a location header set."), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/create_resource?see_other", [ {<<"content-type">>, <<"application/text">>} ], <<"hello">>, #{}), {response, _, 303, RespHeaders} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"location">>, 1, RespHeaders), ok. error_on_malformed_accept(Config) -> doc("A malformed Accept header must result in a 400 response."), do_error_on_malformed_header(Config, <<"accept">>). error_on_malformed_if_match(Config) -> doc("A malformed If-Match header must result in a 400 response."), do_error_on_malformed_header(Config, <<"if-match">>). error_on_malformed_if_none_match(Config) -> doc("A malformed If-None-Match header must result in a 400 response."), do_error_on_malformed_header(Config, <<"if-none-match">>). do_error_on_malformed_header(Config, Name) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>}, {Name, <<"bad">>} ]), {response, _, 400, _} = gun:await(ConnPid, Ref), ok. expires_binary(Config) -> doc("The expires header can also be given as a binary " "to indicate a date in the past. (RFC7234 5.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/expires?binary", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"0">>} = lists:keyfind(<<"expires">>, 1, Headers), ok. expires_missing(Config) -> doc("The expires header must not be sent when the callback is not exported."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/expires?missing", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"expires">>, 1, Headers), ok. expires_tuple(Config) -> doc("The expires header can be given as a date tuple. (RFC7234 5.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/expires?tuple", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"Fri, 21 Sep 2012 22:36:14 GMT">>} = lists:keyfind(<<"expires">>, 1, Headers), ok. expires_undefined(Config) -> doc("The expires header must not be sent when undefined is returned."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/expires?undefined", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"expires">>, 1, Headers), ok. generate_etag_missing(Config) -> doc("The etag header must not be sent when " "the generate_etag callback is not exported."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/generate_etag?missing", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"etag">>, 1, Headers), ok. generate_etag_undefined(Config) -> doc("The etag header must not be sent when " "the generate_etag callback returns undefined."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/generate_etag?undefined", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"etag">>, 1, Headers), ok. generate_etag_binary_strong(Config) -> doc("The etag header must be sent when the generate_etag " "callback returns a strong binary. (RFC7232 2.3)"), do_generate_etag(Config, "binary-strong-quoted", [], 200, {<<"etag">>, <<"\"etag-header-value\"">>}). generate_etag_binary_weak(Config) -> doc("The etag header must be sent when the generate_etag " "callback returns a weak binary. (RFC7232 2.3)"), do_generate_etag(Config, "binary-weak-quoted", [], 200, {<<"etag">>, <<"W/\"etag-header-value\"">>}). generate_etag_invalid_binary_strong_unquoted(Config) -> doc("When Cowboy cannot parse the generate_etag callback's " "return value, a 500 response is returned without the etag header."), do_generate_etag(Config, "binary-strong-unquoted", [], 500, false). generate_etag_invalid_binary_weak_unquoted(Config) -> doc("When Cowboy cannot parse the generate_etag callback's " "return value, a 500 response is returned without the etag header."), do_generate_etag(Config, "binary-weak-unquoted", [], 500, false). generate_etag_tuple_strong(Config) -> doc("The etag header must be sent when the generate_etag " "callback returns a strong tuple. (RFC7232 2.3)"), do_generate_etag(Config, "tuple-strong", [], 200, {<<"etag">>, <<"\"etag-header-value\"">>}). generate_etag_tuple_weak(Config) -> doc("The etag header must be sent when the generate_etag " "callback returns a weak tuple. (RFC7232 2.3)"), do_generate_etag(Config, "tuple-weak", [], 200, {<<"etag">>, <<"W/\"etag-header-value\"">>}). if_none_match_binary_strong(Config) -> doc("When the if-none-match header matches a strong etag, " "a 304 not modified response is returned. (RFC7232 3.2)"), do_generate_etag(Config, "binary-strong-quoted", [{<<"if-none-match">>, <<"\"etag-header-value\"">>}], 304, {<<"etag">>, <<"\"etag-header-value\"">>}). if_none_match_binary_weak(Config) -> doc("When the if-none-match header matches a weak etag, " "a 304 not modified response is returned. (RFC7232 3.2)"), do_generate_etag(Config, "binary-weak-quoted", [{<<"if-none-match">>, <<"W/\"etag-header-value\"">>}], 304, {<<"etag">>, <<"W/\"etag-header-value\"">>}). if_none_match_tuple_strong(Config) -> doc("When the if-none-match header matches a strong etag, " "a 304 not modified response is returned. (RFC7232 3.2)"), do_generate_etag(Config, "tuple-strong", [{<<"if-none-match">>, <<"\"etag-header-value\"">>}], 304, {<<"etag">>, <<"\"etag-header-value\"">>}). if_none_match_tuple_weak(Config) -> doc("When the if-none-match header matches a weak etag, " "a 304 not modified response is returned. (RFC7232 3.2)"), do_generate_etag(Config, "tuple-weak", [{<<"if-none-match">>, <<"W/\"etag-header-value\"">>}], 304, {<<"etag">>, <<"W/\"etag-header-value\"">>}). do_generate_etag(Config, Qs, ReqHeaders, Status, Etag) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/generate_etag?" ++ Qs, [ {<<"accept-encoding">>, <<"gzip">>} |ReqHeaders ]), {response, _, Status, RespHeaders} = do_maybe_h3_error(gun:await(ConnPid, Ref)), Etag = lists:keyfind(<<"etag">>, 1, RespHeaders), ok. %% See do_maybe_h3_error2 comment. do_maybe_h3_error({error, {stream_error, {stream_error, h3_internal_error, _}}}) -> {response, fin, 500, []}; do_maybe_h3_error(Result) -> Result. if_range_etag_equal(Config) -> doc("When the if-range header matches, a 206 partial content " "response is expected for an otherwise valid range request. (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-range">>, <<"\"strong-and-match\"">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes 0-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers), ok. if_range_etag_not_equal(Config) -> doc("When the if-range header does not match, the range header " "must be ignored and a 200 OK response is expected for " "an otherwise valid range request. (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-range">>, <<"\"strong-but-no-match\"">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. if_range_ignored_when_no_range_header(Config) -> doc("When there is no range header the if-range header is ignored " "and a 200 OK response is expected (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"if-range">>, <<"\"strong-and-match\"">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. if_range_ignored_when_ranges_provided_missing(Config) -> doc("When the resource does not support range requests " "the range and if-range headers must be ignored" "and a 200 OK response is expected (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range?missing-ranges_provided", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-range">>, <<"\"strong-and-match\"">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. if_range_ignored_when_ranges_provided_empty(Config) -> doc("When the resource does not support range requests " "the range and if-range headers must be ignored" "and a 200 OK response is expected (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range?empty-ranges_provided", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-range">>, <<"\"strong-and-match\"">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"none">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. if_range_weak_etag_not_equal(Config) -> doc("The if-range header must not match weak etags; the range header " "must be ignored and a 200 OK response is expected for " "an otherwise valid range request. (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range?weak-etag", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-range">>, <<"W/\"weak-no-match\"">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. if_range_date_not_equal(Config) -> doc("The if-range header must not match weak dates. Cowboy " "currently has no way of knowing whether a resource was " "updated twice within the same second. The range header " "must be ignored and a 200 OK response is expected for " "an otherwise valid range request. (RFC7233 3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-range">>, <<"Fri, 22 Feb 2222 11:11:11 GMT">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. last_modified(Config) -> doc("The last-modified header can be given as a date tuple. (RFC7232 2.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/last_modified?tuple", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"Fri, 21 Sep 2012 22:36:14 GMT">>} = lists:keyfind(<<"last-modified">>, 1, Headers), ok. last_modified_missing(Config) -> doc("The last-modified header must not be sent when the callback is not exported."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/last_modified?missing", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"last-modified">>, 1, Headers), ok. last_modified_undefined(Config) -> doc("The last-modified header must not be sent when the callback returns undefined."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/last_modified?undefined", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"last-modified">>, 1, Headers), ok. options_missing(Config) -> doc("A successful OPTIONS request to a simple handler results in " "a 200 OK response with the allow header set. (RFC7231 4.3.7)"), ConnPid = gun_open(Config), Ref = gun:options(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers), ok. provide_callback(Config) -> doc("A successful GET request to a simple handler results in " "a 200 OK response with the content-type set. (RFC7231 4.3.1, RFC7231 6.3.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/", [ {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), {_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers), {ok, <<"This is REST!">>} = gun:await_body(ConnPid, Ref), ok. provide_callback_missing(Config) -> doc("A 500 response must be sent when the ProvideCallback can't be called."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_callback_missing", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)), ok. provide_range_callback(Config) -> doc("A successful request for a single range results in a " "206 partial content response with content-range set. (RFC7233 4.1, RFC7233 4.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_range_callback", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes 0-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, <<"This is ranged REST!">>} = gun:await_body(ConnPid, Ref), ok. provide_range_callback_sendfile(Config) -> doc("A successful request for a single range results in a " "206 partial content response with content-range set. (RFC7233 4.1, RFC7233 4.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), {ok, Body} = file:read_file(Path), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers), ContentRange = iolist_to_binary([ <<"bytes 0-">>, integer_to_binary(Size - 1), <<"/">>, integer_to_binary(Size) ]), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body} = gun:await_body(ConnPid, Ref), ok. provide_range_callback_multipart(Config) -> doc("A successful request for multiple ranges results in a " "206 partial content response using the multipart/byteranges " "content-type and the content-range not being set. The real " "content-type and content-range of the parts can be found in " "the multipart headers. (RFC7233 4.1, RFC7233 A)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_range_callback", [ {<<"accept-encoding">>, <<"gzip">>}, %% This range selects everything except the space characters. {<<"range">>, <<"bytes=0-3, 5-6, 8-13, 15-">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body0} = gun:await_body(ConnPid, Ref), Body = do_decode(Headers, Body0), {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), [ {bytes, 0, 3, 20}, {bytes, 5, 6, 20}, {bytes, 8, 13, 20}, {bytes, 15, 19, 20} ] = ContentRanges, <<"ThisisrangedREST!">> = BodyAcc, ok. provide_range_callback_multipart_sendfile(Config) -> doc("A successful request for multiple ranges results in a " "206 partial content response using the multipart/byteranges " "content-type and the content-range not being set. The real " "content-type and content-range of the parts can be found in " "the multipart headers. (RFC7233 4.1, RFC7233 A)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [ {<<"accept-encoding">>, <<"gzip">>}, %% This range selects a few random chunks of the file. {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>} ]), Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), Skip = Size - 399, {ok, << _:50/binary, Body1:50/binary, _:50/binary, Body2:50/binary, _:50/binary, Body3:50/binary, _:Skip/binary, Body4/bits>>} = file:read_file(Path), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body0} = gun:await_body(ConnPid, Ref), Body = do_decode(Headers, Body0), %% We will receive the ranges in the same order as requested. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), LastFrom = 300 + Skip, LastTo = Size - 1, [ {bytes, 50, 99, Size}, {bytes, 150, 199, Size}, {bytes, 250, 299, Size}, {bytes, LastFrom, LastTo, Size} ] = ContentRanges, BodyAcc = <>, ok. do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyAcc) -> case cow_multipart:parse_headers(Rest, Boundary) of {ok, Headers, Rest1} -> {_, ContentRange0} = lists:keyfind(<<"content-range">>, 1, Headers), ContentRange = cow_http_hd:parse_content_range(ContentRange0), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), case cow_multipart:parse_body(Rest1, Boundary) of {done, Body} -> do_provide_range_callback_multipart_body(<<>>, Boundary, [ContentRange|ContentRangesAcc], <>); {done, Body, Rest2} -> do_provide_range_callback_multipart_body(Rest2, Boundary, [ContentRange|ContentRangesAcc], <>) end; {done, <<>>} -> {lists:reverse(ContentRangesAcc), BodyAcc} end. provide_range_callback_metadata(Config) -> doc("A successful request for a single range results in a " "206 partial content response with the same headers that " "a normal 200 OK response would, like vary or etag. (RFC7233 4.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_range_callback", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"date">>, 1, Headers), {_, _} = lists:keyfind(<<"etag">>, 1, Headers), {_, _} = lists:keyfind(<<"expires">>, 1, Headers), {_, _} = lists:keyfind(<<"last-modified">>, 1, Headers), {_, _} = lists:keyfind(<<"vary">>, 1, Headers), %% Also cache-control and content-location but we don't send those. ok. provide_range_callback_missing(Config) -> doc("A 500 response must be sent when the ProvideRangeCallback can't be called."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/provide_range_callback?missing", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)), ok. range_ignore_unknown_unit(Config) -> doc("The range header must be ignored when the range unit " "is not found in ranges_provided. (RFC7233 3.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"chapters=1-">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. range_ignore_when_not_modified(Config) -> doc("The range header must be ignored when a conditional " "GET results in a 304 not modified response. (RFC7233 3.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/if_range", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>}, {<<"if-none-match">>, <<"\"strong-and-match\"">>} ]), {response, fin, 304, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. range_satisfiable(Config) -> doc("When the range_satisfiable callback returns true " "a 206 partial content response is expected for " "an otherwise valid range request. (RFC7233 4.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/range_satisfiable?true", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes 0-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers), ok. range_not_satisfiable(Config) -> doc("When the range_satisfiable callback returns false " "a 416 range not satisfiable response is expected for " "an otherwise valid range request. (RFC7233 4.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/range_satisfiable?false", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, fin, 416, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), ok. range_not_satisfiable_int(Config) -> doc("When the range_satisfiable callback returns false " "a 416 range not satisfiable response is expected for " "an otherwise valid range request. If an integer is " "provided it is used to construct the content-range " "header. (RFC7233 4.2, RFC7233 4.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/range_satisfiable?false-int", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, fin, 416, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes */123">>} = lists:keyfind(<<"content-range">>, 1, Headers), ok. range_not_satisfiable_bin(Config) -> doc("When the range_satisfiable callback returns false " "a 416 range not satisfiable response is expected for " "an otherwise valid range request. If a binary is " "provided it is used to construct the content-range " "header. (RFC7233 4.2, RFC7233 4.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/range_satisfiable?false-bin", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, fin, 416, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes */456">>} = lists:keyfind(<<"content-range">>, 1, Headers), ok. range_satisfiable_missing(Config) -> doc("When the range_satisfiable callback is missing " "a 206 partial content response is expected for " "an otherwise valid range request. (RFC7233 4.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/range_satisfiable?missing", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, _, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes ", _/bits>>} = lists:keyfind(<<"content-range">>, 1, Headers), ok. ranges_provided_accept_ranges(Config) -> doc("When the ranges_provided callback exists the accept-ranges header " "is sent in the response. (RFC7233 2.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided?list", [{<<"accept-encoding">>, <<"gzip">>}]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes, pages, chapters">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), ok. %% @todo Probably should have options to do this automatically for auto at least. %% %% A server that supports range requests MAY ignore or reject a Range %% header field that consists of more than two overlapping ranges, or a %% set of many small ranges that are not listed in ascending order, %% since both are indications of either a broken client or a deliberate %% denial-of-service attack (Section 6.1). %% @todo Probably should have options for auto as well to join ranges that %% are very close from each other. ranges_provided_auto_data(Config) -> doc("When the unit range is bytes and the callback is 'auto' " "Cowboy will call the normal ProvideCallback and perform " "the range calculations automatically."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=8-">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes 8-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, <<"ranged REST!">>} = gun:await_body(ConnPid, Ref), ok. ranges_provided_auto_sendfile(Config) -> doc("When the unit range is bytes and the callback is 'auto' " "Cowboy will call the normal ProvideCallback and perform " "the range calculations automatically."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=8-">>} ]), Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), {ok, <<_:8/binary, Body/bits>>} = file:read_file(Path), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers), ContentRange = iolist_to_binary([ <<"bytes 8-">>, integer_to_binary(Size - 1), <<"/">>, integer_to_binary(Size) ]), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body} = gun:await_body(ConnPid, Ref), ok. ranges_provided_auto_multipart_data(Config) -> doc("When the unit range is bytes and the callback is 'auto' " "Cowboy will call the normal ProvideCallback and perform " "the range calculations automatically."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [ {<<"accept-encoding">>, <<"gzip">>}, %% This range selects everything except the space characters. {<<"range">>, <<"bytes=0-3, 5-6, 8-13, 15-">>} ]), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body0} = gun:await_body(ConnPid, Ref), Body = do_decode(Headers, Body0), %% We will receive the ranges in the same order as requested. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), [ {bytes, 0, 3, 20}, {bytes, 5, 6, 20}, {bytes, 8, 13, 20}, {bytes, 15, 19, 20} ] = ContentRanges, <<"ThisisrangedREST!">> = BodyAcc, ok. ranges_provided_auto_multipart_sendfile(Config) -> doc("When the unit range is bytes and the callback is 'auto' " "Cowboy will call the normal ProvideCallback and perform " "the range calculations automatically."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [ {<<"accept-encoding">>, <<"gzip">>}, %% This range selects a few random chunks of the file. {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>} ]), Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), Skip = Size - 399, {ok, << _:50/binary, Body1:50/binary, _:50/binary, Body2:50/binary, _:50/binary, Body3:50/binary, _:Skip/binary, Body4/bits>>} = file:read_file(Path), {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), false = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body0} = gun:await_body(ConnPid, Ref), Body = do_decode(Headers, Body0), %% We will receive the ranges in the same order as requested. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), LastFrom = 300 + Skip, LastTo = Size - 1, [ {bytes, 50, 99, Size}, {bytes, 150, 199, Size}, {bytes, 250, 299, Size}, {bytes, LastFrom, LastTo, Size} ] = ContentRanges, BodyAcc = <>, ok. ranges_provided_auto_not_satisfiable_data(Config) -> doc("When the unit range is bytes and the callback is 'auto' " "Cowboy will call the normal ProvideCallback and perform " "the range calculations automatically. When the requested " "range is not satisfiable a 416 range not satisfiable response " "is expected. The content-range header will be set. (RFC7233 4.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=1000-">>} ]), {response, fin, 416, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes */20">>} = lists:keyfind(<<"content-range">>, 1, Headers), ok. ranges_provided_auto_not_satisfiable_sendfile(Config) -> doc("When the unit range is bytes and the callback is 'auto' " "Cowboy will call the normal ProvideCallback and perform " "the range calculations automatically. When the requested " "range is not satisfiable a 416 range not satisfiable response " "is expected. The content-range header will be set. (RFC7233 4.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=1000-">>} ]), {response, fin, 416, Headers} = gun:await(ConnPid, Ref), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", Size = filelib:file_size(Path), ContentRange = iolist_to_binary([<<"bytes */">>, integer_to_binary(Size)]), {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers), ok. ranges_provided_empty_accept_ranges_none(Config) -> doc("When the ranges_provided callback exists but returns an empty list " "the accept-ranges header is sent in the response with the value none. (RFC7233 2.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided?none", [{<<"accept-encoding">>, <<"gzip">>}]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"none">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), ok. ranges_provided_missing_no_accept_ranges(Config) -> doc("When the ranges_provided callback does not exist " "the accept-ranges header is not sent in the response."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ranges_provided?missing", [{<<"accept-encoding">>, <<"gzip">>}]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"accept-ranges">>, 1, Headers), ok. rate_limited(Config) -> doc("A 429 response must be sent when the rate_limited callback returns true. " "The retry-after header is specified as an integer. (RFC6585 4, RFC7231 7.1.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/rate_limited?true", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 429, Headers} = gun:await(ConnPid, Ref), {_, <<"3600">>} = lists:keyfind(<<"retry-after">>, 1, Headers), ok. rate_limited_datetime(Config) -> doc("A 429 response must be sent when the rate_limited callback returns true. " "The retry-after header is specified as a date/time tuple. (RFC6585 4, RFC7231 7.1.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/rate_limited?true-date", [{<<"accept-encoding">>, <<"gzip">>}]), {response, fin, 429, Headers} = gun:await(ConnPid, Ref), {_, <<"Fri, 22 Feb 2222 11:11:11 GMT">>} = lists:keyfind(<<"retry-after">>, 1, Headers), ok. rate_not_limited(Config) -> doc("A success response must be sent when the rate_limited callback returns false."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/rate_limited?false", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), ok. stop_handler_allowed_methods(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_allow_missing_post(Config) -> do_req_body_stop_handler(Config, post, ?FUNCTION_NAME). stop_handler_charsets_provided(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_content_types_accepted(Config) -> do_req_body_stop_handler(Config, post, ?FUNCTION_NAME). stop_handler_content_types_provided(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_delete_completed(Config) -> do_no_body_stop_handler(Config, delete, ?FUNCTION_NAME). stop_handler_delete_resource(Config) -> do_no_body_stop_handler(Config, delete, ?FUNCTION_NAME). stop_handler_forbidden(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_is_authorized(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_is_conflict(Config) -> do_req_body_stop_handler(Config, put, ?FUNCTION_NAME). stop_handler_known_methods(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_languages_provided(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_malformed_request(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_moved_permanently(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_moved_temporarily(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_multiple_choices(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_options(Config) -> do_no_body_stop_handler(Config, options, ?FUNCTION_NAME). stop_handler_previously_existed(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_range_satisfiable(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_ranges_provided(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_rate_limited(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_resource_exists(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_service_available(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_uri_too_long(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_valid_content_headers(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_valid_entity_length(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_accept(Config) -> do_req_body_stop_handler(Config, post, ?FUNCTION_NAME). stop_handler_provide(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). stop_handler_provide_range(Config) -> do_no_body_stop_handler(Config, get, ?FUNCTION_NAME). do_no_body_stop_handler(Config, Method, StateName0) -> doc("Send a response manually and stop the REST handler."), ConnPid = gun_open(Config), "stop_handler_" ++ StateName = atom_to_list(StateName0), Ref = gun:Method(ConnPid, "/stop_handler?" ++ StateName, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, fin, 248, _} = gun:await(ConnPid, Ref), ok. do_req_body_stop_handler(Config, Method, StateName0) -> doc("Send a response manually and stop the REST handler."), ConnPid = gun_open(Config), "stop_handler_" ++ StateName = atom_to_list(StateName0), Ref = gun:Method(ConnPid, "/stop_handler?" ++ StateName, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ], <<"Hocus PocuSwitch!">>), {response, fin, 248, _} = gun:await(ConnPid, Ref), ok. switch_handler_allowed_methods(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_allow_missing_post(Config) -> do_req_body_switch_handler(Config, post, ?FUNCTION_NAME). switch_handler_charsets_provided(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_content_types_accepted(Config) -> do_req_body_switch_handler(Config, post, ?FUNCTION_NAME). switch_handler_content_types_provided(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_delete_completed(Config) -> do_no_body_switch_handler(Config, delete, ?FUNCTION_NAME). switch_handler_delete_resource(Config) -> do_no_body_switch_handler(Config, delete, ?FUNCTION_NAME). switch_handler_forbidden(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_is_authorized(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_is_conflict(Config) -> do_req_body_switch_handler(Config, put, ?FUNCTION_NAME). switch_handler_known_methods(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_languages_provided(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_malformed_request(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_moved_permanently(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_moved_temporarily(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_multiple_choices(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_options(Config) -> do_no_body_switch_handler(Config, options, ?FUNCTION_NAME). switch_handler_previously_existed(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_range_satisfiable(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_ranges_provided(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_rate_limited(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_resource_exists(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_service_available(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_uri_too_long(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_valid_content_headers(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_valid_entity_length(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_accept(Config) -> do_req_body_switch_handler(Config, post, ?FUNCTION_NAME). switch_handler_provide(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). switch_handler_provide_range(Config) -> do_no_body_switch_handler(Config, get, ?FUNCTION_NAME). do_no_body_switch_handler(Config, Method, StateName0) -> doc("Switch REST to loop handler for streaming the response body, " "with and without options."), "switch_handler_" ++ StateName = atom_to_list(StateName0), do_no_body_switch_handler1(Config, Method, "/switch_handler?" ++ StateName), do_no_body_switch_handler1(Config, Method, "/switch_handler_opts?" ++ StateName). do_no_body_switch_handler1(Config, Method, Path) -> ConnPid = gun_open(Config), Ref = gun:Method(ConnPid, Path, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"range">>, <<"bytes=0-">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {ok, Body} = gun:await_body(ConnPid, Ref), <<"Hello\nstreamed\nworld!\n">> = do_decode(Headers, Body), ok. do_req_body_switch_handler(Config, Method, StateName0) -> doc("Switch REST to loop handler for streaming the response body, " "with and without options."), "switch_handler_" ++ StateName = atom_to_list(StateName0), do_req_body_switch_handler1(Config, Method, "/switch_handler?" ++ StateName), do_req_body_switch_handler1(Config, Method, "/switch_handler_opts?" ++ StateName). do_req_body_switch_handler1(Config, Method, Path) -> ConnPid = gun_open(Config), Ref = gun:Method(ConnPid, Path, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"text/plain">>} ], <<"Hocus PocuSwitch!">>), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {ok, Body} = gun:await_body(ConnPid, Ref), <<"Hello\nstreamed\nworld!\n">> = do_decode(Headers, Body), ok. ================================================ FILE: test/rfc6585_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc6585_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_dispatch(_) -> cowboy_router:compile([{"[...]", [ {"/resp/:key[/:arg]", resp_h, []} ]}]). status_code_428(Config) -> doc("The 428 Precondition Required status code can be sent. (RFC6585 3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/428", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 428, _} = gun:await(ConnPid, Ref), ok. status_code_429(Config) -> doc("The 429 Too Many Requests status code can be sent. (RFC6585 4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/429", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 429, _} = gun:await(ConnPid, Ref), ok. %% @todo % The (429) response MAY include a Retry-After header indicating how long % to wait before making a new request. (RFC6585 4) status_code_431(Config) -> doc("The 431 Request Header Fields Too Large status code can be sent. (RFC6585 5)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/431", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 431, _} = gun:await(ConnPid, Ref), ok. status_code_511(Config) -> doc("The 511 Network Authentication Required status code can be sent. (RFC6585 6)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/511", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 511, _} = gun:await(ConnPid, Ref), ok. ================================================ FILE: test/rfc7230_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc7230_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_down/1]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv_rest/3]). -import(cowboy_test, [raw_recv/3]). suite() -> [{timetrap, 30000}]. all() -> [{group, http}]. groups() -> [{http, [parallel], ct_helper:all(?MODULE)}]. init_per_group(Name = http, Config) -> cowboy_test:init_http(Name = http, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, max_keepalive => 100 }, Config). end_per_group(Name, _) -> ok = cowboy:stop_listener(Name). init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, {"/echo/:key[/:arg]", echo_h, []}, {"/full/:key[/:arg]", echo_h, []}, {"/length/echo/:key", echo_h, []}, {"/resp/:key[/:arg]", resp_h, []}, {"/send_message", send_message_h, []}, {"*", asterisk_h, []} ]}, {"127.0.0.1", [{"/echo/:key", echo_h, []}]}, {"example.org", [{"/echo/:key", echo_h, []}]} %% @todo Add IPv6 addresses support to the router. This fails: %% {"[2001:db8:85a3::8a2e:370:7334]", [{"/echo/:key", echo_h, []}]} ]. do_raw(Config, Data) -> Client = raw_open(Config), ok = raw_send(Client, Data), {Version, Code, Reason, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, Rest2} = cow_http:parse_headers(Rest), case lists:keyfind(<<"content-length">>, 1, Headers) of {_, LengthBin} when LengthBin =/= <<"0">> -> Body = raw_recv_rest(Client, binary_to_integer(LengthBin), Rest2), #{client => Client, version => Version, code => Code, reason => Reason, headers => Headers, body => Body}; _ -> #{client => Client, version => Version, code => Code, reason => Reason, headers => Headers, body => <<>>} end. %% Listener. %% @todo Add to documentation. %The default port for "http" connections is 80. The connection %uses plain TCP. (RFC7230 2.7.1) % %The default port for "https" connections is 443. The connection %uses TLS. (RFC7230 2.7.2) % %Any other port may be used for either of them. %% Before the request. accept_at_least_1_empty_line(Config) -> doc("A configurable number of empty lines (CRLF) preceding the request " "must be ignored. At least 1 empty line must be ignored. (RFC7230 3.5)"), #{code := 200} = do_raw(Config, "\r\n" "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). reject_response(Config) -> doc("When receiving a response instead of a request, identified by the " "status-line which starts with the HTTP version, the server must " "reject the message with a 400 status code and close the connection. (RFC7230 3.1)"), #{code := 400, client := Client} = do_raw(Config, "HTTP/1.1 200 OK\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% Request. only_parse_necessary_elements(Config) -> doc("It is only necessary to parse elements required to process the request. (RFC7230 2.5)"), #{code := 200} = do_raw(Config, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Content-type: purposefully bad header value\r\n" "\r\n"). %% @todo Add to documentation. %Parsed elements are subject to configurable limits. A server must %be able to parse elements at least as long as it generates. (RFC7230 2.5) no_empty_line_after_request_line(Config) -> doc("The general format of HTTP requests is strict. No empty line is " "allowed in-between components except for the empty line " "indicating the end of the list of headers."), #{code := 400} = do_raw(Config, "GET / HTTP/1.1\r\n" "\r\n" "Host: localhost\r\n" "\r\n"). no_empty_line_in_headers(Config) -> doc("The general format of HTTP requests is strict. No empty line is " "allowed in-between components except for the empty line " "indicating the end of the list of headers."), #{code := 400} = do_raw(Config, "GET / HTTP/1.1\r\n" "User-Agent: RFC7230\r\n" "\r\n" "Host: localhost\r\n" "\r\n"). timeout_before_request_line(Config) -> doc("The time the request (request line and headers) takes to be " "received by the server must be limited and subject to configuration. " "No response must be sent before closing if no request was initiated " "by the reception of a complete request-line."), Client = raw_open(Config), ok = raw_send(Client, "GET / HTTP/1.1\r"), {error, closed} = raw_recv(Client, 0, 6000). timeout_after_request_line(Config) -> doc("The time the request (request line and headers) takes to be " "received by the server must be limited and subject to configuration. " "A 408 status code must be sent if the request line was received."), #{code := 408, client := Client1} = do_raw(Config, "GET / HTTP/1.1\r\n"), {error, closed} = raw_recv(Client1, 0, 6000). timeout_after_request_line_host(Config) -> doc("The time the request (request line and headers) takes to be " "received by the server must be limited and subject to configuration. " "A 408 status code must be sent if the request line was received."), #{code := 408, client := Client2} = do_raw(Config, "GET / HTTP/1.1\r\nHost: localhost"), {error, closed} = raw_recv(Client2, 0, 6000). timeout_after_request_line_host_crlf(Config) -> doc("The time the request (request line and headers) takes to be " "received by the server must be limited and subject to configuration. " "A 408 status code must be sent if the request line was received."), #{code := 408, client := Client3} = do_raw(Config, "GET / HTTP/1.1\r\nHost: localhost\r\n"), {error, closed} = raw_recv(Client3, 0, 6000). timeout_after_request_line_host_crlfcr(Config) -> doc("The time the request (request line and headers) takes to be " "received by the server must be limited and subject to configuration. " "A 408 status code must be sent if the request line was received."), #{code := 408, client := Client4} = do_raw(Config, "GET / HTTP/1.1\r\nHost: localhost\r\n\r"), {error, closed} = raw_recv(Client4, 0, 6000). %% Request line. limit_request_line_8000(Config) -> doc("It is recommended to limit the request-line length to a configurable " "limit of at least 8000 octets."), LongPath = ["/long-path" || _ <- lists:seq(1, 799)], #{code := 200} = do_raw(Config, [ "GET /?qs=", LongPath, " HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]). limit_request_line_9000(Config) -> doc("It is recommended to limit the request-line length to a configurable " "limit of at least 8000 octets. A request line too long must be rejected " "with a 414 status code and the closing of the connection. (RFC7230 3.1.1)"), LongPath = ["/long-path" || _ <- lists:seq(1, 899)], #{code := 414, client := Client} = do_raw(Config, [ "GET /very", LongPath, " HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %% Method. reject_invalid_method(Config) -> doc("The request method is defined as 1+ token characters. An invalid " "method must be rejected with a 400 status code and the " "closing of the connection. (RFC7230 3.1.1, RFC7230 3.2.6)"), #{code := 400, client := Client} = do_raw(Config, "GET\0 / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). reject_empty_method(Config) -> doc("The request method is defined as 1+ token characters. An empty " "method must be rejected with a 400 status code and the " "closing of the connection. (RFC7230 3.1.1, RFC7230 3.2.6)"), #{code := 400, client := Client} = do_raw(Config, " / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% @todo We probably want to directly match commonly used methods. %In practice the only characters in use by registered methods are %uppercase letters [A-Z] and the dash "-". (IANA HTTP Method Registry) limit_method_name(Config) -> doc("The length of the method must be subject to a configurable limit. " "A method too long must be rejected with a 501 status code and the " "closing of the connection. A good default for the method length limit " "is the longest method length the server implements. (RFC7230 3.1.1)"), LongMethod = [$G || _ <- lists:seq(1, 1000)], #{code := 501, client := Client} = do_raw(Config, [ LongMethod, " / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %% Between method and request-target. reject_tab_between_method_and_request_target(Config) -> doc("A request that uses anything other than SP as separator between " "the method and the request-target must be rejected with a 400 " "status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.5)"), #{code := 400, client := Client} = do_raw(Config, "GET\t/ HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). reject_two_sp_between_method_and_request_target(Config) -> doc("A request that uses anything other than SP as separator between " "the method and the request-target must be rejected with a 400 " "status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.5)"), #{code := 400, client := Client} = do_raw(Config, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% Request target. ignore_uri_fragment_after_path(Config) -> doc("The fragment part of the target URI is not sent. It must be " "ignored by a server receiving it. (RFC7230 5.1)"), Echo = <<"http://localhost/echo/uri">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/uri#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). ignore_uri_fragment_after_query(Config) -> doc("The fragment part of the target URI is not sent. It must be " "ignored by a server receiving it. (RFC7230 5.1)"), Echo = <<"http://localhost/echo/uri?key=value">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/uri?key=value#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). %% Request target: origin-form. must_understand_origin_form(Config) -> doc("A server must be able to handle at least origin-form and absolute-form. (RFC7230 5.3.2)"), #{code := 200} = do_raw(Config, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). %% @todo Reenable this test once support for CONNECT is added. %origin_form_reject_if_connect(Config) -> % doc("origin-form is used when the client does not connect to a proxy, " % "does not use the CONNECT method and does not issue a site-wide " % "OPTIONS request. (RFC7230 5.3.1)"), % #{code := 400, client := Client} = do_raw(Config, % "CONNECT / HTTP/1.1\r\n" % "Host: localhost\r\n" % "\r\n"), % {error, closed} = raw_recv(Client, 0, 1000). %% @todo Equivalent test for https. origin_form_tcp_scheme(Config) -> doc("The scheme is either resolved from configuration or is \"https\" " "when on a TLS connection and \"http\" otherwise. (RFC7230 5.5)"), Echo = <<"http://localhost/echo/uri">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/uri HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). origin_form_path(Config) -> doc("The absolute-path always starts with \"/\" and ends with either \"?\", \"#\" " "or the end of the URI. (RFC3986 3.3)"), Echo = <<"/echo/path">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/path HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). origin_form_path_query(Config) -> doc("The absolute-path always starts with \"/\" and ends with either \"?\", \"#\" " "or the end of the URI. (RFC3986 3.3)"), Echo = <<"/echo/path">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/path?key=value HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). origin_form_path_fragment(Config) -> doc("The absolute-path always starts with \"/\" and ends with either \"?\", \"#\" " "or the end of the URI. (RFC3986 3.3)"), Echo = <<"/echo/path">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/path#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). origin_form_query(Config) -> doc("The query starts with \"?\" and ends with \"#\" or the end of the URI. (RFC3986 3.4)"), Echo = <<"key=value">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/qs?key=value HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). origin_form_query_fragment(Config) -> doc("The query starts with \"?\" and ends with \"#\" or the end of the URI. (RFC3986 3.4)"), Echo = <<"key=value">>, #{code := 200, body := Echo} = do_raw(Config, "GET /echo/qs?key=value#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). %% @todo origin_form: reject paths with too large depth or query strings with too many keys %% Request target: absolute-form. must_understand_absolute_form(Config) -> doc("A server must be able to handle at least origin-form and absolute-form. (RFC7230 5.3.2)"), #{code := 200} = do_raw(Config, "GET http://localhost HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_case_insensitive_scheme(Config) -> doc("The scheme is case insensitive and normally provided in lowercase. (RFC7230 2.7.3)"), Echo = <<"http://localhost/echo/uri">>, #{code := 200, body := Echo} = do_raw(Config, "GET HttP://localhost/echo/uri HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_case_insensitive_host(Config) -> doc("The host is case insensitive and normally provided in lowercase. (RFC7230 2.7.3)"), Echo = <<"http://localhost/echo/uri">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://LoCaLHOsT/echo/uri HTTP/1.1\r\n" "Host: LoCaLHOsT\r\n" "\r\n"). absolute_form_reject_unknown_schemes(Config) -> doc("Unknown schemes must be rejected with a 400 status code and the closing of the connection."), #{code := 400, client := Client} = do_raw(Config, "GET bad://localhost/ HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% @todo Equivalent test for https. absolute_form_drop_scheme_tcp(Config) -> doc("The scheme provided with the request must be dropped. The effective " "scheme is either resolved from configuration or is \"https\" when on " "a TLS connection and \"http\" otherwise. (RFC7230 5.5)"), Echo = <<"http://localhost/echo/uri">>, #{code := 200, body := Echo} = do_raw(Config, "GET https://localhost/echo/uri HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_reject_userinfo(Config) -> doc("An authority component with a userinfo component (and its " "\"@\" delimiter) is invalid. The request must be rejected with " "a 400 status code and the closing of the connection. (RFC7230 2.7.1)"), #{code := 400, client := Client} = do_raw(Config, "GET http://username:password@localhost HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). absolute_form_reject_missing_host_without_path(Config) -> doc("A URI with a missing host identifier is invalid. The request must " "be rejected with a 400 status code and the closing of the connection. (RFC7230 2.7.1)"), #{code := 400, client := Client} = do_raw(Config, "GET http:// HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). absolute_form_reject_missing_host_with_path(Config) -> doc("A URI with a missing host identifier is invalid. The request must " "be rejected with a 400 status code and the closing of the connection. (RFC7230 2.7.1)"), #{code := 400, client := Client} = do_raw(Config, "GET http:/// HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). absolute_form_ipv4(Config) -> doc("Absolute form with an IPv4 address for the host. (RFC3986 3.2.2)"), Echo = <<"127.0.0.1">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://127.0.0.1/echo/host HTTP/1.1\r\n" "Host: 127.0.0.1\r\n" "\r\n"). absolute_form_ipv4_port(Config) -> doc("Absolute form with an IPv4 address for the host and a port number. (RFC3986 3.2.2)"), Host = <<"127.0.0.1">>, #{code := 200, body := Host} = do_raw(Config, "GET http://127.0.0.1:8080/echo/host HTTP/1.1\r\n" "Host: 127.0.0.1:8080\r\n" "\r\n"), Port = <<"8080">>, #{code := 200, body := Port} = do_raw(Config, "GET http://127.0.0.1:8080/echo/port HTTP/1.1\r\n" "Host: 127.0.0.1:8080\r\n" "\r\n"). %% @todo We need the router to support IPv6 addresses to write proper tests for these: %absolute_form_ipv6(Config) -> %absolute_form_ipv6_ipv4(Config) -> %absolute_form_ipv6_zoneid(Config) -> absolute_form_reg_name(Config) -> doc("Absolute form with a regular name for the host. (RFC3986 3.2.2)"), Echo = <<"example.org">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://example.org/echo/host HTTP/1.1\r\n" "Host: example.org\r\n" "\r\n"). absolute_form_reg_name_port(Config) -> doc("Absolute form with an IPv4 address for the host and a port number. (RFC3986 3.2.2)"), Host = <<"example.org">>, #{code := 200, body := Host} = do_raw(Config, "GET http://example.org:8080/echo/host HTTP/1.1\r\n" "Host: example.org:8080\r\n" "\r\n"), Port = <<"8080">>, #{code := 200, body := Port} = do_raw(Config, "GET http://example.org:8080/echo/port HTTP/1.1\r\n" "Host: example.org:8080\r\n" "\r\n"). absolute_form_limit_host(Config) -> doc("The maximum length for the host component of the URI must be subject " "to a configurable limit. A good default is 255 characters. " "(RFC7230 3.1.1, RFC3986 3.2.2, RFC1034 3.1)"), LongHost = ["host." || _ <- lists:seq(1, 100)], #{code := 414, client := Client} = do_raw(Config, [ "GET http://", LongHost, "/ HTTP/1.1\r\n" "Host: ", LongHost, "\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). absolute_form_invalid_port_0(Config) -> doc("Port number 0 is reserved. The request must be rejected and the connection closed."), #{code := 400, client := Client} = do_raw(Config, "GET http://localhost:0/ HTTP/1.1\r\n" "Host: localhost:0\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). absolute_form_invalid_port_65536(Config) -> doc("Port numbers above 65535 are invalid. The request must be rejected " "and the connection closed."), #{code := 400, client := Client} = do_raw(Config, "GET http://localhost:65536/ HTTP/1.1\r\n" "Host: localhost:65536\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% @todo The RFC says to discard the Host header if we are a proxy, %% and replace it with the content of absolute-form. This means %% that we should probably keep the absolute-form value when %% operating in proxy mode. Otherwise the absolute-form value %% is simply dropped and the Host header is used. %% @todo The authority is sent both in the URI and in the host header. %% The authority from the URI must be dropped, and the host header %% must be used instead. (RFC7230 5.5) %% %% It is not possible to test that the absolute-form value is dropped %% because one of the Host header test ensures that the authority %% is the same in both, and errors out otherwise. absolute_form_path(Config) -> doc("The path always starts with \"/\" and ends with either \"?\", \"#\" " "or the end of the URI. (RFC3986 3.3)"), Echo = <<"/echo/path">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://localhost/echo/path HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_path_query(Config) -> doc("The path always starts with \"/\" and ends with either \"?\", \"#\" " "or the end of the URI. (RFC3986 3.3)"), Echo = <<"/echo/path">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://localhost/echo/path?key=value HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_path_fragment(Config) -> doc("The path always starts with \"/\" and ends with either \"?\", \"#\" " "or the end of the URI. (RFC3986 3.3)"), Echo = <<"/echo/path">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://localhost/echo/path#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_no_path(Config) -> doc("An empty path component is equivalent to \"/\". (RFC7230 2.7.3)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, "GET http://localhost HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_no_path_then_query(Config) -> doc("An empty path component is equivalent to \"/\". (RFC7230 2.7.3)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, "GET http://localhost?key=value HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_no_path_then_fragment(Config) -> doc("An empty path component is equivalent to \"/\". (RFC7230 2.7.3)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, "GET http://localhost#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_query(Config) -> doc("The query starts with \"?\" and ends with \"#\" or the end of the URI. (RFC3986 3.4)"), Echo = <<"key=value">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://localhost/echo/qs?key=value HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). absolute_form_query_fragment(Config) -> doc("The query starts with \"?\" and ends with \"#\" or the end of the URI. (RFC3986 3.4)"), Echo = <<"key=value">>, #{code := 200, body := Echo} = do_raw(Config, "GET http://localhost/echo/qs?key=value#fragment HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"). %% @todo absolute_form: reject paths with too large depth or query strings with too many keys %% Request-target: authority-form. authority_form_reject_if_not_connect(Config) -> doc("When the method is CONNECT, authority-form must be used. This " "form does not apply to any other methods which must reject the " "request with a 400 status code and the closing of the connection. (RFC7230 5.3.3)"), #{code := 400, client := Client} = do_raw(Config, "GET localhost:80 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% @todo Implement CONNECT. %authority_form_reject_userinfo(Config) -> %An authority component with a userinfo component (and its %"@" delimiter) is invalid. The request must be rejected with %a 400 status code and the closing of the connection. (RFC7230 2.7.1) % %authority_form_limit_host(Config) -> %authority_form_limit_port0(Config) -> %authority_form_limit_port65536(Config) -> % %A request with a too long component of authority-form must be rejected with %a 414 status code and the closing of the connection. (RFC7230 3.1.1) % %The authority is either resolved from configuration or is taken %directly from authority-form. (RFC7230 5.5) % %authority_form_empty_path(Config) -> %authority_form_empty_query(Config) -> %The path and query are empty when using authority-form. (RFC7230 5.5) %% Request-target: asterisk-form. asterisk_form_reject_if_not_options(Config) -> doc("asterisk-form is used for server-wide OPTIONS requests. " "It is invalid with any other methods which must reject the " "request with a 400 status code and the closing of the connection. (RFC7230 5.3.4)"), #{code := 400, client := Client} = do_raw(Config, "GET * HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). asterisk_form_empty_path_query(Config) -> doc("The path and query components are empty when using asterisk-form. (RFC7230 5.5)"), #{code := 200, body := <<"http://localhost">>} = do_raw(Config, "OPTIONS * HTTP/1.1\r\n" "Host: localhost\r\n" "X-Echo: uri\r\n" "\r\n"). %% Invalid request-target. invalid_request_target(Config) -> doc("Any other form is invalid and must be rejected with a 400 status code " "and the closing of the connection."), #{code := 400, client := Client} = do_raw(Config, "GET \0 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). missing_request_target(Config) -> doc("The lack of request target must be rejected with a 400 status code " "and the closing of the connection."), #{code := 400, client := Client} = do_raw(Config, "GET HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% Between request-target and version. reject_tab_between_request_target_and_version(Config) -> doc("A request that uses anything other than SP as separator between " "the request-target and the version must be rejected with a 400 " "status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.5)"), #{code := 400, client := Client} = do_raw(Config, "GET /\tHTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). reject_two_sp_between_request_target_and_version(Config) -> doc("A request that uses anything other than SP as separator between " "the request-target and the version must be rejected with a 400 " "status code and the closing of the connection. (RFC7230 3.1.1, RFC7230 3.5)"), #{code := 400, client := Client} = do_raw(Config, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% Request version. reject_invalid_version_http09(Config) -> doc("Any version number other than HTTP/1.0 or HTTP/1.1 must be " "rejected by a server or intermediary with a 505 status code. (RFC7230 2.6, RFC7230 A.2)"), #{code := 505} = do_raw(Config, "GET / HTTP/0.9\r\n" "Host: localhost\r\n" "\r\n"). reject_invalid_version_http100(Config) -> doc("Any version number other than HTTP/1.0 or HTTP/1.1 must be " "rejected by a server or intermediary with a 505 status code. (RFC7230 2.6, RFC7230 A.2)"), #{code := 505} = do_raw(Config, "GET / HTTP/1.00\r\n" "Host: localhost\r\n" "\r\n"). reject_invalid_version_http111(Config) -> doc("Any version number other than HTTP/1.0 or HTTP/1.1 must be " "rejected by a server or intermediary with a 505 status code. (RFC7230 2.6, RFC7230 A.2)"), #{code := 505} = do_raw(Config, "GET / HTTP/1.11\r\n" "Host: localhost\r\n" "\r\n"). reject_invalid_version_http12(Config) -> doc("Any version number other than HTTP/1.0 or HTTP/1.1 must be " "rejected by a server or intermediary with a 505 status code. (RFC7230 2.6, RFC7230 A.2)"), #{code := 505} = do_raw(Config, "GET / HTTP/1.2\r\n" "Host: localhost\r\n" "\r\n"). reject_invalid_version_http2(Config) -> doc("Any version number other than HTTP/1.0 or HTTP/1.1 must be " "rejected by a server or intermediary with a 505 status code. (RFC7230 2.6, RFC7230 A.2)"), #{code := 505} = do_raw(Config, "GET / HTTP/2\r\n" "Host: localhost\r\n" "\r\n"). reject_empty_version(Config) -> doc("Any version number other than HTTP/1.0 or HTTP/1.1 must be " "rejected by a server or intermediary with a 505 status code. " "(RFC7230 2.6, RFC7230 A, RFC7230 A.2)"), #{code := 505} = do_raw(Config, "GET / \r\n" "Host: localhost\r\n" "\r\n"). reject_invalid_whitespace_after_version(Config) -> doc("A request that has whitespace different than CRLF following the " "version must be rejected with a 400 status code and the closing " "of the connection. (RFC7230 3.1.1)"), #{code := 400, client := Client} = do_raw(Config, "GET / HTTP/1.1 \r\n" "Host: localhost\r\n" "\r\n"), {error, closed} = raw_recv(Client, 0, 1000). %% Request headers. invalid_header_name(Config) -> doc("Header field names are tokens. (RFC7230 3.2)"), #{code := 400} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host\0: localhost\r\n" "\r\n"]). invalid_header_value(Config) -> doc("Header field values are made of printable characters, " "horizontal tab or space. (RFC7230 3.2)"), #{code := 400} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\0rm rf the world\r\n" "\r\n"]). lower_case_header(Config) -> doc("The header field name is case insensitive. (RFC7230 3.2)"), #{code := 200} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "host: localhost\r\n" "\r\n"]). upper_case_header(Config) -> doc("The header field name is case insensitive. (RFC7230 3.2)"), #{code := 200} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "HOST: localhost\r\n" "\r\n"]). mixed_case_header(Config) -> doc("The header field name is case insensitive. (RFC7230 3.2)"), #{code := 200} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "hOsT: localhost\r\n" "\r\n"]). reject_whitespace_before_header_name(Config) -> doc("Messages that contain whitespace before the header name must " "be rejected with a 400 status code and the closing of the " "connection. (RFC7230 3.2.4)"), #{code := 400, client := Client1} = do_raw(Config, [ "GET / HTTP/1.1\r\n" " Host: localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "\tHost: localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000). reject_whitespace_between_header_name_and_colon(Config) -> doc("Messages that contain whitespace between the header name and " "colon must be rejected with a 400 status code and the closing " "of the connection. (RFC7230 3.2.4)"), #{code := 400, client := Client1} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host : localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host\t: localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000). reject_header_name_without_colon(Config) -> doc("Messages that contain a header name that is not followed by a " "colon must be rejected with a 400 status code and the closing " "of the connection. (RFC7230 3.2.4)"), #{code := 400, client := Client1} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host\r\n" "\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000), #{code := 400, client := Client3} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host\r\n" " : localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client3, 0, 1000). limit_header_name(Config) -> doc("The header name must be subject to a configurable limit. A " "good default is 50 characters, well above the longest registered " "header. Such a request must be rejected with a 431 status code " "and the closing of the connection. " "(RFC7230 3.2.5, RFC6585 5, IANA Message Headers registry)"), #{code := 431, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n", binary:copy(<<$a>>, 32768), ": bad\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). limit_header_value(Config) -> doc("The header value and the optional whitespace around it must be " "subject to a configurable limit. There is no recommendations " "for the default. 4096 characters is known to work well. Such " "a request must be rejected with a 431 status code and the closing " "of the connection. (RFC7230 3.2.5, RFC6585 5)"), #{code := 431, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "bad: ", binary:copy(<<$a>>, 32768), "\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). drop_whitespace_before_header_value(Config) -> doc("Optional whitespace before and after the header value is not " "part of the value and must be dropped."), #{code := 200} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: \t 12\r\n" "\r\n" "Hello world!"]). drop_whitespace_after_header_value(Config) -> doc("Optional whitespace before and after the header value is not " "part of the value and must be dropped."), #{code := 200} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 12 \t \r\n" "\r\n" "Hello world!"]). reject_lf_line_breaks(Config) -> doc("A server may accept header names separated by a single LF, instead of " "CRLF. Cowboy rejects all requests that use LF as separator. (RFC7230 3.5)"), #{code := 400, client := Client} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %@todo %The order of header fields with differing names is not significant. (RFC7230 3.2.2) % %@todo %The normal procedure for parsing headers is to read each header %field into a hash table by field name until the empty line. (RFC7230 3) reject_duplicate_content_length_header(Config) -> doc("Requests with duplicate content-length headers must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 3.3.2)"), #{code := 400, client := Client} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 12\r\n" "Content-length: 12\r\n" "\r\n" "Hello world!"]), {error, closed} = raw_recv(Client, 0, 1000). reject_duplicate_host_header(Config) -> doc("Requests with duplicate host headers must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 3.3.2)"), #{code := 400, client := Client} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Host: localhost\r\n" "\r\n" "Hello world!"]), {error, closed} = raw_recv(Client, 0, 1000). combine_duplicate_headers(Config) -> doc("Other duplicate header fields must be combined by inserting a comma " "between the values in the order they were received. (RFC7230 3.2.2)"), #{code := 200, body := Body} = do_raw(Config, [ "GET /echo/headers HTTP/1.1\r\n" "Host: localhost\r\n" "Accept-encoding: gzip\r\n" "Accept-encoding: brotli\r\n" "\r\n"]), <<"#{<<\"accept-encoding\">> => <<\"gzip, brotli\">>,", _/bits>> = Body, ok. %Duplicate header field names are only allowed when their value is %a comma-separated list. In practice there is no need to perform %a check while reading the headers as the value will become invalid %and the error can be handled while parsing the header later on. (RFC7230 3.2.2) % %wait_for_eoh_before_processing_request(Config) -> %The request must not be processed until all headers have arrived. (RFC7230 3.2.2) limit_headers(Config) -> doc("The number of headers allowed in a request must be subject to " "a configurable limit. There is no recommendations for the default. " "100 headers is known to work well. Such a request must be rejected " "with a 431 status code and the closing of the connection. (RFC7230 3.2.5, RFC6585 5)"), %% 100 headers. #{code := 200} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n", [["H-", integer_to_list(N), ": value\r\n"] || N <- lists:seq(1, 99)], "\r\n"]), %% 101 headers. #{code := 431, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n", [["H-", integer_to_list(N), ": value\r\n"] || N <- lists:seq(1, 100)], "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %ignore_header_empty_list_elements(Config) -> %When parsing header field values, the server must ignore empty %list elements, and not count those as the count of elements present. (RFC7230 7) % %@todo %The information in the via header is largely unreliable. (RFC7230 5.7.1) %% Request body. %@todo %The message body is the octets after decoding any transfer %codings. (RFC7230 3.3) no_request_body(Config) -> doc("A request has a message body only if it includes a transfer-encoding " "header or a non-zero content-length header. (RFC7230 3.3)"), #{code := 200, body := <<"false">>} = do_raw(Config, [ "POST /echo/has_body HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), #{code := 200, body := <<>>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), ok. no_request_body_content_length_zero(Config) -> doc("A request has a message body only if it includes a transfer-encoding " "header or a non-zero content-length header. (RFC7230 3.3)"), #{code := 200, body := <<"false">>} = do_raw(Config, [ "POST /echo/has_body HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 0\r\n" "\r\n"]), #{code := 200, body := <<>>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 0\r\n" "\r\n"]), ok. request_body_content_length(Config) -> doc("A request has a message body only if it includes a transfer-encoding " "header or a non-zero content-length header. (RFC7230 3.3)"), #{code := 200, body := <<"true">>} = do_raw(Config, [ "POST /echo/has_body HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 12\r\n" "\r\n" "Hello world!"]), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 12\r\n" "\r\n" "Hello world!"]), ok. request_body_transfer_encoding(Config) -> doc("A request has a message body only if it includes a transfer-encoding " "header or a non-zero content-length header. (RFC7230 3.3)"), #{code := 200, body := <<"true">>} = do_raw(Config, [ "POST /echo/has_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), ok. %``` %Transfer-Encoding = 1#transfer-coding % %transfer-coding = "chunked" / "compress" / "deflate" / "gzip" / transfer-extension %transfer-extension = token *( OWS ";" OWS transfer-parameter ) %transfer-parameter = token BWS "=" BWS ( token / quoted-string ) %``` case_insensitive_transfer_encoding(Config) -> doc("The transfer-coding is case insensitive. (RFC7230 4)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: ChUnKeD\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), ok. %@todo %There are no known other transfer-extension with the exception of %deprecated aliases "x-compress" and "x-gzip". (IANA HTTP Transfer Coding Registry, %RFC7230 4.2.1, RFC7230 4.2.3, RFC7230 8.4.2) %% This is the exact same test as request_body_transfer_encoding. must_understand_chunked(Config) -> doc("A server must be able to handle at least chunked transfer-encoding. " "This is also the only coding that sees widespread use. (RFC7230 3.3.1, RFC7230 4.1)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), ok. reject_double_chunked_encoding(Config) -> doc("Messages encoded more than once with chunked transfer-encoding " "must be rejected with a 400 status code and the closing of the " "connection. (RFC7230 3.3.1)"), #{code := 400, client := Client} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked, chunked\r\n" "\r\n" "20\r\n6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). reject_non_terminal_chunked(Config) -> doc("Messages where chunked, when present, is not the last " "transfer-encoding must be rejected with a 400 status code " "and the closing of the connection. (RFC7230 3.3.3)"), #{code := 400, client := Client1} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked, gzip\r\n" "\r\n", zlib:gzip(<<"6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n">>)]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "Transfer-encoding: gzip\r\n" "\r\n", zlib:gzip(<<"6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n">>)]), {error, closed} = raw_recv(Client2, 0, 1000). %@todo %Some non-conformant implementations send the "deflate" compressed %data without the zlib wrapper. (RFC7230 4.2.2) reject_unknown_transfer_encoding(Config) -> doc("Messages encoded with a transfer-encoding the server does not " "understand must be rejected with a 501 status code and the " "closing of the connection. (RFC7230 3.3.1)"), #{code := 400, client := Client1} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: unknown, chunked\r\n" "\r\n", "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: unknown\r\n" "Transfer-encoding: chunked\r\n" "\r\n", "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000). %@todo %A server may reject requests with a body and no content-length %header with a 411 status code. (RFC7230 3.3.3) %``` %Content-Length = 1*DIGIT %``` reject_invalid_content_length(Config) -> doc("A request with an invalid content-length header must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 3.3.3)"), #{code := 400, client := Client1} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 12,12\r\n" "\r\n" "Hello world!"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "POST / HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: NaN\r\n" "\r\n" "Hello world!"]), {error, closed} = raw_recv(Client2, 0, 1000). %@todo %The content-length header ranges from 0 to infinity. Requests %with a message body too large must be rejected with a 413 status %code and the closing of the connection. (RFC7230 3.3.2) reject_when_both_content_length_and_transfer_encoding(Config) -> doc("When a message includes both transfer-encoding and content-length " "headers, the message may be an attempt at request smuggling. It " "must be rejected with a 400 status code and the closing of the " "connection. (RFC7230 3.3.3)"), #{code := 400, client := Client} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "Content-length: 12\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %socket_error_while_reading_body(Config) -> %If a socket error occurs while reading the body the server %must send a 400 status code response and close the connection. (RFC7230 3.3.3, RFC7230 3.4) % %timeout_while_reading_body(Config) -> %If a timeout occurs while reading the body the server must %send a 408 status code response and close the connection. (RFC7230 3.3.3, RFC7230 3.4) %% Body length. body_length_chunked_before(Config) -> doc("The length of a message with a transfer-encoding header can " "only be determined on decoding completion. (RFC7230 3.3.3)"), #{code := 200, body := <<"undefined">>} = do_raw(Config, [ "POST /echo/body_length HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), ok. body_length_chunked_after(Config) -> doc("Upon completion of chunk decoding the server must add a content-length " "header with the value set to the total length of data read. (RFC7230 4.1.3)"), #{code := 200, body := <<"12">>} = do_raw(Config, [ "POST /length/echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), ok. body_length_content_length(Config) -> doc("The length of a message with a content-length header is " "the numeric value in octets found in the header. (RFC7230 3.3.3)"), #{code := 200, body := <<"12">>} = do_raw(Config, [ "POST /echo/body_length HTTP/1.1\r\n" "Host: localhost\r\n" "Content-length: 12\r\n" "\r\n" "Hello world!"]), ok. body_length_zero(Config) -> doc("A message with no transfer-encoding or content-length header " "has a body length of 0. (RFC7230 3.3.3)"), #{code := 200, body := <<"0">>} = do_raw(Config, [ "POST /echo/body_length HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), ok. %% Chunked transfer-encoding. reject_invalid_chunk_size(Config) -> doc("A request with an invalid chunk size must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 4.1)"), #{code := 400, client := Client} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\nFIVE\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %``` %chunked-body = *chunk last-chunk trailer-part CRLF % %chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF %chunk-size = 1*HEXDIG %chunk-data = 1*OCTET ; a sequence of chunk-size octets % %last-chunk = 1*("0") [ chunk-ext ] CRLF %``` % %The chunk-size field is a string of hex digits indicating the size of %the chunk-data in octets. % %``` %chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) %chunk-ext-name = token %chunk-ext-val = token / quoted-string %``` ignore_unknown_chunk_extensions(Config) -> doc("Unknown chunk extensions must be ignored. (RFC7230 4.1.1)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6; hello=\"cool world\"\r\nHello \r\n" "5 ; one ; two ; three;four;five\r\nworld" "\r\n1;ok\r\n!\r\n0\r\n\r\n"]), ok. %% Since we skip everything right now, the only reason %% we might reject chunk extensions is if they are too large. limit_chunk_size_line(Config) -> doc("A request with chunk extensions larger than the server allows must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 4.1.1)"), #{code := 200, body := <<"Hello world!">>} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6; hello=\"cool world\"\r\nHello \r\n" "5;", lists:duplicate(128, $a), "\r\nworld" "\r\n1;ok\r\n!\r\n0\r\n\r\n"]), #{code := 400, client := Client} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6; hello=\"cool world\"\r\nHello \r\n" "5;", lists:duplicate(129, $a), "\r\nworld" "\r\n1;ok\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). reject_invalid_chunk_size_crlf(Config) -> doc("A request with an invalid line break after the chunk size must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 4.1)"), #{code := 400, client := Client1} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\rHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000), #{code := 400, client := Client3} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6Hello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client3, 0, 1000). reject_invalid_chunk_ext_crlf(Config) -> doc("A request with an invalid line break after chunk extensions must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 4.1)"), #{code := 400, client := Client1} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6; extensions\rHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6; extensions\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000), #{code := 400, client := Client3} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6; extensionsHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client3, 0, 1000). reject_invalid_chunk_data_crlf(Config) -> doc("A request with an invalid line break after the chunk data must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 4.1)"), #{code := 400, client := Client1} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client1, 0, 1000), #{code := 400, client := Client2} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client2, 0, 1000), #{code := 400, client := Client3} = do_raw(Config, [ "POST /echo/read_body HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello 5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), {error, closed} = raw_recv(Client3, 0, 1000). %``` %trailer-part = *( header-field CRLF ) %``` % %%% @todo see headers above and reject the same way, space etc. %reject_invalid_request_trailer(Config) -> % %ignore_request_trailer_transfer_encoding(Config) -> %ignore_request_trailer_content_length(Config) -> %ignore_request_trailer_host(Config) -> %ignore_request_trailer_cache_control(Config) -> %ignore_request_trailer_expect(Config) -> %ignore_request_trailer_max_forwards(Config) -> %ignore_request_trailer_pragma(Config) -> %ignore_request_trailer_range(Config) -> %ignore_request_trailer_te(Config) -> %ignore_request_trailer_if_match(Config) -> %ignore_request_trailer_if_none_match(Config) -> %ignore_request_trailer_if_modified_since(Config) -> %ignore_request_trailer_if_unmodified_since(Config) -> %ignore_request_trailer_if_range(Config) -> %ignore_request_trailer_www_authenticate(Config) -> %ignore_request_trailer_authorization(Config) -> %ignore_request_trailer_proxy_authenticate(Config) -> %ignore_request_trailer_proxy_authorization(Config) -> %ignore_request_trailer_content_encoding(Config) -> %ignore_request_trailer_content_type(Config) -> %ignore_request_trailer_content_range(Config) -> %ignore_request_trailer_trailer(Config) -> % %ignore_response_trailer_header(Config, Header) -> %Trailing headers must not include transfer-encoding, content-length, %host, cache-control, expect, max-forwards, pragma, range, te, %if-match, if-none-match, if-modified-since, if-unmodified-since, %if-range, www-authenticate, authorization, proxy-authenticate, %proxy-authorization, age, cache-control, expires, date, location, %retry-after, vary, warning, content-encoding, content-type, %content-range, or trailer. (RFC7230 4.1.2) % %When trailer headers are processed, invalid headers must be ignored. %Valid headers must be added to the list of headers of the request. (RFC7230 4.1.2) % %ignore_request_trailers(Config) -> %Trailer headers can be ignored safely. (RFC7230 4.1.2) % %limit_request_trailer_headers(Config) -> %The number of trailer headers must be subject to configuration. %There is no known recommendations for the default. A value of 10 %should cover most cases. Requests with too many trailer headers %must be rejected with a 431 status code and the closing of the %connection. (RFC6585 5) %% We remove the header immediately so there's no need %% to try to read the body before checking. remove_transfer_encoding_chunked_after_body_read(Config) -> doc("Upon completion of chunk decoding the server must remove \"chunked\" " "from the transfer-encoding header. This header must be removed if " "it becomes empty following this removal. (RFC7230 4.1.3)"), #{code := 200, body := <<"undefined">>} = do_raw(Config, [ "POST /echo/header/transfer-encoding HTTP/1.1\r\n" "Host: localhost\r\n" "Transfer-encoding: chunked\r\n" "\r\n" "6\r\nHello \r\n5\r\nworld\r\n1\r\n!\r\n0\r\n\r\n"]), ok. %remove_trailer_after_body_read(Config) -> %Upon completion of chunk decoding the server must remove the trailer %header from the list of headers. (RFC7230 4.1.3) % %``` %Trailer = 1#field-name %``` % %ignore_chunked_headers_not_in_trailer(Config) -> %The trailer header can be used to list the headers found in the %trailer. A server must have the option of ignoring trailer headers %that were not listed in the trailer header. (RFC7230 4.4) % %ignore_chunked_headers_if_trailer_not_in_connection(Config) -> %The trailer header must be listed in the connection header field. %Trailers must be ignored otherwise. % %%% @todo Though we need a compatibility mode as some clients don't send it... %reject_chunked_missing_end_crlf(Config) -> %@todo ending CRLF %% Connection management. %@todo can probably test using auth %Never assume any two requests on a single connection come %from the same user agent. (RFC7230 2.3) % %``` %Connection = 1#token ; case-insensitive %``` % %The connection token is either case insensitive "close", "keep-alive" %or a header field name. % %There are no corresponding "close" or "keep-alive" headers. (RFC7230 8.1, RFC7230 A.2) % %The connection header is valid only for the immediate connection, %alongside any header field it lists. (RFC7230 6.1) % %The server must determine if the connection is persistent for %every message received by looking at the connection header and %HTTP version. (RFC7230 6.3) no_connection_header_keepalive(Config) -> doc("HTTP/1.1 requests with no \"close\" option " "indicate the connection will persist. (RFC7230 6.1, RFC7230 6.3)"), #{code := 200, headers := RespHeaders, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), false = lists:keyfind(<<"connection">>, 1, RespHeaders), {error, timeout} = raw_recv(Client, 0, 1000). http10_connection_keepalive(Config) -> doc("HTTP/1.0 requests with the \"keep-alive\" option " "indicate the connection will persist. " "(RFC7230 6.1, RFC7230 6.3, RFC7230 A.1.2)"), #{code := 200, headers := RespHeaders, client := Client} = do_raw(Config, [ "GET / HTTP/1.0\r\n" "Host: localhost\r\n" "Connection: keep-alive\r\n" "\r\n"]), {_, <<"keep-alive">>} = lists:keyfind(<<"connection">>, 1, RespHeaders), {error, timeout} = raw_recv(Client, 0, 1000). connection_close(Config) -> doc("HTTP/1.1 requests with the \"close\" option and HTTP/1.0 with no " "\"keep-alive\" option indicate the connection will be closed " "upon reception of the response by the client. (RFC7230 6.1, RFC7230 6.3)"), #{code := 200, headers := RespHeaders, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"]), {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders), {error, closed} = raw_recv(Client, 0, 1000). http10_no_connection_header_close(Config) -> doc("HTTP/1.0 with no \"keep-alive\" option indicate " "the connection will be closed upon reception of " "the response by the client. (RFC7230 6.1, RFC7230 6.3, RFC7230 A.1.2)"), #{code := 200, headers := RespHeaders, client := Client} = do_raw(Config, [ "GET / HTTP/1.0\r\n" "Host: localhost\r\n" "\r\n"]), %% Cowboy always sends a close header back to HTTP/1.0 clients %% that support keep-alive, even though it is not required. {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders), {error, closed} = raw_recv(Client, 0, 1000). connection_invalid(Config) -> doc("HTTP/1.1 requests with an invalid Connection header " "must be rejected with a 400 status code and the closing " "of the connection. (RFC7230 6.1)"), #{code := 400, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: jndi{ldap127\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). http10_connection_invalid(Config) -> doc("HTTP/1.0 requests with an invalid Connection header " "must be rejected with a 400 status code and the closing " "of the connection. (RFC7230 6.1)"), #{code := 400, client := Client} = do_raw(Config, [ "GET / HTTP/1.0\r\n" "Host: localhost\r\n" "Connection: jndi{ldap127\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). limit_requests_keepalive(Config) -> doc("The maximum number of requests sent using a persistent connection " "must be subject to configuration. The connection must be closed " "when the limit is reached. (RFC7230 6.3)"), ConnPid = gun_open(Config), _ = [begin Ref = gun:get(ConnPid, "/"), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref), false = lists:keyfind(<<"connection">>, 1, RespHeaders) end || _ <- lists:seq(1,99)], %% Final request closes the connection. Ref = gun:get(ConnPid, "/"), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref), {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders), gun_down(ConnPid). accept_at_least_1_empty_line_keepalive(Config) -> doc("A configurable number of empty lines (CRLF) preceding the request " "must be ignored. At least 1 empty line must be ignored. (RFC7230 3.5)"), #{code := 200, client := Client} = do_raw(Config, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n" %% We send an extra CRLF that must be ignored. "\r\n"), ok = raw_send(Client, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {'HTTP/1.1', 200, _, _} = cow_http:parse_status_line(raw_recv_head(Client)), ok. %skip_request_body_by_closing_connection(Config) -> %%A server that doesn't want to read the entire body of a message %%must close the connection, if possible after sending the "close" %%connection option in the response. (RFC7230 6.3) pipeline(Config) -> doc("A server can receive more than one request before any response " "is sent. This is called pipelining. Responses must be sent " "in the same order as the requests. (RFC7230 6.3.2)"), ConnPid = gun_open(Config), Refs = [{ gun:get(ConnPid, "/"), gun:post(ConnPid, "/full/read_body", [], <<0:80000>>) } || _ <- lists:seq(1, 25)], _ = [begin {response, nofin, 200, _} = gun:await(ConnPid, Ref1, infinity), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref1, infinity), {response, nofin, 200, _} = gun:await(ConnPid, Ref2, infinity), {ok, <<0:80000>>} = gun:await_body(ConnPid, Ref2, infinity) end || {Ref1, Ref2} <- Refs], ok. %% @todo pipeline_parallel (safe methods can, others can't) %The requests can be processed in parallel if they all have safe methods. %@todo %A server that does parallel pipelining must send responses in the %same order as the requests came in. (RFC7230 5.6) %@todo %The server must reject abusive traffic by closing the connection. %Abusive traffic can come from the form of too many requests in a %given amount of time, or too many concurrent connections. Limits %must be subject to configuration. (RFC7230 6.4) close_inactive_connections(Config) -> doc("The server must close inactive connections. The timeout " "must be subject to configuration. (RFC7230 6.5)"), Client = raw_open(Config), {error, closed} = raw_recv(Client, 0, 6000). %@todo %The server must monitor connections for the close signal %and close the socket on its end accordingly. (RFC7230 6.5) % %@todo %A connection close may occur at any time. (RFC7230 6.5) ignore_requests_after_request_connection_close(Config) -> doc("The server must not process any request after " "receiving the \"close\" connection option. (RFC7230 6.6)"), Self = self(), #{code := 200, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n" "GET /send_message HTTP/1.1\r\n" "Host: localhost\r\n" "x-test-pid: ", pid_to_list(Self), "\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000), %% We receive a message if the second request is wrongly processed. receive {Self, _, init, Req, Opts} -> error({init, Req, Opts}) after 1000 -> ok end. ignore_requests_after_response_connection_close(Config) -> doc("The server must not process any request after " "sending the \"close\" connection option. (RFC7230 6.6)"), Self = self(), Client = raw_open(Config), ok = raw_send(Client, [ [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n" || _ <- lists:seq(1, 100)], "GET /send_message HTTP/1.1\r\n" "Host: localhost\r\n" "x-test-pid: ", pid_to_list(Self), "\r\n" "\r\n"]), %% We have a separate test for the connection close so we don't %% double check the connection gets closed here. We only need to %% know whether the 101st request was wrongly processed. receive {Self, _, init, Req, Opts} -> error({init, Req, Opts}) after 1000 -> ok end. %@todo %The server must close the connection in stages to avoid the %TCP reset problem. The server starts by closing the write %side of the socket. The server then reads until it detects %the socket has been closed, until it can be certain its %last response has been received by the client, or until %a close or timeout occurs. The server then fully close the %connection. (6.6) %% Routing. %``` %Host = authority ; same as authority-form %``` reject_missing_host(Config) -> doc("An HTTP/1.1 request that lacks a host header must be rejected with " "a 400 status code and the closing of the connection. (RFC7230 5.4)"), #{code := 400, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). http10_allow_missing_host(Config0) -> doc("An HTTP/1.0 request that lacks a host header may be accepted. " "(RFC7230 5.4, RFC7230 5.5, RFC7230 A.1.1)"), Routes = [{'_', [{"/echo/:key[/:arg]", echo_h, []}]}], Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(Routes)} }, Config0), try #{code := 200, body := <<>>} = do_raw(Config, [ "GET /echo/host HTTP/1.0\r\n" "\r\n"]) after cowboy:stop_listener(?FUNCTION_NAME) end. reject_invalid_host(Config) -> doc("A request with an invalid host header must be rejected with a " "400 status code and the closing of the connection. (RFC7230 5.4)"), #{code := 400, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost:port\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). reject_userinfo(Config) -> doc("An authority component with a userinfo component (and its " "\"@\" delimiter) is invalid. The request must be rejected with " "a 400 status code and the closing of the connection. (RFC7230 2.7.1)"), #{code := 400, client := Client} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: user@localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). reject_absolute_form_different_host(Config) -> doc("When using absolute-form the URI authority component must be " "identical to the host header. Invalid requests must be rejected " "with a 400 status code and the closing of the connection. (RFC7230 5.4)"), #{code := 400, client := Client} = do_raw(Config, [ "GET http://example.org/ HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {error, closed} = raw_recv(Client, 0, 1000). %reject_authority_form_different_host(Config) -> %When using authority-form the URI authority component must be %identical to the host header. Invalid requests must be rejected %with a 400 status code and the closing of the connection. empty_host(Config0) -> doc("The host header is empty when the authority component is undefined. (RFC7230 5.4)"), Routes = [{'_', [{"/echo/:key[/:arg]", echo_h, []}]}], Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(Routes)} }, Config0), try #{code := 200, body := <<>>} = do_raw(Config, [ "GET /echo/host HTTP/1.1\r\n" "Host:\r\n" "\r\n"]), #{code := 200, body := <<>>} = do_raw(Config, [ "GET /echo/host HTTP/1.1\r\n" "Host: \r\n" "\r\n"]) after cowboy:stop_listener(?FUNCTION_NAME) end. %% The effective request URI can be rebuilt by concatenating scheme, %% "://", authority, path and query components. (RFC7230 5.5) %% %% This is covered in req_SUITE in the tests for cowboy_req:uri/1,2. reject_non_authoritative_host(Config) -> doc("A request with a host header for which the origin server is " "not authoritative must be rejected with a 400 status code. " "(RFC7230 5.5, RFC7230 9.1)"), #{code := 400} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: ninenines.eu\r\n" "\r\n"]), ok. %@todo %Resources with identical URI except for the scheme component %must be treated as different. (RFC7230 2.7.2) %% Response. %@todo %A server can send more than one response per request only when a %1xx response is sent preceding the final response. (RFC7230 5.6) % %``` %HTTP-response = status-line *( header-field CRLF ) CRLF [ message-body ] %``` % %@todo %The response format must be followed strictly. % %``` %status-line = HTTP-version SP status-code SP reason-phrase CRLF %status-code = 3DIGIT %reason-phrase = *( HTAB / SP / VCHAR / obs-text ) %``` http10_request_http11_response(Config) -> doc("A server must send its own HTTP version in responses. (RFC7230 2.6)"), #{code := 200, version := 'HTTP/1.1'} = do_raw(Config, [ "GET / HTTP/1.0\r\n" "Host: localhost\r\n" "\r\n"]), ok. %@todo %An HTTP/1.1 server may send an HTTP/1.0 version for compatibility purposes. (RFC7230 2.6) % %@todo %RFC6585 defines additional status code a server can use to reject %messages. (RFC7230 9.3, RFC6585) %% Response headers. %@todo %In responses, OWS must be generated as SP or not generated %at all. RWS must be generated as SP. BWS must not be %generated. (RFC7230 3.2.3) % %``` %header-field = field-name ":" SP field-value % %field-name = token ; case-insensitive %field-value = *( SP / %21-7E / %80-FF ) %``` % %@todo %In quoted-string found in field-value, quoted-pair must only be %used for DQUOTE and backslash. (RFC7230 3.2.6) % %@todo %HTTP header values must use US-ASCII encoding and must only send %printable characters or SP. (RFC7230 3.2.4, RFC7230 9.4) % %@todo %The server must not generate empty list elements in headers. (RFC7230 7) % %@todo %When encoding an URI as part of a response, only characters that %are reserved need to be percent-encoded. (RFC7230 2.7.3) special_set_cookie_handling(Config) -> doc("The set-cookie header must be handled as a special case. There " "must be exactly one set-cookie header field per cookie. (RFC7230 3.2.2)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/set_resp_cookie3/multiple HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), [_, _] = [H || H={<<"set-cookie">>, _} <- RespHeaders], ok. %@todo %The server must list headers for or about the immediate connection %in the connection header field. (RFC7230 6.1) % %@todo %A server that does not support persistent connections must %send "close" in every non-1xx response. (RFC7230 6.1) % %no_close_in_100_response(Config) -> %no_close_in_101_response(Config) -> %no_close_in_102_response(Config) -> %A server must not send a "close" connection option %in 1xx responses. (RFC7230 6.1) % %@todo %The "close" connection must be sent in a message when the %sender knows it will close the connection after fully sending %the response. (RFC7230 6.6) % %@todo %A server must close the connection after sending or %receiving a "close" once the response has been sent. (RFC7230 6.6) close_request_close_response(Config) -> doc("A server must send a \"close\" in a response to a request " "containing a \"close\". (RFC7230 6.6)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n" "\r\n"]), {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders), ok. %% Response body. no_body_in_head_response(Config) -> doc("Responses to HEAD requests never include a message body. (RFC7230 3.3)"), Client = raw_open(Config), ok = raw_send(Client, [ "HEAD / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 200, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), {_, LengthBin} = lists:keyfind(<<"content-length">>, 1, Headers), Length = binary_to_integer(LengthBin), {error, timeout} = raw_recv(Client, Length, 1000), ok. %% @todo test different ways to send a body in response %%% @todo Implement CONNECT %2xx responses to CONNECT requests never include a message %body. (RFC7230 3.3) % %no_body_in_100_response(Config) -> %no_body_in_101_response(Config) -> %no_body_in_102_response(Config) -> %1xx responses never include a message body. (RFC7230 3.3) no_body_in_204_response(Config) -> doc("204 responses never include a message body. Cowboy produces " "a 500 error response when attempting to do so. (RFC7230 3.3)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/reply4/204body HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 500, _, _} = cow_http:parse_status_line(raw_recv_head(Client)), ok. no_body_in_204_response_stream(Config) -> doc("204 responses never include a message body. Attempting to " "stream the body produces a crash on the server-side. (RFC7230 3.3)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/stream_reply2/204body HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 204, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {_, <<>>} = cow_http:parse_headers(Rest), {error, timeout} = raw_recv(Client, 1, 1000), ok. no_body_in_304_response(Config) -> doc("304 responses never include a message body. Cowboy produces " "a 500 error response when attempting to do so. (RFC7230 3.3)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/reply4/304body HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 500, _, _} = cow_http:parse_status_line(raw_recv_head(Client)), ok. no_body_in_304_response_stream(Config) -> doc("304 responses never include a message body. Attempting to " "stream the body produces a crash on the server-side. (RFC7230 3.3)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/stream_reply2/304body HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 304, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {_, <<>>} = cow_http:parse_headers(Rest), {error, timeout} = raw_recv(Client, 1, 1000), ok. same_content_length_as_get_in_head_response(Config) -> doc("Responses to HEAD requests can include a content-length header. " "Its value must be the same as if the request was an unconditional " "GET. (RFC7230 3.3, RFC7230 3.3.1, RFC7230 3.3.2)"), Client = raw_open(Config), ok = raw_send(Client, [ "HEAD / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 200, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), {_, <<"12">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. same_transfer_encoding_as_get_in_head_response(Config) -> doc("Responses to HEAD requests can include a transfer-encoding header. " "Its value must be the same as if the request was an unconditional " "GET. (RFC7230 3.3, RFC7230 3.3.1, RFC7230 3.3.2)"), Client = raw_open(Config), ok = raw_send(Client, [ "HEAD /resp/stream_reply2/200 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 200, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, Headers), ok. %same_content_length_as_200_in_304_response(Config) -> %same_transfer_encoding_as_200_in_304_response(Config) -> %304 responses can include a %content-length or transfer-encoding header. Their value must %be the same as if the request was an unconditional GET. (RFC7230 3.3, RFC7230 3.3.1, RFC7230 3.3.2) % %no_content_length_in_100_response(Config) -> %no_content_length_in_101_response(Config) -> %no_content_length_in_102_response(Config) -> %1xx, 204 responses and "2xx responses to CONNECT requests" must %not include a content-length or transfer-encoding header. (RFC7230 3.3.1, RFC7230 3.3.2) no_content_length_in_204_response(Config) -> doc("204 responses must not include a content-length header. " "(RFC7230 3.3.1, RFC7230 3.3.2)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/reply3/204 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 204, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), false = lists:keyfind(<<"content-length">>, 1, Headers), ok. no_content_length_in_empty_304_response(Config) -> doc("304 responses should not include a content-length header, " "unless it matches the resource's and was therefore set " "explicitly by the user. (RFC7230 3.3.1, RFC7230 3.3.2)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/reply3/304 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 304, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), false = lists:keyfind(<<"content-length">>, 1, Headers), ok. %%% @todo CONNECT no_content_length_in_2xx_response_to_connect_request(Config) -> %no_transfer_encoding_in_100_response(Config) -> %no_transfer_encoding_in_101_response(Config) -> %no_transfer_encoding_in_102_response(Config) -> %1xx, 204 responses and "2xx responses to CONNECT requests" must %not include a content-length or transfer-encoding header. (RFC7230 3.3.1, RFC7230 3.3.2) %% We only send transfer-encoding when streaming a response body. %% We therefore need a streamed response in order to see a potential bug. no_transfer_encoding_in_204_response(Config) -> doc("204 responses must not include a transfer-encoding header. " "(RFC7230 3.3.1, RFC7230 3.3.2)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/stream_reply2/204 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, 204, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), false = lists:keyfind(<<"transfer-encoding">>, 1, Headers), ok. %%% @todo CONNECT no_transfer_encoding_in_2xx_response_to_connect_request(Config) -> %1xx, 204 responses and "2xx responses to CONNECT requests" must %not include a content-length or transfer-encoding header. (RFC7230 3.3.1, RFC7230 3.3.2) % %``` %message-body = *OCTET %``` % %The message body is the octets after decoding any transfer %codings. (RFC7230 3.3) content_length_0_when_no_body(Config) -> doc("When the length is known in advance, the server must send a " "content-length header, including if the length is 0. (RFC7230 3.3.2, RFC7230 3.3.3)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/reply2/200 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, <<"0">>} = lists:keyfind(<<"content-length">>, 1, RespHeaders), ok. content_length_response(Config) -> doc("When the length is known in advance, the server must send a " "content-length header. (RFC7230 3.3.2, RFC7230 3.3.3)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, <<"12">>} = lists:keyfind(<<"content-length">>, 1, RespHeaders), ok. chunked_response(Config) -> doc("When the length is not known in advance, the chunked transfer-encoding " "must be used. (RFC7230 3.3.2, RFC7230 3.3.3)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/stream_reply2/200 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, RespHeaders), %% @todo We probably want to check the body received too. ok. %compat_no_content_length_or_transfer_encoding_close_on_body_end(Config) -> %For compatibility purposes a server can send no content-length or %transfer-encoding header. In this case the connection must be %closed after the response has been sent fully. (RFC7230 3.3.2, RFC7230 3.3.3) no_content_length_if_transfer_encoding(Config) -> doc("The content-length header must not be sent when a transfer-encoding " "header already exists. (RFC7230 3.3.2)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/stream_reply2/200 HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), false = lists:keyfind(<<"content-length">>, 1, RespHeaders), ok. %@todo %The server must not apply the chunked transfer-encoding more than %once. (RFC7230 3.3.1) % %@todo %The server must apply the chunked transfer-encoding last. (RFC7230 3.3.1) http10_request_no_transfer_encoding_in_response(Config) -> doc("The transfer-encoding header must not be sent in responses to " "HTTP/1.0 requests, or in responses that use the HTTP/1.0 version. " "No transfer codings must be applied in these cases. " "(RFC7230 3.3.1, RFC7230 A.1.3)"), Client = raw_open(Config), ok = raw_send(Client, [ "GET /resp/stream_reply2/200 HTTP/1.0\r\n" "Host: localhost\r\n" "\r\n"]), {_, 200, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {RespHeaders, Body0} = cow_http:parse_headers(Rest), false = lists:keyfind(<<"content-length">>, 1, RespHeaders), false = lists:keyfind(<<"transfer-encoding">>, 1, RespHeaders), Body = <<0:8000000>>, {ok, Body1} = raw_recv(Client, byte_size(Body) - byte_size(Body0), 5000), Body = << Body0/binary, Body1/binary >>, %% The end of body is indicated by a connection close. {error, closed} = raw_recv(Client, 0, 1000), ok. no_te_no_trailers(Config) -> doc("Trailers can only be sent if the request includes a TE header " "containing \"trailers\". (RFC7230 4.1.2)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/stream_trailers HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"]), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, RespHeaders), false = lists:keyfind(<<"trailer">>, 1, RespHeaders), %% @todo We probably want to check the body received too. ok. te_trailers(Config) -> doc("Trailers can only be sent if the request includes a TE header " "containing \"trailers\". (RFC7230 4.1.2)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/stream_trailers HTTP/1.1\r\n" "Host: localhost\r\n" "TE: trailers\r\n" "\r\n"]), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, RespHeaders), {_, <<"grpc-status">>} = lists:keyfind(<<"trailer">>, 1, RespHeaders), %% @todo We probably want to check the body received too. ok. te_ignore_chunked(Config) -> doc("The presence of \"chunked\" in a TE header must be ignored as it " "is always acceptable with HTTP/1.1. (RFC7230 4.3)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/stream_reply2/200 HTTP/1.1\r\n" "Host: localhost\r\n" "TE: chunked\r\n" "\r\n"]), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, RespHeaders), %% @todo We probably want to check the body received too. ok. te_ignore_chunked_0(Config) -> doc("The presence of \"chunked\" in a TE header must be ignored as it " "is always acceptable with HTTP/1.1. (RFC7230 4.3)"), #{code := 200, headers := RespHeaders} = do_raw(Config, [ "GET /resp/stream_reply2/200 HTTP/1.1\r\n" "Host: localhost\r\n" "TE: chunked;q=0\r\n" "\r\n"]), {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, RespHeaders), %% @todo We probably want to check the body received too. ok. %%% @todo te_not_acceptable_coding(Config) -> %A qvalue of 0 in the TE header means "not acceptable". (RFC7230 4.3) % %@todo %The lack of a TE header or an empty TE header means only "chunked" %(with no trailers) or no transfer-encoding is acceptable. (RFC7230 4.3) % %@todo %Trailer headers must be listed in the trailer header field value. (RFC7230 4.4) %% Upgrade. %``` %Upgrade = 1#protocol % %protocol = protocol-name ["/" protocol-version] %protocol-name = token %protocol-version = token %``` % %The upgrade header contains the list of protocols the %client wishes to upgrade to, in order of preference. (RFC7230 6.7) upgrade_safely_ignored(Config) -> doc("The upgrade header can be safely ignored. (RFC7230 6.7)"), #{code := 200} = do_raw(Config, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: upgrade\r\n" "Upgrade: websocket\r\n" "\r\n"). %upgrade_must_be_in_connection_header(Config) -> %The upgrade header must be listed under the connection header, %or must be ignored otherwise. (RFC7230 6.7) % %@todo %A server accepting an upgrade request must send a 101 status %code with a upgrade header listing the protocol(s) it upgrades %to, in layer-ascending order. In addition the upgrade header %must be listed in the connection header. (RFC7230 6.7) % %%A server must not switch to a protocol not listed in the %%request's upgrade header. (RFC7230 6.7) % %@todo %A server that sends a 426 status code must include a upgrade %header listing acceptable protocols in order of preference. (RFC7230 6.7) % %@todo %A server can send a upgrade header to any response to advertise %its support for other protocols listed in order of preference. (RFC7230 6.7) % %@todo %Immediately after a server responds with a 101 status code %it must respond to the original request using the new protocol. (RFC7230 6.7) % %@todo %%A server must not switch protocols unless the original message's %%semantics can be honored by the new protocol. OPTIONS requests %%can be honored by any protocol. (RFC7230 6.7) % %http10_ignore_upgrade_header(Config) -> %A server must ignore an upgrade header received by an HTTP/1.0 %request. (RFC7230 6.7) % %expect_then_upgrade(Config) -> %A server receiving both an upgrade header and an expect header %containing "100-continue" must send a 100 response before the %101 response. (RFC7230 6.7) % %The upgrade header field cannot be used for switching the %connection protocol (e.g. TCP) or switching connections. (RFC7230 6.7) %% Compatibility. %@todo %A server can choose to be non-conformant to the specifications %for the sake of compatibility. Such behavior can be enabled %through configuration and/or software identification. (RFC7230 2.5) ================================================ FILE: test/rfc7231_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc7231_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_open/2]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv/3]). all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_dispatch(_) -> cowboy_router:compile([{"[...]", [ {"*", asterisk_h, []}, {"/", hello_h, []}, {"/echo/:key", echo_h, []}, {"/delay/echo/:key", echo_h, []}, {"/resp/:key[/:arg]", resp_h, []}, {"/ws", ws_init_h, #{}} ]}]). %% @todo The documentation should list what methods, headers and status codes %% are handled automatically so users can know what befalls to them to implement. %% Representations. %% Cowboy has cowboy_compress_h that could be concerned with this. %% However Cowboy will not attempt to compress if any content-coding %% is already applied, regardless of what they are. % % If one or more encodings have been applied to a representation, the % sender that applied the encodings MUST generate a Content-Encoding % header field that lists the content codings in the order in which % they were applied. Additional information about the encoding % parameters can be provided by other header fields not defined by this % specification. (RFC7231 3.1.2.2) %% Methods. method_get(Config) -> doc("The GET method is accepted. (RFC7231 4.3.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref), ok. method_head(Config) -> doc("The HEAD method is accepted. (RFC7231 4.3.2)"), ConnPid = gun_open(Config), Ref = gun:head(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 200, _} = gun:await(ConnPid, Ref), ok. method_head_same_resp_headers_as_get(Config) -> doc("Responses to HEAD should return the same headers as GET. (RFC7231 4.3.2)"), ConnPid = gun_open(Config), Ref1 = gun:get(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, Headers1} = gun:await(ConnPid, Ref1), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref1), Ref2 = gun:head(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 200, Headers2} = gun:await(ConnPid, Ref2), %% We remove the date header since the date might have changed between requests. Headers = lists:keydelete(<<"date">>, 1, Headers1), Headers = lists:keydelete(<<"date">>, 1, Headers2), ok. method_head_same_resp_headers_as_get_stream_reply(Config) -> doc("Responses to HEAD should return the same headers as GET. (RFC7231 4.3.2)"), ConnPid = gun_open(Config), Ref1 = gun:get(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, Headers1} = gun:await(ConnPid, Ref1), {ok, _} = gun:await_body(ConnPid, Ref1), Ref2 = gun:head(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 200, Headers2} = gun:await(ConnPid, Ref2), %% We remove the date header since the date might have changed between requests. Headers = lists:keydelete(<<"date">>, 1, Headers1), Headers = lists:keydelete(<<"date">>, 1, Headers2), ok. method_post(Config) -> doc("The POST method is accepted. (RFC7231 4.3.3)"), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/echo/read_body", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>} ], <<"hello=world">>), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"hello=world">>} = gun:await_body(ConnPid, Ref), ok. method_put(Config) -> doc("The PUT method is accepted. (RFC7231 4.3.4)"), ConnPid = gun_open(Config), Ref = gun:put(ConnPid, "/echo/read_body", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>} ], <<"hello=world">>), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"hello=world">>} = gun:await_body(ConnPid, Ref), ok. method_delete(Config) -> doc("The DELETE method is accepted. (RFC7231 4.3.5)"), ConnPid = gun_open(Config), Ref = gun:delete(ConnPid, "/echo/method", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"DELETE">>} = gun:await_body(ConnPid, Ref), ok. %% @todo This test is currently broken because Gun does not %% send a proper CONNECT request. %method_connect(Config) -> % doc("The CONNECT method is currently not implemented. (RFC7231 4.3.6)"), % ConnPid = gun_open(Config), % Ref = gun:request(ConnPid, <<"CONNECT">>, "localhost:8080", [ % {<<"accept-encoding">>, <<"gzip">>} % ], <<>>), % {response, fin, 501, _} = gun:await(ConnPid, Ref), % ok. % A client sending a CONNECT request MUST send the authority form of % request-target (Section 5.3 of [RFC7230]); i.e., the request-target % consists of only the host name and port number of the tunnel % destination, separated by a colon. % % A server MUST NOT send any Transfer-Encoding or Content-Length header % fields in a 2xx (Successful) response to CONNECT. A client MUST % ignore any Content-Length or Transfer-Encoding header fields received % in a successful response to CONNECT. % % A payload within a CONNECT request message has no defined semantics; % sending a payload body on a CONNECT request might cause some existing % implementations to reject the request. method_options(Config) -> doc("The OPTIONS method is accepted. (RFC7231 4.3.7)"), ConnPid = gun_open(Config), Ref = gun:options(ConnPid, "/echo/method", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"OPTIONS">>} = gun:await_body(ConnPid, Ref), ok. method_options_asterisk(Config) -> doc("The OPTIONS method is accepted with an asterisk. (RFC7231 4.3.7)"), ConnPid = gun_open(Config), Ref = gun:options(ConnPid, "*", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-echo">>, <<"method">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"OPTIONS">>} = gun:await_body(ConnPid, Ref), ok. method_options_content_length_0(Config) -> doc("The OPTIONS method must set the content-length header " "to 0 when no body is returned. (RFC7231 4.3.7)"), ConnPid = gun_open(Config), Ref = gun:options(ConnPid, "*", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, fin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"0">>} = lists:keyfind(<<"content-length">>, 1, Headers), ok. method_trace(Config) -> doc("The TRACE method is currently not implemented. (RFC7231 4.3.8)"), ConnPid = gun_open(Config), Ref = gun:request(ConnPid, <<"TRACE">>, "/", [ {<<"accept-encoding">>, <<"gzip">>} ], <<>>), {response, fin, 501, _} = gun:await(ConnPid, Ref), ok. %% Request headers. %% @todo It could be useful to check that we can parse all request headers defined in this RFC. %% @todo The same applies to any other RFC for which we have a test suite. expect(Config) -> doc("A server that receives a 100-continue expectation should honor it. (RFC7231 5.1.1)"), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/echo/read_body", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, {<<"expect">>, <<"100-continue">>} ]), {inform, 100, _} = gun:await(ConnPid, Ref), gun:close(ConnPid). http10_expect(Config) -> case config(protocol, Config) of http -> do_http10_expect(Config); http2 -> expect(Config); http3 -> expect(Config) end. do_http10_expect(Config) -> doc("A server that receives a 100-continue expectation " "in an HTTP/1.0 request must ignore it. (RFC7231 5.1.1)"), Body = <<"hello=world">>, ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}), Ref = gun:post(ConnPid, "/echo/read_body", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, {<<"content-length">>, integer_to_binary(byte_size(Body))}, {<<"expect">>, <<"100-continue">>} ]), timer:sleep(500), ok = gun:data(ConnPid, Ref, fin, Body), {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, Body} = gun:await_body(ConnPid, Ref), ok. %% Cowboy ignores the expect header when the value is not 100-continue. % % A server that receives an Expect field-value other than 100-continue % MAY respond with a 417 (Expectation Failed) status code to indicate % that the unexpected expectation cannot be met. expect_receive_body_omit_100_continue(Config) -> doc("A server may omit sending a 100 Continue response if it has " "already started receiving the request body. (RFC7231 5.1.1)"), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/delay/echo/read_body", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, {<<"expect">>, <<"100-continue">>} ], <<"hello=world">>), %% We receive the response directly without a 100 Continue. {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, <<"hello=world">>} = gun:await_body(ConnPid, Ref), ok. expect_discard_body_skip(Config) -> doc("A server that responds with a final status code before reading " "the entire message body should keep the connection open and skip " "the body when appropriate. (RFC7231 5.1.1)"), ConnPid = gun_open(Config), Ref1 = gun:post(ConnPid, "/echo/method", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, {<<"expect">>, <<"100-continue">>} ], <<"hello=world">>), {response, nofin, 200, _} = gun:await(ConnPid, Ref1), {ok, <<"POST">>} = gun:await_body(ConnPid, Ref1), Ref2 = gun:get(ConnPid, "/echo/method", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>} ]), {response, nofin, 200, _} = gun:await(ConnPid, Ref2), {ok, <<"GET">>} = gun:await_body(ConnPid, Ref2), ok. expect_discard_body_close(Config) -> case config(protocol, Config) of http -> do_expect_discard_body_close(Config); http2 -> doc("There's no reason to close the connection when using HTTP/2, " "even if a stream body is too large. We just cancel the stream."); http3 -> doc("There's no reason to close the connection when using HTTP/3, " "even if a stream body is too large. We just cancel the stream.") end. do_expect_discard_body_close(Config) -> doc("A server that responds with a final status code before reading " "the entire message body may close the connection to avoid " "reading a potentially large request body. (RFC7231 5.1.1, RFC7230 6.6)"), ConnPid = gun_open(Config), Ref1 = gun:post(ConnPid, "/echo/method", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-length">>, <<"10000000">>}, {<<"content-type">>, <<"application/x-www-form-urlencoded">>}, {<<"expect">>, <<"100-continue">>} ]), {response, nofin, 200, _Headers} = gun:await(ConnPid, Ref1), %% Ideally we would send a connection: close. Cowboy however %% cannot know the intent of the application until after we %% sent the response. % {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, Headers), {ok, <<"POST">>} = gun:await_body(ConnPid, Ref1), %% The connection is gone. receive {gun_down, ConnPid, _, closed, _} -> ok after 1000 -> error(timeout) end. no_accept_encoding(Config) -> doc("While a request with no accept-encoding header implies the " "user agent has no preferences and any would be acceptable, " "Cowboy will not serve content-codings by defaults to ensure " "the content can safely be read. (RFC7231 5.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/200"), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"content-encoding">>, 1, Headers), ok. %% Cowboy currently ignores any information about the identity content-coding %% and instead considers it always acceptable. % % 2. If the representation has no content-coding, then it is % acceptable by default unless specifically excluded by the % Accept-Encoding field stating either "identity;q=0" or "*;q=0" % without a more specific entry for "identity". accept_encoding_gzip(Config) -> doc("No qvalue means the content-coding is acceptable. (RFC7231 5.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), _ = case config(flavor, Config) of compress -> {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers); _ -> false = lists:keyfind(<<"content-encoding">>, 1, Headers) end, ok. accept_encoding_gzip_1(Config) -> doc("A qvalue different than 0 means the content-coding is acceptable. (RFC7231 5.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<"gzip;q=1.0">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), _ = case config(flavor, Config) of compress -> {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers); _ -> false = lists:keyfind(<<"content-encoding">>, 1, Headers) end, ok. accept_encoding_gzip_0(Config) -> doc("A qvalue of 0 means the content-coding is not acceptable. (RFC7231 5.3.1, RFC7231 5.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<"gzip;q=0">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"content-encoding">>, 1, Headers), ok. %% Cowboy currently only supports gzip automatically via cowboy_compress_h. % % 4. If multiple content-codings are acceptable, then the acceptable % content-coding with the highest non-zero qvalue is preferred. accept_encoding_empty(Config) -> doc("An empty content-coding means that the user agent does not " "want any content-coding applied to the response. (RFC7231 5.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<>>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"content-encoding">>, 1, Headers), ok. accept_encoding_unknown(Config) -> doc("An accept-encoding header only containing unknown content-codings " "should result in no content-coding being applied. (RFC7231 5.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/stream_reply2/200", [ {<<"accept-encoding">>, <<"deflate">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"content-encoding">>, 1, Headers), ok. %% Status codes. http10_status_code_100(Config) -> case config(protocol, Config) of http -> doc("The 100 Continue status code must not " "be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"), do_unsupported_status_code_1xx(100, Config); http2 -> status_code_100(Config); http3 -> status_code_100(Config) end. http10_status_code_101(Config) -> case config(protocol, Config) of http -> doc("The 101 Switching Protocols status code must not " "be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"), do_unsupported_status_code_1xx(101, Config); http2 -> status_code_101(Config); http3 -> %% While 101 is not supported by HTTP/3, there is no %% wording in RFC9114 that forbids sending it. status_code_101(Config) end. do_unsupported_status_code_1xx(StatusCode, Config) -> ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}), Ref = gun:get(ConnPid, "/resp/inform2/" ++ integer_to_list(StatusCode), [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, _} = gun:await(ConnPid, Ref), ok. status_code_100(Config) -> doc("The 100 Continue status code can be sent. (RFC7231 6.2.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/inform2/100", [ {<<"accept-encoding">>, <<"gzip">>} ]), {inform, 100, []} = gun:await(ConnPid, Ref), ok. status_code_101(Config) -> doc("The 101 Switching Protocols status code can be sent. (RFC7231 6.2.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/inform2/101", [ {<<"accept-encoding">>, <<"gzip">>} ]), {inform, 101, []} = gun:await(ConnPid, Ref), ok. status_code_200(Config) -> doc("The 200 OK status code can be sent. (RFC7231 6.3.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/200", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, _} = gun:await(ConnPid, Ref), ok. status_code_201(Config) -> doc("The 201 Created status code can be sent. (RFC7231 6.3.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/201", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 201, _} = gun:await(ConnPid, Ref), ok. status_code_202(Config) -> doc("The 202 Accepted status code can be sent. (RFC7231 6.3.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/202", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 202, _} = gun:await(ConnPid, Ref), ok. status_code_203(Config) -> doc("The 203 Non-Authoritative Information status code can be sent. (RFC7231 6.3.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/203", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 203, _} = gun:await(ConnPid, Ref), ok. status_code_204(Config) -> doc("The 204 No Content status code can be sent. (RFC7231 6.3.5)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/204", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 204, _} = gun:await(ConnPid, Ref), ok. status_code_205(Config) -> doc("The 205 Reset Content status code can be sent. (RFC7231 6.3.6)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/205", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 205, _} = gun:await(ConnPid, Ref), ok. status_code_300(Config) -> doc("The 300 Multiple Choices status code can be sent. (RFC7231 6.4.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/300", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 300, _} = gun:await(ConnPid, Ref), ok. status_code_301(Config) -> doc("The 301 Moved Permanently status code can be sent. (RFC7231 6.4.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/301", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 301, _} = gun:await(ConnPid, Ref), ok. status_code_302(Config) -> doc("The 302 Found status code can be sent. (RFC7231 6.4.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/302", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 302, _} = gun:await(ConnPid, Ref), ok. status_code_303(Config) -> doc("The 303 See Other status code can be sent. (RFC7231 6.4.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/303", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 303, _} = gun:await(ConnPid, Ref), ok. status_code_305(Config) -> doc("The 305 Use Proxy status code can be sent. (RFC7231 6.4.5)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/305", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 305, _} = gun:await(ConnPid, Ref), ok. %% The status code 306 is no longer used. (RFC7231 6.4.6) status_code_307(Config) -> doc("The 307 Temporary Redirect status code can be sent. (RFC7231 6.4.7)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/307", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 307, _} = gun:await(ConnPid, Ref), ok. status_code_400(Config) -> doc("The 400 Bad Request status code can be sent. (RFC7231 6.5.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/400", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 400, _} = gun:await(ConnPid, Ref), ok. status_code_402(Config) -> doc("The 402 Payment Required status code can be sent. (RFC7231 6.5.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/402", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 402, _} = gun:await(ConnPid, Ref), ok. status_code_403(Config) -> doc("The 403 Forbidden status code can be sent. (RFC7231 6.5.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/403", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 403, _} = gun:await(ConnPid, Ref), ok. status_code_404(Config) -> doc("The 404 Not Found status code can be sent. (RFC7231 6.5.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/404", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 404, _} = gun:await(ConnPid, Ref), ok. status_code_404_not_found(Config) -> doc("The 404 Not Found status code is sent when the target " "resource does not exist. (RFC7231 6.5.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/not/found", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 404, _} = gun:await(ConnPid, Ref), ok. status_code_405(Config) -> doc("The 405 Method Not Allowed status code can be sent. (RFC7231 6.5.5)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/405", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 405, _} = gun:await(ConnPid, Ref), ok. status_code_406(Config) -> doc("The 406 Not Acceptable status code can be sent. (RFC7231 6.5.6)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/406", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 406, _} = gun:await(ConnPid, Ref), ok. status_code_408(Config) -> doc("The 408 Request Timeout status code can be sent. (RFC7231 6.5.7)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/408", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 408, _} = gun:await(ConnPid, Ref), ok. status_code_408_connection_close(Config) -> case config(protocol, Config) of http -> do_http11_status_code_408_connection_close(Config); http2 -> doc("HTTP/2 connections are not closed on 408 responses."); http3 -> doc("HTTP/3 connections are not closed on 408 responses.") end. do_http11_status_code_408_connection_close(Config) -> doc("A 408 response should result in a connection close " "for HTTP/1.1 connections. (RFC7231 6.5.7)"), Client = raw_open(Config), ok = raw_send(Client, "GET / HTTP/1.1\r\n"), {_, 408, _, Rest} = cow_http:parse_status_line(raw_recv_head(Client)), {Headers, <<>>} = cow_http:parse_headers(Rest), {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, Headers), {error, closed} = raw_recv(Client, 0, 1000), ok. status_code_409(Config) -> doc("The 409 Conflict status code can be sent. (RFC7231 6.5.8)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/409", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 409, _} = gun:await(ConnPid, Ref), ok. status_code_410(Config) -> doc("The 410 Gone status code can be sent. (RFC7231 6.5.9)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/410", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 410, _} = gun:await(ConnPid, Ref), ok. status_code_411(Config) -> doc("The 411 Length Required status code can be sent. (RFC7231 6.5.10)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/411", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 411, _} = gun:await(ConnPid, Ref), ok. status_code_413(Config) -> doc("The 413 Payload Too Large status code can be sent. (RFC7231 6.5.11)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/413", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 413, _} = gun:await(ConnPid, Ref), ok. status_code_414(Config) -> doc("The 414 URI Too Long status code can be sent. (RFC7231 6.5.12)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/414", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 414, _} = gun:await(ConnPid, Ref), ok. status_code_415(Config) -> doc("The 415 Unsupported Media Type status code can be sent. (RFC7231 6.5.13)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/415", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 415, _} = gun:await(ConnPid, Ref), ok. status_code_417(Config) -> doc("The 417 Expectation Failed status code can be sent. (RFC7231 6.5.14)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/417", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 417, _} = gun:await(ConnPid, Ref), ok. status_code_426(Config) -> doc("The 426 Upgrade Required status code can be sent. (RFC7231 6.5.15)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/426", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 426, _} = gun:await(ConnPid, Ref), ok. status_code_426_upgrade_header(Config) -> case config(protocol, Config) of http -> do_status_code_426_upgrade_header(Config); http2 -> doc("HTTP/2 does not support the HTTP/1.1 Upgrade mechanism."); http3 -> doc("HTTP/3 does not support the HTTP/1.1 Upgrade mechanism.") end. do_status_code_426_upgrade_header(Config) -> doc("A 426 response must include a upgrade header. (RFC7231 6.5.15)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/ws?ok", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 426, Headers} = gun:await(ConnPid, Ref), {_, <<"upgrade">>} = lists:keyfind(<<"connection">>, 1, Headers), {_, <<"websocket">>} = lists:keyfind(<<"upgrade">>, 1, Headers), ok. status_code_500(Config) -> doc("The 500 Internal Server Error status code can be sent. (RFC7231 6.6.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/500", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 500, _} = gun:await(ConnPid, Ref), ok. status_code_501(Config) -> doc("The 501 Not Implemented status code can be sent. (RFC7231 6.6.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/501", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 501, _} = gun:await(ConnPid, Ref), ok. status_code_502(Config) -> doc("The 502 Bad Gateway status code can be sent. (RFC7231 6.6.3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/502", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 502, _} = gun:await(ConnPid, Ref), ok. status_code_503(Config) -> doc("The 503 Service Unavailable status code can be sent. (RFC7231 6.6.4)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/503", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 503, _} = gun:await(ConnPid, Ref), ok. status_code_504(Config) -> doc("The 504 Gateway Timeout status code can be sent. (RFC7231 6.6.5)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/504", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 504, _} = gun:await(ConnPid, Ref), ok. status_code_505(Config) -> doc("The 505 HTTP Version Not Supported status code can be sent. (RFC7231 6.6.6)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/505", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 505, _} = gun:await(ConnPid, Ref), ok. %% The 505 response code is supposed to be about the major HTTP version. %% Cowboy instead rejects any version that isn't HTTP/1.0 or HTTP/1.1 %% when expecting an h1 request. While this is not correct in theory %% it works in practice because there are no other minor versions. %% %% Cowboy does not do version checking for HTTP/2 since the protocol %% does not include a version number in the messages. %% Response headers. %% @todo No such header in this suite, but some in other suites (if-(un)modified-since). % A recipient that parses a timestamp value in an HTTP header field % MUST accept all three HTTP-date formats. (RFC7231 7.1.1.1) date_imf_fixdate(Config) -> doc("The date header uses the IMF-fixdate format. (RFC7231 7.1.1.1, RFC7231 7.1.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), {_, <<_,_,_,", ",_,_," ",_,_,_," ",_,_,_,_," ",_,_,":",_,_,":",_,_," GMT">>} = lists:keyfind(<<"date">>, 1, Headers), ok. %% @todo Applies to both date and other headers (if-(un)modified-since). % HTTP-date is case sensitive. A sender MUST NOT generate additional % whitespace in an HTTP-date beyond that specifically included as SP in % the grammar. The semantics of day-name, day, month, year, and % time-of-day are the same as those defined for the Internet Message % Format constructs with the corresponding name ([RFC5322], Section % 3.3). (RFC7231 7.1.1.1) %% @todo No such header in this suite, but some in other suites (if-(un)modified-since). % Recipients of a timestamp value in rfc850-date format, which uses a % two-digit year, MUST interpret a timestamp that appears to be more % than 50 years in the future as representing the most recent year in % the past that had the same last two digits. (RFC7231 7.1.1.1) %% @todo Add an option to disable sending the date header. % An origin server MUST NOT send a Date header field if it does not % have a clock capable of providing a reasonable approximation of the % current instance in Coordinated Universal Time. (RFC7231 7.1.1.2) no_date_1xx(Config) -> doc("The date header is optional for 1xx responses. " "Cowboy does not send it with those responses. (RFC7231 7.1.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/inform2/100", [ {<<"accept-encoding">>, <<"gzip">>} ]), {inform, 100, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(<<"date">>, 1, Headers), ok. date_2xx(Config) -> doc("A date header must be sent for 2xx status codes. (RFC7231 7.1.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/200", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"date">>, 1, Headers), ok. date_3xx(Config) -> doc("A date header must be sent for 3xx status codes. (RFC7231 7.1.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/300", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 300, Headers} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"date">>, 1, Headers), ok. date_4xx(Config) -> doc("A date header must be sent for 4xx status codes. (RFC7231 7.1.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/400", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 400, Headers} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"date">>, 1, Headers), ok. date_5xx(Config) -> doc("The date header is optional for 5xx status codes. " "Cowboy however does send it with those responses. (RFC7231 7.1.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/500", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 500, Headers} = gun:await(ConnPid, Ref), {_, _} = lists:keyfind(<<"date">>, 1, Headers), ok. server_header(Config) -> doc("An origin server may generate a server header field. " "Cowboy generates a small one by default. (RFC7231 7.4.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/"), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"Cowboy">>} = lists:keyfind(<<"server">>, 1, Headers), ok. server_header_override(Config) -> doc("An origin server may generate a server header field. " "Cowboy allows the user to override the default. (RFC7231 7.4.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/set_resp_header_server"), {response, _, 200, Headers} = gun:await(ConnPid, Ref), {_, <<"nginx">>} = lists:keyfind(<<"server">>, 1, Headers), ok. %% @todo It's worth revisiting this RFC in the context of cowboy_rest %% to ensure the state machine is doing what's expected by the RFC. ================================================ FILE: test/rfc7538_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc7538_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_dispatch(_) -> cowboy_router:compile([{"[...]", [ {"/resp/:key[/:arg]", resp_h, []} ]}]). status_code_308(Config) -> doc("The 308 Permanent Redirect status code can be sent. (RFC7538 3)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/308", [ {<<"accept-encoding">>, <<"gzip">>} ]), {response, _, 308, _} = gun:await(ConnPid, Ref), ok. %% @todo % The server SHOULD generate a Location header field ([RFC7231], % Section 7.1.2) in the response containing a preferred URI reference % for the new permanent URI. ================================================ FILE: test/rfc7540_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% Note that Cowboy does not implement the PRIORITY mechanism. %% Everyone has been moving away from it and it is widely seen %% as a failure. Setting priorities has been counter productive %% with regards to performance. Clients have been moving away %% from the mechanism. -module(rfc7540_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(ct_helper, [get_remote_pid_tcp/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv/3]). all() -> [{group, clear}, {group, tls}]. groups() -> Tests = ct_helper:all(?MODULE), RejectTLS = [http_upgrade_reject_tls, prior_knowledge_reject_tls], Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- RejectTLS, TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ RejectTLS, [{clear, [parallel], Clear}, {tls, [parallel], TLS}]. init_per_group(Name = clear, Config) -> [{protocol, http2}|cowboy_test:init_http(Name, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, %% Disable the DATA threshold for this test suite. stream_window_data_threshold => 0 }, Config)]; init_per_group(Name = tls, Config) -> cowboy_test:init_http2(Name, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, %% Disable the DATA threshold for this test suite. stream_window_data_threshold => 0 }, Config). end_per_group(Name, _) -> ok = cowboy:stop_listener(Name). init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, {"/echo/:key", echo_h, []}, {"/delay_hello", delay_hello_h, 1200}, {"/long_polling", long_polling_h, []}, {"/loop_handler_abort", loop_handler_abort_h, []}, {"/resp/:key[/:arg]", resp_h, []} ]} ]. %% Starting HTTP/2 for "http" URIs. http_upgrade_reject_tls(Config) -> doc("Implementations that support HTTP/2 over TLS must use ALPN. (RFC7540 3.4)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [binary, {active, false}|TlsOpts]), %% Send a valid preface. ok = ssl:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), %% We expect the server to send an HTTP 400 error %% when trying to use HTTP/2 without going through ALPN negotiation. {ok, <<"HTTP/1.1 400">>} = ssl:recv(Socket, 12, 1000), ok. http_upgrade_ignore_h2(Config) -> doc("An h2 token in an Upgrade field must be ignored. (RFC7540 3.2)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_ignore_if_http_10(Config) -> doc("The Upgrade header must be ignored if part of an HTTP/1.0 request. (RFC7230 6.7)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.0\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_ignore_missing_upgrade_in_connection(Config) -> doc("The Upgrade header must be listed in the " "Connection header field. (RFC7230 6.7)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_ignore_missing_http2_settings_in_connection(Config) -> doc("The HTTP2-Settings header must be listed in the " "Connection header field. (RFC7540 3.2.1, RFC7230 6.7)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_ignore_zero_http2_settings_header(Config) -> doc("The HTTP Upgrade request must include " "exactly one HTTP2-Settings header field (RFC7540 3.2, RFC7540 3.2.1)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_reject_two_http2_settings_header(Config) -> doc("The HTTP Upgrade request must include " "exactly one HTTP2-Settings header field (RFC7540 3.2, RFC7540 3.2.1)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 400">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_reject_bad_http2_settings_header(Config) -> doc("The HTTP Upgrade request must include " "a valid HTTP2-Settings header field (RFC7540 3.2, RFC7540 3.2.1)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" %% We send a full SETTINGS frame on purpose. "HTTP2-Settings: ", base64:encode(iolist_to_binary(cow_http2:settings(#{}))), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 400">>} = gen_tcp:recv(Socket, 12, 1000), ok. %% Match directly for now. do_recv_101(Socket) -> {ok, << "HTTP/1.1 101 Switching Protocols\r\n" "connection: Upgrade\r\n" "upgrade: h2c\r\n" "\r\n" >>} = gen_tcp:recv(Socket, 71, 1000), ok. http_upgrade_101(Config) -> doc("A 101 response must be sent on successful upgrade " "to HTTP/2 when using the HTTP Upgrade mechanism. (RFC7540 3.2, RFC7230 6.7)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), ok. http_upgrade_server_preface(Config) -> doc("The first frame after the upgrade must be a " "SETTINGS frame for the server connection preface. (RFC7540 3.2, RFC7540 3.5, RFC7540 6.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Receive the server preface. {ok, << _:24, 4:8, 0:40 >>} = gen_tcp:recv(Socket, 9, 1000), ok. http_upgrade_client_preface_timeout(Config) -> doc("Clients negotiating HTTP/2 and not sending a preface in " "a timely manner must be disconnected."), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Receive the response to the initial HTTP/1.1 request. {ok, << HeadersSkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, HeadersSkipLen, 1000), {ok, << DataSkipLen:24, 0:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, DataSkipLen, 1000), %% Do not send the preface. Wait for the server to disconnect us. {error, closed} = gen_tcp:recv(Socket, 9, 6000), ok. http_upgrade_reject_missing_client_preface(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.2, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a SETTINGS frame directly instead of the proper preface. ok = gen_tcp:send(Socket, cow_http2:settings(#{})), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. %% The server may however have already started sending the response to the %% initial HTTP/1.1 request. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {error, _} -> [closed|Acc] end end, [], [1, 2, 3])), case Received of [closed|_] -> ok; [headers, closed|_] -> ok; [headers, data, closed] -> ok end. http_upgrade_reject_invalid_client_preface(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.2, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a slightly incorrect preface. ok = gen_tcp:send(Socket, "PRI * HTTP/2.0\r\n\r\nSM: Value\r\n\r\n"), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. %% The server may however have already started sending the response to the %% initial HTTP/1.1 request. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {error, _} -> [closed|Acc] end end, [], [1, 2, 3])), case Received of [closed|_] -> ok; [headers, closed|_] -> ok; [headers, data, closed] -> ok end. http_upgrade_reject_missing_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.2, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface sequence except followed by a PING instead of a SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:ping(0)]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. %% The server may however have already started sending the response to the %% initial HTTP/1.1 request. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {error, _} -> [closed|Acc] end end, [], [1, 2, 3])), case Received of [closed|_] -> ok; [headers, closed|_] -> ok; [headers, data, closed] -> ok end. http_upgrade_reject_invalid_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.2, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface sequence except followed by a badly formed SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", << 0:24, 4:8, 0:9, 1:31 >>]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. %% The server may however have already started sending the response to the %% initial HTTP/1.1 request. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {error, _} -> [closed|Acc] end end, [], [1, 2, 3])), case Received of [closed|_] -> ok; [headers, closed|_] -> ok; [headers, data, closed] -> ok end. http_upgrade_accept_client_preface_empty_settings(Config) -> doc("The SETTINGS frame in the client preface may be empty. (RFC7540 3.2, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface sequence except followed by an empty SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Receive the SETTINGS ack. The response might arrive beforehand. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> [settings_ack|Acc] end end, [], [1, 2, 3])), case Received of [settings_ack|_] -> ok; [headers, settings_ack|_] -> ok; [headers, data, settings_ack] -> ok end. http_upgrade_client_preface_settings_ack_timeout(Config) -> doc("The SETTINGS frames sent by the client must be acknowledged. (RFC7540 3.5, RFC7540 6.5.3)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Skip the SETTINGS ack. Receive a GOAWAY with reason SETTINGS_TIMEOUT, %% possibly following a HEADERS or HEADERS and DATA frames. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 6000) of {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> Acc; {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {ok, << 8:24, 7:8, 0:40 >>} -> %% We expect a SETTINGS_TIMEOUT reason. {ok, << 1:32, 4:32 >>} = gen_tcp:recv(Socket, 8, 1000), [goaway|Acc]; {error, _} -> %% Can be timeouts, ignore them. Acc end end, [], [1, 2, 3, 4])), case Received of [goaway] -> ok; [headers, goaway] -> ok; [headers, data, goaway] -> ok end. %% @todo We need a successful test with actual options in HTTP2-Settings. %% SETTINGS_MAX_FRAME_SIZE is probably the easiest to test. The relevant %% RFC quote is: %% %% 3.2.1 %% A server decodes and interprets these values as it would any other %% SETTINGS frame. Explicit acknowledgement of these settings %% (Section 6.5.3) is not necessary, since a 101 response serves as %% implicit acknowledgement. %% @todo We need to test an upgrade with a request body. It is probably %% worth having a configuration value for how much we accept while still %% upgrading (if too big, we would just stay on HTTP/1.1). We therefore %% needs a test for when the body is small enough, and one for when the %% body is larger than we accept. The relevant RFC quote is: %% %% 3.2 %% Requests that contain a payload body MUST be sent in their entirety %% before the client can send HTTP/2 frames. This means that a large %% request can block the use of the connection until it is completely %% sent. %% @todo We should definitely have a test with OPTIONS. The relevant %% RFC quote is: %% %% 3.2 %% If concurrency of an initial request with subsequent requests is %% important, an OPTIONS request can be used to perform the upgrade to %% HTTP/2, at the cost of an additional round trip. http_upgrade_response(Config) -> doc("A response must be sent to the initial HTTP/1.1 request " "after switching to HTTP/2. The response must use " "the stream identifier 1. (RFC7540 3.2)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface. %% @todo Use non-empty SETTINGS here. Just because. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack, and the response HEADERS and DATA (Stream ID 1). Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> [settings_ack|Acc]; {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc] end end, [], [1, 2, 3])), case Received of [settings_ack, headers, data] -> ok; [headers, settings_ack, data] -> ok; [headers, data, settings_ack] -> ok end. http_upgrade_response_half_closed(Config) -> doc("The stream for the initial HTTP/1.1 request is half-closed. (RFC7540 3.2)"), %% Try sending more data after the upgrade and get an error. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET /long_polling HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface followed by an unexpected DATA frame. ok = gen_tcp:send(Socket, [ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{}), cow_http2:data(1, fin, <<"Unexpected DATA frame.">>) ]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Skip the SETTINGS ack. Receive an RST_STREAM possibly following by %% a HEADERS frame, or a GOAWAY following HEADERS and DATA. This %% corresponds to the stream being in half-closed and closed states. %% The reason must be STREAM_CLOSED. Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> Acc; {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc]; {ok, << 4:24, 3:8, 0:8, 1:32 >>} -> %% We expect a STREAM_CLOSED reason. {ok, << 5:32 >>} = gen_tcp:recv(Socket, 4, 1000), [rst_stream|Acc]; {ok, << 8:24, 7:8, 0:40 >>} -> %% We expect a STREAM_CLOSED reason. {ok, << 1:32, 5:32 >>} = gen_tcp:recv(Socket, 8, 1000), [goaway|Acc]; {error, _} -> %% Can be timeouts, ignore them. Acc end end, [], [1, 2, 3, 4])), case Received of [rst_stream] -> ok; [headers, rst_stream] -> ok; [headers, data, goaway] -> ok end. %% Starting HTTP/2 for "https" URIs. alpn_ignore_h2c(Config) -> doc("An h2c ALPN protocol identifier must be ignored. (RFC7540 3.3)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2c">>, <<"http/1.1">>]}, binary, {active, false}|TlsOpts]), {ok, <<"http/1.1">>} = ssl:negotiated_protocol(Socket), ok. alpn_server_preface(Config) -> doc("The first frame must be a SETTINGS frame " "for the server connection preface. (RFC7540 3.3, RFC7540 3.5, RFC7540 6.5)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Receive the server preface. {ok, << _:24, 4:8, 0:40 >>} = ssl:recv(Socket, 9, 1000), ok. alpn_client_preface_timeout(Config) -> doc("Clients negotiating HTTP/2 and not sending a preface in " "a timely manner must be disconnected."), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% Do not send the preface. Wait for the server to disconnect us. {error, closed} = ssl:recv(Socket, 3, 6000), ok. alpn_reject_missing_client_preface(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.3, RFC7540 3.5)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a SETTINGS frame directly instead of the proper preface. ok = ssl:send(Socket, cow_http2:settings(#{})), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = ssl:recv(Socket, 3, 1000), ok. alpn_reject_invalid_client_preface(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.3, RFC7540 3.5)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a slightly incorrect preface. ok = ssl:send(Socket, "PRI * HTTP/2.0\r\n\r\nSM: Value\r\n\r\n"), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = ssl:recv(Socket, 3, 1000), ok. alpn_reject_missing_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.3, RFC7540 3.5)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a valid preface sequence except followed by a PING instead of a SETTINGS frame. ok = ssl:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:ping(0)]), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = ssl:recv(Socket, 3, 1000), ok. alpn_reject_invalid_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.3, RFC7540 3.5)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a valid preface sequence except followed by a badly formed SETTINGS frame. ok = ssl:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", << 0:24, 4:8, 0:9, 1:31 >>]), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = ssl:recv(Socket, 3, 1000), ok. alpn_accept_client_preface_empty_settings(Config) -> doc("The SETTINGS frame in the client preface may be empty. (RFC7540 3.3, RFC7540 3.5)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a valid preface sequence except followed by an empty SETTINGS frame. ok = ssl:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = ssl:recv(Socket, 9, 1000), ok. alpn_client_preface_settings_ack_timeout(Config) -> doc("Failure to acknowledge the server's SETTINGS frame " "results in a SETTINGS_TIMEOUT connection error. (RFC7540 3.5, RFC7540 6.5.3)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a valid preface. ok = ssl:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = ssl:recv(Socket, 9, 1000), %% Do not ack the server preface. Expect a GOAWAY with reason SETTINGS_TIMEOUT. {ok, << _:24, 7:8, _:72, 4:32 >>} = ssl:recv(Socket, 17, 6000), ok. alpn(Config) -> doc("Successful ALPN negotiation. (RFC7540 3.3)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, binary, {active, false}|TlsOpts]), {ok, <<"h2">>} = ssl:negotiated_protocol(Socket), %% Send a valid preface. %% @todo Use non-empty SETTINGS here. Just because. ok = ssl:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = ssl:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = ssl:recv(Socket, 9, 1000), %% Wait until after the SETTINGS ack timeout was supposed to trigger. receive after 6000 -> ok end, %% Send a PING. ok = ssl:send(Socket, cow_http2:ping(0)), %% Receive a PING ack back, indicating the connection is still up. {ok, << 8:24, 6:8, 0:7, 1:1, 0:96 >>} = ssl:recv(Socket, 17, 1000), ok. %% Starting HTTP/2 with prior knowledge. prior_knowledge_reject_tls(Config) -> doc("Implementations that support HTTP/2 over TLS must use ALPN. (RFC7540 3.4)"), TlsOpts = ct_helper:get_certs_from_ets(), {ok, Socket} = ssl:connect("localhost", config(port, Config), [binary, {active, false}|TlsOpts]), %% Send a valid preface. ok = ssl:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% We expect the server to send an HTTP 400 error %% when trying to use HTTP/2 without going through ALPN negotiation. {ok, <<"HTTP/1.1 400">>} = ssl:recv(Socket, 12, 1000), ok. prior_knowledge_server_preface(Config) -> doc("The first frame must be a SETTINGS frame " "for the server connection preface. (RFC7540 3.4, RFC7540 3.5, RFC7540 6.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << _:24, 4:8, 0:40 >>} = gen_tcp:recv(Socket, 9, 1000), ok. %% Note: the client preface timeout doesn't apply in this case, %% so we don't test it. An HTTP/1.1 client that does not send %% a request in a timely manner will get disconnected by the %% HTTP protocol code, not by HTTP/2's. %% Note: the test that starts by sending a SETTINGS frame is %% redundant with tests sending garbage on the connection. %% From the point of view of an HTTP/1.1 connection, a %% SETTINGS frame is indistinguishable from garbage. prior_knowledge_reject_invalid_client_preface(Config) -> doc("An incorrect preface is an invalid HTTP/1.1 request. (RFC7540 3.4)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a slightly incorrect preface. ok = gen_tcp:send(Socket, "PRI * HTTP/2.0\r\n\r\nSM: Value\r\n\r\n"), %% We propagate to HTTP/2 after checking only the request-line. %% The server then sends its preface before checking the full client preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_reject_missing_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.4, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface sequence except followed by a PING instead of a SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:ping(0)]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_reject_invalid_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " "connection error of type PROTOCOL_ERROR. (RFC7540 3.4, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface sequence except followed by a badly formed SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", << 0:24, 4:8, 0:9, 1:31 >>]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_accept_client_preface_empty_settings(Config) -> doc("The SETTINGS frame in the client preface may be empty. (RFC7540 3.4, RFC7540 3.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface sequence except followed by an empty SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_client_preface_settings_ack_timeout(Config) -> doc("The SETTINGS frames sent by the client must be acknowledged. (RFC7540 3.5, RFC7540 6.5.3)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Do not ack the server preface. Expect a GOAWAY with reason SETTINGS_TIMEOUT. {ok, << _:24, 7:8, _:72, 4:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% Do a prior knowledge handshake. do_handshake(Config) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, Socket}. prior_knowledge(Config) -> doc("Streams can be initiated after a successful HTTP/2 connection " "with prior knowledge of server capabilities. (RFC7540 3.4)"), %% @todo Use non-empty SETTINGS here. Just because. {ok, Socket} = do_handshake(Config), %% Wait until after the SETTINGS ack timeout was supposed to trigger. receive after 6000 -> ok end, %% Send a PING. ok = gen_tcp:send(Socket, cow_http2:ping(0)), %% Receive a PING ack back, indicating the connection is still up. {ok, << 8:24, 6:8, 0:7, 1:1, 0:96 >>} = gen_tcp:recv(Socket, 17, 1000), ok. %% @todo If we ever add an option to disable HTTP/2, we need to check %% the following things: %% * HTTP/1.1 Upgrade returns an HTTP/1.1 response (3.2) %% * HTTP/1.1 Upgrade errors out if the client sends HTTP/2 frames %% without waiting for the 101 response (3.2, 3.5) %% * Prior knowledge handshake fails (3.4) %% * ALPN selects HTTP/1.1 (3.3) %% Frame format. ignore_unknown_frames(Config) -> doc("Frames of unknown type must be ignored and discarded. (RFC7540 4.1)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a single DATA frame, %% and an unknown frame type interleaved. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 10:24, 99:8, 0:40, 0:80 >>, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_data_unknown_flags(Config) -> doc("Undefined DATA frame flags must be ignored. (RFC7540 4.1, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a DATA frame with unknown flags. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 100:24, 0:8, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:1, %% PADDED. 1:1, 1:1, %% Undefined. 1:1, %% END_STREAM. 0:1, 1:31, 0:100/unit:8 >> ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_headers_unknown_flags(Config) -> doc("Undefined HEADERS frame flags must be ignored. (RFC7540 4.1, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a HEADERS frame with unknown flags. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), Len = iolist_size(HeadersBlock), ok = gen_tcp:send(Socket, [ << Len:24, 1:8, 1:1, 1:1, %% Undefined. 0:1, %% PRIORITY. 1:1, %% Undefined. 0:1, %% PADDED. 1:1, %% END_HEADERS. 1:1, %% Undefined. 0:1, %% END_STREAM. 0:1, 1:31 >>, HeadersBlock, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_priority_unknown_flags(Config) -> doc("Undefined PRIORITY frame flags must be ignored. (RFC7540 4.1, RFC7540 6.3)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with an interleaved PRIORITY frame with unknown flags. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 5:24, 2:8, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:1, 1:31, 0:1, 3:31, 0:8 >>, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_rst_stream_unknown_flags(Config) -> doc("Undefined RST_STREAM frame flags must be ignored. (RFC7540 4.1, RFC7540 6.4)"), {ok, Socket} = do_handshake(Config), %% Send a POST request then cancel it with an RST_STREAM frame with unknown flags. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 4:24, 3:8, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:1, 1:31, 8:32 >>, cow_http2:headers(3, nofin, HeadersBlock), cow_http2:data(3, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 3:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 3:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_settings_unknown_flags(Config) -> doc("Undefined SETTINGS frame flags must be ignored. (RFC7540 4.1, RFC7540 6.5)"), {ok, Socket} = do_handshake(Config), %% Send a SETTINGS frame with unknown flags. ok = gen_tcp:send(Socket, << 6:24, 4:8, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:1, %% ACK. 0:32, 2:16, 0:32 >>), %% Receive a SETTINGS ack. {ok, << 0:24, 4:8, 0:7, 1:1, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), ok. ignore_push_promise_unknown_flags(Config) -> doc("Undefined PUSH_PROMISE frame flags must be ignored. (RFC7540 4.1, RFC7540 6.6)"), {ok, Socket} = do_handshake(Config), %% Send a PUSH_PROMISE frame with unknown flags. ok = gen_tcp:send(Socket, << 4:24, 5:8, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:1, %% PADDED. 1:1, %% END_HEADERS. 1:1, 1:1, %% Undefined. 0:1, 1:31, 0:1, 3:31 >> ), %% Receive a PROTOCOL_ERROR connection error. %% %% Note that it is not possible to distinguish between the expected %% result and the server rejecting PUSH_PROMISE frames. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_ping_unknown_flags(Config) -> doc("Undefined PING frame flags must be ignored. (RFC7540 4.1, RFC7540 6.7)"), {ok, Socket} = do_handshake(Config), %% Send a PING frame with unknown flags. ok = gen_tcp:send(Socket, << 8:24, 6:8, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:1, %% ACK. 0:32, 0:64 >>), %% Receive a PING ACK in return. {ok, << 8:24, 6:8, _:7, 1:1, _:32, 0:64 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_goaway_unknown_flags(Config) -> doc("Undefined GOAWAY frame flags must be ignored. (RFC7540 4.1, RFC7540 6.8)"), {ok, Socket} = do_handshake(Config), %% Send a GOAWAY frame with unknown flags. ok = gen_tcp:send(Socket, << 8:24, 7:8, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:32, 0:64 >>), %% Receive a GOAWAY frame back. {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_window_update_unknown_flags(Config) -> doc("Undefined WINDOW_UPDATE frame flags must be ignored. (RFC7540 4.1, RFC7540 6.9)"), {ok, Socket} = do_handshake(Config), %% Send a WINDOW_UPDATE frame with unknown flags. ok = gen_tcp:send(Socket, << 4:24, 8:8, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 0:32, 1000:32 >>), %% We expect no errors or replies, therefore we send a PING frame. ok = gen_tcp:send(Socket, cow_http2:ping(0)), %% And receive a PING ACK in return. {ok, << 8:24, 6:8, _:7, 1:1, _:32, 0:64 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_continuation_unknown_flags(Config) -> doc("Undefined CONTINUATION frame flags must be ignored. (RFC7540 4.1, RFC7540 6.10)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a CONTINUATION frame with unknown flags. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), Len = iolist_size(HeadersBlock), ok = gen_tcp:send(Socket, [ << 0:24, 1:8, 0:8, 0:1, 1:31 >>, << Len:24, 9:8, 1:1, 1:1, 1:1, 1:1, 1:1, %% Undefined. 1:1, %% END_HEADERS. 1:1, 1:1, %% Undefined. 0:1, 1:31 >>, HeadersBlock, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. %% @todo Flags that have no defined semantics for %% a particular frame type MUST be left unset (0x0) when sending. (RFC7540 4.1) ignore_data_reserved_bit(Config) -> doc("Reserved 1-bit field of DATA frame must be ignored. (RFC7540 4.1, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a DATA frame with the reserved bit set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 100:24, 0:8, 0:7, 1:1, 1:1, %% Reserved bit. 1:31, 0:100/unit:8 >> ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_headers_reserved_bit(Config) -> doc("Reserved 1-bit field of HEADERS frame must be ignored. (RFC7540 4.1, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a HEADERS frame with the reserved bit set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), Len = iolist_size(HeadersBlock), ok = gen_tcp:send(Socket, [ << Len:24, 1:8, 0:5, 1:1, 0:2, 1:1, %% Reserved bit. 1:31 >>, HeadersBlock, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_priority_reserved_bit(Config) -> doc("Reserved 1-bit field of PRIORITY frame must be ignored. (RFC7540 4.1, RFC7540 6.3)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with an interleaved PRIORITY frame with the reserved bit set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 5:24, 2:8, 0:8, 1:1, %% Reserved bit. 1:31, 0:1, 3:31, 0:8 >>, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_rst_stream_reserved_bit(Config) -> doc("Reserved 1-bit field of RST_STREAM frame must be ignored. (RFC7540 4.1, RFC7540 6.4)"), {ok, Socket} = do_handshake(Config), %% Send a POST request then cancel it with an RST_STREAM frame with the reserved bit set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 4:24, 3:8, 0:8, 1:1, %% Reserved bit. 1:31, 8:32 >>, cow_http2:headers(3, nofin, HeadersBlock), cow_http2:data(3, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 3:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 3:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. ignore_settings_reserved_bit(Config) -> doc("Reserved 1-bit field of SETTINGS frame must be ignored. (RFC7540 4.1, RFC7540 6.5)"), {ok, Socket} = do_handshake(Config), %% Send a SETTINGS frame with the reserved bit set. ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:8, 1:1, %% Reserved bit. 0:31, 2:16, 0:32 >>), %% Receive a SETTINGS ack. {ok, << 0:24, 4:8, 0:7, 1:1, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), ok. ignore_push_promise_reserved_bit(Config) -> doc("Reserved 1-bit field of PUSH_PROMISE frame must be ignored. (RFC7540 4.1, RFC7540 6.6)"), {ok, Socket} = do_handshake(Config), %% Send a PUSH_PROMISE frame with the reserved bit set. ok = gen_tcp:send(Socket, << 4:24, 5:8, 0:5, 1:1, 0:2, 1:1, %% Reserved bit. 1:31, 0:1, 3:31 >> ), %% Receive a PROTOCOL_ERROR connection error. %% %% Note that it is not possible to distinguish between the expected %% result and the server rejecting PUSH_PROMISE frames. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_ping_reserved_bit(Config) -> doc("Reserved 1-bit field of PING frame must be ignored. (RFC7540 4.1, RFC7540 6.7)"), {ok, Socket} = do_handshake(Config), %% Send a PING frame with the reserved bit set. ok = gen_tcp:send(Socket, << 8:24, 6:8, 0:8, 1:1, %% Reserved bit. 0:31, 0:64 >>), %% Receive a PING ACK in return. {ok, << 8:24, 6:8, _:7, 1:1, _:32, 0:64 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_goaway_reserved_bit(Config) -> doc("Reserved 1-bit field of GOAWAY frame must be ignored. (RFC7540 4.1, RFC7540 6.8)"), {ok, Socket} = do_handshake(Config), %% Send a GOAWAY frame with the reserved bit set. ok = gen_tcp:send(Socket, << 8:24, 7:8, 0:8, 1:1, %% Reserved bit. 0:31, 0:64 >>), %% Receive a GOAWAY frame back. {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_window_update_reserved_bit(Config) -> doc("Reserved 1-bit field of WINDOW_UPDATE frame must be ignored. (RFC7540 4.1, RFC7540 6.9)"), {ok, Socket} = do_handshake(Config), %% Send a WINDOW_UPDATE frame with the reserved bit set. ok = gen_tcp:send(Socket, << 4:24, 8:8, 0:8, 1:1, %% Reserved bit. 0:31, 1000:32 >>), %% We expect no errors or replies, therefore we send a PING frame. ok = gen_tcp:send(Socket, cow_http2:ping(0)), %% And receive a PING ACK in return. {ok, << 8:24, 6:8, _:7, 1:1, _:32, 0:64 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ignore_continuation_reserved_bit(Config) -> doc("Reserved 1-bit field of CONTINUATION frame must be ignored. (RFC7540 4.1, RFC7540 6.10)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a CONTINUATION frame with the reserved bit set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), Len = iolist_size(HeadersBlock), ok = gen_tcp:send(Socket, [ << 0:24, 1:8, 0:8, 0:1, 1:31 >>, << Len:24, 9:8, 0:5, 1:1, 0:2, 1:1, %% Reserved bit. 1:31 >>, HeadersBlock, cow_http2:data(1, fin, << 0:100/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 100:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:100/unit:8 >>} = gen_tcp:recv(Socket, 100, 1000), ok. %% @todo The reserved 1-bit field MUST remain unset (0x0) when sending. (RFC7540 4.1) %% Frame size. max_frame_size_allow_exactly_default(Config) -> doc("All implementations must allow frame sizes of at least 16384. (RFC7540 4.1, RFC7540 4.2)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a DATA frame of exactly 16384 bytes. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, fin, << 0:16384/unit:8 >>) ]), %% Receive a response with the same DATA frame. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = case gen_tcp:recv(Socket, 9, 1000) of %% We received a WINDOW_UPDATE first. Skip it and the next. {ok, <<4:24, 8:8, 0:40>>} -> {ok, _} = gen_tcp:recv(Socket, 4 + 13, 1000), gen_tcp:recv(Socket, 9, 1000); Res -> Res end, {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 16384:24, 0:8, 1:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << 0:16384/unit:8 >>} = gen_tcp:recv(Socket, 16384, 1000), ok. max_frame_size_reject_larger_than_default(Config) -> doc("A FRAME_SIZE_ERROR connection error must be sent when receiving " "frames larger than the default 16384 length. (RFC7540 4.1, RFC7540 4.2)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with a DATA frame larger than 16384 bytes. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, fin, << 0:16385/unit:8 >>) ]), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. max_frame_size_allow_exactly_custom(Config0) -> doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must allow frames " "of up to that size. (RFC7540 4.2, RFC7540 6.5.2)"), %% Create a new listener that sets the maximum frame size to 30000. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, max_frame_size_received => 30000 }, Config0), try %% Do the handshake. {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame initiating a stream followed by %% a single 30000 bytes DATA frame. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, fin, <<0:30000/unit:8>>) ]), %% Receive a proper response. {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Len2, 6000), %% No errors follow due to our sending of a 25000 bytes frame. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. max_frame_size_reject_larger_than_custom(Config0) -> doc("An endpoint that sets SETTINGS_MAX_FRAME_SIZE must reject frames " "of up to that size with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.5.2)"), %% Create a new listener that sets the maximum frame size to 30000. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, max_frame_size_received => 30000 }, Config0), try %% Do the handshake. {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame initiating a stream followed by %% a single DATA frame larger than 30000 bytes. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, fin, <<0:30001/unit:8>>) ]), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. %% I am using FRAME_SIZE_ERROR here because the information in the %% frame header tells us this frame is at least 1 byte long, while %% the given length is smaller; i.e. it is too small to contain %% mandatory frame data (the pad length). data_reject_frame_size_0_padded_flag(Config) -> doc("DATA frames of size 0 with the PADDED flag set must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with an incorrect padded DATA frame size. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 0:24, 0:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31 >> ]), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% This case on the other hand is noted specifically in the RFC %% as being a PROTOCOL_ERROR. It can be thought of as the Pad Length %% being incorrect, rather than the frame size. data_reject_frame_size_too_small_padded_flag(Config) -> doc("DATA frames with Pad Length >= Length must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a POST request with an incorrect padded DATA frame size. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), << 10:24, 0:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31, 10:8, 0:80 >> ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. headers_reject_frame_size_0_padded_flag(Config) -> doc("HEADERS frames of size 0 with the PADDED flag set must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a padded HEADERS frame with an incorrect size. ok = gen_tcp:send(Socket, << 0:24, 1:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. headers_reject_frame_size_too_small_padded_flag(Config) -> doc("HEADERS frames with no priority flag and Pad Length >= Length " "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a padded HEADERS frame with an incorrect size. ok = gen_tcp:send(Socket, << 10:24, 1:8, 0:4, 1:1, 0:2, 1:1, 0:1, 1:31, 10:8, 0:80 >>), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. headers_reject_frame_size_too_small_priority_flag(Config) -> doc("HEADERS frames of size smaller than 5 with the PRIORITY flag set must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with priority set and an incorrect size. ok = gen_tcp:send(Socket, << 4:24, 1:8, 0:2, 1:1, 0:4, 1:1, 0:1, 1:31, 0:1, 3:31, 0:8 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. headers_reject_frame_size_5_padded_and_priority_flags(Config) -> doc("HEADERS frames of size smaller than 6 with the PADDED " "and PRIORITY flags set must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a padded HEADERS frame with an incorrect size. ok = gen_tcp:send(Socket, << 5:24, 1:8, 0:2, 1:1, 0:1, 1:1, 0:2, 1:1, 0:1, 1:31, 0:8, 0:1, 3:31, 0:8 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. headers_reject_frame_size_too_small_padded_and_priority_flags(Config) -> doc("HEADERS frames of size smaller than Length+6 with the PADDED and PRIORITY flags set " "must be rejected with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a padded HEADERS frame with an incorrect size. ok = gen_tcp:send(Socket, << 15:24, 1:8, 0:2, 1:1, 0:1, 1:1, 0:2, 1:1, 0:1, 1:31, 10:8, 0:1, 3:31, 0:8, 0:80 >>), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. priority_reject_frame_size_too_small(Config) -> doc("PRIORITY frames of size smaller than 5 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.3)"), {ok, Socket} = do_handshake(Config), %% Send a PRIORITY frame with an incorrect size. ok = gen_tcp:send(Socket, << 4:24, 2:8, 0:9, 1:31, 0:1, 3:31, 0:8 >>), %% Receive a FRAME_SIZE_ERROR stream error. {ok, << _:24, 3:8, _:40, 6:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. priority_reject_frame_size_too_large(Config) -> doc("PRIORITY frames of size larger than 5 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.3)"), {ok, Socket} = do_handshake(Config), %% Send a PRIORITY frame with an incorrect size. ok = gen_tcp:send(Socket, << 6:24, 2:8, 0:9, 1:31, 0:1, 3:31, 0:16 >>), %% Receive a FRAME_SIZE_ERROR stream error. {ok, << _:24, 3:8, _:40, 6:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. rst_stream_reject_frame_size_too_small(Config) -> doc("RST_STREAM frames of size smaller than 4 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.4)"), {ok, Socket} = do_handshake(Config), %% Send a request and reset it immediately. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, fin, HeadersBlock), << 3:24, 3:8, 0:9, 1:31, 8:32 >> ]), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. rst_stream_reject_frame_size_too_large(Config) -> doc("RST_STREAM frames of size larger than 4 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.4)"), {ok, Socket} = do_handshake(Config), %% Send a request and reset it immediately. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, fin, HeadersBlock), << 5:24, 3:8, 0:9, 1:31, 8:32 >> ]), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. settings_reject_bad_frame_size(Config) -> doc("SETTINGS frames must have a size multiple of 6 or be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.5)"), {ok, Socket} = do_handshake(Config), %% Send a SETTINGS frame with an incorrect size. ok = gen_tcp:send(Socket, << 5:24, 4:8, 0:40, 1:16, 4096:32 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. settings_ack_reject_non_empty_frame_size(Config) -> doc("SETTINGS frames with the ACK flag set and a non-empty payload " "must be rejected with a FRAME_SIZE_ERROR connection error (RFC7540 4.2, RFC7540 6.5)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send a SETTINGS ack with a payload. ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:7, 1:1, 0:32, 1:16, 4096:32 >>), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% Note that clients are not supposed to send PUSH_PROMISE frames. %% However when they do, we need to be able to parse it in order %% to reject it, and so these errors may still occur. push_promise_reject_frame_size_too_small(Config) -> doc("PUSH_PROMISE frames of size smaller than 4 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.6)"), {ok, Socket} = do_handshake(Config), %% Send a PUSH_PROMISE frame with an incorrect size. ok = gen_tcp:send(Socket, << 3:24, 5:8, 0:5, 1:1, 0:3, 1:31, 0:1, 3:31 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. push_promise_reject_frame_size_4_padded_flag(Config) -> doc("PUSH_PROMISE frames of size smaller than 5 with the PADDED flag set must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.6)"), {ok, Socket} = do_handshake(Config), %% Send a PUSH_PROMISE frame with an incorrect size. ok = gen_tcp:send(Socket, << 4:24, 5:8, 0:4, 1:1, 1:1, 0:3, 1:31, 0:1, 0:8, 3:31 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. push_promise_reject_frame_size_too_small_padded_flag(Config) -> doc("PUSH_PROMISE frames of size smaller than Length+5 with the PADDED flag set " "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 4.2, RFC7540 6.6)"), {ok, Socket} = do_handshake(Config), %% Send a PUSH_PROMISE frame with an incorrect size. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), Len = 14 + iolist_size(HeadersBlock), ok = gen_tcp:send(Socket, [ << Len:24, 5:8, 0:4, 1:1, 1:1, 0:3, 1:31, 10:8, 0:1, 3:31 >>, HeadersBlock, << 0:80 >> ]), %% Receive a PROTOCOL_ERROR connection error. %% %% Note that it is not possible to distinguish between a Pad Length %% error and the server rejecting PUSH_PROMISE frames. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ping_reject_frame_size_too_small(Config) -> doc("PING frames of size smaller than 8 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.7)"), {ok, Socket} = do_handshake(Config), %% Send a PING frame with an incorrect size. ok = gen_tcp:send(Socket, << 7:24, 6:8, 0:40, 0:56 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. ping_reject_frame_size_too_large(Config) -> doc("PING frames of size larger than 8 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.7)"), {ok, Socket} = do_handshake(Config), %% Send a PING frame with an incorrect size. ok = gen_tcp:send(Socket, << 9:24, 6:8, 0:40, 0:72 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. goaway_reject_frame_size_too_small(Config) -> doc("GOAWAY frames of size smaller than 8 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.8)"), {ok, Socket} = do_handshake(Config), %% Send a GOAWAY frame with an incorrect size. ok = gen_tcp:send(Socket, << 7:24, 7:8, 0:40, 0:56 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. goaway_allow_frame_size_too_large(Config) -> doc("GOAWAY frames of size larger than 8 must be allowed. (RFC7540 6.8)"), {ok, Socket} = do_handshake(Config), %% Send a GOAWAY frame with debug data. ok = gen_tcp:send(Socket, << 12:24, 7:8, 0:40, 0:64, 99999:32 >>), %% Receive a GOAWAY frame back. {ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. window_update_reject_frame_size_too_small(Config) -> doc("WINDOW_UPDATE frames of size smaller than 4 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.9)"), {ok, Socket} = do_handshake(Config), %% Send a WINDOW_UPDATE frame with an incorrect size. ok = gen_tcp:send(Socket, << 3:24, 8:8, 0:40, 1000:24 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. window_update_reject_frame_size_too_large(Config) -> doc("WINDOW_UPDATE frames of size larger than 4 must be rejected " "with a FRAME_SIZE_ERROR connection error. (RFC7540 4.2, RFC7540 6.9)"), {ok, Socket} = do_handshake(Config), %% Send a WINDOW_UPDATE frame with an incorrect size. ok = gen_tcp:send(Socket, << 5:24, 8:8, 0:40, 1000:40 >>), %% Receive a FRAME_SIZE_ERROR connection error. {ok, << _:24, 7:8, _:72, 6:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% Note: There is no particular limits on the size of CONTINUATION frames, %% they can go from 0 to SETTINGS_MAX_FRAME_SIZE. %% Header compression and decompression. headers_compression_error(Config) -> doc("A decoding error in a HEADERS frame's header block must be rejected " "with a COMPRESSION_ERROR connection error. (RFC7540 4.3, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with an invalid header block. ok = gen_tcp:send(Socket, << 10:24, 1:8, 0:5, 1:1, 0:1, 1:1, 0:1, 1:31, 0:10/unit:8 >>), %% Receive a COMPRESSION_ERROR connection error. {ok, << _:24, 7:8, _:72, 9:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. continuation_compression_error(Config) -> doc("A decoding error in a CONTINUATION frame's header block must be rejected " "with a COMPRESSION_ERROR connection error. (RFC7540 4.3, RFC7540 6.10)"), {ok, Socket} = do_handshake(Config), %% Send a CONTINUATION frame with an invalid header block. ok = gen_tcp:send(Socket, [ << 0:24, 1:8, 0:7, 1:1, 0:1, 1:31 >>, << 10:24, 9:8, 0:5, 1:1, 0:3, 1:31, 0:10/unit:8 >> ]), %% Receive a COMPRESSION_ERROR connection error. {ok, << _:24, 7:8, _:72, 9:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. continuation_with_frame_interleaved_error(Config) -> doc("Frames interleaved in a header block must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 4.3, RFC7540 6.2, RFC7540 6.10)"), {ok, Socket} = do_handshake(Config), %% Send an unterminated HEADERS frame followed by a PING frame. ok = gen_tcp:send(Socket, [ << 0:24, 1:8, 0:7, 1:1, 0:1, 1:31 >>, cow_http2:ping(0) ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. continuation_wrong_stream_error(Config) -> doc("CONTINUATION frames with an incorrect stream identifier must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 4.3, RFC7540 6.2)"), {ok, Socket} = do_handshake(Config), %% Send an unterminated HEADERS frame followed by a CONTINUATION frame for another stream. ok = gen_tcp:send(Socket, [ << 0:24, 1:8, 0:7, 1:1, 0:1, 1:31 >>, << 0:24, 9:8, 0:9, 3:31 >> ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% Stream states. idle_stream_reject_data(Config) -> doc("DATA frames received on an idle stream must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 5.1, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a DATA frame on an idle stream. ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. idle_stream_accept_headers(Config) -> doc("HEADERS frames received on an idle stream must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame on an idle stream. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. idle_stream_accept_priority(Config) -> doc("PRIORITY frames received on an idle stream must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a PRIORITY frame on an idle stream. ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), %% Receive no error. {error, timeout} = gen_tcp:recv(Socket, 7, 1000), %% Send a HEADERS frame on the same stream. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. idle_stream_reject_rst_stream(Config) -> doc("RST_STREAM frames received on an idle stream must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send an RST_STREAM frame on an idle stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, no_error)), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. idle_stream_reject_push_promise(Config) -> doc("PUSH_PROMISE frames received on an idle stream must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a PUSH_PROMISE frame on an idle stream. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:push_promise(1, 3, HeadersBlock)), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. idle_stream_reject_window_update(Config) -> doc("WINDOW_UPDATE frames received on an idle stream must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a WINDOW_UPDATE frame on an idle stream. ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %reserved (local) - after sending PUSH_PROMISE: % An endpoint MUST NOT send any type of frame other than HEADERS, % RST_STREAM, or PRIORITY in this state. %%% how to test this? % % A PRIORITY or WINDOW_UPDATE frame MAY be received in this state. % Receiving any type of frame other than RST_STREAM, PRIORITY, or % WINDOW_UPDATE on a stream in this state MUST be treated as a % connection error (Section 5.4.1) of type PROTOCOL_ERROR. %%% we need to use a large enough file for this % %reserved_local_reject_data %reserved_local_reject_headers %reserved_local_accept_priority %reserved_local_accept_rst_stream %reserved_local_reject_push_promise %% do we even care? we reject it always %reserved_local_accept_window_update % %half-closed (remote): % If an endpoint receives additional frames, other than % WINDOW_UPDATE, PRIORITY, or RST_STREAM, for a stream that is in % this state, it MUST respond with a stream error (Section 5.4.2) of % type STREAM_CLOSED. half_closed_remote_reject_data(Config) -> doc("DATA frames received on a half-closed (remote) stream must be rejected " "with a STREAM_CLOSED stream error. (RFC7540 5.1, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with the FIN flag set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Send a DATA frame on that now half-closed (remote) stream. ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), %% Receive a STREAM_CLOSED stream error. {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. %% We reject all invalid HEADERS with a connection error because %% we do not want to waste resources decoding them. half_closed_remote_reject_headers(Config) -> doc("HEADERS frames received on a half-closed (remote) stream must be rejected " "with a STREAM_CLOSED connection error. (RFC7540 4.3, RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with the FIN flag set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Send a HEADERS frame on that now half-closed (remote) stream. ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. half_closed_remote_accept_priority(Config) -> doc("PRIORITY frames received on a half-closed stream must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with the FIN flag set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Send a PRIORITY frame on that now half-closed (remote) stream. ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. half_closed_remote_accept_rst_stream(Config) -> doc("RST_STREAM frames received on a half-closed stream must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with the FIN flag set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Send an RST_STREAM frame on that now half-closed (remote) stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, no_error)), %% Receive nothing back. {error, timeout} = gen_tcp:recv(Socket, 9, 6000), ok. %% half_closed_remote_reject_push_promise %% %% We respond to all PUSH_PROMISE frames with a PROTOCOL_ERROR connection error %% because PUSH is disabled in that direction. We therefore cannot test other %% error conditions. half_closed_remote_accept_window_update(Config) -> doc("WINDOW_UPDATE frames received on a half-closed stream must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with the FIN flag set. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Send a WINDOW_UPDATE frame on that now half-closed (remote) stream. ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. %% We reject DATA frames sent on closed streams with a STREAM_CLOSED %% connection error regardless of how the stream was closed to simplify %% the implementation. This excludes the few frames we ignore from %% lingering streams that we canceled. rst_stream_closed_reject_data(Config) -> doc("DATA frames received on a stream closed via RST_STREAM must be rejected " "with a STREAM_CLOSED connection error. (RFC7540 5.1, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Send an RST_STREAM frame to close the stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Send a DATA frame on the now RST_STREAM closed stream. ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% We reject all invalid HEADERS with a connection error because %% we do not want to waste resources decoding them. rst_stream_closed_reject_headers(Config) -> doc("HEADERS frames received on a stream closed via RST_STREAM must be rejected " "with a STREAM_CLOSED connection error. (RFC7540 4.3, RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Send an RST_STREAM frame to close the stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Send a HEADERS frame on the now RST_STREAM closed stream. ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. rst_stream_closed_accept_priority(Config) -> doc("PRIORITY frames received on a stream closed via RST_STREAM " "must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Send an RST_STREAM frame to close the stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Send a PRIORITY frame on that now RST_STREAM closed stream. ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), %% Receive nothing back. {error, timeout} = gen_tcp:recv(Socket, 9, 6000), ok. rst_stream_closed_ignore_rst_stream(Config) -> doc("RST_STREAM frames received on a stream closed via RST_STREAM " "must be ignored to avoid looping. (RFC7540 5.1, RFC7540 5.4.2)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Send an RST_STREAM frame to close the stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Send an extra RST_STREAM. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Receive nothing back. {error, timeout} = gen_tcp:recv(Socket, 9, 6000), ok. %% rst_stream_closed_reject_push_promise %% %% We respond to all PUSH_PROMISE frames with a PROTOCOL_ERROR connection error %% because PUSH is disabled in that direction. We therefore cannot test other %% error conditions. rst_stream_closed_reject_window_update(Config) -> doc("WINDOW_UPDATE frames received on a stream closed via RST_STREAM " "must be rejected with a STREAM_CLOSED stream error. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Send an RST_STREAM frame to close the stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Send a WINDOW_UPDATE frame on the now RST_STREAM closed stream. ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), %% Receive a STREAM_CLOSED stream error. {ok, << _:24, 3:8, _:8, 1:32, 5:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. stream_closed_reject_data(Config) -> doc("DATA frames received on a stream closed normally must be rejected " "with a STREAM_CLOSED connection error. (RFC7540 5.1, RFC7540 6.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive the response. {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length1, 6000), {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length2, 6000), %% Send a DATA frame on the now closed stream. ok = gen_tcp:send(Socket, cow_http2:data(1, fin, <<"Unexpected DATA frame.">>)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. stream_closed_reject_headers(Config) -> doc("HEADERS frames received on a stream closed normally must be rejected " "with a STREAM_CLOSED connection error. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive the response. {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length1, 6000), {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length2, 6000), %% Send a HEADERS frame on the now closed stream. ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. stream_closed_accept_priority(Config) -> doc("PRIORITY frames received on a stream closed normally must be accepted. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive the response. {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length1, 6000), {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length2, 6000), %% Send a PRIORITY frame on the now closed stream. ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 3, 123)), %% Receive nothing back. {error, timeout} = gen_tcp:recv(Socket, 9, 6000), ok. stream_closed_accept_rst_stream(Config) -> doc("RST_STREAM frames received on a stream closed normally " "must be accepted for a short period. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive the response. {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length1, 6000), {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length2, 6000), %% Send an RST_STREAM frame on the now closed stream. ok = gen_tcp:send(Socket, cow_http2:rst_stream(1, cancel)), %% Receive nothing back. {error, timeout} = gen_tcp:recv(Socket, 9, 6000), ok. %% stream_closed_reject_push_promise %% %% We respond to all PUSH_PROMISE frames with a PROTOCOL_ERROR connection error %% because PUSH is disabled in that direction. We therefore cannot test other %% error conditions. stream_closed_accept_window_update(Config) -> doc("WINDOW_UPDATE frames received on a stream closed normally " "must be accepted for a short period. (RFC7540 5.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive the response. {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length1, 6000), {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length2, 6000), %% Send a WINDOW_UPDATE frame on the now closed stream. ok = gen_tcp:send(Socket, cow_http2:window_update(1, 12345)), %% Receive nothing back. {error, timeout} = gen_tcp:recv(Socket, 9, 6000), ok. %% @todo While we accept RST_STREAM and WINDOW_UPDATE for a short period %% after the stream closed normally, we may want to reject the ones coming %% a significant amount of time after that. %% @todo Frames may arrive on a stream after we send an RST_STREAM for it. %% They must be ignored for a short period of time: % % If this state is reached as a result of sending a RST_STREAM % frame, the peer that receives the RST_STREAM might have already % sent -- or enqueued for sending -- frames on the stream that % cannot be withdrawn. An endpoint MUST ignore frames that it % receives on closed streams after it has sent a RST_STREAM frame. % An endpoint MAY choose to limit the period over which it ignores % frames and treat frames that arrive after this time as being in % error. %% @todo Ensure that rejected DATA frames result in the connection %% flow-control window being updated. How to test this? % % Flow-controlled frames (i.e., DATA) received after sending % RST_STREAM are counted toward the connection flow-control window. % Even though these frames might be ignored, because they are sent % before the sender receives the RST_STREAM, the sender will % consider the frames to count against the flow-control window. %% Stream identifiers. reject_streamid_even(Config) -> doc("HEADERS frames received with an even-numbered streamid " "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.1.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with an even-numbered streamid. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(2, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. reject_streamid_0(Config) -> doc("HEADERS frames received with streamid 0 (zero) " "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.1.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with an streamid 0. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(0, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% See the comment for reject_streamid_lower for the rationale behind %% having a STREAM_CLOSED connection error. http_upgrade_reject_reuse_streamid_1(Config) -> doc("Attempts to reuse streamid 1 after upgrading to HTTP/2 " "must be rejected with a STREAM_CLOSED connection error. (RFC7540 5.1.1)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), ok = do_recv_101(Socket), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack, and the response HEADERS and DATA (Stream ID 1). Received = lists:reverse(lists:foldl(fun(_, Acc) -> case gen_tcp:recv(Socket, 9, 1000) of {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> [settings_ack|Acc]; {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [headers|Acc]; {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), [data|Acc] end end, [], [1, 2, 3])), case Received of [settings_ack, headers, data] -> ok; [headers, settings_ack, data] -> ok; [headers, data, settings_ack] -> ok end, %% Send a HEADERS frame with streamid 1. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% The RFC gives us various error codes to return for this case, %% depending on whether the stream existed previously and how it %% ended up being (half-)closed. Cowboy rejects all these HEADERS %% frames the same way: with a STREAM_CLOSED connection error. %% Making it a connection error is particularly important in the %% cases where a stream error would be allowed because we avoid %% having to decode the headers and save up resources. reject_streamid_lower(Config) -> doc("HEADERS frames received with streamid lower than the previous stream " "must be rejected with a STREAM_CLOSED connection error. (RFC7540 5.1.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with streamid 5. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(5, fin, HeadersBlock)), %% Receive the response. {ok, << Length1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length1, 6000), {ok, << Length2:24, 0:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Length2, 6000), %% Send a HEADERS frame with streamid 3. ok = gen_tcp:send(Socket, cow_http2:headers(3, fin, HeadersBlock)), %% Receive a STREAM_CLOSED connection error. {ok, << _:24, 7:8, _:72, 5:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% @todo We need an option to limit the number of streams one can open %% on a connection. And we need to enforce it. (RFC7540 5.1.1) % % Stream identifiers cannot be reused. Long-lived connections can % result in an endpoint exhausting the available range of stream % identifiers. A server % that is unable to establish a new stream identifier can send a GOAWAY % frame so that the client is forced to open a new connection for new % streams. %% (RFC7540 5.2.1) % 3. Flow control is directional with overall control provided by the % receiver. A receiver MAY choose to set any window size that it % desires for each stream and for the entire connection. A sender % MUST respect flow-control limits imposed by a receiver. Clients, % servers, and intermediaries all independently advertise their % flow-control window as a receiver and abide by the flow-control % limits set by their peer when sending. % % 4. The initial value for the flow-control window is 65,535 octets % for both new streams and the overall connection. % % 5. The frame type determines whether flow control applies to a % frame. Of the frames specified in this document, only DATA % frames are subject to flow control; all other frame types do not % consume space in the advertised flow-control window. This % ensures that important control frames are not blocked by flow % control. % % 6. Flow control cannot be disabled. %% (RFC7540 5.2.2) % Even with full awareness of the current bandwidth-delay product, % implementation of flow control can be difficult. When using flow % control, the receiver MUST read from the TCP receive buffer in a % timely fashion. Failure to do so could lead to a deadlock when % critical frames, such as WINDOW_UPDATE, are not read and acted upon. %% (RFC7540 5.3.1) % Inside the dependency tree, a dependent stream SHOULD only be % allocated resources if either all of the streams that it depends on % (the chain of parent streams up to 0x0) are closed or it is not % possible to make progress on them. %% We reject all invalid HEADERS with a connection error because %% we do not want to waste resources decoding them. reject_self_dependent_stream_headers(Config) -> doc("HEADERS frames opening a stream that depends on itself " "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.3.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with priority set to depend on itself. ok = gen_tcp:send(Socket, << 5:24, 1:8, 0:2, 1:1, 0:4, 1:1, 0:1, 1:31, 0:1, 1:31, 0:8 >>), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% We reject all invalid HEADERS with a connection error because %% we do not want to waste resources decoding them. reject_self_dependent_stream_headers_with_padding(Config) -> doc("HEADERS frames opening a stream that depends on itself " "must be rejected with a PROTOCOL_ERROR connection error. (RFC7540 5.3.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with priority set to depend on itself. ok = gen_tcp:send(Socket, << 6:24, 1:8, 0:2, 1:1, 0:1, 1:1, 0:2, 1:1, 0:1, 1:31, 0:8, 0:1, 1:31, 0:8 >>), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. reject_self_dependent_stream_priority(Config) -> doc("PRIORITY frames making a stream depend on itself " "must be rejected with a PROTOCOL_ERROR stream error. (RFC7540 5.3.1)"), {ok, Socket} = do_handshake(Config), %% Send a PRIORITY frame making a stream depend on itself. ok = gen_tcp:send(Socket, cow_http2:priority(1, shared, 1, 123)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. %% @todo Stream priorities. (RFC7540 5.3.2 5.3.3 5.3.4 5.3.5) %% (RFC7540 5.4.1) % An endpoint that encounters a connection error SHOULD first send a % GOAWAY frame (Section 6.8) with the stream identifier of the last % stream that it successfully received from its peer. The GOAWAY frame % includes an error code that indicates why the connection is % terminating. After sending the GOAWAY frame for an error condition, % the endpoint MUST close the TCP connection. % % An endpoint can end a connection at any time. In particular, an % endpoint MAY choose to treat a stream error as a connection error. % Endpoints SHOULD send a GOAWAY frame when ending a connection, % providing that circumstances permit it. %% (RFC7540 5.4.2) % A RST_STREAM is the last frame that an endpoint can send on a stream. % The peer that sends the RST_STREAM frame MUST be prepared to receive % any frames that were sent or enqueued for sending by the remote peer. % These frames can be ignored, except where they modify connection % state (such as the state maintained for header compression % (Section 4.3) or flow control). % % Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame % for any stream. However, an endpoint MAY send additional RST_STREAM % frames if it receives frames on a closed stream after more than a % round-trip time. This behavior is permitted to deal with misbehaving % implementations. % % To avoid looping, an endpoint MUST NOT send a RST_STREAM in response % to a RST_STREAM frame. %% (RFC7540 5.5) % Extensions are permitted to use new frame types (Section 4.1), new % settings (Section 6.5.2), or new error codes (Section 7). Registries % are established for managing these extension points: frame types % (Section 11.2), settings (Section 11.3), and error codes % (Section 11.4). % % Implementations MUST ignore unknown or unsupported values in all % extensible protocol elements. Implementations MUST discard frames % that have unknown or unsupported types. This means that any of these % extension points can be safely used by extensions without prior % arrangement or negotiation. However, extension frames that appear in % the middle of a header block (Section 4.3) are not permitted; these % MUST be treated as a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. continuation_with_extension_frame_interleaved_error(Config) -> doc("Extension frames interleaved in a header block must be rejected " "with a PROTOCOL_ERROR connection error. " "(RFC7540 4.3, RFC7540 5.5, RFC7540 6.2, RFC7540 6.10)"), {ok, Socket} = do_handshake(Config), %% Send an unterminated HEADERS frame followed by an extension frame. ok = gen_tcp:send(Socket, [ << 0:24, 1:8, 0:7, 1:1, 0:1, 1:31 >>, << 0:24, 128:8, 0:8, 0:32 >> ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% (RFC7540 6.1) DATA % Padding: Padding octets that contain no application semantic value. % Padding octets MUST be set to zero when sending. A receiver is % not obligated to verify padding but MAY treat non-zero padding as % a connection error (Section 5.4.1) of type PROTOCOL_ERROR. % % DATA frames MUST be associated with a stream. If a DATA frame is % received whose stream identifier field is 0x0, the recipient MUST % respond with a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. %% (RFC7540 6.2) HEADERS % Padding: Padding octets that contain no application semantic value. % Padding octets MUST be set to zero when sending. A receiver is % not obligated to verify padding but MAY treat non-zero padding as % a connection error (Section 5.4.1) of type PROTOCOL_ERROR. % % A HEADERS frame carries the END_STREAM flag that signals the end % of a stream. However, a HEADERS frame with the END_STREAM flag % set can be followed by CONTINUATION frames on the same stream. % Logically, the CONTINUATION frames are part of the HEADERS frame. % %% @todo We probably need a test for the server sending HEADERS too large. % The payload of a HEADERS frame contains a header block fragment % (Section 4.3). A header block that does not fit within a HEADERS % frame is continued in a CONTINUATION frame (Section 6.10). % % HEADERS frames MUST be associated with a stream. If a HEADERS frame % is received whose stream identifier field is 0x0, the recipient MUST % respond with a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. %% (RFC7540 6.3) PRIORITY % The PRIORITY frame always identifies a stream. If a PRIORITY frame % is received with a stream identifier of 0x0, the recipient MUST % respond with a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. %% (RFC7540 6.4) RST_STREAM % The RST_STREAM frame fully terminates the referenced stream and % causes it to enter the "closed" state. After receiving a RST_STREAM % on a stream, the receiver MUST NOT send additional frames for that % stream, with the exception of PRIORITY. However, after sending the % RST_STREAM, the sending endpoint MUST be prepared to receive and % process additional frames sent on the stream that might have been % sent by the peer prior to the arrival of the RST_STREAM. % % RST_STREAM frames MUST be associated with a stream. If a RST_STREAM % frame is received with a stream identifier of 0x0, the recipient MUST % treat this as a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. %% (RFC7540 6.5) SETTINGS % A SETTINGS frame MUST be sent by both endpoints at the start of a % connection and MAY be sent at any other time by either endpoint over % the lifetime of the connection. Implementations MUST support all of % the parameters defined by this specification. % % SETTINGS frames always apply to a connection, never a single stream. % The stream identifier for a SETTINGS frame MUST be zero (0x0). If an % endpoint receives a SETTINGS frame whose stream identifier field is % anything other than 0x0, the endpoint MUST respond with a connection % error (Section 5.4.1) of type PROTOCOL_ERROR. % % The SETTINGS frame affects connection state. A badly formed or % incomplete SETTINGS frame MUST be treated as a connection error % (Section 5.4.1) of type PROTOCOL_ERROR. %% Settings. settings_header_table_size_client(Config) -> doc("The SETTINGS_HEADER_TABLE_SIZE setting can be used to " "inform the server of the maximum header table size " "used by the client to decode header blocks. (RFC7540 6.5.2)"), HeaderTableSize = 128, %% Do the handhsake. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{header_table_size => HeaderTableSize})]), %% Receive the server preface. {ok, << Len0:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len0/binary >>} = gen_tcp:recv(Socket, 6 + Len0, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Initialize decoding/encoding states. DecodeState = cow_hpack:set_max_size(HeaderTableSize, cow_hpack:init()), EncodeState = cow_hpack:init(), %% Send a HEADERS frame as a request. {ReqHeadersBlock1, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ], EncodeState), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, ReqHeadersBlock1)), %% Receive a HEADERS frame as a response. {ok, << Len1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, RespHeadersBlock1} = gen_tcp:recv(Socket, Len1, 6000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock1, DecodeState), {_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders), %% The decoding succeeded, confirming that the table size is %% lower than or equal to HeaderTableSize. ok. settings_header_table_size_server(Config0) -> doc("The SETTINGS_HEADER_TABLE_SIZE setting can be used to " "inform the client of the maximum header table size " "used by the server to decode header blocks. (RFC7540 6.5.2)"), HeaderTableSize = 128, %% Create a new listener that allows larger header table sizes. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, max_decode_table_size => HeaderTableSize }, Config0), try %% Do the handhsake. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{header_table_size => HeaderTableSize})]), %% Receive the server preface. {ok, << Len0:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, Data = <<_:48, _:Len0/binary>>} = gen_tcp:recv(Socket, 6 + Len0, 1000), %% Confirm the server's SETTINGS_HEADERS_TABLE_SIZE uses HeaderTableSize. {ok, {settings, #{header_table_size := HeaderTableSize}}, <<>>} = cow_http2:parse(<>), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Initialize decoding/encoding states. DecodeState = cow_hpack:init(), EncodeState = cow_hpack:set_max_size(HeaderTableSize, cow_hpack:init()), %% Send a HEADERS frame as a request. {ReqHeadersBlock1, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ], EncodeState), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, ReqHeadersBlock1)), %% Receive a HEADERS frame as a response. {ok, << Len1:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, RespHeadersBlock1} = gen_tcp:recv(Socket, Len1, 6000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock1, DecodeState), {_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders), %% The decoding succeeded on the server, confirming that %% the table size was updated to HeaderTableSize. gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_max_concurrent_streams(Config0) -> doc("The SETTINGS_MAX_CONCURRENT_STREAMS setting can be used to " "restrict the number of concurrent streams. (RFC7540 5.1.2, RFC7540 6.5.2)"), %% Create a new listener that allows only a single concurrent stream. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, max_concurrent_streams => 1 }, Config0), try {ok, Socket} = do_handshake(Config), %% Send two HEADERS frames as two separate streams. Headers = [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {ReqHeadersBlock1, EncodeState} = cow_hpack:encode(Headers), {ReqHeadersBlock2, _} = cow_hpack:encode(Headers, EncodeState), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, fin, ReqHeadersBlock1), cow_http2:headers(3, fin, ReqHeadersBlock2) ]), %% Receive a REFUSED_STREAM stream error. {ok, << _:24, 3:8, _:8, 3:32, 7:32 >>} = gen_tcp:recv(Socket, 13, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_max_concurrent_streams_0(Config0) -> doc("The SETTINGS_MAX_CONCURRENT_STREAMS setting can be set to " "0 to refuse all incoming streams. (RFC7540 5.1.2, RFC7540 6.5.2)"), %% Create a new listener that allows only a single concurrent stream. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, max_concurrent_streams => 0 }, Config0), try {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a REFUSED_STREAM stream error. {ok, << _:24, 3:8, _:8, 1:32, 7:32 >>} = gen_tcp:recv(Socket, 13, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. %% @todo The client can limit the number of concurrent streams too. (RFC7540 5.1.2) % % A peer can limit the number of concurrently active streams using the % SETTINGS_MAX_CONCURRENT_STREAMS parameter (see Section 6.5.2) within % a SETTINGS frame. The maximum concurrent streams setting is specific % to each endpoint and applies only to the peer that receives the % setting. That is, clients specify the maximum number of concurrent % streams the server can initiate, and servers specify the maximum % number of concurrent streams the client can initiate. % % Endpoints MUST NOT exceed the limit set by their peer. An endpoint % that receives a HEADERS frame that causes its advertised concurrent % stream limit to be exceeded MUST treat this as a stream error % (Section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. The choice % of error code determines whether the endpoint wishes to enable % automatic retry (see Section 8.1.4) for details). settings_initial_window_size(Config0) -> doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " "change the initial window size of streams. (RFC7540 6.5.2)"), %% Create a new listener that sets initial window sizes to 100000. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_connection_window_size => 100000, initial_stream_window_size => 100000 }, Config0), try %% We need to do the handshake manually because a WINDOW_UPDATE %% frame will be sent to update the connection window. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the WINDOW_UPDATE for the connection. {ok, << 4:24, 8:8, 0:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame initiating a stream followed by %% DATA frames totaling 90000 bytes of body. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, fin, <<0:15000/unit:8>>) ]), %% Receive a proper response. {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Len2, 6000), %% No errors follow due to our sending of more than 65535 bytes of data. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_initial_window_size_after_ack(Config0) -> doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " "change the initial window size of streams. It is applied " "to all existing streams upon receipt of the SETTINGS ack. (RFC7540 6.5.2)"), %% Create a new listener that sets the initial stream window sizes to 0. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_stream_window_size => 0 }, Config0), try %% We need to do the handshake manually because we don't %% want to send the SETTINGS ack immediately. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), %% %% Don't send the SETTINGS ack yet! We want to create a stream first. %% %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame initiating a stream, a SETTINGS ack %% and a small DATA frame despite no window available in the stream. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:settings_ack(), cow_http2:data(1, fin, <<0:32/unit:8>>) ]), %% Receive a FLOW_CONTROL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 3:32 >>} = gen_tcp:recv(Socket, 13, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_initial_window_size_before_ack(Config0) -> doc("The SETTINGS_INITIAL_WINDOW_SIZE setting can be used to " "change the initial window size of streams. It is only " "applied upon receipt of the SETTINGS ack. (RFC7540 6.5.2)"), %% Create a new listener that sets the initial stream window sizes to 0. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_stream_window_size => 0 }, Config0), try %% We need to do the handshake manually because we don't %% want to send the SETTINGS ack. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), %% %% Don't send the SETTINGS ack! We want the server to keep the original settings. %% %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame initiating a stream followed by %% DATA frames totaling 60000 bytes of body. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, fin, <<0:15000/unit:8>>) ]), %% Receive a proper response. {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Len2, 6000), %% No errors follow due to our sending of more than 0 bytes of data. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_max_frame_size(Config0) -> doc("The SETTINGS_MAX_FRAME_SIZE setting can be used to " "change the maximum frame size allowed. (RFC7540 6.5.2)"), %% Create a new listener that sets the maximum frame size to 30000. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, max_frame_size_received => 30000 }, Config0), try %% Do the handshake. {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame initiating a stream followed by %% a single 25000 bytes DATA frame. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, fin, <<0:25000/unit:8>>) ]), %% Receive a proper response. {ok, << Len2:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, _} = gen_tcp:recv(Socket, Len2, 6000), %% No errors follow due to our sending of a 25000 bytes frame. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. settings_max_frame_size_reject_too_small(Config) -> doc("A SETTINGS_MAX_FRAME_SIZE smaller than 16384 must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"), {ok, Socket} = do_handshake(Config), %% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE lower than 16384. ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16383:32 >>), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. settings_max_frame_size_reject_too_large(Config) -> doc("A SETTINGS_MAX_FRAME_SIZE larger than 16777215 must be rejected " "with a PROTOCOL_ERROR connection error. (RFC7540 6.5.2)"), {ok, Socket} = do_handshake(Config), %% Send a SETTINGS frame with a SETTINGS_MAX_FRAME_SIZE larger than 16777215. ok = gen_tcp:send(Socket, << 6:24, 4:8, 0:40, 5:16, 16777216:32 >>), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. % SETTINGS_MAX_HEADER_LIST_SIZE (0x6): This advisory setting informs a % peer of the maximum size of header list that the sender is % prepared to accept, in octets. The value is based on the % uncompressed size of header fields, including the length of the % name and value in octets plus an overhead of 32 octets for each % header field. % % For any given request, a lower limit than what is advertised MAY % be enforced. The initial value of this setting is unlimited. % % An endpoint that receives a SETTINGS frame with any unknown or % unsupported identifier MUST ignore that setting. (6.5.2 and 6.5.3) %% (RFC7540 6.5.3) % Upon receiving a SETTINGS frame with the ACK flag set, the % sender of the altered parameters can rely on the setting having been % applied. % % If the sender of a SETTINGS frame does not receive an acknowledgement % within a reasonable amount of time, it MAY issue a connection error % (Section 5.4.1) of type SETTINGS_TIMEOUT. %% (RFC7540 6.6) PUSH_PROMISE % @todo PUSH_PROMISE frames have a reserved bit in the payload that must be ignored. % % Padding: Padding octets that contain no application semantic value. % Padding octets MUST be set to zero when sending. A receiver is % not obligated to verify padding but MAY treat non-zero padding as % a connection error (Section 5.4.1) of type PROTOCOL_ERROR. % % PUSH_PROMISE frames MUST only be sent on a peer-initiated stream that % is in either the "open" or "half-closed (remote)" state. The stream % identifier of a PUSH_PROMISE frame indicates the stream it is % associated with. If the stream identifier field specifies the value % 0x0, a recipient MUST respond with a connection error (Section 5.4.1) % of type PROTOCOL_ERROR. client_settings_disable_push(Config) -> doc("PUSH_PROMISE frames must not be sent when the setting " "SETTINGS_ENABLE_PUSH is disabled. (RFC7540 6.5.2, RFC7540 6.6, RFC7540 8.2)"), %% Do a prior knowledge handshake. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{ enable_push => false })]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame on a resource that sends PUSH_PROMISE frames. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/resp/push">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a HEADERS frame as a response, no PUSH_PROMISE frames. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. % Since PUSH_PROMISE reserves a stream, ignoring a PUSH_PROMISE frame % causes the stream state to become indeterminate. A receiver MUST % treat the receipt of a PUSH_PROMISE on a stream that is neither % "open" nor "half-closed (local)" as a connection error % (Section 5.4.1) of type PROTOCOL_ERROR. % % A receiver MUST treat the receipt of a PUSH_PROMISE that promises an % illegal stream identifier (Section 5.1.1) as a connection error % (Section 5.4.1) of type PROTOCOL_ERROR. Note that an illegal stream % identifier is an identifier for a stream that is not currently in the % "idle" state. %% (RFC7540 6.7) PING % PING frames are not associated with any individual stream. If a PING % frame is received with a stream identifier field value other than % 0x0, the recipient MUST respond with a connection error % (Section 5.4.1) of type PROTOCOL_ERROR. %% (RFC7540 6.8) GOAWAY % @todo GOAWAY frames have a reserved bit in the payload that must be ignored. % % A GOAWAY frame might not immediately precede closing of the % connection; a receiver of a GOAWAY that has no more use for the % connection SHOULD still send a GOAWAY frame before terminating the % connection. graceful_shutdown_client_stays(Config) -> doc("A server gracefully shutting down must send a GOAWAY frame with the " "last stream identifier set to 2^31-1 and a NO_ERROR code. After allowing " "time for any in-flight stream creation the server can send another GOAWAY " "frame with an updated last stream identifier. (RFC7540 6.8)"), {ok, Socket} = do_handshake(Config), ServerConnPid = get_remote_pid_tcp(Socket), ok = sys:terminate(ServerConnPid, whatever), %% First GOAWAY frame. {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 16#7fffffff:31, 0:32>>} = gen_tcp:recv(Socket, 17, 500), %% Second GOAWAY frame. {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 0:31, 0:32>>} = gen_tcp:recv(Socket, 17, 1500), {error, closed} = gen_tcp:recv(Socket, 3, 1000), ok. %% @todo We should add this test also for discarded DATA and CONTINUATION frames. %% The test can be the same for CONTINUATION (just send headers differently) but %% the DATA test should make sure the global window is not corrupted. %% %% @todo We should extend this test to have two requests: one initiated before %% the second GOAWAY, but not terminated; another initiated after the GOAWAY, terminated. %% Finally the first request is terminated by sending a body and a trailing %% HEADERS frame. This way we know for sure that the connection state is not corrupt. graceful_shutdown_race_condition(Config) -> doc("A server in the process of gracefully shutting down must discard frames " "for streams initiated by the receiver with identifiers higher than the " "identified last stream. This may include frames that alter connection " "state such as HEADERS frames. (RFC7540 6.8)"), {ok, Socket} = do_handshake(Config), ServerConnPid = get_remote_pid_tcp(Socket), ok = sys:terminate(ServerConnPid, whatever), %% First GOAWAY frame. {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 16#7fffffff:31, 0:32>>} = gen_tcp:recv(Socket, 17, 500), %% Simulate an in-flight request, sent by the client before the %% GOAWAY frame arrived to the client. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/delay_hello">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Second GOAWAY frame. {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 1:31, 0:32>>} = gen_tcp:recv(Socket, 17, 2000), %% The client tries to send another request, ignoring the GOAWAY. ok = gen_tcp:send(Socket, cow_http2:headers(3, fin, HeadersBlock)), %% The server responds to the first request (streamid 1) and closes. {ok, <>} = gen_tcp:recv(Socket, 9, 1000), {ok, _RespHeaders} = gen_tcp:recv(Socket, RespHeadersPayloadLength, 1000), {ok, <<12:24, 0, 1, 0:1, 1:31, "Hello world!">>} = gen_tcp:recv(Socket, 21, 1000), {error, closed} = gen_tcp:recv(Socket, 3, 1000), ok. % The GOAWAY frame applies to the connection, not a specific stream. % An endpoint MUST treat a GOAWAY frame with a stream identifier other % than 0x0 as a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. %% (RFC7540 6.9) WINDOW_UPDATE % @todo WINDOW_UPDATE frames have a reserved bit in the payload that must be ignored. window_update_reject_0(Config) -> doc("WINDOW_UPDATE frames with an increment of 0 for the connection " "flow control window must be rejected with a " "PROTOCOL_ERROR connection error. (RFC7540 6.9.1)"), {ok, Socket} = do_handshake(Config), %% Send connection-wide WINDOW_UPDATE frame with a value of 0. ok = gen_tcp:send(Socket, [ cow_http2:window_update(0) ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. window_update_reject_0_stream(Config) -> doc("WINDOW_UPDATE frames with an increment of 0 for a stream " "flow control window must be rejected with a " "PROTOCOL_ERROR stream error. (RFC7540 6.9.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame immediately followed by %% a WINDOW_UPDATE frame with a value of 0. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, fin, HeadersBlock), cow_http2:window_update(1, 0) ]), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % A receiver that receives a flow-controlled frame MUST always account % for its contribution against the connection flow-control window, % unless the receiver treats this as a connection error % (Section 5.4.1). This is necessary even if the frame is in error. % The sender counts the frame toward the flow-control window, but if % the receiver does not, the flow-control window at the sender and % receiver can become different. data_reject_overflow(Config0) -> doc("DATA frames that cause the connection flow control window " "to overflow must be rejected with a FLOW_CONTROL_ERROR " "connection error. (RFC7540 6.9.1)"), %% Create a new listener that allows only a single concurrent stream. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_stream_window_size => 100000 }, Config0), try {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame initiating a stream followed by %% DATA frames totaling 90000 bytes of body. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, fin, <<0:15000/unit:8>>) ]), %% Receive a FLOW_CONTROL_ERROR connection error. {ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. data_reject_overflow_stream(Config0) -> doc("DATA frames that cause the stream flow control window " "to overflow must be rejected with a FLOW_CONTROL_ERROR " "stream error. (RFC7540 6.9.1)"), %% Create a new listener that allows only a single concurrent stream. Config = cowboy_test:init_http(?FUNCTION_NAME, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, initial_connection_window_size => 100000 }, Config0), try %% We need to do the handshake manually because a WINDOW_UPDATE %% frame will be sent to update the connection window. {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the WINDOW_UPDATE for the connection. {ok, << 4:24, 8:8, 0:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame initiating a stream followed by %% DATA frames totaling 90000 bytes of body. Headers = [ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ], {HeadersBlock, _} = cow_hpack:encode(Headers), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, nofin, <<0:15000/unit:8>>), cow_http2:data(1, fin, <<0:15000/unit:8>>) ]), %% Receive a FLOW_CONTROL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 3:32 >>} = gen_tcp:recv(Socket, 13, 6000), gen_tcp:close(Socket) after cowboy:stop_listener(?FUNCTION_NAME) end. %% (RFC7540 6.9.1) % Frames with zero length with the END_STREAM flag set (that % is, an empty DATA frame) MAY be sent if there is no available space % in either flow-control window. window_update_reject_overflow(Config) -> doc("WINDOW_UPDATE frames that cause the connection flow control " "window to exceed 2^31-1 must be rejected with a " "FLOW_CONTROL_ERROR connection error. (RFC7540 6.9.1)"), {ok, Socket} = do_handshake(Config), %% Send a connection-wide WINDOW_UPDATE frame that causes the window to overflow. ok = gen_tcp:send(Socket, [ cow_http2:window_update(16#7fffffff) ]), %% Receive a FLOW_CONTROL_ERROR connection error. {ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. window_update_reject_overflow_stream(Config) -> doc("WINDOW_UPDATE frames that cause a stream flow control " "window to exceed 2^31-1 must be rejected with a " "FLOW_CONTROL_ERROR stream error. (RFC7540 6.9.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame immediately followed by a WINDOW_UPDATE %% frame that causes the stream window to overflow. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, fin, HeadersBlock), cow_http2:window_update(1, 16#7fffffff) ]), %% Receive a FLOW_CONTROL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 3:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. settings_initial_window_size_changes(Config) -> doc("When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, the server " "must adjust the size of the flow control windows of the active " "streams. (RFC7540 6.9.2)"), {ok, Socket} = do_handshake(Config), %% Set SETTINGS_INITIAL_WINDOW_SIZE to 0 to prevent sending of DATA. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 0})), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a response but no DATA frames are coming. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {error, timeout} = gen_tcp:recv(Socket, 9, 1000), %% Set SETTINGS_INITIAL_WINDOW_SIZE to a larger value. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 5})), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Receive a DATA frame of that size and no other. {ok, << 5:24, 0:8, 0:8, 1:32, "Hello" >>} = gen_tcp:recv(Socket, 14, 1000), {error, timeout} = gen_tcp:recv(Socket, 9, 1000), %% Set SETTINGS_INITIAL_WINDOW_SIZE to exactly the size in the body. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 12})), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Receive the rest of the response. {ok, << 7:24, 0:8, 1:8, 1:32, " world!" >>} = gen_tcp:recv(Socket, 16, 1000), ok. settings_initial_window_size_changes_negative(Config) -> doc("When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, the server " "must adjust the size of the flow control windows of the active " "streams even if their window end up negative. (RFC7540 6.9.2)"), {ok, Socket} = do_handshake(Config), %% Set SETTINGS_INITIAL_WINDOW_SIZE to 5. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 5})), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a response with a single DATA frame of the initial size we set. {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, << 5:24, 0:8, 0:8, 1:32, "Hello" >>} = gen_tcp:recv(Socket, 14, 1000), {error, timeout} = gen_tcp:recv(Socket, 9, 1000), %% Set SETTINGS_INITIAL_WINDOW_SIZE to 0 to make the stream's window negative. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 0})), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Set SETTINGS_INITIAL_WINDOW_SIZE to exactly the size in the body. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 12})), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Receive the rest of the response. {ok, << 7:24, 0:8, 1:8, 1:32, " world!" >>} = gen_tcp:recv(Socket, 16, 1000), ok. settings_initial_window_size_reject_overflow(Config) -> doc("A SETTINGS_INITIAL_WINDOW_SIZE that causes a flow control window " "to exceed 2^31-1 must be rejected with a FLOW_CONTROL_ERROR " "connection error. (RFC7540 6.5.2, RFC7540 6.9.2)"), {ok, Socket} = do_handshake(Config), %% Set SETTINGS_INITIAL_WINDOW_SIZE to 2^31. ok = gen_tcp:send(Socket, cow_http2:settings(#{initial_window_size => 16#80000000})), %% Receive a FLOW_CONTROL_ERROR connection error. {ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% (RFC7540 6.9.3) %% @todo The right way to do this seems to be to wait for the SETTINGS ack %% before we KNOW the flow control window was updated on the other side. % A receiver that wishes to use a smaller flow-control window than the % current size can send a new SETTINGS frame. However, the receiver % MUST be prepared to receive data that exceeds this window size, since % the sender might send data that exceeds the lower limit prior to % processing the SETTINGS frame. %% (RFC7540 6.10) CONTINUATION % CONTINUATION frames MUST be associated with a stream. If a % CONTINUATION frame is received whose stream identifier field is 0x0, % the recipient MUST respond with a connection error (Section 5.4.1) of % type PROTOCOL_ERROR. % % A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE or % CONTINUATION frame without the END_HEADERS flag set. A recipient % that observes violation of this rule MUST respond with a connection % error (Section 5.4.1) of type PROTOCOL_ERROR. %% (RFC7540 7) Error Codes % Unknown or unsupported error codes MUST NOT trigger any special % behavior. These MAY be treated by an implementation as being % equivalent to INTERNAL_ERROR. accept_trailers(Config) -> doc("Trailing HEADERS frames must be accepted. (RFC7540 8.1)"), {ok, Socket} = do_handshake(Config), %% Send a request containing DATA and trailing HEADERS frames. {HeadersBlock, EncodeState} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"trailer">>, <<"x-checksum">>} ]), {TrailersBlock, _} = cow_hpack:encode([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>} ], EncodeState), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:10000/unit:8>>), cow_http2:headers(1, fin, TrailersBlock) ]), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. accept_trailers_continuation(Config) -> doc("Trailing HEADERS and CONTINUATION frames must be accepted. (RFC7540 8.1)"), {ok, Socket} = do_handshake(Config), %% Send a request containing DATA and trailing HEADERS and CONTINUATION frames. {HeadersBlock, EncodeState} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"trailer">>, <<"x-checksum">>} ]), {TrailersBlock, _} = cow_hpack:encode([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>} ], EncodeState), Len = iolist_size(TrailersBlock), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:10000/unit:8>>), <<0:24, 1:8, 0:7, 1:1, 0:1, 1:31>>, <>, TrailersBlock ]), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = gen_tcp:recv(Socket, 9, 6000), ok. %% We reject all invalid HEADERS with a connection error because %% we do not want to waste resources decoding them. reject_trailers_nofin(Config) -> doc("Trailing HEADERS frames received without the END_STREAM flag " "set must be rejected with a PROTOCOL_ERROR connection error. " "(RFC7540 8.1, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a request containing DATA and trailing HEADERS frames. %% The trailing HEADERS does not have the END_STREAM flag set. {HeadersBlock, EncodeState} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"trailer">>, <<"x-checksum">>} ]), {TrailersBlock, _} = cow_hpack:encode([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>} ], EncodeState), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:10000/unit:8>>), cow_http2:headers(1, nofin, TrailersBlock) ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. %% We reject all invalid HEADERS with a connection error because %% we do not want to waste resources decoding them. reject_trailers_nofin_continuation(Config) -> doc("Trailing HEADERS frames received without the END_STREAM flag " "set must be rejected with a PROTOCOL_ERROR connection error. " "(RFC7540 8.1, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a request containing DATA and trailing HEADERS and CONTINUATION frames. %% The trailing HEADERS does not have the END_STREAM flag set. {HeadersBlock, EncodeState} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"trailer">>, <<"x-checksum">>} ]), {TrailersBlock, _} = cow_hpack:encode([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>} ], EncodeState), Len = iolist_size(TrailersBlock), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:10000/unit:8>>), <<0:24, 1:8, 0:9, 1:31>>, <>, TrailersBlock ]), %% Receive a PROTOCOL_ERROR connection error. {ok, << _:24, 7:8, _:72, 1:32 >>} = gen_tcp:recv(Socket, 17, 6000), ok. headers_informational_nofin(Config) -> doc("Informational HEADERS frames must not have the END_STREAM flag set. (RFC7540 8.1)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame on an idle stream. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>}, {<<"expect">>, <<"100-continue">>}, {<<"content-length">>, <<"1000000">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Receive an informational HEADERS frame without the END_STREAM flag. {ok, << Len:24, 1:8, 0:5, 1:1, 0:2, _:32 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len, 6000), %% Confirm it has a 100 status code. {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"100">>} = lists:keyfind(<<":status">>, 1, RespHeaders), ok. %% @todo This one is interesting to implement because Cowboy DOES this. % A server can % send a complete response prior to the client sending an entire % request if the response does not depend on any portion of the request % that has not been sent and received. When this is true, a server MAY % request that the client abort transmission of a request without error % by sending a RST_STREAM with an error code of NO_ERROR after sending % a complete response (i.e., a frame with the END_STREAM flag). headers_reject_uppercase_header_name(Config) -> doc("Requests containing uppercase header names must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a uppercase header name. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"HELLO">>, <<"world">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_response_pseudo_headers(Config) -> doc("Requests containing response pseudo-headers must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a response pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<":status">>, <<"200">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_unknown_pseudo_headers(Config) -> doc("Requests containing unknown pseudo-headers must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with an unknown pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<":upgrade">>, <<"websocket">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_pseudo_headers_in_trailers(Config) -> doc("Requests containing pseudo-headers in trailers must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a request containing DATA and trailing HEADERS frames. %% The trailing HEADERS contains pseudo-headers. {HeadersBlock, EncodeState} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"trailer">>, <<"x-checksum">>} ]), {TrailersBlock, _} = cow_hpack:encode([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>}, {<<":path">>, <<"/">>} ], EncodeState), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<0:10000/unit:8>>), cow_http2:headers(1, fin, TrailersBlock) ]), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_pseudo_headers_after_regular_headers(Config) -> doc("Requests containing pseudo-headers after regular headers must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a pseudo-header after regular headers. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"content-length">>, <<"0">>}, {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_connection_header(Config) -> doc("Requests containing a connection header must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a connection header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"connection">>, <<"close">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_keep_alive_header(Config) -> doc("Requests containing a keep-alive header must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a keep-alive header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"keep-alive">>, <<"timeout=5, max=1000">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_proxy_authenticate_header(Config) -> doc("Requests containing a connection header must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a proxy-authenticate header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"proxy-authenticate">>, <<"Basic">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_proxy_authorization_header(Config) -> doc("Requests containing a connection header must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a proxy-authorization header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"proxy-authorization">>, <<"Basic YWxhZGRpbjpvcGVuc2VzYW1l">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_transfer_encoding_header(Config) -> doc("Requests containing a connection header must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a transfer-encoding header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"transfer-encoding">>, <<"chunked">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_upgrade_header(Config) -> doc("Requests containing a connection header must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a upgrade header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"upgrade">>, <<"websocket">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. accept_te_header_value_trailers(Config) -> doc("Requests containing a TE header with a value of \"trailers\" " "must be accepted. (RFC7540 8.1.2.2)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a TE header with value "trailers". {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"te">>, <<"trailers">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a response. {ok, << _:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), ok. reject_te_header_other_values(Config) -> doc("Requests containing a TE header with a value other than \"trailers\" must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a TE header with a different value. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"te">>, <<"trailers, deflate;q=0.5">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. %% (RFC7540 8.1.2.2) % This means that an intermediary transforming an HTTP/1.x message to % HTTP/2 will need to remove any header fields nominated by the % Connection header field, along with the Connection header field % itself. Such intermediaries SHOULD also remove other connection- % specific header fields, such as Keep-Alive, Proxy-Connection, % Transfer-Encoding, and Upgrade, even if they are not nominated by the % Connection header field. response_dont_send_header_in_connection(Config) -> doc("Intermediaries must remove HTTP/1.1 connection headers when " "transforming an HTTP/1.1 messages to HTTP/2. The server must " "not send them either. All headers listed in the connection " "header must be removed. (RFC7540 8.1.2.2)"), do_response_dont_send_http11_header(Config, <<"custom-header">>). response_dont_send_connection_header(Config) -> doc("Intermediaries must remove HTTP/1.1 connection headers when " "transforming an HTTP/1.1 messages to HTTP/2. The server must " "not send them either. The connection header must be removed. (RFC7540 8.1.2.2)"), do_response_dont_send_http11_header(Config, <<"connection">>). response_dont_send_keep_alive_header(Config) -> doc("Intermediaries must remove HTTP/1.1 connection headers when " "transforming an HTTP/1.1 messages to HTTP/2. The server must " "not send them either. The keep-alive header must be removed " "even if not listed in the connection header. (RFC7540 8.1.2.2)"), do_response_dont_send_http11_header(Config, <<"keep-alive">>). response_dont_send_proxy_connection_header(Config) -> doc("Intermediaries must remove HTTP/1.1 connection headers when " "transforming an HTTP/1.1 messages to HTTP/2. The server must " "not send them either. The proxy-connection header must be removed " "even if not listed in the connection header. (RFC7540 8.1.2.2)"), do_response_dont_send_http11_header(Config, <<"proxy-connection">>). response_dont_send_transfer_encoding_header(Config) -> doc("Intermediaries must remove HTTP/1.1 connection headers when " "transforming an HTTP/1.1 messages to HTTP/2. The server must " "not send them either. The transfer-encoding header must be removed " "even if not listed in the connection header. (RFC7540 8.1.2.2)"), do_response_dont_send_http11_header(Config, <<"transfer-encoding">>). response_dont_send_upgrade_header(Config) -> doc("Intermediaries must remove HTTP/1.1 connection headers when " "transforming an HTTP/1.1 messages to HTTP/2. The server must " "not send them either. The upgrade header must be removed " "even if not listed in the connection header. (RFC7540 8.1.2.2)"), do_response_dont_send_http11_header(Config, <<"upgrade">>). do_response_dont_send_http11_header(Config, Name) -> ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/set_resp_headers_http11"), {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), false = lists:keyfind(Name, 1, Headers), ok. reject_userinfo(Config) -> doc("An authority containing a userinfo component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a userinfo authority component. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"user@localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. %% (RFC7540 8.1.2.3) % To ensure that the HTTP/1.1 request line can be reproduced % accurately, this pseudo-header field MUST be omitted when % translating from an HTTP/1.1 request that has a request target in % origin or asterisk form (see [RFC7230], Section 5.3). Clients % that generate HTTP/2 requests directly SHOULD use the ":authority" % pseudo-header field instead of the Host header field. An % intermediary that converts an HTTP/2 request to HTTP/1.1 MUST % create a Host header field if one is not present in a request by % copying the value of the ":authority" pseudo-header field. reject_empty_path(Config) -> doc("A request containing an empty path component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with an empty path component. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<>>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_missing_pseudo_header_method(Config) -> doc("A request without a method component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame without a :method pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_many_pseudo_header_method(Config) -> doc("A request containing more than one method component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with more than one :method pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_missing_pseudo_header_scheme(Config) -> doc("A request without a scheme component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame without a :scheme pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_many_pseudo_header_scheme(Config) -> doc("A request containing more than one scheme component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with more than one :scheme pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_missing_pseudo_header_authority(Config) -> doc("A request without an authority or host component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame without an :authority pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. accept_host_header_on_missing_pseudo_header_authority(Config) -> doc("A request without an authority but with a host header must be accepted. " "(RFC7540 8.1.2.3, RFC7540 8.1.3)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with host header and without an :authority pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/">>}, {<<"host">>, <<"localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a 200 response. {ok, << Len:24, 1:8, _:8, _:32 >>} = gen_tcp:recv(Socket, 9, 6000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len, 6000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders), ok. %% When both :authority and host headers are received, the current behavior %% is to favor :authority and ignore the host header. The specification does %% not describe the correct behavior to follow in that case. %% @todo The HTTP/3 spec says both values must be identical and non-empty. reject_many_pseudo_header_authority(Config) -> doc("A request containing more than one authority component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with more than one :authority pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_missing_pseudo_header_path(Config) -> doc("A request without a path component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame without a :path pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>} %% @todo Correct port number. ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_many_pseudo_header_path(Config) -> doc("A request containing more than one path component must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with more than one :path pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<":path">>, <<"/">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. %% (RFC7540 8.1.2.4) % For HTTP/2 responses, a single ":status" pseudo-header field is % defined that carries the HTTP status code field (see [RFC7231], % Section 6). This pseudo-header field MUST be included in all % responses; otherwise, the response is malformed (Section 8.1.2.6). %% (RFC7540 8.1.2.5) % To allow for better compression efficiency, the Cookie header field % MAY be split into separate header fields, each with one or more % cookie-pairs. If there are multiple Cookie header fields after % decompression, these MUST be concatenated into a single octet string % using the two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") % before being passed into a non-HTTP/2 context, such as an HTTP/1.1 % connection, or a generic HTTP server application. reject_data_size_smaller_than_content_length(Config) -> doc("Requests that have a content-length header whose value does not " "match the total length of the DATA frames must be rejected with " "a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a content-length header different %% than the sum of the DATA frame sizes. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"content-length">>, <<"12">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, fin, <<"Hello!">>) ]), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_data_size_larger_than_content_length(Config) -> doc("Requests that have a content-length header whose value does not " "match the total length of the DATA frames must be rejected with " "a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a content-length header different %% than the sum of the DATA frame sizes. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"content-length">>, <<"12">>} ]), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<"Hello! World! Universe!">>), cow_http2:data(1, fin, <<"Multiverse!">>) ]), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_content_length_without_data(Config) -> doc("Requests that have a content-length header whose value does not " "match the total length of the DATA frames must be rejected with " "a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a content-length header different %% than the sum of the DATA frame sizes. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"content-length">>, <<"12">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_data_size_different_than_content_length_with_trailers(Config) -> doc("Requests that have a content-length header whose value does not " "match the total length of the DATA frames must be rejected with " "a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with a content-length header different %% than the sum of the DATA frame sizes. {HeadersBlock, EncodeState} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>}, {<<"content-length">>, <<"12">>}, {<<"trailer">>, <<"x-checksum">>} ]), {TrailersBlock, _} = cow_hpack:encode([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>} ], EncodeState), ok = gen_tcp:send(Socket, [ cow_http2:headers(1, nofin, HeadersBlock), cow_http2:data(1, nofin, <<"Hello!">>), cow_http2:headers(1, fin, TrailersBlock) ]), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_duplicate_content_length_header(Config) -> doc("A request with duplicate content-length headers must be rejected " "with a PROTOCOL_ERROR stream error. (RFC7230 3.3.2, RFC7540 8.1.2.6)"), {ok, Socket} = do_handshake(Config), %% Send a HEADERS frame with more than one content-length header. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<"content-length">>, <<"12">>}, {<<"content-length">>, <<"12">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % Intermediaries that process HTTP requests or responses (i.e., any % intermediary not acting as a tunnel) MUST NOT forward a malformed % request or response. Malformed requests or responses that are % detected MUST be treated as a stream error (Section 5.4.2) of type % PROTOCOL_ERROR. % % For malformed requests, a server MAY send an HTTP response prior to % closing or resetting the stream. Clients MUST NOT accept a malformed % response. Note that these requirements are intended to protect % against several types of common attacks against HTTP; they are % deliberately strict because being permissive can expose % implementations to these vulnerabilities. %% @todo It migh be worth reproducing the good examples. (RFC7540 8.1.3) %% (RFC7540 8.1.4) % A server MUST NOT indicate that a stream has not been processed % unless it can guarantee that fact. If frames that are on a stream % are passed to the application layer for any stream, then % REFUSED_STREAM MUST NOT be used for that stream, and a GOAWAY frame % MUST include a stream identifier that is greater than or equal to the % given stream identifier. %% (RFC7540 8.2) % Promised requests MUST be cacheable (see [RFC7231], Section 4.2.3), % MUST be safe (see [RFC7231], Section 4.2.1), and MUST NOT include a % request body. % % The server MUST include a value in the ":authority" pseudo-header % field for which the server is authoritative (see Section 10.1). % % A client cannot push. Thus, servers MUST treat the receipt of a % PUSH_PROMISE frame as a connection error (Section 5.4.1) of type % PROTOCOL_ERROR. push_has_no_request_body(Config) -> doc("PUSH_PROMISE frames include the complete set of request headers " "and the request can never include a body. (RFC7540 8.2.1)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/push/read_body"), {push, PushRef, <<"GET">>, _, _} = gun:await(ConnPid, Ref), {response, fin, 200, _} = gun:await(ConnPid, Ref), %% We should not get a body in the pushed resource %% since there was no body in the request. {response, fin, 200, _} = gun:await(ConnPid, PushRef), ok. %% (RFC7540 8.2.1) % The header fields in PUSH_PROMISE and any subsequent CONTINUATION % frames MUST be a valid and complete set of request header fields % (Section 8.1.2.3). The server MUST include a method in the ":method" % pseudo-header field that is safe and cacheable. If a client receives % a PUSH_PROMISE that does not include a complete and valid set of % header fields or the ":method" pseudo-header field identifies a % method that is not safe, it MUST respond with a stream error % (Section 5.4.2) of type PROTOCOL_ERROR. % %% @todo This probably should be documented. % The server SHOULD send PUSH_PROMISE (Section 6.6) frames prior to % sending any frames that reference the promised responses. This % avoids a race where clients issue requests prior to receiving any % PUSH_PROMISE frames. % % PUSH_PROMISE frames MUST NOT be sent by the client. % % PUSH_PROMISE frames can be sent by the server in response to any % client-initiated stream, but the stream MUST be in either the "open" % or "half-closed (remote)" state with respect to the server. % PUSH_PROMISE frames are interspersed with the frames that comprise a % response, though they cannot be interspersed with HEADERS and % CONTINUATION frames that comprise a single header block. %% (RFC7540 8.2.2) % If the client determines, for any reason, that it does not wish to % receive the pushed response from the server or if the server takes % too long to begin sending the promised response, the client can send % a RST_STREAM frame, using either the CANCEL or REFUSED_STREAM code % and referencing the pushed stream's identifier. % % A client can use the SETTINGS_MAX_CONCURRENT_STREAMS setting to limit % the number of responses that can be concurrently pushed by a server. % Advertising a SETTINGS_MAX_CONCURRENT_STREAMS value of zero disables % server push by preventing the server from creating the necessary % streams. This does not prohibit a server from sending PUSH_PROMISE % frames; clients need to reset any promised streams that are not % wanted. %% @todo Implement CONNECT. (RFC7540 8.3) status_code_421(Config) -> doc("The 421 Misdirected Request status code can be sent. (RFC7540 9.1.2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/reply2/421"), {response, fin, 421, _} = gun:await(ConnPid, Ref), ok. %% @todo Review (RFC7540 9.2, 9.2.1, 9.2.2) TLS 1.2 usage. %% We probably want different ways to enforce these to simplify the life %% of users. A function cowboy:start_h2_tls could do the same as start_tls %% but with the security requirements of HTTP/2 enforced. Another way is to %% have an option at the establishment of the connection that checks that %% the security of the connection is adequate. %% (RFC7540 10.3) % The HTTP/2 header field encoding allows the expression of names that % are not valid field names in the Internet Message Syntax used by % HTTP/1.1. Requests or responses containing invalid header field % names MUST be treated as malformed (Section 8.1.2.6). % % Similarly, HTTP/2 allows header field values that are not valid. % While most of the values that can be encoded will not alter header % field parsing, carriage return (CR, ASCII 0xd), line feed (LF, ASCII % 0xa), and the zero character (NUL, ASCII 0x0) might be exploited by % an attacker if they are translated verbatim. Any request or response % that contains a character not permitted in a header field value MUST % be treated as malformed (Section 8.1.2.6). Valid characters are % defined by the "field-content" ABNF rule in Section 3.2 of [RFC7230]. %% (RFC7540 10.5) Denial-of-Service Considerations % An endpoint that doesn't monitor this behavior exposes itself to a % risk of denial-of-service attack. Implementations SHOULD track the % use of these features and set limits on their use. An endpoint MAY % treat activity that is suspicious as a connection error % (Section 5.4.1) of type ENHANCE_YOUR_CALM. %% (RFC7540 10.5.1) % A server that receives a larger header block than it is willing to % handle can send an HTTP 431 (Request Header Fields Too Large) status % code [RFC6585]. A client can discard responses that it cannot % process. The header block MUST be processed to ensure a consistent % connection state, unless the connection is closed. %% @todo Implement CONNECT and limit the number of CONNECT streams (RFC7540 10.5.2). %% @todo This probably should be documented. (RFC7540 10.6) % Implementations communicating on a secure channel MUST NOT compress % content that includes both confidential and attacker-controlled data % unless separate compression dictionaries are used for each source of % data. Compression MUST NOT be used if the source of data cannot be % reliably determined. Generic stream compression, such as that % provided by TLS, MUST NOT be used with HTTP/2 (see Section 9.2). %% (RFC7540 A) % An HTTP/2 implementation MAY treat the negotiation of any of the % following cipher suites with TLS 1.2 as a connection error % (Section 5.4.1) of type INADEQUATE_SECURITY. ================================================ FILE: test/rfc8297_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc8297_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_dispatch(_) -> cowboy_router:compile([{"[...]", [ {"/resp/:key[/:arg]", resp_h, []} ]}]). status_code_103(Config) -> doc("The 103 Early Hints status code can be sent. (RFC8297 2)"), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/resp/inform2/103", [ {<<"accept-encoding">>, <<"gzip">>} ]), {inform, 103, []} = gun:await(ConnPid, Ref), ok. ================================================ FILE: test/rfc8441_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc8441_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). all() -> [{group, enabled}]. groups() -> Tests = ct_helper:all(?MODULE), [{enabled, [parallel], Tests}]. init_per_group(Name = enabled, Config) -> cowboy_test:init_http(Name, #{ enable_connect_protocol => true, env => #{dispatch => cowboy_router:compile(init_routes(Config))} }, Config). end_per_group(Name, _) -> ok = cowboy:stop_listener(Name). init_routes(_) -> [ {"localhost", [ {"/ws", ws_echo, []} ]} ]. %% Do a prior knowledge handshake. do_handshake(Config) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, SettingsPayload:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), Settings = cow_http2:parse_settings_payload(SettingsPayload), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, Socket, Settings}. % The SETTINGS_ENABLE_CONNECT_PROTOCOL SETTINGS Parameter. % The new parameter name is SETTINGS_ENABLE_CONNECT_PROTOCOL. The % value of the parameter MUST be 0 or 1. % Upon receipt of SETTINGS_ENABLE_CONNECT_PROTOCOL with a value of 1 a % client MAY use the Extended CONNECT definition of this document when % creating new streams. Receipt of this parameter by a server does not % have any impact. %% @todo ignore_client_enable_setting(Config) -> % A sender MUST NOT send a SETTINGS_ENABLE_CONNECT_PROTOCOL parameter % with the value of 0 after previously sending a value of 1. reject_handshake_when_disabled(Config0) -> doc("Extended CONNECT requests MUST be rejected with a " "PROTOCOL_ERROR stream error when enable_connect_protocol=false. (draft-01 3)"), Config = cowboy_test:init_http(disabled, #{ enable_connect_protocol => false, env => #{dispatch => cowboy_router:compile(init_routes(Config0))} }, Config0), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0. {ok, Socket, Settings} = do_handshake(Config), case Settings of #{enable_connect_protocol := false} -> ok; _ when map_size(Settings) =:= 0 -> ok end, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_handshake_disabled_by_default(Config0) -> doc("Extended CONNECT requests MUST be rejected with a " "PROTOCOL_ERROR stream error with default enable_connect_protocol. (draft-01 3)"), Config = cowboy_test:init_http(disabled_by_default, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))} }, Config0), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0. {ok, Socket, Settings} = do_handshake(Config), case Settings of #{enable_connect_protocol := false} -> ok; _ when map_size(Settings) =:= 0 -> ok end, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % The Extended CONNECT Method. %% @todo Refer to RFC9110 7.8 about the case insensitive comparison. accept_uppercase_pseudo_header_protocol(Config) -> doc("The :protocol pseudo header is case insensitive. (draft-01 4)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"WEBSOCKET">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a 200 response. {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders), ok. reject_many_pseudo_header_protocol(Config) -> doc("An extended CONNECT request containing more than one protocol component " "must be rejected with a PROTOCOL_ERROR stream error. (draft-01 4, RFC7540 8.1.2.6)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with more than one :protocol pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":protocol">>, <<"mqtt">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_unknown_pseudo_header_protocol(Config) -> %% @todo This probably shouldn't send 400 but 501 instead based on RFC 9220. doc("An extended CONNECT request with an unknown protocol must be rejected " "with a 400 error. (draft-01 4)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with an unknown :protocol pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"mqtt">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a 400 response. {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders), ok. reject_invalid_pseudo_header_protocol(Config) -> %% @todo This probably shouldn't send 400 but 501 instead based on RFC 9220. doc("An extended CONNECT request with an invalid protocol must be rejected " "with a 400 error. (draft-01 4)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with an invalid :protocol pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket mqtt">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a 400 response. {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders), ok. reject_missing_pseudo_header_scheme(Config) -> doc("An extended CONNECT request without a scheme component must be rejected " "with a PROTOCOL_ERROR stream error. (draft-01 4, RFC7540 8.1.2.6)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without a :scheme pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_missing_pseudo_header_path(Config) -> doc("An extended CONNECT request without a path component must be rejected " "with a PROTOCOL_ERROR stream error. (draft-01 4, RFC7540 8.1.2.6)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without a :path pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % On requests bearing the :protocol pseudo-header, the :authority % pseudo-header field is interpreted according to Section 8.1.2.3 of % [RFC7540] instead of Section 8.3 of [RFC7540]. In particular the % server MUST not make a new TCP connection to the host and port % indicated by the :authority. reject_missing_pseudo_header_authority(Config) -> doc("An extended CONNECT request without an authority component must be rejected " "with a PROTOCOL_ERROR stream error. (draft-01 4, draft-01 5)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without an :authority pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % Using Extended CONNECT To Bootstrap The WebSocket Protocol. reject_missing_pseudo_header_protocol(Config) -> doc("An extended CONNECT request without a protocol component must be rejected " "with a PROTOCOL_ERROR stream error. (draft-01 4, RFC7540 8.1.2.6)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without a :protocol pseudo-header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % The scheme of the Target URI [RFC7230] MUST be https for wss schemed % WebSockets and http for ws schemed WebSockets. The websocket URI is % still used for proxy autoconfiguration. reject_connection_header(Config) -> doc("An extended CONNECT request with a connection header must be rejected " "with a PROTOCOL_ERROR stream error. (draft-01 5, RFC7540 8.1.2.6)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with a connection header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"connection">>, <<"upgrade">>}, {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. reject_upgrade_header(Config) -> doc("An extended CONNECT request with a upgrade header must be rejected " "with a PROTOCOL_ERROR stream error. (draft-01 5, RFC7540 8.1.2.6)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with a upgrade header. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"upgrade">>, <<"websocket">>}, {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000), ok. % After successfully processing the opening handshake the peers should % proceed with The WebSocket Protocol [RFC6455] using the HTTP/2 stream % from the CONNECT transaction as if it were the TCP connection % referred to in [RFC6455]. The state of the WebSocket connection at % this point is OPEN as defined by [RFC6455], Section 4.1. %% @todo I'm guessing we should test for things like RST_STREAM, %% closing the connection and others? % Examples. %% @todo Probably worth testing that we get the correct option %% over all different connection types (alpn, prior, upgrade). accept_handshake_when_enabled(Config) -> doc("Confirm the example for Websocket over HTTP/2 works. (draft-01 5.1)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. {ok, Socket, Settings} = do_handshake(Config), #{enable_connect_protocol := true} = Settings, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a 200 response. {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders), %% Masked text hello echoed back clear by the server. Mask = 16#37fa213d, MaskedHello = ws_SUITE:do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, cow_http2:data(1, nofin, <<1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary>>)), %% Ignore expected WINDOW_UPDATE frames. {ok, <<4:24, 8:8, _:72>>} = gen_tcp:recv(Socket, 13, 1000), {ok, <<4:24, 8:8, _:72>>} = gen_tcp:recv(Socket, 13, 1000), {ok, <>} = gen_tcp:recv(Socket, 9, 1000), {ok, <<1:1, 0:3, 1:4, 0:1, 5:7, "Hello">>} = gen_tcp:recv(Socket, Len2, 1000), ok. %% Closing a Websocket stream. % The HTTP/2 stream closure is also analagous to the TCP connection closure of % [RFC6455]. Orderly TCP level closures are represented as END_STREAM % ([RFC7540] Section 6.1) flags and RST exceptions are represented with % the RST_STREAM ([RFC7540] Section 6.4) frame with the CANCEL % ([RFC7540] Secion 7) error code. %% @todo client close frame with END_STREAM %% @todo server close frame with END_STREAM %% @todo client other frame with END_STREAM %% @todo server other frame with END_STREAM %% @todo client close connection ================================================ FILE: test/rfc9114_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc9114_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -ifdef(COWBOY_QUICER). -include_lib("quicer/include/quicer.hrl"). all() -> [{group, h3}]. groups() -> %% @todo Enable parallel tests but for this issues in the %% QUIC accept loop need to be figured out (can't connect %% concurrently somehow, no backlog?). [{h3, [], ct_helper:all(?MODULE)}]. init_per_group(Name = h3, Config) -> cowboy_test:init_http3(Name, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))} }, Config). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, {"/echo/:key", echo_h, []} ]} ]. %% Starting HTTP/3 for "https" URIs. alpn(Config) -> doc("Successful ALPN negotiation. (RFC9114 3.1)"), {ok, Conn} = quicer:connect("localhost", config(port, Config), #{alpn => ["h3"], verify => none}, 5000), {ok, <<"h3">>} = quicer:negotiated_protocol(Conn), %% To make sure the connection is fully established we wait %% to receive the SETTINGS frame on the control stream. {ok, _ControlRef, _Settings} = do_wait_settings(Conn), ok. alpn_error(Config) -> doc("Failed ALPN negotiation using the 'h2' token. (RFC9114 3.1)"), {error, transport_down, #{status := alpn_neg_failure}} = quicer:connect("localhost", config(port, Config), #{alpn => ["h2"], verify => none}, 5000), ok. %% @todo 3.2. Connection Establishment %% After the QUIC connection is established, a SETTINGS frame MUST be sent by each endpoint as the initial frame of their respective HTTP control stream. %% @todo 3.3. Connection Reuse %% Servers are encouraged to maintain open HTTP/3 connections for as long as %possible but are permitted to terminate idle connections if necessary. When %either endpoint chooses to close the HTTP/3 connection, the terminating %endpoint SHOULD first send a GOAWAY frame (Section 5.2) so that both endpoints %can reliably determine whether previously sent frames have been processed and %gracefully complete or terminate any necessary remaining tasks. %% Frame format. req_stream(Config) -> doc("Complete lifecycle of a request stream. (RFC9114 4.1)"), {ok, Conn} = quicer:connect("localhost", config(port, Config), #{alpn => ["h3"], verify => none}, 5000), %% To make sure the connection is fully established we wait %% to receive the SETTINGS frame on the control stream. {ok, ControlRef, _Settings} = do_wait_settings(Conn), %% Send a request on a request stream. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ], ?QUIC_SEND_FLAG_FIN), %% Receive the response. {ok, Data} = do_receive_data(StreamRef), {HLenEnc, HLenBits} = do_guess_int_encoding(Data), << 1, %% HEADERS frame. HLenEnc:2, HLen:HLenBits, EncodedResponse:HLen/bytes, Rest/bits >> = Data, {ok, DecodedResponse, _DecData, _DecSt} = cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)), #{ <<":status">> := <<"200">>, <<"content-length">> := BodyLen } = maps:from_list(DecodedResponse), {DLenEnc, DLenBits} = do_guess_int_encoding(Rest), << 0, %% DATA frame. DLenEnc:2, DLen:DLenBits, Body:DLen/bytes >> = Rest, <<"Hello world!">> = Body, BodyLen = integer_to_binary(byte_size(Body)), ok = do_wait_peer_send_shutdown(StreamRef), ok = do_wait_stream_closed(StreamRef). %% @todo Same test as above but with content-length unset? req_stream_two_requests(Config) -> doc("Receipt of multiple requests on a single stream must " "be rejected with an H3_MESSAGE_ERROR stream error. " "(RFC9114 4.1, RFC9114 4.1.2)"), {ok, Conn} = quicer:connect("localhost", config(port, Config), #{alpn => ["h3"], verify => none}, 5000), %% To make sure the connection is fully established we wait %% to receive the SETTINGS frame on the control stream. {ok, ControlRef, _Settings} = do_wait_settings(Conn), %% Send two requests on a request stream. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest1, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedRequest2, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest1)), EncodedRequest1, <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest2)), EncodedRequest2 ]), %% The stream should have been aborted. #{reason := h3_message_error} = do_wait_stream_aborted(StreamRef), ok. headers_then_trailers(Config) -> doc("Receipt of HEADERS followed by trailer HEADERS must be accepted. (RFC9114 4.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_data_then_trailers(Config) -> doc("Receipt of HEADERS followed by DATA followed by trailer HEADERS " "must be accepted. (RFC9114 4.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. data_then_headers(Config) -> doc("Receipt of DATA before HEADERS must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 4.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. headers_then_trailers_then_data(Config) -> doc("Receipt of DATA after trailer HEADERS must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 4.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">> ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. headers_then_data_then_trailers_then_data(Config) -> doc("Receipt of DATA after trailer HEADERS must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 4.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">> ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. headers_then_data_then_trailers_then_trailers(Config) -> doc("Receipt of DATA after trailer HEADERS must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 4.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers1, _EncData2, EncSt1} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, EncodedTrailers2, _EncData3, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt1), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers1)), EncodedTrailers1, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers2)), EncodedTrailers2 ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. unknown_then_headers(Config) -> doc("Receipt of unknown frame followed by HEADERS " "must be accepted. (RFC9114 4.1, RFC9114 9)"), unknown_then_headers(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). unknown_then_headers(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes, <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_unknown(Config) -> doc("Receipt of HEADERS followed by unknown frame " "must be accepted. (RFC9114 4.1, RFC9114 9)"), headers_then_unknown(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). headers_then_unknown(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_data_then_unknown(Config) -> doc("Receipt of HEADERS followed by DATA followed by unknown frame " "must be accepted. (RFC9114 4.1, RFC9114 9)"), headers_then_data_then_unknown(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_unknown(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_trailers_then_unknown(Config) -> doc("Receipt of HEADERS followed by trailer HEADERS followed by unknown frame " "must be accepted. (RFC9114 4.1, RFC9114 9)"), headers_then_data_then_unknown(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). headers_then_trailers_then_unknown(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers, cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_data_then_unknown_then_trailers(Config) -> doc("Receipt of HEADERS followed by DATA followed by " "unknown frame followed by trailer HEADERS " "must be accepted. (RFC9114 4.1, RFC9114 9)"), headers_then_data_then_unknown_then_trailers(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_unknown_then_trailers(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_data_then_unknown_then_data(Config) -> doc("Receipt of HEADERS followed by DATA followed by " "unknown frame followed by DATA " "must be accepted. (RFC9114 4.1, RFC9114 9)"), headers_then_data_then_unknown_then_data(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_unknown_then_data(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(6), <<"Hello ">>, cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes, <<0>>, %% DATA frame. cow_http3:encode_int(7), <<"server!">> ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. headers_then_data_then_trailers_then_unknown(Config) -> doc("Receipt of HEADERS followed by DATA followed by " "trailer HEADERS followed by unknown frame " "must be accepted. (RFC9114 4.1, RFC9114 9)"), headers_then_data_then_trailers_then_unknown(Config, do_unknown_frame_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_trailers_then_unknown(Config, Type, Bytes) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello server!">>, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers, cow_http3:encode_int(Type), %% Unknown frame. cow_http3:encode_int(iolist_size(Bytes)), Bytes ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. do_unknown_frame_type() -> Type = rand:uniform(4611686018427387904) - 1, %% Retry if we get a value that's specified. case lists:member(Type, [ 16#0, 16#1, 16#3, 16#4, 16#5, 16#7, 16#d, %% HTTP/3 core frame types. 16#2, 16#6, 16#8, 16#9 %% HTTP/3 reserved frame types that must be rejected. ]) of true -> do_unknown_frame_type(); false -> Type end. reserved_then_headers(Config) -> doc("Receipt of reserved frame followed by HEADERS " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), unknown_then_headers(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). headers_then_reserved(Config) -> doc("Receipt of HEADERS followed by reserved frame " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), headers_then_unknown(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_reserved(Config) -> doc("Receipt of HEADERS followed by DATA followed by reserved frame " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), headers_then_data_then_unknown(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). headers_then_trailers_then_reserved(Config) -> doc("Receipt of HEADERS followed by trailer HEADERS followed by reserved frame " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), headers_then_trailers_then_unknown(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_reserved_then_trailers(Config) -> doc("Receipt of HEADERS followed by DATA followed by " "reserved frame followed by trailer HEADERS " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), headers_then_data_then_unknown_then_trailers(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_reserved_then_data(Config) -> doc("Receipt of HEADERS followed by DATA followed by " "reserved frame followed by DATA " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), headers_then_data_then_unknown_then_data(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). headers_then_data_then_trailers_then_reserved(Config) -> doc("Receipt of HEADERS followed by DATA followed by " "trailer HEADERS followed by reserved frame " "must be accepted when the reserved frame type is " "of the format 0x1f * N + 0x21. (RFC9114 4.1, RFC9114 7.2.8)"), headers_then_data_then_trailers_then_unknown(Config, do_reserved_type(), rand:bytes(rand:uniform(4096))). reject_transfer_encoding_header_with_body(Config) -> doc("Requests containing a transfer-encoding header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.1, RFC9114 4.1.2, RFC9114 4.2)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, _EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"transfer-encoding">>, <<"chunked">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(24), <<"13\r\nHello server!\r\n0\r\n\r\n">> ]), %% The stream should have been aborted. #{reason := h3_message_error} = do_wait_stream_aborted(StreamRef), ok. %% 4. Expressing HTTP Semantics in HTTP/3 %% 4.1. HTTP Message Framing %% An HTTP request/response exchange fully consumes a client-initiated %bidirectional QUIC stream. After sending a request, a client MUST close the %stream for sending. Unless using the CONNECT method (see Section 4.4), clients %MUST NOT make stream closure dependent on receiving a response to their %request. After sending a final response, the server MUST close the stream for %sending. At this point, the QUIC stream is fully closed. %% @todo What to do with clients that DON'T close the stream %% for sending after the request is sent? %% If a client-initiated stream terminates without enough of the HTTP message %to provide a complete response, the server SHOULD abort its response stream %with the error code H3_REQUEST_INCOMPLETE. %% @todo difficult!! %% When the server does not need to receive the remainder of the request, it %MAY abort reading the request stream, send a complete response, and cleanly %close the sending part of the stream. The error code H3_NO_ERROR SHOULD be %used when requesting that the client stop sending on the request stream. %% @todo read_body related; h2 has this behavior but there is no corresponding test %% 4.1.1. Request Cancellation and Rejection %% When possible, it is RECOMMENDED that servers send an HTTP response with an %appropriate status code rather than cancelling a request it has already begun %processing. %% Implementations SHOULD cancel requests by abruptly terminating any %directions of a stream that are still open. To do so, an implementation resets %the sending parts of streams and aborts reading on the receiving parts of %streams; see Section 2.4 of [QUIC-TRANSPORT]. %% When the server cancels a request without performing any application %processing, the request is considered "rejected". The server SHOULD abort its %response stream with the error code H3_REQUEST_REJECTED. In this context, %"processed" means that some data from the stream was passed to some higher %layer of software that might have taken some action as a result. The client %can treat requests rejected by the server as though they had never been sent %at all, thereby allowing them to be retried later. %% Servers MUST NOT use the H3_REQUEST_REJECTED error code for requests that %were partially or fully processed. When a server abandons a response after %partial processing, it SHOULD abort its response stream with the error code %H3_REQUEST_CANCELLED. %% @todo %% Client SHOULD use the error code H3_REQUEST_CANCELLED to cancel requests. %Upon receipt of this error code, a server MAY abruptly terminate the response %using the error code H3_REQUEST_REJECTED if no processing was performed. %Clients MUST NOT use the H3_REQUEST_REJECTED error code, except when a server %has requested closure of the request stream with this error code. %% @todo %4.1.2. Malformed Requests and Responses %A malformed request or response is one that is an otherwise valid sequence of %frames but is invalid due to: % %the presence of prohibited fields or pseudo-header fields, %% @todo reject_response_pseudo_headers %% @todo reject_unknown_pseudo_headers %% @todo reject_pseudo_headers_in_trailers %the absence of mandatory pseudo-header fields, %invalid values for pseudo-header fields, %pseudo-header fields after fields, %% @todo reject_pseudo_headers_after_regular_headers %an invalid sequence of HTTP messages, %the inclusion of invalid characters in field names or values. % %A request or response that is defined as having content when it contains a %Content-Length header field (Section 8.6 of [HTTP]) is malformed if the value %of the Content-Length header field does not equal the sum of the DATA frame %lengths received. A response that is defined as never having content, even %when a Content-Length is present, can have a non-zero Content-Length header %field even though no content is included in DATA frames. % %For malformed requests, a server MAY send an HTTP response indicating the %error prior to closing or resetting the stream. %% @todo All the malformed tests headers_reject_uppercase_header_name(Config) -> doc("Requests containing uppercase header names must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"I-AM-GIGANTIC">>, <<"How's the weather up there?">>} ). %% 4.2. HTTP Fields %% An endpoint MUST NOT generate an HTTP/3 field section containing %connection-specific fields; any message containing connection-specific fields %MUST be treated as malformed. reject_connection_header(Config) -> doc("Requests containing a connection header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"connection">>, <<"close">>} ). reject_keep_alive_header(Config) -> doc("Requests containing a keep-alive header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"keep-alive">>, <<"timeout=5, max=1000">>} ). reject_proxy_authenticate_header(Config) -> doc("Requests containing a proxy-authenticate header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"proxy-authenticate">>, <<"Basic">>} ). reject_proxy_authorization_header(Config) -> doc("Requests containing a proxy-authorization header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"proxy-authorization">>, <<"Basic YWxhZGRpbjpvcGVuc2VzYW1l">>} ). reject_transfer_encoding_header(Config) -> doc("Requests containing a transfer-encoding header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"transfer-encoding">>, <<"chunked">>} ). reject_upgrade_header(Config) -> doc("Requests containing an upgrade header must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.5, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"upgrade">>, <<"websocket">>} ). accept_te_header_value_trailers(Config) -> doc("Requests containing a TE header with a value of \"trailers\" " "must be accepted. (RFC9114 4.2)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>}, {<<"te">>, <<"trailers">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"content-type">>, <<"text/plain">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. reject_te_header_other_values(Config) -> doc("Requests containing a TE header with a value other than \"trailers\" must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.2, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<"te">>, <<"trailers, deflate;q=0.5">>} ). %% @todo response_dont_send_header_in_connection %% @todo response_dont_send_connection_header %% @todo response_dont_send_keep_alive_header %% @todo response_dont_send_proxy_connection_header %% @todo response_dont_send_transfer_encoding_header %% @todo response_dont_send_upgrade_header %% 4.2.1. Field Compression %% To allow for better compression efficiency, the Cookie header field %([COOKIES]) MAY be split into separate field lines, each with one or more %cookie-pairs, before compression. If a decompressed field section contains %multiple cookie field lines, these MUST be concatenated into a single byte %string using the two-byte delimiter of "; " (ASCII 0x3b, 0x20) before being %passed into a context other than HTTP/2 or HTTP/3, such as an HTTP/1.1 %connection, or a generic HTTP server application. %% 4.2.2. Header Size Constraints %% An HTTP/3 implementation MAY impose a limit on the maximum size of the %message header it will accept on an individual HTTP message. A server that %receives a larger header section than it is willing to handle can send an HTTP %431 (Request Header Fields Too Large) status code ([RFC6585]). The size of a %field list is calculated based on the uncompressed size of fields, including %the length of the name and value in bytes plus an overhead of 32 bytes for %each field. %% If an implementation wishes to advise its peer of this limit, it can be %conveyed as a number of bytes in the SETTINGS_MAX_FIELD_SECTION_SIZE %parameter. reject_unknown_pseudo_headers(Config) -> doc("Requests containing unknown pseudo-headers must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<":upgrade">>, <<"websocket">>} ). reject_response_pseudo_headers(Config) -> doc("Requests containing response pseudo-headers must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3, RFC9114 4.1.2)"), do_reject_malformed_header(Config, {<<":status">>, <<"200">>} ). reject_pseudo_headers_in_trailers(Config) -> doc("Requests containing pseudo-headers in trailers must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3, RFC9114 4.1.2)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"trailer">>, <<"x-checksum">>} ], 0, cow_qpack:init(encoder)), {ok, EncodedTrailers, _EncData2, _EncSt} = cow_qpack:encode_field_section([ {<<"x-checksum">>, <<"md5:4cc909a007407f3706399b6496babec3">>}, {<<":path">>, <<"/">>} ], 0, EncSt0), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(10000), <<0:10000/unit:8>>, <<1>>, %% HEADERS frame for trailers. cow_http3:encode_int(iolist_size(EncodedTrailers)), EncodedTrailers ]), %% The stream should have been aborted. #{reason := h3_message_error} = do_wait_stream_aborted(StreamRef), ok. reject_pseudo_headers_after_regular_headers(Config) -> doc("Requests containing pseudo-headers after regular headers must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<"content-length">>, <<"0">>}, {<<":path">>, <<"/">>} ]). reject_userinfo(Config) -> doc("An authority containing a userinfo component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"user@localhost">>}, {<<":path">>, <<"/">>} ]). %% To ensure that the HTTP/1.1 request line can be reproduced accurately, this %% pseudo-header field (:authority) MUST be omitted when translating from an %% HTTP/1.1 request that has a request target in a method-specific form; %% see Section 7.1 of [HTTP]. reject_empty_path(Config) -> doc("A request containing an empty path component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<>>} ]). reject_missing_pseudo_header_method(Config) -> doc("A request without a method component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ]). reject_many_pseudo_header_method(Config) -> doc("A request containing more than one method component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ]). reject_missing_pseudo_header_scheme(Config) -> doc("A request without a scheme component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ]). reject_many_pseudo_header_scheme(Config) -> doc("A request containing more than one scheme component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ]). reject_missing_pseudo_header_authority(Config) -> doc("A request without an authority or host component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/">>} ]). accept_host_header_on_missing_pseudo_header_authority(Config) -> doc("A request without an authority but with a host header must be accepted. " "(RFC9114 4.3.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, _EncSt0} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/">>}, {<<"host">>, <<"localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. %% @todo %% If the :scheme pseudo-header field identifies a scheme that has a mandatory %% authority component (including "http" and "https"), the request MUST contain %% either an :authority pseudo-header field or a Host header field. %% - If both fields are present, they MUST NOT be empty. %% - If both fields are present, they MUST contain the same value. reject_many_pseudo_header_authority(Config) -> doc("A request containing more than one authority component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ]). reject_missing_pseudo_header_path(Config) -> doc("A request without a path component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>} ]). reject_many_pseudo_header_path(Config) -> doc("A request containing more than one path component must be rejected " "with an H3_MESSAGE_ERROR stream error. (RFC9114 4.3.1, RFC9114 4.1.2)"), do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<":path">>, <<"/">>} ]). do_reject_malformed_header(Config, Header) -> do_reject_malformed_headers(Config, [ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, Header ]). do_reject_malformed_headers(Config, Headers) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData1, _EncSt0} = cow_qpack:encode_field_section(Headers, 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ]), %% The stream should have been aborted. #{reason := h3_message_error} = do_wait_stream_aborted(StreamRef), ok. %% For responses, a single ":status" pseudo-header field is defined that %% carries the HTTP status code; see Section 15 of [HTTP]. This pseudo-header %% field MUST be included in all responses; otherwise, the response is malformed %% (see Section 4.1.2). %% @todo Implement CONNECT. (RFC9114 4.4. The CONNECT Method) %% @todo Maybe block the sending of 101 responses? (RFC9114 4.5. HTTP Upgrade) - also HTTP/2. %% @todo Implement server push (RFC9114 4.6. Server Push) %% @todo - need a way to list connections %% 5.2. Connection Shutdown %% Endpoints initiate the graceful shutdown of an HTTP/3 connection by sending %% a GOAWAY frame. The GOAWAY frame contains an identifier that indicates to the %% receiver the range of requests or pushes that were or might be processed in %% this connection. The server sends a client-initiated bidirectional stream ID; %% the client sends a push ID. Requests or pushes with the indicated identifier %% or greater are rejected (Section 4.1.1) by the sender of the GOAWAY. This %% identifier MAY be zero if no requests or pushes were processed. %% @todo %% Upon sending a GOAWAY frame, the endpoint SHOULD explicitly cancel (see %% Sections 4.1.1 and 7.2.3) any requests or pushes that have identifiers greater %% than or equal to the one indicated, in order to clean up transport state for %% the affected streams. The endpoint SHOULD continue to do so as more requests %% or pushes arrive. %% @todo %% Endpoints MUST NOT initiate new requests or promise new pushes on the %% connection after receipt of a GOAWAY frame from the peer. %% @todo %% Requests on stream IDs less than the stream ID in a GOAWAY frame from the %% server might have been processed; their status cannot be known until a %% response is received, the stream is reset individually, another GOAWAY is %% received with a lower stream ID than that of the request in question, or the %% connection terminates. %% @todo %% Servers MAY reject individual requests on streams below the indicated ID if %% these requests were not processed. %% @todo %% If a server receives a GOAWAY frame after having promised pushes with a push %% ID greater than or equal to the identifier contained in the GOAWAY frame, %% those pushes will not be accepted. %% @todo %% Servers SHOULD send a GOAWAY frame when the closing of a connection is known %% in advance, even if the advance notice is small, so that the remote peer can %% know whether or not a request has been partially processed. %% @todo %% An endpoint MAY send multiple GOAWAY frames indicating different %% identifiers, but the identifier in each frame MUST NOT be greater than the %% identifier in any previous frame, since clients might already have retried %% unprocessed requests on another HTTP connection. Receiving a GOAWAY containing %% a larger identifier than previously received MUST be treated as a connection %% error of type H3_ID_ERROR. %% @todo %% An endpoint that is attempting to gracefully shut down a connection can send %% a GOAWAY frame with a value set to the maximum possible value (2^62-4 for %% servers, 2^62-1 for clients). %% @todo %% Even when a GOAWAY indicates that a given request or push will not be %% processed or accepted upon receipt, the underlying transport resources still %% exist. The endpoint that initiated these requests can cancel them to clean up %% transport state. %% @todo %% Once all accepted requests and pushes have been processed, the endpoint can %% permit the connection to become idle, or it MAY initiate an immediate closure %% of the connection. An endpoint that completes a graceful shutdown SHOULD use %% the H3_NO_ERROR error code when closing the connection. %% @todo %% If a client has consumed all available bidirectional stream IDs with %% requests, the server need not send a GOAWAY frame, since the client is unable %% to make further requests. @todo OK that one's some weird stuff lol %% @todo %% 5.3. Immediate Application Closure %% Before closing the connection, a GOAWAY frame MAY be sent to allow the %% client to retry some requests. Including the GOAWAY frame in the same packet %% as the QUIC CONNECTION_CLOSE frame improves the chances of the frame being %% received by clients. bidi_allow_at_least_a_hundred(Config) -> doc("Endpoints must allow the peer to create at least " "one hundred bidirectional streams. (RFC9114 6.1"), #{conn := Conn} = do_connect(Config), receive {quic, streams_available, Conn, #{bidi_streams := NumStreams}} -> true = NumStreams >= 100, ok after 5000 -> error(timeout) end. unidi_allow_at_least_three(Config) -> doc("Endpoints must allow the peer to create at least " "three unidirectional streams. (RFC9114 6.2"), #{conn := Conn} = do_connect(Config), %% Confirm that the server advertised support for at least 3 unidi streams. receive {quic, streams_available, Conn, #{unidi_streams := NumStreams}} -> true = NumStreams >= 3, ok after 5000 -> error(timeout) end, %% Confirm that we can create the unidi streams. {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(ControlRef, [<<0>>, SettingsBin]), {ok, EncoderRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(EncoderRef, <<2>>), {ok, DecoderRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(DecoderRef, <<3>>), %% Streams shouldn't get closed. fun Loop() -> receive %% We don't care about these messages. {quic, dgram_state_changed, Conn, _} -> Loop(); {quic, peer_needs_streams, Conn, _} -> Loop(); %% Any other we do care. Msg -> error(Msg) after 1000 -> ok end end(). unidi_create_critical_first(Config) -> doc("Endpoints should create the HTTP control stream as well as " "the QPACK encoder and decoder streams first. (RFC9114 6.2"), %% The control stream is accepted in the do_connect/1 function. #{conn := Conn} = do_connect(Config, #{peer_unidi_stream_count => 3}), Unidi1 = do_accept_qpack_stream(Conn), Unidi2 = do_accept_qpack_stream(Conn), case {Unidi1, Unidi2} of {{encoder, _}, {decoder, _}} -> ok; {{decoder, _}, {encoder, _}} -> ok end. do_accept_qpack_stream(Conn) -> receive {quic, new_stream, StreamRef, #{flags := Flags}} -> ok = quicer:setopt(StreamRef, active, true), true = quicer:is_unidirectional(Flags), receive {quic, <>, StreamRef, _} -> {case Type of 2 -> encoder; 3 -> decoder end, StreamRef} after 5000 -> error(timeout) end after 5000 -> error(timeout) end. %% @todo We should also confirm that there's at least 1,024 bytes of %% flow-control credit for each unidi stream the server creates. (How?) %% It can be set via stream_recv_window_default in quicer. unidi_abort_unknown_type(Config) -> doc("Receipt of an unknown stream type must be aborted " "with an H3_STREAM_CREATION_ERROR stream error. (RFC9114 6.2, RFC9114 9)"), #{conn := Conn} = do_connect(Config), %% Create an unknown unidirectional stream. {ok, StreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(StreamRef, [ cow_http3:encode_int(1 + do_reserved_type()), rand:bytes(rand:uniform(4096)) ]), %% The stream should have been aborted. #{reason := h3_stream_creation_error} = do_wait_stream_aborted(StreamRef), ok. unidi_abort_reserved_type(Config) -> doc("Receipt of a reserved stream type must be aborted " "with an H3_STREAM_CREATION_ERROR stream error. " "(RFC9114 6.2, RFC9114 6.2.3, RFC9114 9)"), #{conn := Conn} = do_connect(Config), %% Create a reserved unidirectional stream. {ok, StreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(StreamRef, [ cow_http3:encode_int(do_reserved_type()), rand:bytes(rand:uniform(4096)) ]), %% The stream should have been aborted. #{reason := h3_stream_creation_error} = do_wait_stream_aborted(StreamRef), ok. %% As certain stream types can affect connection state, a recipient SHOULD NOT %% discard data from incoming unidirectional streams prior to reading the stream type. %% Implementations MAY send stream types before knowing whether the peer %supports them. However, stream types that could modify the state or semantics %of existing protocol components, including QPACK or other extensions, MUST NOT %be sent until the peer is known to support them. %% @todo It may make sense for Cowboy to delay the creation of unidi streams %% a little in order to save resources. We could create them when the %% client does as well, or something similar. %% A receiver MUST tolerate unidirectional streams being closed or reset prior %% to the reception of the unidirectional stream header. %% Each side MUST initiate a single control stream at the beginning of the %% connection and send its SETTINGS frame as the first frame on this stream. %% @todo What to do when the client never opens a control stream? %% @todo Similarly, a stream could be opened but with no data being sent. %% @todo Similarly, a control stream could be opened with no SETTINGS frame sent. control_reject_first_frame_data(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<0>>, %% DATA frame. cow_http3:encode_int(12), <<"Hello world!">> ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_reject_first_frame_headers(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_reject_first_frame_cancel_push(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<3>>, %% CANCEL_PUSH frame. cow_http3:encode_int(1), cow_http3:encode_int(0) ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_accept_first_frame_settings(Config) -> doc("The first frame on a control stream " "must be a SETTINGS frame. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin ]), %% The connection should remain up. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), error(Reason) after 1000 -> ok end. control_reject_first_frame_push_promise(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<5>>, %% PUSH_PROMISE frame. cow_http3:encode_int(iolist_size(EncodedHeaders) + 1), cow_http3:encode_int(0), EncodedHeaders ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_reject_first_frame_goaway(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<7>>, %% GOAWAY frame. cow_http3:encode_int(1), cow_http3:encode_int(0) ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_reject_first_frame_max_push_id(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<13>>, %% MAX_PUSH_ID frame. cow_http3:encode_int(1), cow_http3:encode_int(0) ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_reject_first_frame_reserved(Config) -> doc("The first frame on a control stream must be a SETTINGS frame " "or the connection must be closed with an H3_MISSING_SETTINGS " "connection error. (RFC9114 6.2.1, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), Len = rand:uniform(512), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. cow_http3:encode_int(do_reserved_type()), cow_http3:encode_int(Len), rand:bytes(Len) ]), %% The connection should have been closed. #{reason := h3_missing_settings} = do_wait_connection_closed(Conn), ok. control_reject_multiple(Config) -> doc("Endpoints must not create multiple control streams. (RFC9114 6.2.1)"), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), do_critical_reject_multiple(Config, [<<0>>, SettingsBin]). do_critical_reject_multiple(Config, HeaderData) -> #{conn := Conn} = do_connect(Config), %% Create two critical streams. {ok, StreamRef1} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(StreamRef1, HeaderData), {ok, StreamRef2} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(StreamRef2, HeaderData), %% The connection should have been closed. #{reason := h3_stream_creation_error} = do_wait_connection_closed(Conn), ok. control_local_closed_abort(Config) -> doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), do_critical_local_closed_abort(Config, [<<0>>, SettingsBin]). do_critical_local_closed_abort(Config, HeaderData) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(StreamRef, HeaderData), %% Wait a little to make sure the stream data was received before we abort. timer:sleep(100), %% Close the critical stream. quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0), %% The connection should have been closed. timer:sleep(1000), #{reason := h3_closed_critical_stream} = do_wait_connection_closed(Conn), ok. control_local_closed_graceful(Config) -> doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), do_critical_local_closed_graceful(Config, [<<0>>, SettingsBin]). do_critical_local_closed_graceful(Config, HeaderData) -> #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, _} = quicer:send(StreamRef, HeaderData), %% Close the critical stream. quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0), %% The connection should have been closed. #{reason := h3_closed_critical_stream} = do_wait_connection_closed(Conn), ok. control_remote_closed_abort(Config) -> doc("Endpoints must not close the control stream. (RFC9114 6.2.1)"), #{conn := Conn, control := ControlRef} = do_connect(Config), %% Close the control stream. quicer:async_shutdown_stream(ControlRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0), %% The connection should have been closed. #{reason := h3_closed_critical_stream} = do_wait_connection_closed(Conn), ok. %% We cannot gracefully shutdown a remote unidi stream; only abort reading. %% Because the contents of the control stream are used to manage the behavior %% of other streams, endpoints SHOULD provide enough flow-control credit to keep %% the peer's control stream from becoming blocked. %% @todo Implement server push (RFC9114 6.2.2 Push Streams) data_frame_can_span_multiple_packets(Config) -> doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/echo/read_body">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello ">> ]), timer:sleep(100), {ok, _} = quicer:send(StreamRef, [ <<"server!">> ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello server!">> } = do_receive_response(StreamRef), ok. headers_frame_can_span_multiple_packets(Config) -> doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), Half = iolist_size(EncodedHeaders) div 2, <> = iolist_to_binary(EncodedHeaders), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeadersPart1 ]), timer:sleep(100), {ok, _} = quicer:send(StreamRef, [ EncodedHeadersPart2 ]), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. %% @todo Implement server push. cancel_push_frame_can_span_multiple_packets(Config) -> settings_frame_can_span_multiple_packets(Config) -> doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), <> = SettingsBin, {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsPart1 ]), timer:sleep(100), {ok, _} = quicer:send(ControlRef, [ SettingsPart2 ]), %% The connection should remain up. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), error(Reason) after 1000 -> ok end. goaway_frame_can_span_multiple_packets(Config) -> doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<7>>, cow_http3:encode_int(1) %% GOAWAY part 1. ]), timer:sleep(100), {ok, _} = quicer:send(ControlRef, [ cow_http3:encode_int(0) %% GOAWAY part 2. ]), %% The connection should be closed gracefully. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> h3_no_error = cow_http3:code_to_error(Code), ok; %% @todo Temporarily also accept this message. I am %% not sure why it happens but it isn't wrong per se. {quic, shutdown, Conn, success} -> ok after 1000 -> error(timeout) end. max_push_id_frame_can_span_multiple_packets(Config) -> doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<13>>, cow_http3:encode_int(1) %% MAX_PUSH_ID part 1. ]), timer:sleep(100), {ok, _} = quicer:send(ControlRef, [ cow_http3:encode_int(0) %% MAX_PUSH_ID part 2. ]), %% The connection should remain up. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), error(Reason) after 1000 -> ok end. unknown_frame_can_span_multiple_packets(Config) -> doc("HTTP/3 frames can span multiple packets. (RFC9114 7)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(StreamRef, [ cow_http3:encode_int(do_unknown_frame_type()), cow_http3:encode_int(16383) ]), timer:sleep(100), {ok, _} = quicer:send(StreamRef, rand:bytes(4096)), timer:sleep(100), {ok, _} = quicer:send(StreamRef, rand:bytes(4096)), timer:sleep(100), {ok, _} = quicer:send(StreamRef, rand:bytes(4096)), timer:sleep(100), {ok, _} = quicer:send(StreamRef, rand:bytes(4095)), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ], ?QUIC_SEND_FLAG_FIN), #{ headers := #{<<":status">> := <<"200">>}, body := <<"Hello world!">> } = do_receive_response(StreamRef), ok. %% The DATA and SETTINGS frames can be zero-length therefore %% they cannot be too short. headers_frame_too_short(Config) -> doc("Frames that terminate before the end of identified fields " "must be rejected with an H3_FRAME_ERROR connection error. " "(RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(0) ]), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. %% @todo Implement server push. cancel_push_frame_too_short(Config) -> goaway_frame_too_short(Config) -> doc("Frames that terminate before the end of identified fields " "must be rejected with an H3_FRAME_ERROR connection error. " "(RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<7>>, cow_http3:encode_int(0) %% GOAWAY. ]), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. max_push_id_frame_too_short(Config) -> doc("Frames that terminate before the end of identified fields " "must be rejected with an H3_FRAME_ERROR connection error. " "(RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<13>>, cow_http3:encode_int(0) %% MAX_PUSH_ID. ]), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. data_frame_truncated(Config) -> doc("Truncated frames must be rejected with an " "H3_FRAME_ERROR connection error. (RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/echo/read_body">>}, {<<"content-length">>, <<"13">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders, <<0>>, %% DATA frame. cow_http3:encode_int(13), <<"Hello ">> ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. headers_frame_truncated(Config) -> doc("Truncated frames must be rejected with an " "H3_FRAME_ERROR connection error. (RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)) ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. %% I am not sure how to test truncated CANCEL_PUSH, SETTINGS, GOAWAY %% or MAX_PUSH_ID frames, as those are sent on the control stream, %% which we cannot terminate. %% The DATA, HEADERS and SETTINGS frames can be of any length %% therefore they cannot be too long per se, even if unwanted %% data can be included at the end of the frame's payload. %% @todo Implement server push. cancel_push_frame_too_long(Config) -> goaway_frame_too_long(Config) -> doc("Frames that contain additional bytes after the end of identified fields " "must be rejected with an H3_FRAME_ERROR connection error. " "(RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<7>>, cow_http3:encode_int(3), %% GOAWAY. <<0, 1, 2>> ]), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. max_push_id_frame_too_long(Config) -> doc("Frames that contain additional bytes after the end of identified fields " "must be rejected with an H3_FRAME_ERROR connection error. " "(RFC9114 7.1, RFC9114 10.8)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<13>>, cow_http3:encode_int(9), %% MAX_PUSH_ID. <<0, 1, 2, 3, 4, 5, 6, 7, 8>> ]), %% The connection should have been closed. #{reason := h3_frame_error} = do_wait_connection_closed(Conn), ok. %% Streams may terminate abruptly in the middle of frames. data_frame_rejected_on_control_stream(Config) -> doc("DATA frames received on the control stream must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.1)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<0>>, %% DATA frame. cow_http3:encode_int(12), <<"Hello world!">> ]), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. headers_frame_rejected_on_control_stream(Config) -> doc("HEADERS frames received on the control stream must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.2)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ]), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. %% @todo Implement server push. (RFC9114 7.2.3. CANCEL_PUSH) settings_twice(Config) -> doc("Receipt of a second SETTINGS frame on the control stream " "must be rejected with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.4)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, SettingsBin ]), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. settings_on_bidi_stream(Config) -> doc("Receipt of a SETTINGS frame on a bidirectional stream " "must be rejected with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.4)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ SettingsBin, <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. settings_identifier_twice(Config) -> doc("Receipt of a duplicate SETTINGS identifier must be rejected " "with an H3_SETTINGS_ERROR connection error. (RFC9114 7.2.4)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), SettingsPayload = [ cow_http3:encode_int(6), cow_http3:encode_int(4096), cow_http3:encode_int(6), cow_http3:encode_int(8192) ], {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<4>>, %% SETTINGS frame. cow_http3:encode_int(iolist_size(SettingsPayload)), SettingsPayload ]), %% The connection should have been closed. #{reason := h3_settings_error} = do_wait_connection_closed(Conn), ok. settings_ignore_unknown_identifier(Config) -> doc("Unknown SETTINGS identifiers must be ignored (RFC9114 7.2.4, RFC9114 9)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), SettingsPayload = [ cow_http3:encode_int(999), cow_http3:encode_int(4096) ], {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<4>>, %% SETTINGS frame. cow_http3:encode_int(iolist_size(SettingsPayload)), SettingsPayload ]), %% The connection should remain up. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), error(Reason) after 1000 -> ok end. settings_ignore_reserved_identifier(Config) -> doc("Reserved SETTINGS identifiers must be ignored (RFC9114 7.2.4.1)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), SettingsPayload = [ cow_http3:encode_int(do_reserved_type()), cow_http3:encode_int(4096) ], {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<4>>, %% SETTINGS frame. cow_http3:encode_int(iolist_size(SettingsPayload)), SettingsPayload ]), %% The connection should remain up. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), error(Reason) after 1000 -> ok end. %% @todo Check that we send a reserved SETTINGS identifier when sending a %% non-empty SETTINGS frame. (7.2.4.1. Defined SETTINGS Parameters) %% @todo Check that setting SETTINGS_MAX_FIELD_SECTION_SIZE works. %% It is unclear whether the SETTINGS identifier 0x00 must be rejected or ignored. settings_reject_http2_0x02(Config) -> do_settings_reject_http2(Config, 2, 1). settings_reject_http2_0x03(Config) -> do_settings_reject_http2(Config, 3, 100). settings_reject_http2_0x04(Config) -> do_settings_reject_http2(Config, 4, 128000). settings_reject_http2_0x05(Config) -> do_settings_reject_http2(Config, 5, 1000000). do_settings_reject_http2(Config, Identifier, Value) -> doc("Receipt of an unused HTTP/2 SETTINGS identifier must be rejected " "with an H3_SETTINGS_ERROR connection error. (RFC9114 7.2.4, RFC9114 11.2.2)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), SettingsPayload = [ cow_http3:encode_int(Identifier), cow_http3:encode_int(Value) ], {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. <<4>>, %% SETTINGS frame. cow_http3:encode_int(iolist_size(SettingsPayload)), SettingsPayload ]), %% The connection should have been closed. #{reason := h3_settings_error} = do_wait_connection_closed(Conn), ok. %% 7.2.4.2. Initialization %% An HTTP implementation MUST NOT send frames or requests that would be %% invalid based on its current understanding of the peer's settings. %% @todo In the case of SETTINGS_MAX_FIELD_SECTION_SIZE I don't think we have a choice. %% All settings begin at an initial value. Each endpoint SHOULD use these %% initial values to send messages before the peer's SETTINGS frame has arrived, %% as packets carrying the settings can be lost or delayed. When the SETTINGS %% frame arrives, any settings are changed to their new values. %% Endpoints MUST NOT require any data to be received from the peer prior to %% sending the SETTINGS frame; settings MUST be sent as soon as the transport is %% ready to send data. %% @todo Implement 0-RTT. (7.2.4.2. Initialization) %% @todo Implement server push. (7.2.5. PUSH_PROMISE) goaway_on_bidi_stream(Config) -> doc("Receipt of a GOAWAY frame on a bidirectional stream " "must be rejected with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.6)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(StreamRef, [ <<7>>, cow_http3:encode_int(1), cow_http3:encode_int(0) %% GOAWAY. ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. %% @todo Implement server push. (7.2.6 GOAWAY - will have to reject too large push IDs) max_push_id_on_bidi_stream(Config) -> doc("Receipt of a MAX_PUSH_ID frame on a bidirectional stream " "must be rejected with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.7)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(StreamRef, [ <<13>>, cow_http3:encode_int(1), cow_http3:encode_int(0) %% MAX_PUSH_ID. ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. %% @todo Implement server push. (7.2.7 MAX_PUSH_ID) max_push_id_reject_lower(Config) -> doc("Receipt of a MAX_PUSH_ID value lower than previously received " "must be rejected with an H3_ID_ERROR connection error. (RFC9114 7.2.7)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, <<13>>, cow_http3:encode_int(1), cow_http3:encode_int(20), %% MAX_PUSH_ID. <<13>>, cow_http3:encode_int(1), cow_http3:encode_int(10) %% MAX_PUSH_ID. ]), %% The connection should have been closed. #{reason := h3_id_error} = do_wait_connection_closed(Conn), ok. reserved_on_control_stream(Config) -> doc("Receipt of a reserved frame type on a control stream " "must be ignored. (RFC9114 7.2.8)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), Len = rand:uniform(512), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, cow_http3:encode_int(do_reserved_type()), cow_http3:encode_int(Len), rand:bytes(Len) ]), %% The connection should remain up. receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), error(Reason) after 1000 -> ok end. reserved_reject_http2_0x02_control(Config) -> do_reserved_reject_http2_control(Config, 2). reserved_reject_http2_0x06_control(Config) -> do_reserved_reject_http2_control(Config, 6). reserved_reject_http2_0x08_control(Config) -> do_reserved_reject_http2_control(Config, 8). reserved_reject_http2_0x09_control(Config) -> do_reserved_reject_http2_control(Config, 9). do_reserved_reject_http2_control(Config, Type) -> doc("Receipt of an unused HTTP/2 frame type must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.8, RFC9114 11.2.1)"), #{conn := Conn} = do_connect(Config), {ok, ControlRef} = quicer:start_stream(Conn, #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), Len = rand:uniform(512), {ok, _} = quicer:send(ControlRef, [ <<0>>, %% CONTROL stream. SettingsBin, cow_http3:encode_int(Type), cow_http3:encode_int(Len), rand:bytes(Len) ]), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. reserved_reject_http2_0x02_bidi(Config) -> do_reserved_reject_http2_bidi(Config, 2). reserved_reject_http2_0x06_bidi(Config) -> do_reserved_reject_http2_bidi(Config, 6). reserved_reject_http2_0x08_bidi(Config) -> do_reserved_reject_http2_bidi(Config, 8). reserved_reject_http2_0x09_bidi(Config) -> do_reserved_reject_http2_bidi(Config, 9). do_reserved_reject_http2_bidi(Config, Type) -> doc("Receipt of an unused HTTP/2 frame type must be rejected " "with an H3_FRAME_UNEXPECTED connection error. (RFC9114 7.2.8, RFC9114 11.2.1)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedHeaders, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, {<<":path">>, <<"/">>}, {<<"content-length">>, <<"0">>} ], 0, cow_qpack:init(encoder)), Len = rand:uniform(512), {ok, _} = quicer:send(StreamRef, [ cow_http3:encode_int(Type), cow_http3:encode_int(Len), rand:bytes(Len), <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedHeaders)), EncodedHeaders ], ?QUIC_SEND_FLAG_FIN), %% The connection should have been closed. #{reason := h3_frame_unexpected} = do_wait_connection_closed(Conn), ok. %% An endpoint MAY choose to treat a stream error as a connection error under %% certain circumstances, closing the entire connection in response to a %% condition on a single stream. %% Because new error codes can be defined without negotiation (see Section 9), %% use of an error code in an unexpected context or receipt of an unknown error %% code MUST be treated as equivalent to H3_NO_ERROR. %% 8.1. HTTP/3 Error Codes %% H3_INTERNAL_ERROR (0x0102): An internal error has occurred in the HTTP stack. %% H3_EXCESSIVE_LOAD (0x0107): The endpoint detected that its peer is %% exhibiting a behavior that might be generating excessive load. %% H3_MISSING_SETTINGS (0x010a): No SETTINGS frame was received %% at the beginning of the control stream. %% H3_REQUEST_REJECTED (0x010b): A server rejected a request without %% performing any application processing. %% H3_REQUEST_CANCELLED (0x010c): The request or its response %% (including pushed response) is cancelled. %% H3_REQUEST_INCOMPLETE (0x010d): The client's stream terminated %% without containing a fully formed request. %% H3_CONNECT_ERROR (0x010f): The TCP connection established in %% response to a CONNECT request was reset or abnormally closed. %% H3_VERSION_FALLBACK (0x0110): The requested operation cannot %% be served over HTTP/3. The peer should retry over HTTP/1.1. %% 9. Extensions to HTTP/3 %% If a setting is used for extension negotiation, the default value MUST be %% defined in such a fashion that the extension is disabled if the setting is %% omitted. %% 10. Security Considerations %% 10.3. Intermediary-Encapsulation Attacks %% Requests or responses containing invalid field names MUST be treated as malformed. %% Any request or response that contains a character not permitted in a field %% value MUST be treated as malformed. %% 10.5. Denial-of-Service Considerations %% Implementations SHOULD track the use of these features and set limits on %% their use. An endpoint MAY treat activity that is suspicious as a connection %% error of type H3_EXCESSIVE_LOAD, but false positives will result in disrupting %% valid connections and requests. reject_large_unknown_frame(Config) -> doc("Large unknown frames may risk denial-of-service " "and should be rejected. (RFC9114 10.5)"), #{conn := Conn} = do_connect(Config), {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, _} = quicer:send(StreamRef, [ cow_http3:encode_int(do_unknown_frame_type()), cow_http3:encode_int(16385) ]), #{reason := h3_excessive_load} = do_wait_connection_closed(Conn), ok. %% 10.5.1. Limits on Field Section Size %% An endpoint can use the SETTINGS_MAX_FIELD_SECTION_SIZE (Section 4.2.2) %% setting to advise peers of limits that might apply on the size of field %% sections. %% %% A server that receives a larger field section than it is willing to handle %% can send an HTTP 431 (Request Header Fields Too Large) status code %% ([RFC6585]). %% 10.6. Use of Compression %% Implementations communicating on a secure channel MUST NOT compress content %% that includes both confidential and attacker-controlled data unless separate %% compression contexts are used for each source of data. Compression MUST NOT be %% used if the source of data cannot be reliably determined. %% 10.9. Early Data %% The anti-replay mitigations in [HTTP-REPLAY] MUST be applied when using HTTP/3 with 0-RTT. %% 10.10. Migration %% Certain HTTP implementations use the client address for logging or %% access-control purposes. Since a QUIC client's address might change during a %% connection (and future versions might support simultaneous use of multiple %% addresses), such implementations will need to either actively retrieve the %% client's current address or addresses when they are relevant or explicitly %% accept that the original address might change. @todo Document this behavior. %% Appendix A. Considerations for Transitioning from HTTP/2 %% A.1. Streams %% QUIC considers a stream closed when all data has been received and sent data %% has been acknowledged by the peer. HTTP/2 considers a stream closed when the %% frame containing the END_STREAM bit has been committed to the transport. As a %% result, the stream for an equivalent exchange could remain "active" for a %% longer period of time. HTTP/3 servers might choose to permit a larger number %% of concurrent client-initiated bidirectional streams to achieve equivalent %% concurrency to HTTP/2, depending on the expected usage patterns. @todo Document this. %% Helper functions. %% @todo Maybe have a function in cow_http3. do_reserved_type() -> 16#1f * (rand:uniform(148764065110560900) - 1) + 16#21. do_connect(Config) -> do_connect(Config, #{}). do_connect(Config, Opts) -> {ok, Conn} = quicer:connect("localhost", config(port, Config), Opts#{alpn => ["h3"], verify => none}, 5000), %% To make sure the connection is fully established we wait %% to receive the SETTINGS frame on the control stream. {ok, ControlRef, Settings} = do_wait_settings(Conn), #{ conn => Conn, control => ControlRef, %% This is the peer control stream. settings => Settings }. do_wait_settings(Conn) -> receive {quic, new_stream, StreamRef, #{flags := Flags}} -> ok = quicer:setopt(StreamRef, active, true), true = quicer:is_unidirectional(Flags), receive {quic, << 0, %% Control stream. SettingsFrame/bits >>, StreamRef, _} -> {ok, {settings, Settings}, <<>>} = cow_http3:parse(SettingsFrame), {ok, StreamRef, Settings} after 5000 -> {error, timeout} end after 5000 -> {error, timeout} end. do_receive_data(StreamRef) -> receive {quic, Data, StreamRef, _Flags} when is_binary(Data) -> {ok, Data} after 5000 -> {error, timeout} end. do_guess_int_encoding(Data) -> SizeWithLen = byte_size(Data) - 1, if SizeWithLen < 64 + 1 -> {0, 6}; SizeWithLen < 16384 + 2 -> {1, 14}; SizeWithLen < 1073741824 + 4 -> {2, 30}; SizeWithLen < 4611686018427387904 + 8 -> {3, 62} end. do_wait_peer_send_shutdown(StreamRef) -> receive {quic, peer_send_shutdown, StreamRef, undefined} -> ok after 5000 -> {error, timeout} end. do_wait_stream_aborted(StreamRef) -> receive {quic, peer_send_aborted, StreamRef, Code} -> Reason = cow_http3:code_to_error(Code), #{reason => Reason}; {quic, peer_receive_aborted, StreamRef, Code} -> Reason = cow_http3:code_to_error(Code), #{reason => Reason} after 5000 -> {error, timeout} end. do_wait_stream_closed(StreamRef) -> receive {quic, stream_closed, StreamRef, #{error := Error, is_conn_shutdown := false}} -> 0 = Error, ok after 5000 -> {error, timeout} end. do_receive_response(StreamRef) -> {ok, Data} = do_receive_data(StreamRef), {HLenEnc, HLenBits} = do_guess_int_encoding(Data), << 1, %% HEADERS frame. HLenEnc:2, HLen:HLenBits, EncodedResponse:HLen/bytes, Rest/bits >> = Data, {ok, DecodedResponse, _DecData, _DecSt} = cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)), Headers = maps:from_list(DecodedResponse), #{<<"content-length">> := BodyLen} = Headers, {DLenEnc, DLenBits} = do_guess_int_encoding(Rest), Body = case Rest of <<>> -> <<>>; << 0, %% DATA frame. DLenEnc:2, DLen:DLenBits, Body0:DLen/bytes >> -> BodyLen = integer_to_binary(byte_size(Body0)), Body0 end, ok = do_wait_peer_send_shutdown(StreamRef), #{ headers => Headers, body => Body }. do_wait_connection_closed(Conn) -> receive {quic, shutdown, Conn, {unknown_quic_status, Code}} -> Reason = cow_http3:code_to_error(Code), #{reason => Reason} after 5000 -> {error, timeout} end. -endif. ================================================ FILE: test/rfc9114_SUITE_data/client.key ================================================ -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVJakPYfQA1Hr6Gnq GYmpMfXpxUi2QwDBrZfw8dBcVqKhRANCAAQDHeeAvjwD7p+Mg1F+G9FBNy+7Wcms HEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx0l77vFjrUc6X1al4+ZM5 -----END PRIVATE KEY----- ================================================ FILE: test/rfc9114_SUITE_data/client.pem ================================================ -----BEGIN CERTIFICATE----- MIIBtTCCAVugAwIBAgIUeAPi9oyMIE/KRpsRdukfx2eMuuswCgYIKoZIzj0EAwIw IDELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9EWUFCMB4XDTIzMDcwNTEwMjIy MloXDTI0MTExNjEwMjIyMlowMTELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9E WUFCMQ8wDQYDVQQDDAZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQD HeeAvjwD7p+Mg1F+G9FBNy+7WcmsHEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx 0l77vFjrUc6X1al4+ZM5o2IwYDALBgNVHQ8EBAMCA4gwEQYDVR0RBAowCIIGY2xp ZW50MB0GA1UdDgQWBBTnhPpO+rSIFAxvkwVjlkKOO2jOeDAfBgNVHSMEGDAWgBSD Hw8A4XXG3jB1Atrqux7AUsf+KjAKBggqhkjOPQQDAgNIADBFAiEA2qf29EBp2hcL sEO7MM0ZLm4gnaMdcxtyneF3+c7Lg3cCIBFTVP8xHlhCJyb8ESV7S052VU0bKQFN ioyoYtcycxuZ -----END CERTIFICATE----- ================================================ FILE: test/rfc9114_SUITE_data/server.key ================================================ -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvykUYMOS2gW8XTTh HgmeJM36NT8GGTNXzzt4sIs0o9ahRANCAATnQOMkKbLFQCZY/cxf8otEJG2tVuG6 QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBMB4ZfLA/PdSiO+KrOeOcj -----END PRIVATE KEY----- ================================================ FILE: test/rfc9114_SUITE_data/server.pem ================================================ -----BEGIN CERTIFICATE----- MIIBtTCCAVugAwIBAgIUeAPi9oyMIE/KRpsRdukfx2eMuuowCgYIKoZIzj0EAwIw IDELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9EWUFCMB4XDTIzMDcwNTEwMjIy MloXDTI0MTExNjEwMjIyMlowMTELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9E WUFCMQ8wDQYDVQQDDAZzZXJ2ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATn QOMkKbLFQCZY/cxf8otEJG2tVuG6QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBM B4ZfLA/PdSiO+KrOeOcjo2IwYDALBgNVHQ8EBAMCA4gwEQYDVR0RBAowCIIGc2Vy dmVyMB0GA1UdDgQWBBS+Np5J8BtmWU534pm9hqhrG/EQ7zAfBgNVHSMEGDAWgBSD Hw8A4XXG3jB1Atrqux7AUsf+KjAKBggqhkjOPQQDAgNIADBFAiEApRfjIEJfO1VH ETgNG3/MzDayYScPocVn4v8U15ygEw8CIFUY3xMZzJ5AmiRe9PhIUgueOKQNMtds wdF9+097+Ey0 -----END CERTIFICATE----- ================================================ FILE: test/rfc9204_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc9204_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -ifdef(COWBOY_QUICER). -include_lib("quicer/include/quicer.hrl"). all() -> [{group, h3}]. groups() -> %% @todo Enable parallel tests but for this issues in the %% QUIC accept loop need to be figured out (can't connect %% concurrently somehow, no backlog?). [{h3, [], ct_helper:all(?MODULE)}]. init_per_group(Name = h3, Config) -> cowboy_test:init_http3(Name, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))} }, Config). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_routes(_) -> [ {"localhost", [ {"/", hello_h, []} ]} ]. %% Encoder. %% 2.1 %% QPACK preserves the ordering of field lines within %% each field section. An encoder MUST emit field %% representations in the order they appear in the %% input field section. %% 2.1.1 %% If the dynamic table does not contain enough room %% for a new entry without evicting other entries, %% and the entries that would be evicted are not evictable, %% the encoder MUST NOT insert that entry into the dynamic %% table (including duplicates of existing entries). %% In order to avoid this, an encoder that uses the %% dynamic table has to keep track of each dynamic %% table entry referenced by each field section until %% those representations are acknowledged by the decoder; %% see Section 4.4.1. %% 2.1.2 %% The decoder specifies an upper bound on the number %% of streams that can be blocked using the %% SETTINGS_QPACK_BLOCKED_STREAMS setting; see Section 5. %% An encoder MUST limit the number of streams that could %% become blocked to the value of SETTINGS_QPACK_BLOCKED_STREAMS %% at all times. If a decoder encounters more blocked streams %% than it promised to support, it MUST treat this as a %% connection error of type QPACK_DECOMPRESSION_FAILED. %% 2.1.3 %% To avoid these deadlocks, an encoder SHOULD NOT %% write an instruction unless sufficient stream and %% connection flow-control credit is available for %% the entire instruction. %% Decoder. %% 2.2 %% The decoder MUST emit field lines in the order their %% representations appear in the encoded field section. %% 2.2.1 %% While blocked, encoded field section data SHOULD %% remain in the blocked stream's flow-control window. %% If it encounters a Required Insert Count smaller than %% expected, it MUST treat this as a connection error of %% type QPACK_DECOMPRESSION_FAILED; see Section 2.2.3. %% If it encounters a Required Insert Count larger than %% expected, it MAY treat this as a connection error of %% type QPACK_DECOMPRESSION_FAILED. %% After the decoder finishes decoding a field section %% encoded using representations containing dynamic table %% references, it MUST emit a Section Acknowledgment %% instruction (Section 4.4.1). %% 2.2.2.2 %% A decoder with a maximum dynamic table capacity %% (Section 3.2.3) equal to zero MAY omit sending Stream %% Cancellations, because the encoder cannot have any %% dynamic table references. %% 2.2.3 %% If the decoder encounters a reference in a field line %% representation to a dynamic table entry that has already %% been evicted or that has an absolute index greater than %% or equal to the declared Required Insert Count (Section 4.5.1), %% it MUST treat this as a connection error of type %% QPACK_DECOMPRESSION_FAILED. %% If the decoder encounters a reference in an encoder %% instruction to a dynamic table entry that has already %% been evicted, it MUST treat this as a connection error %% of type QPACK_ENCODER_STREAM_ERROR. %% Static table. %% 3.1 %% When the decoder encounters an invalid static table index %% in a field line representation, it MUST treat this as a %% connection error of type QPACK_DECOMPRESSION_FAILED. %% %% If this index is received on the encoder stream, this %% MUST be treated as a connection error of type %% QPACK_ENCODER_STREAM_ERROR. %% Dynamic table. %% 3.2 %% The dynamic table can contain duplicate entries %% (i.e., entries with the same name and same value). %% Therefore, duplicate entries MUST NOT be treated %% as an error by the decoder. %% 3.2.2 %% The encoder MUST NOT cause a dynamic table entry to be %% evicted unless that entry is evictable; see Section 2.1.1. %% It is an error if the encoder attempts to add an entry %% that is larger than the dynamic table capacity; the %% decoder MUST treat this as a connection error of type %% QPACK_ENCODER_STREAM_ERROR. %% 3.2.3 %% The encoder MUST NOT set a dynamic table capacity that %% exceeds this maximum, but it can choose to use a lower %% dynamic table capacity; see Section 4.3.1. %% When the client's 0-RTT value of the SETTING is zero, %% the server MAY set it to a non-zero value in its SETTINGS %% frame. If the remembered value is non-zero, the server %% MUST send the same non-zero value in its SETTINGS frame. %% If it specifies any other value, or omits %% SETTINGS_QPACK_MAX_TABLE_CAPACITY from SETTINGS, %% the encoder must treat this as a connection error of %% type QPACK_DECODER_STREAM_ERROR. %% When the maximum table capacity is zero, the encoder %% MUST NOT insert entries into the dynamic table and %% MUST NOT send any encoder instructions on the encoder stream. %% Wire format. %% 4.1.1 %% QPACK implementations MUST be able to decode integers %% up to and including 62 bits long. %% Encoder and decoder streams. decoder_reject_multiple(Config) -> doc("Endpoints must not create multiple decoder streams. (RFC9204 4.2)"), rfc9114_SUITE:do_critical_reject_multiple(Config, <<3>>). encoder_reject_multiple(Config) -> doc("Endpoints must not create multiple encoder streams. (RFC9204 4.2)"), rfc9114_SUITE:do_critical_reject_multiple(Config, <<2>>). %% 4.2 %% The sender MUST NOT close either of these streams, %% and the receiver MUST NOT request that the sender close %% either of these streams. Closure of either unidirectional %% stream type MUST be treated as a connection error of type %% H3_CLOSED_CRITICAL_STREAM. decoder_local_closed_abort(Config) -> doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"), rfc9114_SUITE:do_critical_local_closed_abort(Config, <<3>>). decoder_local_closed_graceful(Config) -> doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"), rfc9114_SUITE:do_critical_local_closed_graceful(Config, <<3>>). decoder_remote_closed_abort(Config) -> doc("Endpoints must not close the decoder stream. (RFC9204 4.2)"), #{conn := Conn} = rfc9114_SUITE:do_connect(Config, #{peer_unidi_stream_count => 3}), {ok, #{decoder := StreamRef}} = do_wait_unidi_streams(Conn, #{}), %% Close the control stream. quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0), %% The connection should have been closed. #{reason := h3_closed_critical_stream} = rfc9114_SUITE:do_wait_connection_closed(Conn), ok. encoder_local_closed_abort(Config) -> doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"), rfc9114_SUITE:do_critical_local_closed_abort(Config, <<2>>). encoder_local_closed_graceful(Config) -> doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"), rfc9114_SUITE:do_critical_local_closed_graceful(Config, <<2>>). encoder_remote_closed_abort(Config) -> doc("Endpoints must not close the encoder stream. (RFC9204 4.2)"), #{conn := Conn} = rfc9114_SUITE:do_connect(Config, #{peer_unidi_stream_count => 3}), {ok, #{encoder := StreamRef}} = do_wait_unidi_streams(Conn, #{}), %% Close the control stream. quicer:async_shutdown_stream(StreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0), %% The connection should have been closed. #{reason := h3_closed_critical_stream} = rfc9114_SUITE:do_wait_connection_closed(Conn), ok. do_wait_unidi_streams(_, Acc=#{decoder := _, encoder := _}) -> {ok, Acc}; do_wait_unidi_streams(Conn, Acc) -> receive {quic, new_stream, StreamRef, #{flags := Flags}} -> ok = quicer:setopt(StreamRef, active, true), true = quicer:is_unidirectional(Flags), receive {quic, <>, StreamRef, _} -> Type = case TypeValue of 2 -> encoder; 3 -> decoder end, do_wait_unidi_streams(Conn, Acc#{Type => StreamRef}) after 5000 -> {error, timeout} end after 5000 -> {error, timeout} end. %% An endpoint MAY avoid creating an encoder stream if it will %% not be used (for example, if its encoder does not wish to %% use the dynamic table or if the maximum size of the dynamic %% table permitted by the peer is zero). %% An endpoint MAY avoid creating a decoder stream if its %% decoder sets the maximum capacity of the dynamic table to zero. %% An endpoint MUST allow its peer to create an encoder stream %% and a decoder stream even if the connection's settings %% prevent their use. %% Encoder instructions. %% 4.3.1 %% The new capacity MUST be lower than or equal to the limit %% described in Section 3.2.3. In HTTP/3, this limit is the %% value of the SETTINGS_QPACK_MAX_TABLE_CAPACITY parameter %% (Section 5) received from the decoder. The decoder MUST %% treat a new dynamic table capacity value that exceeds this %% limit as a connection error of type QPACK_ENCODER_STREAM_ERROR. %% Reducing the dynamic table capacity can cause entries to be %% evicted; see Section 3.2.2. This MUST NOT cause the eviction %% of entries that are not evictable; see Section 2.1.1. %% Decoder instructions. %% 4.4.1 %% If an encoder receives a Section Acknowledgment instruction %% referring to a stream on which every encoded field section %% with a non-zero Required Insert Count has already been %% acknowledged, this MUST be treated as a connection error %% of type QPACK_DECODER_STREAM_ERROR. %% 4.4.3 %% An encoder that receives an Increment field equal to zero, %% or one that increases the Known Received Count beyond what %% the encoder has sent, MUST treat this as a connection error %% of type QPACK_DECODER_STREAM_ERROR. %% Field line representation. %% 4.5.1.1 %% If the decoder encounters a value of EncodedInsertCount that %% could not have been produced by a conformant encoder, it MUST %% treat this as a connection error of type QPACK_DECOMPRESSION_FAILED. %% 4.5.1.2 %% The value of Base MUST NOT be negative. Though the protocol %% might operate correctly with a negative Base using post-Base %% indexing, it is unnecessary and inefficient. An endpoint MUST %% treat a field block with a Sign bit of 1 as invalid if the %% value of Required Insert Count is less than or equal to the %% value of Delta Base. %% 4.5.4 %% When the 'N' bit is set, the encoded field line MUST always %% be encoded with a literal representation. In particular, %% when a peer sends a field line that it received represented %% as a literal field line with the 'N' bit set, it MUST use a %% literal representation to forward this field line. This bit %% is intended for protecting field values that are not to be %% put at risk by compressing them; see Section 7.1 for more details. %% Configuration. %% 5 %% SETTINGS_QPACK_MAX_TABLE_CAPACITY %% SETTINGS_QPACK_BLOCKED_STREAMS %% Security considerations. %% 7.1.2 %% (security if used as a proxy merging many connections into one) %% An ideal solution segregates access to the dynamic table %% based on the entity that is constructing the message. %% Field values that are added to the table are attributed %% to an entity, and only the entity that created a particular %% value can extract that value. %% 7.1.3 %% An intermediary MUST NOT re-encode a value that uses a %% literal representation with the 'N' bit set with another %% representation that would index it. If QPACK is used for %% re-encoding, a literal representation with the 'N' bit set %% MUST be used. If HPACK is used for re-encoding, the %% never-indexed literal representation (see Section 6.2.3 %% of [RFC7541]) MUST be used. %% 7.4 %% An implementation has to set a limit for the values it %% accepts for integers, as well as for the encoded length; %% see Section 4.1.1. In the same way, it has to set a limit %% to the length it accepts for string literals; see Section 4.1.2. %% These limits SHOULD be large enough to process the largest %% individual field the HTTP implementation can be configured %% to accept. %% If an implementation encounters a value larger than it is %% able to decode, this MUST be treated as a stream error of %% type QPACK_DECOMPRESSION_FAILED if on a request stream or %% a connection error of the appropriate type if on the %% encoder or decoder stream. -endif. ================================================ FILE: test/rfc9220_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(rfc9220_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). all() -> [{group, enabled}]. groups() -> Tests = ct_helper:all(?MODULE), [{enabled, [], Tests}]. %% @todo Enable parallel when all is better. init_per_group(Name = enabled, Config) -> cowboy_test:init_http3(Name, #{ enable_connect_protocol => true, env => #{dispatch => cowboy_router:compile(init_routes(Config))} }, Config). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_routes(_) -> [ {"localhost", [ {"/ws", ws_echo, []} ]} ]. % The SETTINGS_ENABLE_CONNECT_PROTOCOL SETTINGS Parameter. % The new parameter name is SETTINGS_ENABLE_CONNECT_PROTOCOL. The % value of the parameter MUST be 0 or 1. % Upon receipt of SETTINGS_ENABLE_CONNECT_PROTOCOL with a value of 1 a % client MAY use the Extended CONNECT definition of this document when % creating new streams. Receipt of this parameter by a server does not % have any impact. %% @todo ignore_client_enable_setting(Config) -> reject_handshake_when_disabled(Config0) -> doc("Extended CONNECT requests MUST be rejected with a " "H3_MESSAGE_ERROR stream error when enable_connect_protocol=false. " "(RFC9220, RFC8441 4)"), Config = cowboy_test:init_http3(disabled, #{ enable_connect_protocol => false, env => #{dispatch => cowboy_router:compile(init_routes(Config0))} }, Config0), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0. #{ conn := Conn, settings := Settings } = rfc9114_SUITE:do_connect(Config), case Settings of #{enable_connect_protocol := false} -> ok; _ when map_size(Settings) =:= 0 -> ok end, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. reject_handshake_disabled_by_default(Config0) -> doc("Extended CONNECT requests MUST be rejected with a " "H3_MESSAGE_ERROR stream error when enable_connect_protocol=false. " "(RFC9220, RFC8441 4)"), Config = cowboy_test:init_http3(disabled, #{ env => #{dispatch => cowboy_router:compile(init_routes(Config0))} }, Config0), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 0. #{ conn := Conn, settings := Settings } = rfc9114_SUITE:do_connect(Config), case Settings of #{enable_connect_protocol := false} -> ok; _ when map_size(Settings) =:= 0 -> ok end, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. % The Extended CONNECT Method. accept_uppercase_pseudo_header_protocol(Config) -> doc("The :protocol pseudo header is case insensitive. (RFC9220, RFC8441 4, RFC9110 7.8)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"WEBSOCKET">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% Receive a 200 response. {ok, Data} = rfc9114_SUITE:do_receive_data(StreamRef), {HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data), << 1, %% HEADERS frame. HLenEnc:2, HLen:HLenBits, EncodedResponse:HLen/bytes >> = Data, {ok, DecodedResponse, _DecData, _DecSt} = cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)), #{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse), ok. reject_many_pseudo_header_protocol(Config) -> doc("An extended CONNECT request containing more than one " "protocol component must be rejected with a H3_MESSAGE_ERROR " "stream error. (RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with more than one :protocol pseudo-header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":protocol">>, <<"mqtt">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. reject_unknown_pseudo_header_protocol(Config) -> doc("An extended CONNECT request containing more than one " "protocol component must be rejected with a 501 Not Implemented " "response. (RFC9220, RFC8441 4)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with an unknown protocol. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"mqtt">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been rejected with a 501 Not Implemented. #{headers := #{<<":status">> := <<"501">>}} = rfc9114_SUITE:do_receive_response(StreamRef), ok. reject_invalid_pseudo_header_protocol(Config) -> doc("An extended CONNECT request with an invalid protocol " "component must be rejected with a 501 Not Implemented " "response. (RFC9220, RFC8441 4)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with an invalid protocol. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket mqtt">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been rejected with a 501 Not Implemented. #{headers := #{<<":status">> := <<"501">>}} = rfc9114_SUITE:do_receive_response(StreamRef), ok. reject_missing_pseudo_header_scheme(Config) -> doc("An extended CONNECT request whtout a scheme component " "must be rejected with a H3_MESSAGE_ERROR stream error. " "(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without a :scheme pseudo-header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. reject_missing_pseudo_header_path(Config) -> doc("An extended CONNECT request whtout a path component " "must be rejected with a H3_MESSAGE_ERROR stream error. " "(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without a :path pseudo-header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. % On requests bearing the :protocol pseudo-header, the :authority % pseudo-header field is interpreted according to Section 8.1.2.3 of % [RFC7540] instead of Section 8.3 of [RFC7540]. In particular the % server MUST not make a new TCP connection to the host and port % indicated by the :authority. reject_missing_pseudo_header_authority(Config) -> doc("An extended CONNECT request whtout an authority component " "must be rejected with a H3_MESSAGE_ERROR stream error. " "(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without an :authority pseudo-header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. % Using Extended CONNECT To Bootstrap The WebSocket Protocol. reject_missing_pseudo_header_protocol(Config) -> doc("An extended CONNECT request whtout a protocol component " "must be rejected with a H3_MESSAGE_ERROR stream error. " "(RFC9220, RFC9114 4.3.1, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request without a :protocol pseudo-header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. % The scheme of the Target URI [RFC7230] MUST be https for wss schemed % WebSockets. HTTP/3 does not provide support for ws schemed WebSockets. % The websocket URI is still used for proxy autoconfiguration. reject_connection_header(Config) -> doc("An extended CONNECT request with a connection header " "must be rejected with a H3_MESSAGE_ERROR stream error. " "(RFC9220, RFC8441 4, RFC9114 4.2, RFC9114 4.5, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with a connection header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"connection">>, <<"upgrade">>}, {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. reject_upgrade_header(Config) -> doc("An extended CONNECT request with a upgrade header " "must be rejected with a H3_MESSAGE_ERROR stream error. " "(RFC9220, RFC8441 4, RFC9114 4.2, RFC9114 4.5, RFC9114 4.1.2)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send an extended CONNECT request with a upgrade header. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"upgrade">>, <<"websocket">>}, {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% The stream should have been aborted. #{reason := h3_message_error} = rfc9114_SUITE:do_wait_stream_aborted(StreamRef), ok. % After successfully processing the opening handshake the peers should % proceed with The WebSocket Protocol [RFC6455] using the HTTP/2 stream % from the CONNECT transaction as if it were the TCP connection % referred to in [RFC6455]. The state of the WebSocket connection at % this point is OPEN as defined by [RFC6455], Section 4.1. %% @todo I'm guessing we should test for things like RST_STREAM, %% closing the connection and others? % Examples. accept_handshake_when_enabled(Config) -> doc("Confirm the example for Websocket over HTTP/3 works. (RFC9220, RFC8441 5.1)"), %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config), #{enable_connect_protocol := true} = Settings, %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ok, StreamRef} = quicer:start_stream(Conn, #{}), {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"https">>}, {<<":path">>, <<"/ws">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ], 0, cow_qpack:init(encoder)), {ok, _} = quicer:send(StreamRef, [ <<1>>, %% HEADERS frame. cow_http3:encode_int(iolist_size(EncodedRequest)), EncodedRequest ]), %% Receive a 200 response. {ok, Data} = rfc9114_SUITE:do_receive_data(StreamRef), {HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data), << 1, %% HEADERS frame. HLenEnc:2, HLen:HLenBits, EncodedResponse:HLen/bytes >> = Data, {ok, DecodedResponse, _DecData, _DecSt} = cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)), #{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse), %% Masked text hello echoed back clear by the server. Mask = 16#37fa213d, MaskedHello = ws_SUITE:do_mask(<<"Hello">>, Mask, <<>>), {ok, _} = quicer:send(StreamRef, cow_http3:data( <<1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary>>)), {ok, WsData} = rfc9114_SUITE:do_receive_data(StreamRef), << 0, %% DATA frame. 0:2, 7:6, %% Length (2 bytes header + "Hello"). 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" %% Websocket frame. >> = WsData, ok. %% Closing a Websocket stream. % The HTTP/3 stream closure is also analogous to the TCP connection % closure of [RFC6455]. Orderly TCP-level closures are represented % as a FIN bit on the stream (Section 4.4 of [HTTP/3]). RST exceptions % are represented with a stream error (Section 8 of [HTTP/3]) of type % H3_REQUEST_CANCELLED (Section 8.1 of [HTTP/3]). %% @todo client close frame with FIN %% @todo server close frame with FIN %% @todo client other frame with FIN %% @todo server other frame with FIN %% @todo client close connection ================================================ FILE: test/security_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(security_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). -import(cowboy_test, [raw_recv/3]). %% ct. all() -> cowboy_test:common_all(). groups() -> Tests = [nc_rand, nc_zero], H1Tests = [slowloris, slowloris_chunks], H2CTests = [ http2_cancel_flood, http2_data_dribble, http2_empty_frame_flooding_data, http2_empty_frame_flooding_headers_continuation, http2_empty_frame_flooding_push_promise, http2_infinite_continuations, http2_ping_flood, http2_reset_flood, http2_settings_flood, http2_zero_length_header_leak ], [ {http, [parallel], Tests ++ H1Tests}, {https, [parallel], Tests ++ H1Tests}, {h2, [parallel], Tests}, {h2c, [parallel], Tests ++ H2CTests}, {h3, [], Tests}, {http_compress, [parallel], Tests ++ H1Tests}, {https_compress, [parallel], Tests ++ H1Tests}, {h2_compress, [parallel], Tests}, {h2c_compress, [parallel], Tests ++ H2CTests}, {h3_compress, [], Tests} ]. init_per_suite(Config) -> ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"), Config. end_per_suite(Config) -> ct_helper:delete_static_dir(config(priv_dir, Config) ++ "/static"). init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Routes. init_dispatch(_) -> cowboy_router:compile([{"localhost", [ {"/", hello_h, []}, {"/echo/:key", echo_h, []}, {"/delay_hello", delay_hello_h, 1000}, {"/long_polling", long_polling_h, []}, {"/resp/:key[/:arg]", resp_h, []} ]}]). %% Tests. http2_cancel_flood(Config) -> doc("Confirm that Cowboy detects the rapid reset attack. (CVE-2023-44487)"), do_http2_cancel_flood(Config, 1, 500), do_http2_cancel_flood(Config, 10, 50), do_http2_cancel_flood(Config, 500, 1), ok. do_http2_cancel_flood(Config, NumStreamsPerBatch, NumBatches) -> {ok, Socket} = rfc7540_SUITE:do_handshake(Config), {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/delay_hello">>} ]), AllStreamIDs = lists:seq(1, NumBatches * NumStreamsPerBatch * 2, 2), _ = lists:foldl( fun (_BatchNumber, AvailableStreamIDs) -> %% Take a bunch of IDs from the available stream IDs. %% Send HEADERS for all these and then cancel them. {IDs, RemainingStreamIDs} = lists:split(NumStreamsPerBatch, AvailableStreamIDs), _ = gen_tcp:send(Socket, [cow_http2:headers(ID, fin, HeadersBlock) || ID <- IDs]), _ = gen_tcp:send(Socket, [<<4:24, 3:8, 0:8, ID:32, 8:32>> || ID <- IDs]), RemainingStreamIDs end, AllStreamIDs, lists:seq(1, NumBatches, 1)), %% When Cowboy detects a flood it must close the connection. case gen_tcp:recv(Socket, 17, 6000) of {ok, <<_:24, 7:8, 0:8, 0:32, _LastStreamId:32, 11:32>>} -> %% GOAWAY with error code 11 = ENHANCE_YOUR_CALM. ok; %% We also accept the connection being closed immediately, %% which may happen because we send the GOAWAY right before closing. {error, closed} -> ok end. http2_data_dribble(Config) -> doc("Request a very large response then update the window 1 byte at a time. (CVE-2019-9511)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Send a GET request for a very large response. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/resp/stream_body/loop">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a response with a few DATA frames draining the window. {ok, <>} = gen_tcp:recv(Socket, 9, 1000), {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), {ok, <<16384:24, 0:8, 0:8, 1:32, _:16384/unit:8>>} = gen_tcp:recv(Socket, 9 + 16384, 1000), {ok, <<16384:24, 0:8, 0:8, 1:32, _:16384/unit:8>>} = gen_tcp:recv(Socket, 9 + 16384, 1000), {ok, <<16384:24, 0:8, 0:8, 1:32, _:16384/unit:8>>} = gen_tcp:recv(Socket, 9 + 16384, 1000), {ok, <<16383:24, 0:8, 0:8, 1:32, _:16383/unit:8>>} = gen_tcp:recv(Socket, 9 + 16383, 1000), %% Send WINDOW_UPDATE frames with a value of 1. The server should %% not attempt to send data until the window is over a configurable threshold. ok = gen_tcp:send(Socket, [ cow_http2:window_update(1), cow_http2:window_update(1, 1) ]), {error, timeout} = gen_tcp:recv(Socket, 0, 1000), ok. http2_empty_frame_flooding_data(Config) -> doc("Confirm that Cowboy detects empty DATA frame flooding. (CVE-2019-9518)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Send a POST request followed by many empty DATA frames. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/echo/read_body">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), _ = [gen_tcp:send(Socket, cow_http2:data(1, nofin, <<>>)) || _ <- lists:seq(1, 20000)], %% When Cowboy detects a flood it must close the connection. %% We skip WINDOW_UPDATE frames sent when Cowboy starts to read the body. case gen_tcp:recv(Socket, 43, 6000) of {ok, <<_:26/unit:8, _:24, 7:8, _:72, 11:32>>} -> ok; %% We also accept the connection being closed immediately, %% which may happen because we send the GOAWAY right before closing. {error, closed} -> ok; %% At least on Windows this might also occur. {error, enotconn} -> ok end. http2_empty_frame_flooding_headers_continuation(Config) -> doc("Confirm that Cowboy detects empty HEADERS/CONTINUATION frame flooding. (CVE-2019-9518)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Send many empty HEADERS/CONTINUATION frames before the headers. ok = gen_tcp:send(Socket, <<0:24, 1:8, 0:9, 1:31>>), _ = [gen_tcp:send(Socket, <<0:24, 9:8, 0:9, 1:31>>) || _ <- lists:seq(1, 20000)], {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"POST">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), Len = iolist_size(HeadersBlock), _ = gen_tcp:send(Socket, [<>, HeadersBlock]), %% When Cowboy detects a flood it must close the connection. case gen_tcp:recv(Socket, 17, 6000) of {ok, <<_:24, 7:8, _:72, 11:32>>} -> ok; %% We also accept the connection being closed immediately, %% which may happen because we send the GOAWAY right before closing. {error, closed} -> ok; %% At least on Windows this might also occur. {error, enotconn} -> ok end. http2_empty_frame_flooding_push_promise(Config) -> doc("Confirm that Cowboy detects empty PUSH_PROMISE frame flooding. (CVE-2019-9518)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Send a HEADERS frame to which we will attach a PUSH_PROMISE. %% We use nofin in order to keep the stream alive. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/long_polling">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, HeadersBlock)), %% Send nofin PUSH_PROMISE frame without any data. ok = gen_tcp:send(Socket, <<4:24, 5:8, 0:8, 0:1, 1:31, 0:1, 3:31>>), %% Receive a PROTOCOL_ERROR connection error. %% %% Cowboy rejects all PUSH_PROMISE frames therefore no flooding %% can take place. {ok, <<_:24, 7:8, _:72, 1:32>>} = gen_tcp:recv(Socket, 17, 6000), ok. http2_infinite_continuations(Config) -> doc("Confirm that Cowboy rejects CONTINUATION frames when the " "total size of HEADERS + CONTINUATION(s) exceeds the limit. (VU#421644)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Send a HEADERS frame followed by a large number %% of continuation frames. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), HeadersBlockLen = iolist_size(HeadersBlock), ok = gen_tcp:send(Socket, [ %% HEADERS frame. << HeadersBlockLen:24, 1:8, 0:5, 0:1, %% END_HEADERS 0:1, 1:1, %% END_STREAM 0:1, 1:31 %% Stream ID. >>, HeadersBlock, %% CONTINUATION frames. [<<1024:24, 9:8, 0:8, 0:1, 1:31, 0:1024/unit:8>> || _ <- lists:seq(1, 100)] ]), %% Receive an ENHANCE_YOUR_CALM connection error. {ok, <<_:24, 7:8, _:72, 11:32>>} = gen_tcp:recv(Socket, 17, 6000), ok. %% @todo http2_internal_data_buffering(Config) -> I do not know how to test this. % doc("Request many very large responses, with a larger than necessary window size, " % "but do not attempt to read from the socket. (CVE-2019-9517)"), http2_ping_flood(Config) -> doc("Confirm that Cowboy detects PING floods. (CVE-2019-9512)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Flood the server with PING frames. _ = [gen_tcp:send(Socket, cow_http2:ping(0)) || _ <- lists:seq(1, 20000)], %% Receive a number of PING ACK frames in return, following by the closing of the connection. try [case gen_tcp:recv(Socket, 17, 6000) of {ok, <<8:24, 6:8, _:7, 1:1, _:32, 0:64>>} -> ok; {ok, <<_:24, 7:8, _:72, 11:32>>} -> throw(goaway); %% We also accept the connection being closed immediately, %% which may happen because we send the GOAWAY right before closing. {error, closed} -> throw(goaway) end || _ <- lists:seq(1, 20000)], error(flood_successful) catch throw:goaway -> ok end. http2_reset_flood(Config) -> doc("Confirm that Cowboy detects reset floods. (CVE-2019-9514)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Flood the server with HEADERS frames without a :method pseudo-header. {HeadersBlock, _} = cow_hpack:encode([ {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), _ = [gen_tcp:send(Socket, cow_http2:headers(ID, fin, HeadersBlock)) || ID <- lists:seq(1, 100, 2)], %% Receive a number of RST_STREAM frames in return, following by the closing of the connection. try [case gen_tcp:recv(Socket, 13, 6000) of {ok, <<_:24, 3:8, _:8, ID:32, 1:32>>} -> ok; {ok, <<_:24, 7:8, _:72>>} -> {ok, <<11:32>>} = gen_tcp:recv(Socket, 4, 1000), throw(goaway); %% We also accept the connection being closed immediately, %% which may happen because we send the GOAWAY right before closing. {error, closed} -> throw(goaway) end || ID <- lists:seq(1, 100, 2)], error(flood_successful) catch throw:goaway -> ok end. %% @todo If we ever implement the PRIORITY mechanism, this test should %% be implemented as well. CVE-2019-9513 https://www.kb.cert.org/vuls/id/605641/ %% http2_resource_loop http2_settings_flood(Config) -> doc("Confirm that Cowboy detects SETTINGS floods. (CVE-2019-9515)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Flood the server with empty SETTINGS frames. _ = [gen_tcp:send(Socket, cow_http2:settings(#{})) || _ <- lists:seq(1, 20000)], %% Receive a number of SETTINGS ACK frames in return, following by the closing of the connection. try [case gen_tcp:recv(Socket, 9, 6000) of {ok, <<0:24, 4:8, 0:7, 1:1, 0:32>>} -> ok; {ok, <<_:24, 7:8, _:40>>} -> {ok, <<_:32, 11:32>>} = gen_tcp:recv(Socket, 8, 1000), throw(goaway); %% We also accept the connection being closed immediately, %% which may happen because we send the GOAWAY right before closing. {error, closed} -> throw(goaway) end || _ <- lists:seq(1, 20000)], error(flood_successful) catch throw:goaway -> ok end. http2_zero_length_header_leak(Config) -> doc("Confirm that Cowboy rejects HEADERS frame with a 0-length header name. (CVE-2019-9516)"), {ok, Socket} = rfc7540_SUITE:do_handshake(Config), %% Send a GET request with a 0-length header name. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>}, {<<>>, <<"CVE-2019-9516">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a PROTOCOL_ERROR stream error. {ok, <<_:24, 3:8, _:8, 1:32, 1:32>>} = gen_tcp:recv(Socket, 13, 6000), ok. nc_rand(Config) -> doc("Throw random garbage at the server, then check if it's still up."), do_nc(Config, "/dev/urandom"). nc_zero(Config) -> doc("Throw zeroes at the server, then check if it's still up."), do_nc(Config, "/dev/zero"). do_nc(Config, Input) -> Cat = os:find_executable("cat"), Nc = os:find_executable("nc"), case {Cat, Nc} of {false, _} -> {skip, "The cat executable was not found."}; {_, false} -> {skip, "The nc executable was not found."}; _ -> StrPort = integer_to_list(config(port, Config)), _ = [ os:cmd("cat " ++ Input ++ " | nc localhost " ++ StrPort) || _ <- lists:seq(1, 100)], ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/"), {response, _, 200, _} = gun:await(ConnPid, Ref), ok end. slowloris(Config) -> doc("Send request headers one byte at a time. " "Confirm that the connection gets closed."), Client = raw_open(Config), try [begin ok = raw_send(Client, [C]), timer:sleep(250) end || C <- "GET / HTTP/1.1\r\nHost: localhost\r\n" "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US)\r\n" "Cookie: name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n"], error(failure) catch error:{badmatch, _} -> ok end. slowloris_chunks(Config) -> doc("Send request headers one line at a time. " "Confirm that the connection gets closed."), Client = raw_open(Config), ok = raw_send(Client, "GET / HTTP/1.1\r\n"), timer:sleep(300), ok = raw_send(Client, "Host: localhost\r\n"), timer:sleep(300), Data = raw_recv_head(Client), {'HTTP/1.1', 408, _, Rest} = cow_http:parse_status_line(Data), {Headers, _} = cow_http:parse_headers(Rest), {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, Headers), {error, closed} = raw_recv(Client, 0, 1000). ================================================ FILE: test/static_handler_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(static_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). %% Import useful functions from req_SUITE. %% @todo Maybe move these functions to cowboy_test. -import(req_SUITE, [do_get/2]). -import(req_SUITE, [do_get/3]). -import(req_SUITE, [do_maybe_h3_error3/1]). %% ct. all() -> cowboy_test:common_all() ++ [ {group, http_no_sendfile}, {group, h2c_no_sendfile} ]. groups() -> AllTests = ct_helper:all(?MODULE), %% The directory tests are shared between dir and priv_dir options. DirTests = lists:usort([F || {F, 1} <- ?MODULE:module_info(exports), string:substr(atom_to_list(F), 1, 4) =:= "dir_" ]), OtherTests = AllTests -- DirTests, GroupTests = OtherTests ++ [ {dir, [parallel], DirTests}, {priv_dir, [parallel], DirTests} ], GroupTestsNoParallel = OtherTests ++ [ {dir, [], DirTests}, {priv_dir, [], DirTests} ], [ {http, [parallel], GroupTests}, {https, [parallel], GroupTests}, {h2, [parallel], GroupTests}, {h2c, [parallel], GroupTests}, {h3, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better. {http_compress, [parallel], GroupTests}, {https_compress, [parallel], GroupTests}, {h2_compress, [parallel], GroupTests}, {h2c_compress, [parallel], GroupTests}, {h3_compress, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better. %% No real need to test sendfile disabled against https, h2 or h3. {http_no_sendfile, [parallel], GroupTests}, {h2c_no_sendfile, [parallel], GroupTests} ]. init_per_suite(Config) -> %% Two static folders are created: one in ct_helper's private directory, %% and one in the test run private directory. PrivDir = code:priv_dir(ct_helper) ++ "/static", StaticDir = config(priv_dir, Config) ++ "/static", ct_helper:create_static_dir(PrivDir), ct_helper:create_static_dir(StaticDir), init_large_file(PrivDir ++ "/large.bin"), init_large_file(StaticDir ++ "/large.bin"), %% Add a simple Erlang application archive containing one file %% in its priv directory. true = code:add_pathz(filename:join( [config(data_dir, Config), "static_files_app.ez", "static_files_app", "ebin"])), ok = application:load(static_files_app), %% A special folder contains files of 1 character from 1 to 127 %% excluding / and \ as they are always rejected. CharDir = config(priv_dir, Config) ++ "/char", ok = filelib:ensure_dir(CharDir ++ "/file"), Chars0 = lists:flatten([case file:write_file(CharDir ++ [$/, C], [C]) of ok -> C; {error, _} -> [] end || C <- (lists:seq(1, 127) -- "/\\")]), %% Determine whether we are on a case insensitive filesystem and %% remove uppercase characters in that case. On case insensitive %% filesystems we end up overwriting the "A" file with the "a" contents. {CaseSensitive, Chars} = case file:read_file(CharDir ++ "/A") of {ok, <<"A">>} -> {true, Chars0}; {ok, <<"a">>} -> {false, Chars0 -- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"} end, [{static_dir, StaticDir}, {char_dir, CharDir}, {chars, Chars}, {case_sensitive, CaseSensitive}|Config]. end_per_suite(Config) -> %% Special directory. CharDir = config(char_dir, Config), _ = [file:delete(CharDir ++ [$/, C]) || C <- lists:seq(0, 127)], _ = file:del_dir(CharDir), %% Static directories. StaticDir = config(static_dir, Config), PrivDir = code:priv_dir(ct_helper) ++ "/static", %% This file is not created on Windows. _ = file:delete(StaticDir ++ "/large.bin"), _ = file:delete(PrivDir ++ "/large.bin"), ct_helper:delete_static_dir(StaticDir), ct_helper:delete_static_dir(PrivDir). init_per_group(dir, Config) -> [{prefix, "/dir"}|Config]; init_per_group(priv_dir, Config) -> [{prefix, "/priv_dir"}|Config]; init_per_group(Name=http_no_sendfile, Config) -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch(Config)}, middlewares => [?MODULE, cowboy_router, cowboy_handler], sendfile => false }, [{flavor, vanilla}|Config]); init_per_group(Name=h2c_no_sendfile, Config) -> Config1 = cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch(Config)}, middlewares => [?MODULE, cowboy_router, cowboy_handler], sendfile => false }, [{flavor, vanilla}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name=h3, Config) -> cowboy_test:init_http3(Name, #{ env => #{dispatch => init_dispatch(Config)}, middlewares => [?MODULE, cowboy_router, cowboy_handler] }, [{flavor, vanilla}|Config]); init_per_group(Name=h3_compress, Config) -> cowboy_test:init_http3(Name, #{ env => #{dispatch => init_dispatch(Config)}, middlewares => [?MODULE, cowboy_router, cowboy_handler], stream_handlers => [cowboy_compress_h, cowboy_stream_h] }, [{flavor, vanilla}|Config]); init_per_group(Name, Config) -> Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE), Opts = ranch:get_protocol_options(Name), ok = ranch:set_protocol_options(Name, Opts#{ middlewares => [?MODULE, cowboy_router, cowboy_handler] }), Config1. end_per_group(dir, _) -> ok; end_per_group(priv_dir, _) -> ok; end_per_group(Name, _) -> cowboy_test:stop_group(Name). %% Large file. init_large_file(Filename) -> case os:type() of {unix, _} -> "" = os:cmd("truncate -s 32M " ++ Filename), ok; {win32, _} -> Size = 32*1024*1024, ok = file:write_file(Filename, <<0:Size/unit:8>>) end. %% Routes. init_dispatch(Config) -> cowboy_router:compile([{'_', [ {"/priv_dir/[...]", cowboy_static, {priv_dir, ct_helper, "static"}}, {"/dir/[...]", cowboy_static, {dir, config(static_dir, Config)}}, {"/priv_file/style.css", cowboy_static, {priv_file, ct_helper, "static/style.css"}}, {"/file/style.css", cowboy_static, {file, config(static_dir, Config) ++ "/style.css"}}, {"/index", cowboy_static, {file, config(static_dir, Config) ++ "/index.html"}}, {"/mime/all/[...]", cowboy_static, {priv_dir, ct_helper, "static", [{mimetypes, cow_mimetypes, all}]}}, {"/mime/custom/[...]", cowboy_static, {priv_dir, ct_helper, "static", [{mimetypes, ?MODULE, do_mime_custom}]}}, {"/mime/crash/[...]", cowboy_static, {priv_dir, ct_helper, "static", [{mimetypes, ?MODULE, do_mime_crash}]}}, {"/mime/hardcode/binary-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy", [{mimetypes, <<"application/vnd.ninenines.cowboy+xml;v=1">>}]}}, {"/mime/hardcode/tuple-form", cowboy_static, {priv_file, ct_helper, "static/file.cowboy", [{mimetypes, {<<"application">>, <<"vnd.ninenines.cowboy+xml">>, [{<<"v">>, <<"1">>}]}}]}}, {"/charset/custom/[...]", cowboy_static, {priv_dir, ct_helper, "static", [{charset, ?MODULE, do_charset_custom}]}}, {"/charset/crash/[...]", cowboy_static, {priv_dir, ct_helper, "static", [{charset, ?MODULE, do_charset_crash}]}}, {"/charset/hardcode/[...]", cowboy_static, {priv_file, ct_helper, "static/index.html", [{charset, <<"utf-8">>}]}}, {"/etag/custom", cowboy_static, {file, config(static_dir, Config) ++ "/style.css", [{etag, ?MODULE, do_etag_custom}]}}, {"/etag/crash", cowboy_static, {file, config(static_dir, Config) ++ "/style.css", [{etag, ?MODULE, do_etag_crash}]}}, {"/etag/disable", cowboy_static, {file, config(static_dir, Config) ++ "/style.css", [{etag, false}]}}, {"/bad", cowboy_static, bad}, {"/bad/priv_dir/app/[...]", cowboy_static, {priv_dir, bad_app, "static"}}, {"/bad/priv_dir/no-priv/[...]", cowboy_static, {priv_dir, cowboy, "static"}}, {"/bad/priv_dir/path/[...]", cowboy_static, {priv_dir, ct_helper, "bad"}}, {"/bad/priv_dir/route", cowboy_static, {priv_dir, ct_helper, "static"}}, {"/bad/dir/path/[...]", cowboy_static, {dir, "/bad/path"}}, {"/bad/dir/route", cowboy_static, {dir, config(static_dir, Config)}}, {"/bad/priv_file/app", cowboy_static, {priv_file, bad_app, "static/style.css"}}, {"/bad/priv_file/no-priv", cowboy_static, {priv_file, cowboy, "static/style.css"}}, {"/bad/priv_file/path", cowboy_static, {priv_file, ct_helper, "bad/style.css"}}, {"/bad/file/path", cowboy_static, {file, "/bad/path/style.css"}}, {"/bad/options", cowboy_static, {priv_file, ct_helper, "static/style.css", bad}}, {"/bad/options/mime", cowboy_static, {priv_file, ct_helper, "static/style.css", [{mimetypes, bad}]}}, {"/bad/options/charset", cowboy_static, {priv_file, ct_helper, "static/style.css", [{charset, bad}]}}, {"/bad/options/etag", cowboy_static, {priv_file, ct_helper, "static/style.css", [{etag, true}]}}, {"/unknown/option", cowboy_static, {priv_file, ct_helper, "static/style.css", [{bad, option}]}}, {"/char/[...]", cowboy_static, {dir, config(char_dir, Config)}}, {"/ez_priv_file/index.html", cowboy_static, {priv_file, static_files_app, "www/index.html"}}, {"/bad/ez_priv_file/index.php", cowboy_static, {priv_file, static_files_app, "www/index.php"}}, {"/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "www"}}, {"/bad/ez_priv_dir/[...]", cowboy_static, {priv_dir, static_files_app, "cgi-bin"}} ]}]). %% Middleware interface to silence expected errors. execute(Req=#{path := Path}, Env) -> case Path of <<"/bad/priv_dir/app/", _/bits>> -> ct_helper:ignore(cowboy_static, priv_path, 2); <<"/bad/priv_file/app">> -> ct_helper:ignore(cowboy_static, priv_path, 2); <<"/bad/priv_dir/route">> -> ct_helper:ignore(cowboy_static, escape_reserved, 1); <<"/bad/dir/route">> -> ct_helper:ignore(cowboy_static, escape_reserved, 1); <<"/bad">> -> ct_helper:ignore(cowboy_static, init_opts, 2); <<"/bad/options">> -> ct_helper:ignore(cowboy_static, content_types_provided, 2); <<"/bad/options/mime">> -> ct_helper:ignore(cowboy_rest, normalize_content_types, 2); <<"/bad/options/etag">> -> ct_helper:ignore(cowboy_static, generate_etag, 2); <<"/bad/options/charset">> -> ct_helper:ignore(cowboy_static, charsets_provided, 2); _ -> ok end, {ok, Req, Env}. %% Internal functions. -spec do_charset_crash(_) -> no_return(). do_charset_crash(_) -> ct_helper_error_h:ignore(?MODULE, do_charset_crash, 1), exit(crash). do_charset_custom(Path) -> case filename:extension(Path) of <<".cowboy">> -> <<"utf-32">>; <<".html">> -> <<"utf-16">>; _ -> <<"utf-8">> end. -spec do_etag_crash(_, _, _) -> no_return(). do_etag_crash(_, _, _) -> ct_helper_error_h:ignore(?MODULE, do_etag_crash, 3), exit(crash). do_etag_custom(_, _, _) -> {strong, <<"etag">>}. -spec do_mime_crash(_) -> no_return(). do_mime_crash(_) -> ct_helper_error_h:ignore(?MODULE, do_mime_crash, 1), exit(crash). do_mime_custom(Path) -> case filename:extension(Path) of <<".cowboy">> -> <<"application/vnd.ninenines.cowboy+xml;v=1">>; <<".txt">> -> <<"text/plain">>; _ -> {<<"application">>, <<"octet-stream">>, []} end. %% Tests. bad(Config) -> doc("Bad cowboy_static options: not a tuple."), {500, _, _} = do_maybe_h3_error3(do_get("/bad", Config)), ok. bad_dir_path(Config) -> doc("Bad cowboy_static options: wrong path."), {404, _, _} = do_get("/bad/dir/path/style.css", Config), ok. bad_dir_route(Config) -> doc("Bad cowboy_static options: missing [...] in route."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/dir/route", Config)), ok. bad_file_in_priv_dir_in_ez_archive(Config) -> doc("Get a missing file from a priv_dir stored in Erlang application .ez archive."), {404, _, _} = do_get("/ez_priv_dir/index.php", Config), ok. bad_file_path(Config) -> doc("Bad cowboy_static options: wrong path."), {404, _, _} = do_get("/bad/file/path", Config), ok. bad_options(Config) -> doc("Bad cowboy_static extra options: not a list."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options", Config)), ok. bad_options_charset(Config) -> doc("Bad cowboy_static extra options: invalid charset option."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options/charset", Config)), ok. bad_options_etag(Config) -> doc("Bad cowboy_static extra options: invalid etag option."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options/etag", Config)), ok. bad_options_mime(Config) -> doc("Bad cowboy_static extra options: invalid mimetypes option."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options/mime", Config)), ok. bad_priv_dir_app(Config) -> doc("Bad cowboy_static options: wrong application name."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_dir/app/style.css", Config)), ok. bad_priv_dir_in_ez_archive(Config) -> doc("Bad cowboy_static options: priv_dir path missing from Erlang application .ez archive."), {404, _, _} = do_get("/bad/ez_priv_dir/index.html", Config), ok. bad_priv_dir_no_priv(Config) -> doc("Bad cowboy_static options: application has no priv directory."), {404, _, _} = do_get("/bad/priv_dir/no-priv/style.css", Config), ok. bad_priv_dir_path(Config) -> doc("Bad cowboy_static options: wrong path."), {404, _, _} = do_get("/bad/priv_dir/path/style.css", Config), ok. bad_priv_dir_route(Config) -> doc("Bad cowboy_static options: missing [...] in route."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_dir/route", Config)), ok. bad_priv_file_app(Config) -> doc("Bad cowboy_static options: wrong application name."), {500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_file/app", Config)), ok. bad_priv_file_in_ez_archive(Config) -> doc("Bad cowboy_static options: priv_file path missing from Erlang application .ez archive."), {404, _, _} = do_get("/bad/ez_priv_file/index.php", Config), ok. bad_priv_file_no_priv(Config) -> doc("Bad cowboy_static options: application has no priv directory."), {404, _, _} = do_get("/bad/priv_file/no-priv", Config), ok. bad_priv_file_path(Config) -> doc("Bad cowboy_static options: wrong path."), {404, _, _} = do_get("/bad/priv_file/path", Config), ok. dir_cowboy(Config) -> doc("Get a .cowboy file."), {200, Headers, <<"File with custom extension.\n">>} = do_get(config(prefix, Config) ++ "/file.cowboy", Config), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. dir_css(Config) -> doc("Get a .css file."), {200, Headers, <<"body{color:red}\n">>} = do_get(config(prefix, Config) ++ "/style.css", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. dir_css_urlencoded(Config) -> doc("Get a .css file with the extension dot urlencoded."), {200, Headers, <<"body{color:red}\n">>} = do_get(config(prefix, Config) ++ "/style%2ecss", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. dir_dot_file(Config) -> doc("Get a file with extra dot segments in the path."), %% All these are equivalent. {200, _, _} = do_get(config(prefix, Config) ++ "/./style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/././style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/./././style.css", Config), {200, _, _} = do_get("/./priv_dir/style.css", Config), {200, _, _} = do_get("/././priv_dir/style.css", Config), {200, _, _} = do_get("/./././priv_dir/style.css", Config), ok. dir_dotdot_file(Config) -> doc("Get a file with extra dotdot segments in the path."), %% All these are equivalent. {200, _, _} = do_get("/../priv_dir/style.css", Config), {200, _, _} = do_get("/../../priv_dir/style.css", Config), {200, _, _} = do_get("/../../../priv_dir/style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/../priv_dir/style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/../../priv_dir/style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/../../../priv_dir/style.css", Config), {200, _, _} = do_get("/../priv_dir/../priv_dir/style.css", Config), {200, _, _} = do_get("/../../priv_dir/../../priv_dir/style.css", Config), {200, _, _} = do_get("/../../../priv_dir/../../../priv_dir/style.css", Config), %% Try with non-existing segments, which may correspond to real folders. {200, _, _} = do_get("/anything/../priv_dir/style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/anything/../style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/directory/../style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/static/../style.css", Config), %% Try with segments corresponding to real files. It works because %% URI normalization happens before looking at the filesystem. {200, _, _} = do_get(config(prefix, Config) ++ "/style.css/../style.css", Config), {200, _, _} = do_get(config(prefix, Config) ++ "/style.css/../../priv_dir/style.css", Config), %% Try to fool the server to accept segments corresponding to real folders. {404, _, _} = do_get(config(prefix, Config) ++ "/../static/style.css", Config), {404, _, _} = do_get(config(prefix, Config) ++ "/directory/../../static/style.css", Config), ok. dir_empty_file(Config) -> doc("Get an empty .txt file."), {200, _, <<>>} = do_get(config(prefix, Config) ++ "/empty.txt", Config), ok. dir_error_directory(Config) -> doc("Try to get a directory."), {403, _, _} = do_get(config(prefix, Config) ++ "/directory", Config), ok. dir_error_directory_slash(Config) -> doc("Try to get a directory with an extra slash in the path."), {403, _, _} = do_get(config(prefix, Config) ++ "/directory/", Config), ok. dir_error_doesnt_exist(Config) -> doc("Try to get a file that does not exist."), {404, Headers, _} = do_get(config(prefix, Config) ++ "/not.found", Config), false = lists:keyfind(<<"content-type">>, 1, Headers), ok. dir_error_dot(Config) -> doc("Try to get a file named '.'."), {403, _, _} = do_get(config(prefix, Config) ++ "/.", Config), ok. dir_error_dot_urlencoded(Config) -> doc("Try to get a file named '.' percent encoded."), {403, _, _} = do_get(config(prefix, Config) ++ "/%2e", Config), ok. dir_error_dotdot(Config) -> doc("Try to get a file named '..'."), {404, _, _} = do_get(config(prefix, Config) ++ "/..", Config), ok. dir_error_dotdot_urlencoded(Config) -> doc("Try to get a file named '..' percent encoded."), {404, _, _} = do_get(config(prefix, Config) ++ "/%2e%2e", Config), ok. dir_error_empty(Config) -> doc("Try to get the configured directory."), {403, _, _} = do_get(config(prefix, Config) ++ "", Config), ok. dir_error_slash(Config) -> %% I know the description isn't that good considering / has a meaning in URIs. doc("Try to get a file named '/'."), {403, _, _} = do_get(config(prefix, Config) ++ "//", Config), ok. dir_error_reserved_urlencoded(Config) -> doc("Try to get a file named '/' or '\\' or 'NUL' percent encoded."), {400, _, _} = do_get(config(prefix, Config) ++ "/%2f", Config), {400, _, _} = do_get(config(prefix, Config) ++ "/%5c", Config), {400, _, _} = do_get(config(prefix, Config) ++ "/%00", Config), ok. dir_error_slash_urlencoded_dotdot_file(Config) -> doc("Try to use a percent encoded slash to access an existing file."), {200, _, _} = do_get(config(prefix, Config) ++ "/directory/../style.css", Config), {400, _, _} = do_get(config(prefix, Config) ++ "/directory%2f../style.css", Config), ok. dir_error_unreadable(Config) -> case os:type() of {win32, _} -> {skip, "ACL not enabled by default under MSYS2."}; {unix, _} -> doc("Try to get a file that can't be read."), {403, _, _} = do_get(config(prefix, Config) ++ "/unreadable", Config), ok end. dir_html(Config) -> doc("Get a .html file."), {200, Headers, <<"Hello!\n">>} = do_get(config(prefix, Config) ++ "/index.html", Config), {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. dir_large_file(Config) -> doc("Get a large file."), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, config(prefix, Config) ++ "/large.bin", [{<<"accept-encoding">>, <<"gzip">>}]), {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, RespHeaders), Size = 32*1024*1024, {ok, Size} = do_dir_large_file(ConnPid, Ref, 0), ok. do_dir_large_file(ConnPid, Ref, N) -> receive {gun_data, ConnPid, Ref, nofin, Data} -> do_dir_large_file(ConnPid, Ref, N + byte_size(Data)); {gun_data, ConnPid, Ref, fin, Data} -> {ok, N + byte_size(Data)}; {gun_error, ConnPid, Ref, Reason} -> {error, Reason}; {gun_error, ConnPid, Reason} -> {error, Reason} after 5000 -> {error, timeout} end. dir_text(Config) -> doc("Get a .txt file. The extension is unknown by default."), {200, Headers, <<"Timeless space.\n">>} = do_get(config(prefix, Config) ++ "/plain.txt", Config), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. dir_unknown(Config) -> doc("Get a file with no extension."), {200, Headers, <<"File with no extension.\n">>} = do_get(config(prefix, Config) ++ "/unknown", Config), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. etag_crash(Config) -> doc("Get a file with a crashing etag function."), {500, _, _} = do_maybe_h3_error3(do_get("/etag/crash", Config)), ok. etag_custom(Config) -> doc("Get a file with custom Etag function and make sure it is used."), {200, Headers, _} = do_get("/etag/custom", Config), {_, <<"\"etag\"">>} = lists:keyfind(<<"etag">>, 1, Headers), ok. etag_default(Config) -> doc("Get a file twice and make sure the Etag matches."), {200, Headers1, _} = do_get("/dir/style.css", Config), {200, Headers2, _} = do_get("/dir/style.css", Config), {_, Etag} = lists:keyfind(<<"etag">>, 1, Headers1), {_, Etag} = lists:keyfind(<<"etag">>, 1, Headers2), ok. etag_default_change(Config) -> doc("Get a file, modify it, get it again and make sure the Etag doesn't match."), %% We set the file to the current time first, then to a time in the past. ok = file:change_time(config(static_dir, Config) ++ "/index.html", calendar:universal_time()), {200, Headers1, _} = do_get("/dir/index.html", Config), {_, Etag1} = lists:keyfind(<<"etag">>, 1, Headers1), ok = file:change_time(config(static_dir, Config) ++ "/index.html", {{2019, 1, 1}, {1, 1, 1}}), {200, Headers2, _} = do_get("/dir/index.html", Config), {_, Etag2} = lists:keyfind(<<"etag">>, 1, Headers2), true = Etag1 =/= Etag2, ok. etag_disable(Config) -> doc("Get a file with disabled Etag and make sure no Etag is provided."), {200, Headers, _} = do_get("/etag/disable", Config), false = lists:keyfind(<<"etag">>, 1, Headers), ok. file(Config) -> doc("Get a file with hardcoded route."), {200, Headers, <<"body{color:red}\n">>} = do_get("/file/style.css", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. if_match(Config) -> doc("Get a file with If-Match matching."), {200, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"\"etag\"">>} ], Config), ok. if_match_fail(Config) -> doc("Get a file with If-Match not matching."), {412, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"\"invalid\"">>} ], Config), ok. if_match_invalid(Config) -> doc("Try to get a file with an invalid If-Match header."), {400, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"bad input">>} ], Config), ok. if_match_list(Config) -> doc("Get a file with If-Match matching."), {200, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"\"invalid\", \"etag\", \"cowboy\"">>} ], Config), ok. if_match_list_fail(Config) -> doc("Get a file with If-Match not matching."), {412, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"\"invalid\", W/\"etag\", \"cowboy\"">>} ], Config), ok. if_match_weak(Config) -> doc("Try to get a file with a weak If-Match header."), {412, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"W/\"etag\"">>} ], Config), ok. if_match_wildcard(Config) -> doc("Get a file with a wildcard If-Match."), {200, _, _} = do_get("/etag/custom", [ {<<"if-match">>, <<"*">>} ], Config), ok. if_modified_since(Config) -> doc("Get a file with If-Modified-Since in the past."), {200, _, _} = do_get("/etag/custom", [ {<<"if-modified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>} ], Config), ok. if_modified_since_fail(Config) -> doc("Get a file with If-Modified-Since equal to file modification time."), LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"), {304, _, _} = do_get("/etag/custom", [ {<<"if-modified-since">>, httpd_util:rfc1123_date(LastModified)} ], Config), ok. if_modified_since_future(Config) -> doc("Get a file with If-Modified-Since in the future."), {{Year, _, _}, {_, _, _}} = calendar:universal_time(), {200, _, _} = do_get("/etag/custom", [ {<<"if-modified-since">>, [ <<"Sat, 29 Oct ">>, integer_to_binary(Year + 1), <<" 19:43:31 GMT">>]} ], Config), ok. if_modified_since_if_none_match(Config) -> doc("Get a file with both If-Modified-Since and If-None-Match headers." "If-None-Match takes precedence and If-Modified-Since is ignored. (RFC7232 3.3)"), LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"), {200, _, _} = do_get("/etag/custom", [ {<<"if-modified-since">>, httpd_util:rfc1123_date(LastModified)}, {<<"if-none-match">>, <<"\"not-etag\"">>} ], Config), ok. if_modified_since_invalid(Config) -> doc("Get a file with an invalid If-Modified-Since header."), {200, _, _} = do_get("/etag/custom", [ {<<"if-modified-since">>, <<"\"not a date\"">>} ], Config), ok. if_none_match(Config) -> doc("Get a file with If-None-Match not matching."), {200, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"\"not-etag\"">>} ], Config), ok. if_none_match_fail(Config) -> doc("Get a file with If-None-Match matching."), {304, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"\"etag\"">>} ], Config), ok. if_none_match_invalid(Config) -> doc("Try to get a file with an invalid If-None-Match header."), {400, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"bad input">>} ], Config), ok. if_none_match_list(Config) -> doc("Get a file with If-None-Match not matching."), {200, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"\"invalid\", W/\"not-etag\", \"cowboy\"">>} ], Config), ok. if_none_match_list_fail(Config) -> doc("Get a file with If-None-Match matching."), {304, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"\"invalid\", \"etag\", \"cowboy\"">>} ], Config), ok. if_none_match_weak(Config) -> doc("Try to get a file with a weak If-None-Match header matching."), {304, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"W/\"etag\"">>} ], Config), ok. if_none_match_wildcard(Config) -> doc("Try to get a file with a wildcard If-None-Match."), {304, _, _} = do_get("/etag/custom", [ {<<"if-none-match">>, <<"*">>} ], Config), ok. if_unmodified_since(Config) -> doc("Get a file with If-Unmodified-Since equal to file modification time."), LastModified = filelib:last_modified(config(static_dir, Config) ++ "/style.css"), {200, _, _} = do_get("/etag/custom", [ {<<"if-unmodified-since">>, httpd_util:rfc1123_date(LastModified)} ], Config), ok. if_unmodified_since_fail(Config) -> doc("Get a file with If-Unmodified-Since in the past."), {412, _, _} = do_get("/etag/custom", [ {<<"if-unmodified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>} ], Config), ok. if_unmodified_since_future(Config) -> doc("Get a file with If-Unmodified-Since in the future."), {{Year, _, _}, {_, _, _}} = calendar:universal_time(), {200, _, _} = do_get("/etag/custom", [ {<<"if-unmodified-since">>, [ <<"Sat, 29 Oct ">>, integer_to_binary(Year + 1), <<" 19:43:31 GMT">>]} ], Config), ok. if_unmodified_since_if_match(Config) -> doc("Get a file with both If-Unmodified-Since and If-Match headers." "If-Match takes precedence and If-Unmodified-Since is ignored. (RFC7232 3.4)"), {200, _, _} = do_get("/etag/custom", [ {<<"if-unmodified-since">>, <<"Sat, 29 Oct 1994 19:43:31 GMT">>}, {<<"if-match">>, <<"\"etag\"">>} ], Config), ok. if_unmodified_since_invalid(Config) -> doc("Get a file with an invalid If-Unmodified-Since header."), {200, _, _} = do_get("/etag/custom", [ {<<"if-unmodified-since">>, <<"\"not a date\"">>} ], Config), ok. index_file(Config) -> doc("Get an index file."), {200, Headers, <<"Hello!\n">>} = do_get("/index", Config), {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. index_file_slash(Config) -> doc("Get an index file with extra slash."), {200, Headers, <<"Hello!\n">>} = do_get("/index/", Config), {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. last_modified(Config) -> doc("Get a file, modify it, get it again and make sure Last-Modified changes."), %% We set the file to the current time first, then to a time in the past. ok = file:change_time(config(static_dir, Config) ++ "/file.cowboy", calendar:universal_time()), {200, Headers1, _} = do_get("/dir/file.cowboy", Config), {_, LastModified1} = lists:keyfind(<<"last-modified">>, 1, Headers1), ok = file:change_time(config(static_dir, Config) ++ "/file.cowboy", {{2019, 1, 1}, {1, 1, 1}}), {200, Headers2, _} = do_get("/dir/file.cowboy", Config), {_, LastModified2} = lists:keyfind(<<"last-modified">>, 1, Headers2), true = LastModified1 =/= LastModified2, ok. mime_all_cowboy(Config) -> doc("Get a .cowboy file. The extension is unknown."), {200, Headers, _} = do_get("/mime/all/file.cowboy", Config), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_all_css(Config) -> doc("Get a .css file."), {200, Headers, _} = do_get("/mime/all/style.css", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_all_txt(Config) -> doc("Get a .txt file."), {200, Headers, _} = do_get("/mime/all/plain.txt", Config), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_all_uppercase(Config) -> doc("Get an uppercase .TXT file."), {200, Headers, _} = do_get("/mime/all/UPPER.TXT", Config), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_crash(Config) -> doc("Get a file with a crashing mimetype function."), {500, _, _} = do_maybe_h3_error3(do_get("/mime/crash/style.css", Config)), ok. mime_custom_cowboy(Config) -> doc("Get a .cowboy file."), {200, Headers, _} = do_get("/mime/custom/file.cowboy", Config), {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_custom_css(Config) -> doc("Get a .css file. The extension is unknown."), {200, Headers, _} = do_get("/mime/custom/style.css", Config), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_custom_txt(Config) -> doc("Get a .txt file."), {200, Headers, _} = do_get("/mime/custom/plain.txt", Config), {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_hardcode_binary(Config) -> doc("Get a .cowboy file with hardcoded route and media type in binary form."), {200, Headers, _} = do_get("/mime/hardcode/binary-form", Config), {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. mime_hardcode_tuple(Config) -> doc("Get a .cowboy file with hardcoded route and media type in tuple form."), {200, Headers, _} = do_get("/mime/hardcode/tuple-form", Config), {_, <<"application/vnd.ninenines.cowboy+xml;v=1">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_crash(Config) -> doc("Get a file with a crashing charset function."), {500, _, _} = do_maybe_h3_error3(do_get("/charset/crash/style.css", Config)), ok. charset_custom_cowboy(Config) -> doc("Get a .cowboy file."), {200, Headers, _} = do_get("/charset/custom/file.cowboy", Config), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_custom_css(Config) -> doc("Get a .css file."), {200, Headers, _} = do_get("/charset/custom/style.css", Config), {_, <<"text/css; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_custom_html(Config) -> doc("Get a .html file."), {200, Headers, _} = do_get("/charset/custom/index.html", Config), {_, <<"text/html; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. charset_hardcode_binary(Config) -> doc("Get a .html file with hardcoded route and charset."), {200, Headers, _} = do_get("/charset/hardcode", Config), {_, <<"text/html; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. priv_dir_in_ez_archive(Config) -> doc("Get a file from a priv_dir stored in Erlang application .ez archive."), {200, Headers, <<"

It works!

\n">>} = do_get("/ez_priv_dir/index.html", Config), {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. priv_file(Config) -> doc("Get a file with hardcoded route."), {200, Headers, <<"body{color:red}\n">>} = do_get("/priv_file/style.css", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. priv_file_in_ez_archive(Config) -> doc("Get a file stored in Erlang application .ez archive."), {200, Headers, <<"

It works!

\n">>} = do_get("/ez_priv_file/index.html", Config), {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. range_request(Config) -> doc("Confirm that range requests are enabled."), {206, Headers, <<"less space.\n">>} = do_get("/dir/plain.txt", [{<<"range">>, <<"bytes=4-">>}], Config), {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), {_, <<"bytes 4-15/16">>} = lists:keyfind(<<"content-range">>, 1, Headers), {_, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. unicode_basic_latin(Config) -> doc("Get a file with non-urlencoded characters from Unicode Basic Latin block."), %% Excluding the dot which has a special meaning in URLs %% when they are the only content in a path segment, %% and is tested as part of filenames in other test cases. Chars0 = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" ":@-_~!$&'()*+,;=", Chars1 = case config(case_sensitive, Config) of false -> Chars0 -- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; true -> Chars0 end, %% Remove the characters for which we have no corresponding file. Chars = Chars1 -- (Chars1 -- config(chars, Config)), _ = [case do_get("/char/" ++ [C], Config) of {200, _, << C >>} -> ok; Error -> exit({error, C, Error}) end || C <- Chars], ok. unicode_basic_error(Config) -> doc("Try to get a file with invalid non-urlencoded characters from Unicode Basic Latin block."), Exclude = case config(protocol, Config) of %% Some characters trigger different errors in HTTP/1.1 %% because they are used for the protocol. %% %% # and ? indicate fragment and query components %% and are therefore not part of the path. http -> "\r\s#?"; http2 -> "#?"; http3 -> "#?" end, _ = [case do_get("/char/" ++ [C], Config) of {400, _, _} -> ok; Error -> exit({error, C, Error}) end || C <- (config(chars, Config) -- Exclude) -- "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" ":@-_~!$&'()*+,;=" ], ok. unicode_basic_latin_urlencoded(Config) -> doc("Get a file with urlencoded characters from Unicode Basic Latin block."), _ = [case do_get(lists:flatten(["/char/%", io_lib:format("~2.16.0b", [C])]), Config) of {200, _, << C >>} -> ok; Error -> exit({error, C, Error}) end || C <- config(chars, Config)], ok. unknown_option(Config) -> doc("Get a file configured with unknown extra options."), {200, Headers, <<"body{color:red}\n">>} = do_get("/unknown/option", Config), {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), ok. ================================================ FILE: test/stream_handler_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(stream_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_down/1]). %% ct. all() -> cowboy_test:common_all(). groups() -> cowboy_test:common_groups(ct_helper:all(?MODULE)). %% We set this module as a logger in order to silence expected errors. init_per_group(Name = http, Config) -> cowboy_test:init_http(Name, init_plain_opts(), Config); init_per_group(Name = https, Config) -> cowboy_test:init_https(Name, init_plain_opts(), Config); init_per_group(Name = h2, Config) -> cowboy_test:init_http2(Name, init_plain_opts(), Config); init_per_group(Name = h2c, Config) -> Config1 = cowboy_test:init_http(Name, init_plain_opts(), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = h3, Config) -> cowboy_test:init_http3(Name, init_plain_opts(), Config); init_per_group(Name = http_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(), Config); init_per_group(Name = https_compress, Config) -> cowboy_test:init_https(Name, init_compress_opts(), Config); init_per_group(Name = h2_compress, Config) -> cowboy_test:init_http2(Name, init_compress_opts(), Config); init_per_group(Name = h2c_compress, Config) -> Config1 = cowboy_test:init_http(Name, init_compress_opts(), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = h3_compress, Config) -> cowboy_test:init_http3(Name, init_compress_opts(), Config). end_per_group(Name, _) -> cowboy_test:stop_group(Name). init_plain_opts() -> #{ logger => ?MODULE, stream_handlers => [stream_handler_h] }. init_compress_opts() -> #{ logger => ?MODULE, stream_handlers => [cowboy_compress_h, stream_handler_h] }. %% Logger function silencing the expected crashes. error("Unhandled exception " ++ _, [error, crash|_]) -> ok; error(Format, Args) -> error_logger:error_msg(Format, Args). %% Tests. crash_in_init(Config) -> doc("Confirm an error is sent when a stream handler crashes in init/3."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"crash_in_init">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Confirm terminate/3 is NOT called. We have no state to give to it. receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end, %% Confirm early_error/5 is called in HTTP/1.1's case. %% HTTP/2 and HTTP/3 do not send a response back so there is no early_error call. case config(protocol, Config) of http -> receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end; http2 -> ok; http3 -> ok end, do_await_internal_error(ConnPid, Ref, Config). do_await_internal_error(ConnPid, Ref, Config) -> Protocol = config(protocol, Config), case {Protocol, gun:await(ConnPid, Ref)} of {http, {response, fin, 500, _}} -> ok; {http2, {error, {stream_error, {stream_error, internal_error, _}}}} -> ok; {http3, {error, {stream_error, {stream_error, h3_internal_error, _}}}} -> ok end. crash_in_data(Config) -> doc("Confirm an error is sent when a stream handler crashes in data/4."), Self = self(), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"content-length">>, <<"6">>}, {<<"x-test-case">>, <<"crash_in_data">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Send data to make the stream handler crash. gun:data(ConnPid, Ref, fin, <<"Hello!">>), %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, do_await_internal_error(ConnPid, Ref, Config). crash_in_info(Config) -> doc("Confirm an error is sent when a stream handler crashes in info/3."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"crash_in_info">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Send a message to make the stream handler crash. StreamID = case config(protocol, Config) of http3 -> 0; _ -> 1 end, Pid ! {{Pid, StreamID}, crash}, %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, do_await_internal_error(ConnPid, Ref, Config). crash_in_terminate(Config) -> doc("Confirm the state is correct when a stream handler crashes in terminate/3."), Self = self(), ConnPid = gun_open(Config), %% Do a first request. Ref1 = gun:get(ConnPid, "/hello_world", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"crash_in_terminate">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Confirm terminate/3 is called. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% Receive the response. {response, nofin, 200, _} = gun:await(ConnPid, Ref1), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref1), %% Do a second request to make sure the connection state is still good. Ref2 = gun:get(ConnPid, "/hello_world", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"crash_in_terminate">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. The pid shouldn't change. receive {Self, Pid, init, _, _, _} -> ok after 1000 -> error(timeout) end, %% Confirm terminate/3 is called. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% Receive the second response. {response, nofin, 200, _} = gun:await(ConnPid, Ref2), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref2), ok. %% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests. crash_in_early_error(Config) -> case config(protocol, Config) of http -> do_crash_in_early_error(Config); http2 -> doc("The callback early_error/5 is not currently used for HTTP/2."); http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.") end. do_crash_in_early_error(Config) -> doc("Confirm an error is sent when a stream handler crashes in early_error/5." "The connection is kept open by Cowboy."), Self = self(), ConnPid = gun_open(Config), Ref1 = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"crash_in_early_error">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Confirm terminate/3 is NOT called. We have no state to give to it. receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end, %% Confirm early_error/5 is called. receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end, %% Receive a 500 error response. {response, fin, 500, _} = gun:await(ConnPid, Ref1), %% This error is not fatal. We should be able to repeat it on the same connection. Ref2 = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"crash_in_early_error">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. receive {Self, Pid, init, _, _, _} -> ok after 1000 -> error(timeout) end, %% Confirm terminate/3 is NOT called. We have no state to give to it. receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end, %% Confirm early_error/5 is called. receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end, %% Receive a 500 error response. {response, fin, 500, _} = gun:await(ConnPid, Ref2), ok. %% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests. crash_in_early_error_fatal(Config) -> case config(protocol, Config) of http -> do_crash_in_early_error_fatal(Config); http2 -> doc("The callback early_error/5 is not currently used for HTTP/2."); http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.") end. do_crash_in_early_error_fatal(Config) -> doc("Confirm an error is sent when a stream handler crashes in early_error/5." "The error was fatal and the connection is closed by Cowboy."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"host">>, <<"host:port">>}, {<<"x-test-case">>, <<"crash_in_early_error_fatal">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is NOT called. The error occurs before we reach this step. receive {Self, _, init, _, _, _} -> error(init) after 1000 -> ok end, %% Confirm terminate/3 is NOT called. We have no state to give to it. receive {Self, _, terminate, _, _, _} -> error(terminate) after 1000 -> ok end, %% Confirm early_error/5 is called. receive {Self, _, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end, %% Receive a 400 error response. We do not send a 500 when %% early_error/5 crashes, we send the original error. {response, fin, 400, _} = gun:await(ConnPid, Ref), %% Confirm the connection gets closed. gun_down(ConnPid). early_error_stream_error_reason(Config) -> doc("Confirm that the stream_error given to early_error/5 is consistent between protocols."), Self = self(), ConnPid = gun_open(Config), %% We must use different solutions to hit early_error with a stream_error %% reason in both protocols. {Method, Headers, Status, Error} = case config(protocol, Config) of http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error}; http2 -> {<<"TRACE">>, [], 501, no_error}; http3 -> {<<"TRACE">>, [], 501, h3_no_error} end, Ref = gun:request(ConnPid, Method, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"early_error_stream_error_reason">>}, {<<"x-test-pid">>, pid_to_list(Self)} |Headers], <<>>), %% Confirm init/3 is NOT called. The error occurs before we reach this step. receive {Self, _, init, _, _, _} -> error(init) after 1000 -> ok end, %% Confirm terminate/3 is NOT called. We have no state to give to it. receive {Self, _, terminate, _, _, _} -> error(terminate) after 1000 -> ok end, %% Confirm early_error/5 is called. Reason = receive {Self, _, early_error, _, R, _, _, _} -> R after 1000 -> error(timeout) end, %% Confirm that the Reason is a {stream_error, Reason, Human}. {stream_error, Error, HumanReadable} = Reason, true = is_atom(HumanReadable), %% Receive a 400 or 501 error response. {response, fin, Status, _} = gun:await(ConnPid, Ref), ok. flow_after_body_fully_read(Config) -> doc("A flow command may be returned even after the body was read fully."), Self = self(), ConnPid = gun_open(Config), Ref = gun:post(ConnPid, "/long_polling", [ {<<"x-test-case">>, <<"flow_after_body_fully_read">>}, {<<"x-test-pid">>, pid_to_list(Self)} ], <<"Hello world!">>), %% Receive a 200 response, sent after the second flow command, %% confirming that the flow command was accepted. {response, _, 200, _} = gun:await(ConnPid, Ref), gun:close(ConnPid). set_options_ignore_unknown(Config) -> doc("Confirm that unknown options are ignored when using the set_options commands."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"set_options_ignore_unknown">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% Confirm the response is sent. {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, _} = gun:await_body(ConnPid, Ref), ok. shutdown_on_stream_stop(Config) -> doc("Confirm supervised processes are shutdown when stopping the stream."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"shutdown_on_stream_stop">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Receive the pid of the newly started process and monitor it. Spawn = receive {Self, Pid, spawned, S} -> S after 1000 -> error(timeout) end, MRef = monitor(process, Spawn), Spawn ! {Self, ready}, %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% We should receive a DOWN message soon after (or before) because the stream %% handler is stopping the stream immediately after the process started. receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end, %% The response is still sent. {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, _} = gun:await_body(ConnPid, Ref), ok. shutdown_on_socket_close(Config) -> doc("Confirm supervised processes are shutdown when the socket closes."), Self = self(), ConnPid = gun_open(Config), _ = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"shutdown_on_socket_close">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Receive the pid of the newly started process and monitor it. Spawn = receive {Self, Pid, spawned, S} -> S after 1000 -> error(timeout) end, MRef = monitor(process, Spawn), Spawn ! {Self, ready}, %% Close the socket. ok = gun:close(ConnPid), Protocol = config(protocol, Config), try %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% Confirm we receive a DOWN message for the child process. receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end, ok catch error:timeout when Protocol =:= http3 -> %% @todo Figure out why this happens. Could be a timing issue %% or a legitimate bug. I suspect that the server just %% doesn't receive the GOAWAY frame from Gun because %% Gun is too quick to close the connection. shutdown_on_socket_close(Config) end. shutdown_timeout_on_stream_stop(Config) -> doc("Confirm supervised processes are killed " "when the shutdown timeout triggers after stopping the stream."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"shutdown_timeout_on_stream_stop">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Receive the pid of the newly started process and monitor it. Spawn = receive {Self, Pid, spawned, S} -> S after 1000 -> error(timeout) end, MRef = monitor(process, Spawn), Spawn ! {Self, ready}, %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% We should NOT receive a DOWN message immediately. receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end, %% We should received it now. receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end, %% The response is still sent. {response, nofin, 200, _} = gun:await(ConnPid, Ref), {ok, _} = gun:await_body(ConnPid, Ref), ok. shutdown_timeout_on_socket_close(Config) -> doc("Confirm supervised processes are killed " "when the shutdown timeout triggers after the socket has closed."), Self = self(), ConnPid = gun_open(Config), _ = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"shutdown_timeout_on_socket_close">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, %% Receive the pid of the newly started process and monitor it. Spawn = receive {Self, Pid, spawned, S} -> S after 1000 -> error(timeout) end, MRef = monitor(process, Spawn), Spawn ! {Self, ready}, %% Close the socket. ok = gun:close(ConnPid), Protocol = config(protocol, Config), try %% Confirm terminate/3 is called, indicating the stream ended. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% We should NOT receive a DOWN message immediately. receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end, %% We should receive it now. receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end, ok catch error:timeout when Protocol =:= http3 -> %% @todo Figure out why this happens. Could be a timing issue %% or a legitimate bug. I suspect that the server just %% doesn't receive the GOAWAY frame from Gun because %% Gun is too quick to close the connection. shutdown_timeout_on_socket_close(Config) end. switch_protocol_after_headers(Config) -> case config(protocol, Config) of http -> do_switch_protocol_after_response( <<"switch_protocol_after_headers">>, Config); http2 -> doc("The switch_protocol command is not currently supported for HTTP/2."); http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.") end. switch_protocol_after_headers_data(Config) -> case config(protocol, Config) of http -> do_switch_protocol_after_response( <<"switch_protocol_after_headers_data">>, Config); http2 -> doc("The switch_protocol command is not currently supported for HTTP/2."); http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.") end. switch_protocol_after_response(Config) -> case config(protocol, Config) of http -> do_switch_protocol_after_response( <<"switch_protocol_after_response">>, Config); http2 -> doc("The switch_protocol command is not currently supported for HTTP/2."); http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.") end. do_switch_protocol_after_response(TestCase, Config) -> doc("The 101 informational response must not be sent when a response " "has already been sent before the switch_protocol is returned."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, TestCase}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called and receive the response. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, {response, nofin, 200, Headers} = gun:await(ConnPid, Ref), Gzipped = lists:keyfind(<<"content-encoding">>, 1, Headers) =:= {<<"content-encoding">>, <<"gzip">>}, case TestCase of <<"switch_protocol_after_headers">> -> ok; _ -> <<"{}">> = case gun:await_body(ConnPid, Ref) of {ok, Body} when Gzipped -> zlib:gunzip(Body); {ok, Body} -> Body end, ok end, {error, _} = gun:await(ConnPid, Ref), %% Confirm terminate/3 is called. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% Confirm takeover/7 is called. receive {Self, Pid, takeover, _, _, _, _, _, _, _} -> ok after 1000 -> error(timeout) end, ok. terminate_on_socket_close(Config) -> doc("Confirm terminate/3 is called when the socket gets closed brutally."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"terminate_on_socket_close">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called and receive the beginning of the response. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, {response, nofin, 200, _} = gun:await(ConnPid, Ref), %% Close the socket. ok = gun:close(ConnPid), %% Confirm terminate/3 is called. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, ok. terminate_on_stop(Config) -> doc("Confirm terminate/3 is called after stop is returned."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"terminate_on_stop">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called and receive the response. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, {response, fin, 204, _} = gun:await(ConnPid, Ref), %% Confirm the stream is still alive even though we %% received the response fully, and tell it to stop. StreamID = case config(protocol, Config) of http -> 1; http2 -> 1; http3 -> 0 end, Pid ! {{Pid, StreamID}, please_stop}, receive {Self, Pid, info, _, please_stop, _} -> ok after 1000 -> error(timeout) end, %% Confirm terminate/3 is called. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, ok. terminate_on_switch_protocol(Config) -> case config(protocol, Config) of http -> do_terminate_on_switch_protocol(Config); http2 -> doc("The switch_protocol command is not currently supported for HTTP/2."); http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.") end. do_terminate_on_switch_protocol(Config) -> doc("Confirm terminate/3 is called after switch_protocol is returned."), Self = self(), ConnPid = gun_open(Config), Ref = gun:get(ConnPid, "/long_polling", [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-case">>, <<"terminate_on_switch_protocol">>}, {<<"x-test-pid">>, pid_to_list(Self)} ]), %% Confirm init/3 is called and receive the response. Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, {inform, 101, _} = gun:await(ConnPid, Ref), %% Confirm terminate/3 is called. receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, %% Confirm takeover/7 is called. receive {Self, Pid, takeover, _, _, _, _, _, _, _} -> ok after 1000 -> error(timeout) end, ok. ================================================ FILE: test/sys_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(sys_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(ct_helper, [get_parent_pid/1]). -import(ct_helper, [get_remote_pid_tcp/1]). -import(ct_helper, [get_remote_pid_tls/1]). -import(ct_helper, [is_process_down/1]). all() -> [{group, sys}]. groups() -> [{sys, [parallel], ct_helper:all(?MODULE)}]. init_per_suite(Config) -> ProtoOpts = #{ env => #{dispatch => init_dispatch(Config)}, logger => ?MODULE }, %% Clear listener. {ok, _} = cowboy:start_clear(clear, [{port, 0}], ProtoOpts), ClearPort = ranch:get_port(clear), %% TLS listener. TLSOpts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(tls, TLSOpts ++ [{port, 0}], ProtoOpts), TLSPort = ranch:get_port(tls), [ {clear_port, ClearPort}, %% @todo Add the h2 stuff to the opts. {tls_opts, TLSOpts}, {tls_port, TLSPort} |Config]. end_per_suite(_) -> ok = cowboy:stop_listener(clear), ok = cowboy:stop_listener(tls). init_dispatch(_) -> cowboy_router:compile([{"[...]", [ {"/", hello_h, []}, {"/loop", long_polling_sys_h, []}, {"/ws", ws_echo, []} ]}]). %% Logger function silencing the expected warnings. error(Format, Args) -> error_logger:error_msg(Format, Args). warning("Received EXIT signal " ++ _, [{'EXIT', _, {shutdown, ?MODULE}}|_]) -> ok; warning(Format, Args) -> error_logger:warning_msg(Format, Args). %% proc_lib. proc_lib_initial_call_clear(Config) -> doc("Confirm that clear connection processes are started using proc_lib."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {cowboy_clear, _, _} = proc_lib:initial_call(Pid), ok. proc_lib_initial_call_tls(Config) -> doc("Confirm that TLS connection processes are started using proc_lib."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), config(tls_opts, Config)), timer:sleep(100), Pid = get_remote_pid_tls(Socket), {cowboy_tls, _, _} = proc_lib:initial_call(Pid), ok. %% System messages. %% %% Plain system messages are received as {system, From, Msg}. %% The content and meaning of this message are not interpreted by %% the receiving process module. When a system message is received, %% function handle_system_msg/6 is called to handle the request. bad_system_from_h1(Config) -> doc("h1: Sending a system message with a bad From value results in a process crash."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ct_helper_error_h:ignore(Pid, gen, reply, 2), Pid ! {system, bad, get_state}, {error, closed} = gen_tcp:recv(Socket, 0, 1000), false = is_process_alive(Pid), ok. bad_system_from_h2(Config) -> doc("h2: Sending a system message with a bad From value results in a process crash."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), ct_helper_error_h:ignore(Pid, gen, reply, 2), Pid ! {system, bad, get_state}, {error, closed} = ssl:recv(Socket, 0, 1000), false = is_process_alive(Pid), ok. bad_system_from_ws(Config) -> doc("ws: Sending a system message with a bad From value results in a process crash."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ct_helper_error_h:ignore(Pid, gen, reply, 2), Pid ! {system, bad, get_state}, {error, closed} = gen_tcp:recv(Socket, 0, 1000), false = is_process_alive(Pid), ok. bad_system_from_loop(Config) -> doc("loop: Sending a system message with a bad From value results in a process crash."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), ct_helper_error_h:ignore(Pid, gen, reply, 2), Pid ! {system, bad, get_state}, {ok, "HTTP/1.1 500 "} = gen_tcp:recv(Socket, 13, 1000), false = is_process_alive(Pid), ok. bad_system_message_h1(Config) -> doc("h1: Sending a system message with a bad Request value results in an error."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Ref = make_ref(), Pid ! {system, {self(), Ref}, hello}, receive {Ref, {error, {unknown_system_msg, hello}}} -> ok after 1000 -> error(timeout) end. bad_system_message_h2(Config) -> doc("h2: Sending a system message with a bad Request value results in an error."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), Ref = make_ref(), Pid ! {system, {self(), Ref}, hello}, receive {Ref, {error, {unknown_system_msg, hello}}} -> ok after 1000 -> error(timeout) end. bad_system_message_ws(Config) -> doc("ws: Sending a system message with a bad Request value results in an error."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Ref = make_ref(), Pid ! {system, {self(), Ref}, hello}, receive {Ref, {error, {unknown_system_msg, hello}}} -> ok after 1000 -> error(timeout) end. bad_system_message_loop(Config) -> doc("loop: Sending a system message with a bad Request value results in an error."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), Ref = make_ref(), Pid ! {system, {self(), Ref}, hello}, receive {Ref, {error, {unknown_system_msg, hello}}} -> ok after 1000 -> error(timeout) end. good_system_message_h1(Config) -> doc("h1: System messages are handled properly."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Ref = make_ref(), Pid ! {system, {self(), Ref}, get_state}, receive {Ref, Result} when element(1, Result) =/= error -> ok after 1000 -> error(timeout) end. good_system_message_h2(Config) -> doc("h2: System messages are handled properly."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), Ref = make_ref(), Pid ! {system, {self(), Ref}, get_state}, receive {Ref, Result} when element(1, Result) =/= error -> ok after 1000 -> error(timeout) end. good_system_message_ws(Config) -> doc("ws: System messages are handled properly."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Ref = make_ref(), Pid ! {system, {self(), Ref}, get_state}, receive {Ref, Result} when element(1, Result) =/= error -> ok after 1000 -> error(timeout) end. good_system_message_loop(Config) -> doc("loop: System messages are handled properly."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), Ref = make_ref(), Pid ! {system, {self(), Ref}, get_state}, receive {Ref, Result} when element(1, Result) =/= error -> ok after 1000 -> error(timeout) end. %% 'EXIT'. %% %% Shutdown messages. If the process traps exits, it must be able %% to handle a shutdown request from its parent, the supervisor. %% The message {'EXIT', Parent, Reason} from the parent is an order %% to terminate. The process must terminate when this message is %% received, normally with the same Reason as Parent. trap_exit_parent_exit_h1(Config) -> doc("h1: A process trapping exits must stop when receiving " "an 'EXIT' message from its parent."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Parent = get_parent_pid(Pid), Pid ! {'EXIT', Parent, {shutdown, ?MODULE}}, {error, closed} = gen_tcp:recv(Socket, 0, 1000), true = is_process_down(Pid), ok. trap_exit_parent_exit_h2(Config) -> doc("h2: A process trapping exits must stop when receiving " "an 'EXIT' message from its parent."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), Parent = get_parent_pid(Pid), Pid ! {'EXIT', Parent, {shutdown, ?MODULE}}, {error, closed} = ssl:recv(Socket, 0, 1000), true = is_process_down(Pid), ok. trap_exit_parent_exit_ws(Config) -> doc("ws: A process trapping exits must stop when receiving " "an 'EXIT' message from its parent."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Parent = get_parent_pid(Pid), Pid ! {'EXIT', Parent, {shutdown, ?MODULE}}, {error, closed} = gen_tcp:recv(Socket, 0, 1000), true = is_process_down(Pid), ok. trap_exit_parent_exit_loop(Config) -> doc("loop: A process trapping exits must stop when receiving " "an 'EXIT' message from its parent."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), Parent = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(Parent), Pid ! {'EXIT', Parent, {shutdown, ?MODULE}}, %% We exit normally but didn't send a response. {ok, "HTTP/1.1 204 "} = gen_tcp:recv(Socket, 13, 1000), true = is_process_down(Pid), ok. trap_exit_other_exit_h1(Config) -> doc("h1: A process trapping exits must ignore " "'EXIT' messages from unknown processes."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Pid ! {'EXIT', self(), {shutdown, ?MODULE}}, ok = gen_tcp:send(Socket, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {ok, "HTTP/1.1 200 "} = gen_tcp:recv(Socket, 13, 1000), true = is_process_alive(Pid), ok. trap_exit_other_exit_h2(Config) -> doc("h2: A process trapping exits must ignore " "'EXIT' messages from unknown processes."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), Pid ! {'EXIT', self(), {shutdown, ?MODULE}}, %% Send a HEADERS frame as a request. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a HEADERS frame as a response. {ok, << _:24, 1:8, _:40 >>} = ssl:recv(Socket, 9, 6000), true = is_process_alive(Pid), ok. trap_exit_other_exit_ws(Config) -> doc("ws: A process trapping exits must ignore " "'EXIT' messages from unknown processes."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Pid ! {'EXIT', self(), {shutdown, ?MODULE}}, %% The process stays alive. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), true = is_process_alive(Pid), ok. trap_exit_other_exit_loop(Config) -> doc("loop: A process trapping exits must ignore " "'EXIT' messages from unknown processes."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), Parent = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(Parent), Pid ! {'EXIT', self(), {shutdown, ?MODULE}}, %% The process stays alive. {ok, "HTTP/1.1 299 "} = gen_tcp:recv(Socket, 13, 1000), true = is_process_alive(Pid), ok. %% get_modules. %% %% If the modules used to implement the process change dynamically %% during runtime, the process must understand one more message. %% An example is the gen_event processes. The message is %% {_Label, {From, Ref}, get_modules}. The reply to this message is %% From ! {Ref, Modules}, where Modules is a list of the currently %% active modules in the process. %% %% For example: %% %% 1> application:start(sasl). %% ok %% 2> gen:call(alarm_handler, self(), get_modules). %% {ok,[alarm_handler]} %% 3> whereis(alarm_handler) ! {'$gen', {self(), make_ref()}, get_modules}. %% {'$gen',{<0.61.0>,#Ref<0.2900144977.374865921.142102>}, %% get_modules} %% 4> flush(). %% Shell got {#Ref<0.2900144977.374865921.142102>,[alarm_handler]} %% %% Cowboy's connection processes change dynamically: it starts with %% cowboy_clear or cowboy_tls, then becomes cowboy_http or cowboy_http2 %% and may then become or involve cowboy_websocket. On top of that %% it has various callback modules in the form of stream handlers. %% @todo %get_modules_h1(Config) -> %get_modules_h2(Config) -> %get_modules_ws(Config) -> %get_modules_loop(Config) -> %% @todo On top of this we will want to make the supervisor calls %% in ranch_conns_sup return dynamic instead of a list of modules. %% sys:change_code/4,5. %% %% We do not actually change the module code, we just ensure that %% calling this function does not crash the process. The function %% Module:system_code_change/4 will be called within the process. sys_change_code_h1(Config) -> doc("h1: The sys:change_code/4 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ok = sys:suspend(Pid), ok = gen_tcp:send(Socket, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, timeout} = gen_tcp:recv(Socket, 13, 500), ok = sys:change_code(Pid, cowboy_http, undefined, undefined), ok = sys:resume(Pid), {ok, "HTTP/1.1 200 "} = gen_tcp:recv(Socket, 13, 500), ok. sys_change_code_h2(Config) -> doc("h2: The sys:change_code/4 function works as expected."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% Suspend the process and try to get a request in. The %% response will not come back until we resume the process. ok = sys:suspend(Pid), {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a HEADERS frame as a response. {error, timeout} = ssl:recv(Socket, 9, 500), ok = sys:change_code(Pid, cowboy_http2, undefined, undefined), ok = sys:resume(Pid), {ok, << _:24, 1:8, _:40 >>} = ssl:recv(Socket, 9, 6000), ok. sys_change_code_ws(Config) -> doc("ws: The sys:change_code/4 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ok = sys:suspend(Pid), Mask = 16#37fa213d, MaskedHello = ws_SUITE:do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {error, timeout} = gen_tcp:recv(Socket, 0, 500), ok = sys:change_code(Pid, cowboy_websocket, undefined, undefined), ok = sys:resume(Pid), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), ok. sys_change_code_loop(Config) -> doc("loop: The sys:change_code/4 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), %% The process sends a response 500ms after initializing. %% We expect to not receive it until we resume it. ok = sys:suspend(Pid), {error, timeout} = gen_tcp:recv(Socket, 13, 1000), ok = sys:change_code(Pid, cowboy_loop, undefined, undefined), ok = sys:resume(Pid), {ok, "HTTP/1.1 299 "} = gen_tcp:recv(Socket, 13, 500), ok. %% sys:get_state/1,2. %% %% None of the modules implement Module:system_get_state/1 %% at this time so sys:get_state/1,2 returns the Misc value. sys_get_state_h1(Config) -> doc("h1: The sys:get_state/1 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), State = sys:get_state(Pid), state = element(1, State), ok. sys_get_state_h2(Config) -> doc("h2: The sys:get_state/1 function works as expected."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), {State, Buffer} = sys:get_state(Pid), state = element(1, State), true = is_binary(Buffer), ok. sys_get_state_ws(Config) -> doc("ws: The sys:get_state/1 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {State, undefined, ParseState} = sys:get_state(Pid), state = element(1, State), case element(1, ParseState) of ps_header -> ok; ps_payload -> ok end. sys_get_state_loop(Config) -> doc("loop: The sys:get_state/1 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), {Req, Env, long_polling_sys_h, undefined, infinity} = sys:get_state(Pid), #{pid := _, streamid := _} = Req, #{dispatch := _} = Env, ok. %% sys:get_status/1,2. sys_get_status_h1(Config) -> doc("h1: The sys:get_status/1 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {status, Pid, {module, cowboy_http}, _} = sys:get_status(Pid), ok. sys_get_status_h2(Config) -> doc("h2: The sys:get_status/1 function works as expected."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), {status, Pid, {module, cowboy_http2}, _} = sys:get_status(Pid), ok. sys_get_status_ws(Config) -> doc("ws: The sys:get_status/1 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {status, Pid, {module, cowboy_websocket}, _} = sys:get_status(Pid), ok. sys_get_status_loop(Config) -> doc("loop: The sys:get_status/1 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), {status, Pid, {module, cowboy_loop}, _} = sys:get_status(Pid), ok. %% sys:replace_state/2,3. %% %% None of the modules implement Module:system_replace_state/2 %% at this time so sys:replace_state/2,3 handles the Misc value. %% %% We don't actually replace the state, we only care about %% whether the call executes as expected. sys_replace_state_h1(Config) -> doc("h1: The sys:replace_state/2 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), State = sys:replace_state(Pid, fun(S) -> S end), state = element(1, State), ok. sys_replace_state_h2(Config) -> doc("h2: The sys:replace_state/2 function works as expected."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), {State, Buffer} = sys:replace_state(Pid, fun(S) -> S end), state = element(1, State), true = is_binary(Buffer), ok. sys_replace_state_ws(Config) -> doc("ws: The sys:replace_state/2 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {State, undefined, ParseState} = sys:replace_state(Pid, fun(S) -> S end), state = element(1, State), case element(1, ParseState) of ps_header -> ok; ps_payload -> ok end. sys_replace_state_loop(Config) -> doc("loop: The sys:replace_state/2 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), {Req, Env, long_polling_sys_h, undefined, infinity} = sys:replace_state(Pid, fun(S) -> S end), #{pid := _, streamid := _} = Req, #{dispatch := _} = Env, ok. %% sys:suspend/1 and sys:resume/1. sys_suspend_and_resume_h1(Config) -> doc("h1: The sys:suspend/1 and sys:resume/1 functions work as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ok = sys:suspend(Pid), ok = gen_tcp:send(Socket, "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), {error, timeout} = gen_tcp:recv(Socket, 13, 500), ok = sys:resume(Pid), {ok, "HTTP/1.1 200 "} = gen_tcp:recv(Socket, 13, 500), ok. sys_suspend_and_resume_h2(Config) -> doc("h2: The sys:suspend/1 and sys:resume/1 functions work as expected."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% Suspend the process and try to get a request in. The %% response will not come back until we resume the process. ok = sys:suspend(Pid), {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), %% Receive a HEADERS frame as a response. {error, timeout} = ssl:recv(Socket, 9, 500), ok = sys:resume(Pid), {ok, << _:24, 1:8, _:40 >>} = ssl:recv(Socket, 9, 6000), ok. sys_suspend_and_resume_ws(Config) -> doc("ws: The sys:suspend/1 and sys:resume/1 functions work as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ok = sys:suspend(Pid), Mask = 16#37fa213d, MaskedHello = ws_SUITE:do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {error, timeout} = gen_tcp:recv(Socket, 0, 500), ok = sys:resume(Pid), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), ok. sys_suspend_and_resume_loop(Config) -> doc("loop: The sys:suspend/1 and sys:resume/1 functions work as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), %% The process sends a response 500ms after initializing. %% We expect to not receive it until we resume it. ok = sys:suspend(Pid), {error, timeout} = gen_tcp:recv(Socket, 13, 1000), ok = sys:resume(Pid), {ok, "HTTP/1.1 299 "} = gen_tcp:recv(Socket, 13, 500), ok. %% sys:terminate/2,3. %% %% The callback Module:system_terminate/4 is used in all cases. sys_terminate_h1(Config) -> doc("h1: The sys:terminate/2,3 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ok = sys:terminate(Pid, {shutdown, ?MODULE}), {error, closed} = gen_tcp:recv(Socket, 0, 500), ok. sys_terminate_h2(Config) -> doc("h2: The sys:terminate/2,3 function works as expected."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), %% Skip the SETTINGS frame. {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), timer:sleep(100), Pid = get_remote_pid_tls(Socket), ok = sys:terminate(Pid, {shutdown, ?MODULE}), {error, closed} = ssl:recv(Socket, 0, 500), ok. sys_terminate_ws(Config) -> doc("ws: The sys:terminate/2,3 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), ok = sys:terminate(Pid, {shutdown, ?MODULE}), {error, closed} = gen_tcp:recv(Socket, 0, 500), ok. sys_terminate_loop(Config) -> doc("loop: The sys:terminate/2,3 function works as expected."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), SupPid = get_remote_pid_tcp(Socket), [{_, Pid, _, _}] = supervisor:which_children(SupPid), %% We stop the process normally and therefore get a 204. ok = sys:terminate(Pid, {shutdown, ?MODULE}), {ok, "HTTP/1.1 204 "} = gen_tcp:recv(Socket, 13, 500), ok. %% @todo Debugging functionality from sys. %% %% The functions make references to a debug structure. %% The debug structure is a list of dbg_opt(), which is %% an internal data type used by the function handle_system_msg/6. %% No debugging is performed if it is an empty list. %% %% Cowboy currently does not implement sys debugging. %% %% The following functions are concerned: %% %% * sys:install/2,3 %% * sys:log/2,3 %% * sys:log_to_file/2,3 %% * sys:no_debug/1,2 %% * sys:remove/2,3 %% * sys:statistics/2,3 %% * sys:trace/2,3 %% * call debug_options/1 %% * call get_debug/3 %% * call handle_debug/4 %% * call print_log/1 %% supervisor. %% %% The connection processes act as supervisors by default %% so they must handle the supervisor messages. %% supervisor:count_children/1. supervisor_count_children_h1(Config) -> doc("h1: The function supervisor:count_children/1 must work."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), %% No request was sent so there's no children. Counts1 = supervisor:count_children(Pid), 1 = proplists:get_value(specs, Counts1), 0 = proplists:get_value(active, Counts1), 0 = proplists:get_value(supervisors, Counts1), 0 = proplists:get_value(workers, Counts1), %% Send a request, observe that a children exists. ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), Counts2 = supervisor:count_children(Pid), 1 = proplists:get_value(specs, Counts2), 1 = proplists:get_value(active, Counts2), 0 = proplists:get_value(supervisors, Counts2), 1 = proplists:get_value(workers, Counts2), ok. supervisor_count_children_h2(Config) -> doc("h2: The function supervisor:count_children/1 must work."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% No request was sent so there's no children. Counts1 = supervisor:count_children(Pid), 1 = proplists:get_value(specs, Counts1), 0 = proplists:get_value(active, Counts1), 0 = proplists:get_value(supervisors, Counts1), 0 = proplists:get_value(workers, Counts1), %% Send a request, observe that a children exists. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/loop">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), timer:sleep(100), Counts2 = supervisor:count_children(Pid), 1 = proplists:get_value(specs, Counts2), 1 = proplists:get_value(active, Counts2), 0 = proplists:get_value(supervisors, Counts2), 1 = proplists:get_value(workers, Counts2), ok. supervisor_count_children_ws(Config) -> doc("ws: The function supervisor:count_children/1 must work. " "Websocket connections never have children."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), Counts = supervisor:count_children(Pid), 1 = proplists:get_value(specs, Counts), 0 = proplists:get_value(active, Counts), 0 = proplists:get_value(supervisors, Counts), 0 = proplists:get_value(workers, Counts), ok. %% supervisor:delete_child/2. supervisor_delete_child_not_found_h1(Config) -> doc("h1: The function supervisor:delete_child/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), %% When no children exist. {error, not_found} = supervisor:delete_child(Pid, cowboy_http), %% When a child exists. ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), {error, not_found} = supervisor:delete_child(Pid, cowboy_http), ok. supervisor_delete_child_not_found_h2(Config) -> doc("h2: The function supervisor:delete_child/2 must return {error, not_found}."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% When no children exist. {error, not_found} = supervisor:delete_child(Pid, cowboy_http2), %% When a child exists. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/loop">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), timer:sleep(100), {error, not_found} = supervisor:delete_child(Pid, cowboy_http2), ok. supervisor_delete_child_not_found_ws(Config) -> doc("ws: The function supervisor:delete_child/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {error, not_found} = supervisor:delete_child(Pid, cowboy_websocket), ok. %% supervisor:get_childspec/2. supervisor_get_childspec_not_found_h1(Config) -> doc("h1: The function supervisor:get_childspec/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), %% When no children exist. {error, not_found} = supervisor:get_childspec(Pid, cowboy_http), %% When a child exists. ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), {error, not_found} = supervisor:get_childspec(Pid, cowboy_http), ok. supervisor_get_childspec_not_found_h2(Config) -> doc("h2: The function supervisor:get_childspec/2 must return {error, not_found}."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% When no children exist. {error, not_found} = supervisor:get_childspec(Pid, cowboy_http2), %% When a child exists. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/loop">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), timer:sleep(100), {error, not_found} = supervisor:get_childspec(Pid, cowboy_http2), ok. supervisor_get_childspec_not_found_ws(Config) -> doc("ws: The function supervisor:get_childspec/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {error, not_found} = supervisor:get_childspec(Pid, cowboy_websocket), ok. %% supervisor:restart_child/2. supervisor_restart_child_not_found_h1(Config) -> doc("h1: The function supervisor:restart_child/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), %% When no children exist. {error, not_found} = supervisor:restart_child(Pid, cowboy_http), %% When a child exists. ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), {error, not_found} = supervisor:restart_child(Pid, cowboy_http), ok. supervisor_restart_child_not_found_h2(Config) -> doc("h2: The function supervisor:restart_child/2 must return {error, not_found}."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% When no children exist. {error, not_found} = supervisor:restart_child(Pid, cowboy_http2), %% When a child exists. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/loop">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), timer:sleep(100), {error, not_found} = supervisor:restart_child(Pid, cowboy_http2), ok. supervisor_restart_child_not_found_ws(Config) -> doc("ws: The function supervisor:restart_child/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {error, not_found} = supervisor:restart_child(Pid, cowboy_websocket), ok. %% supervisor:start_child/2 must return {error, start_child_disabled} supervisor_start_child_not_found_h1(Config) -> doc("h1: The function supervisor:start_child/2 must return {error, start_child_disabled}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {error, start_child_disabled} = supervisor:start_child(Pid, #{ id => error, start => {error, error, []} }), ok. supervisor_start_child_not_found_h2(Config) -> doc("h2: The function supervisor:start_child/2 must return {error, start_child_disabled}."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), {error, start_child_disabled} = supervisor:start_child(Pid, #{ id => error, start => {error, error, []} }), ok. supervisor_start_child_not_found_ws(Config) -> doc("ws: The function supervisor:start_child/2 must return {error, start_child_disabled}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {error, start_child_disabled} = supervisor:start_child(Pid, #{ id => error, start => {error, error, []} }), ok. %% supervisor:terminate_child/2. supervisor_terminate_child_not_found_h1(Config) -> doc("h1: The function supervisor:terminate_child/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), %% When no children exist. {error, not_found} = supervisor:terminate_child(Pid, cowboy_http), %% When a child exists. ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), {error, not_found} = supervisor:terminate_child(Pid, cowboy_http), ok. supervisor_terminate_child_not_found_h2(Config) -> doc("h2: The function supervisor:terminate_child/2 must return {error, not_found}."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% When no children exist. {error, not_found} = supervisor:terminate_child(Pid, cowboy_http2), %% When a child exists. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/loop">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), timer:sleep(100), {error, not_found} = supervisor:terminate_child(Pid, cowboy_http2), ok. supervisor_terminate_child_not_found_ws(Config) -> doc("ws: The function supervisor:terminate_child/2 must return {error, not_found}."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), {error, not_found} = supervisor:terminate_child(Pid, cowboy_websocket), ok. %% supervisor:which_children/1. %% %% @todo The list of modules returned is probably wrong. This will %% need to be corrected when get_modules gets implemented. supervisor_which_children_h1(Config) -> doc("h1: The function supervisor:which_children/1 must work."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [{active, false}]), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), %% No request was sent so there's no children. [] = supervisor:which_children(Pid), %% Send a request, observe that a children exists. ok = gen_tcp:send(Socket, "GET /loop HTTP/1.1\r\n" "Host: localhost\r\n" "\r\n"), timer:sleep(100), [{cowboy_http, Child, worker, [cowboy_http]}] = supervisor:which_children(Pid), true = is_pid(Child), ok. supervisor_which_children_h2(Config) -> doc("h2: The function supervisor:which_children/1 must work."), {ok, Socket} = ssl:connect("localhost", config(tls_port, Config), [{alpn_advertised_protocols, [<<"h2">>]}, {active, false}, binary|config(tls_opts, Config)]), do_http2_handshake(Socket), Pid = get_remote_pid_tls(Socket), %% No request was sent so there's no children. [] = supervisor:which_children(Pid), %% Send a request, observe that a children exists. {HeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"GET">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<":path">>, <<"/loop">>} ]), ok = ssl:send(Socket, cow_http2:headers(1, fin, HeadersBlock)), timer:sleep(100), [{cowboy_http2, Child, worker, [cowboy_http2]}] = supervisor:which_children(Pid), true = is_pid(Child), ok. supervisor_which_children_ws(Config) -> doc("ws: The function supervisor:which_children/1 must work. " "Websocket connections never have children."), {ok, Socket} = gen_tcp:connect("localhost", config(clear_port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 5000), {ok, {http_response, {1, 1}, 101, _}, _} = erlang:decode_packet(http, Handshake, []), timer:sleep(100), Pid = get_remote_pid_tcp(Socket), [] = supervisor:which_children(Pid), ok. %% Internal. do_http2_handshake(Socket) -> ok = ssl:send(Socket, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"), {ok, <<_,_,_,4,_/bits>>} = ssl:recv(Socket, 0, 1000), ok = ssl:send(Socket, [cow_http2:settings(#{}), cow_http2:settings_ack()]), {ok, << 0:24, 4:8, 1:8, 0:32 >>} = ssl:recv(Socket, 9, 1000), ok. ================================================ FILE: test/tracer_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(tracer_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_down/1]). %% ct. suite() -> [{timetrap, 30000}]. %% We initialize trace patterns here. Appropriate would be in %% init_per_suite/1, but this works just as well. all() -> %% @todo Implement these tests for HTTP/3. cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}]. init_per_suite(Config) -> cowboy_tracer_h:set_trace_patterns(), Config. end_per_suite(_) -> ok. %% We want tests for each group to execute sequentially %% because we need to modify the protocol options. Groups %% can run in parallel however. groups() -> Tests = ct_helper:all(?MODULE), [ {http, [], Tests}, {https, [], Tests}, {h2, [], Tests}, {h2c, [], Tests}, {http_compress, [], Tests}, {https_compress, [], Tests}, {h2_compress, [], Tests}, {h2c_compress, [], Tests} ]. init_per_group(Name = http, Config) -> cowboy_test:init_http(Name, init_plain_opts(Config), Config); init_per_group(Name = https, Config) -> cowboy_test:init_http(Name, init_plain_opts(Config), Config); init_per_group(Name = h2, Config) -> cowboy_test:init_http2(Name, init_plain_opts(Config), Config); init_per_group(Name = h2c, Config) -> Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(Name = http_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(Config), Config); init_per_group(Name = https_compress, Config) -> cowboy_test:init_http(Name, init_compress_opts(Config), Config); init_per_group(Name = h2_compress, Config) -> cowboy_test:init_http2(Name, init_compress_opts(Config), Config); init_per_group(Name = h2c_compress, Config) -> Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config), lists:keyreplace(protocol, 1, Config1, {protocol, http2}). end_per_group(Name, _) -> cowboy:stop_listener(Name). init_plain_opts(Config) -> #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, stream_handlers => [cowboy_tracer_h, cowboy_stream_h] }. init_compress_opts(Config) -> #{ env => #{dispatch => cowboy_router:compile(init_routes(Config))}, stream_handlers => [cowboy_tracer_h, cowboy_compress_h, cowboy_stream_h] }. init_routes(_) -> [ {"localhost", [ {"/", hello_h, []}, {"/longer/hello/path", hello_h, []} ]} ]. do_get(Path, Config) -> %% Perform a GET request. ConnPid = gun_open(Config), Ref = gun:get(ConnPid, Path, [ {<<"accept-encoding">>, <<"gzip">>}, {<<"x-test-pid">>, pid_to_list(self())} ]), {response, nofin, 200, _Headers} = gun:await(ConnPid, Ref), {ok, _Body} = gun:await_body(ConnPid, Ref), gun:close(ConnPid). %% We only care about cowboy_req:reply/4 calls and init/terminate events. do_tracer_callback(Pid) -> fun (Event, _) when Event =:= init; Event =:= terminate -> Pid ! Event, 0; (Event={trace_ts, _, call, {cowboy_req, reply, _}, _}, State) -> Pid ! Event, Pid ! {state, State}, State + 1; (_, State) -> State + 1 end. %% Tests. init(Config) -> doc("Ensure the init event is triggered."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [fun(_,_,_) -> true end] }), do_get("/", Config), receive init -> ok end. terminate(Config) -> doc("Ensure the terminate event is triggered."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [fun(_,_,_) -> true end] }), do_get("/", Config), receive terminate -> ok end. state(Config) -> doc("Ensure the returned state is used."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [fun(_,_,_) -> true end] }), do_get("/", Config), receive {state, St} -> true = St > 0, ok end. empty(Config) -> doc("Empty match specs unconditionally enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. predicate_true(Config) -> doc("Predicate function returns true, unconditionally enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [fun(_,_,_) -> true end] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. predicate_false(Config) -> doc("Predicate function returns false, unconditionally disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [fun(_,_,_) -> false end] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. method(Config) -> doc("Method is the same as the request's, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{method, <<"GET">>}] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. method_no_match(Config) -> doc("Method is different from the request's, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{method, <<"POST">>}] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. host(Config) -> doc("Host is the same as the request's, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{host, <<"localhost">>}] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. host_no_match(Config) -> doc("Host is different from the request's, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{host, <<"ninenines.eu">>}] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. path(Config) -> doc("Path is the same as the request's, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{path, <<"/longer/hello/path">>}] }), do_get("/longer/hello/path", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. path_no_match(Config) -> doc("Path is different from the request's, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{path, <<"/some/other/path">>}] }), do_get("/longer/hello/path", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. path_start(Config) -> doc("Start of path is the same as request's, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{path_start, <<"/longer/hello">>}] }), do_get("/longer/hello/path", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. path_start_no_match(Config) -> doc("Start of path is different from the request's, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{path_start, <<"/shorter/hello">>}] }), do_get("/longer/hello/path", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. header_defined(Config) -> doc("Header is defined in the request, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{header, <<"accept-encoding">>}] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. header_defined_no_match(Config) -> doc("Header is not defined in the request, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{header, <<"accept-language">>}] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. header_value(Config) -> doc("Header value is the same as the request's, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{header, <<"accept-encoding">>, <<"gzip">>}] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. header_value_no_match(Config) -> doc("Header value is different from the request's, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{header, <<"accept-encoding">>, <<"nope">>}] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. peer_ip(Config) -> doc("Peer IP is the same as the request's, enable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{peer_ip, {127, 0, 0, 1}}] }), do_get("/", Config), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end. peer_ip_no_match(Config) -> doc("Peer IP is different from the request's, disable tracing."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [{peer_ip, {8, 8, 8, 8}}] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. missing_callback(Config) -> doc("Ensure the request is still processed if the callback is not provided."), Ref = config(ref, Config), Opts0 = ranch:get_protocol_options(Ref), Opts = maps:remove(tracer_callback, Opts0), ranch:set_protocol_options(Ref, Opts#{ tracer_match_specs => [{method, <<"GET">>}] }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. missing_match_specs(Config) -> doc("Ensure the request is still processed if match specs are not provided."), Ref = config(ref, Config), Opts0 = ranch:get_protocol_options(Ref), Opts = maps:remove(tracer_match_specs, Opts0), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()) }), do_get("/", Config), receive Msg when element(1, Msg) =:= trace_ts -> error(Msg) after 100 -> ok end. two_matching_requests(Config) -> doc("Perform two requests that enable tracing on the same connection."), Ref = config(ref, Config), Opts = ranch:get_protocol_options(Ref), ranch:set_protocol_options(Ref, Opts#{ tracer_callback => do_tracer_callback(self()), tracer_match_specs => [fun(_,_,_) -> true end] }), %% Perform a GET request. ConnPid = gun_open(Config), Ref1 = gun:get(ConnPid, "/", []), {response, nofin, 200, _} = gun:await(ConnPid, Ref1), {ok, _} = gun:await_body(ConnPid, Ref1), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end, %% Perform a second GET request on the same connection. Ref2 = gun:get(ConnPid, "/", []), {response, nofin, 200, _} = gun:await(ConnPid, Ref2), {ok, _} = gun:await_body(ConnPid, Ref2), receive {trace_ts, _, call, {cowboy_req, reply, [200, _, _, _]}, _} -> ok end, gun:close(ConnPid). ================================================ FILE: test/ws_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(ws_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). %% ct. all() -> [{group, ws}]. groups() -> [{ws, [parallel], ct_helper:all(?MODULE)}]. init_per_group(Name, Config) -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch()} }, Config). end_per_group(Listener, _Config) -> cowboy:stop_listener(Listener). %% Dispatch configuration. init_dispatch() -> cowboy_router:compile([ {"localhost", [ {"/ws_echo", ws_echo, []}, {"/ws_echo_timer", ws_echo_timer, []}, {"/ws_init", ws_init_h, #{}}, {"/ws_init_shutdown", ws_init_shutdown, []}, {"/ws_send_many", ws_send_many, [ {sequence, [ {text, <<"one">>}, {text, <<"two">>}, {text, <<"seven!">>}]} ]}, {"/ws_send_close", ws_send_many, [ {sequence, [ {text, <<"send">>}, close, {text, <<"won't be received">>}]} ]}, {"/ws_send_close_payload", ws_send_many, [ {sequence, [ {text, <<"send">>}, {close, 1001, <<"some text!">>}, {text, <<"won't be received">>}]} ]}, {"/ws_subprotocol", ws_subprotocol, []}, {"/terminate", ws_terminate_h, []}, {"/ws_timeout_hibernate", ws_timeout_hibernate, []}, {"/ws_timeout_cancel", ws_timeout_cancel, []}, {"/ws_max_frame_size", ws_max_frame_size, []}, {"/ws_deflate_opts", ws_deflate_opts_h, []}, {"/ws_dont_validate_utf8", ws_dont_validate_utf8_h, []}, {"/ws_ping", ws_ping_h, []} ]} ]). %% Tests. unlimited_connections(Config) -> doc("Websocket connections are not limited. The connections " "are removed from the count after the handshake completes."), case os:type() of {win32, _} -> {skip, "Tests that use too many sockets are disabled on Windows " "to prevent intermittent failures."}; {unix, _} -> case list_to_integer(os:cmd("printf `ulimit -n`")) of Limit when Limit > 6100 -> do_unlimited_connections(Config); _ -> {skip, "`ulimit -n` reports a limit too low for this test."} end end. do_unlimited_connections(Config) -> _ = [begin spawn_link(fun() -> do_connect_and_loop(Config) end), timer:sleep(1) end || _ <- lists:seq(1, 3000)], timer:sleep(1000), %% We have at least 3000 client and 3000 server sockets. true = length(erlang:ports()) > 6000, %% Ranch thinks we have no connections. 0 = ranch_server:count_connections(ws), ok. do_connect_and_loop(Config) -> {ok, Socket, _} = do_handshake("/ws_echo", Config), do_loop(Socket). do_loop(Socket) -> %% Masked text hello echoed back clear by the server. Mask = 16#37fa213d, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), timer:sleep(1000), do_loop(Socket). ws0(Config) -> doc("Websocket version 0 (hixie-76 draft) is no longer supported."), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, "GET /ws_echo_timer HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Upgrade: WebSocket\r\n" "Origin: http://localhost\r\n" "Sec-Websocket-Key1: Y\" 4 1Lj!957b8@0H756!i\r\n" "Sec-Websocket-Key2: 1711 M;4\\74 80<6\r\n" "\r\n"), {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), {ok, {http_response, {1, 1}, 400, _}, _} = erlang:decode_packet(http, Handshake, []), ok. ws7(Config) -> doc("Websocket version 7 (draft) is supported."), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET /ws_echo_timer HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Upgrade: websocket\r\n" "Sec-WebSocket-Origin: http://localhost\r\n" "Sec-WebSocket-Version: 7\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "\r\n"]), {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), {ok, {http_response, {1, 1}, 101, _}, Rest} = erlang:decode_packet(http, Handshake, []), [Headers, <<>>] = do_decode_headers(erlang:decode_packet(httph, Rest, []), []), {_, "Upgrade"} = lists:keyfind('Connection', 1, Headers), {_, "websocket"} = lists:keyfind('Upgrade', 1, Headers), {_, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} = lists:keyfind("sec-websocket-accept", 1, Headers), do_ws_version(Socket). ws8(Config) -> doc("Websocket version 8 (draft) is supported."), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET /ws_echo_timer HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Upgrade: websocket\r\n" "Sec-WebSocket-Origin: http://localhost\r\n" "Sec-WebSocket-Version: 8\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "\r\n"]), {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), {ok, {http_response, {1, 1}, 101, _}, Rest} = erlang:decode_packet(http, Handshake, []), [Headers, <<>>] = do_decode_headers(erlang:decode_packet(httph, Rest, []), []), {_, "Upgrade"} = lists:keyfind('Connection', 1, Headers), {_, "websocket"} = lists:keyfind('Upgrade', 1, Headers), {_, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} = lists:keyfind("sec-websocket-accept", 1, Headers), do_ws_version(Socket). ws13(Config) -> doc("Websocket version 13 (RFC) is supported."), {ok, Socket, _} = do_handshake("/ws_echo_timer", Config), do_ws_version(Socket). do_ws_version(Socket) -> %% Masked text hello echoed back clear by the server. Mask = 16#37fa213d, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), %% Empty binary frame echoed back. ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 1:1, 0:7, 0:32 >>), {ok, << 1:1, 0:3, 2:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% Masked binary hello echoed back clear by the server. ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 2:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), %% Frames sent on timer by the handler. {ok, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>} = gen_tcp:recv(Socket, 0, 6000), {ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>} = gen_tcp:recv(Socket, 0, 6000), {ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>} = gen_tcp:recv(Socket, 0, 6000), {ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>} = gen_tcp:recv(Socket, 0, 6000), %% Client-initiated ping/pong. ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 1:1, 0:7, 0:32 >>), {ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% Client-initiated close. ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_deflate_max_frame_size_close(Config) -> doc("Server closes connection when decompressed frame size exceeds max_frame_size option"), %% max_frame_size is set to 8 bytes in ws_max_frame_size. {ok, Socket, Headers} = do_handshake("/ws_max_frame_size", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers), Mask = 16#11223344, Z = zlib:open(), zlib:deflateInit(Z, best_compression, deflated, -15, 8, default), CompressedData0 = iolist_to_binary(zlib:deflate(Z, <<0:800>>, sync)), CompressedData = binary:part(CompressedData0, 0, byte_size(CompressedData0) - 4), MaskedData = do_mask(CompressedData, Mask, <<>>), Len = byte_size(MaskedData), true = Len < 8, ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, Len:7, Mask:32, MaskedData/binary >>), {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_deflate_opts_client_context_takeover(Config) -> doc("Handler is configured with client context takeover enabled."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?client_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers1), {ok, _, Headers2} = do_handshake("/ws_deflate_opts?client_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover\r\n", Config), {_, "permessage-deflate; client_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. ws_deflate_opts_client_no_context_takeover(Config) -> doc("Handler is configured with client context takeover disabled."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?client_no_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate; client_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers1), {ok, _, Headers2} = do_handshake("/ws_deflate_opts?client_no_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover\r\n", Config), {_, "permessage-deflate; client_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. %% We must send client_max_window_bits to indicate we support it. ws_deflate_opts_client_max_window_bits(Config) -> doc("Handler is configured with client max window bits."), {ok, _, Headers} = do_handshake("/ws_deflate_opts?client_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n", Config), {_, "permessage-deflate; client_max_window_bits=9"} = lists:keyfind("sec-websocket-extensions", 1, Headers), ok. ws_deflate_opts_client_max_window_bits_override(Config) -> doc("Handler is configured with client max window bits."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?client_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=8\r\n", Config), {_, "permessage-deflate; client_max_window_bits=8"} = lists:keyfind("sec-websocket-extensions", 1, Headers1), {ok, _, Headers2} = do_handshake("/ws_deflate_opts?client_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=12\r\n", Config), {_, "permessage-deflate; client_max_window_bits=9"} = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. %% @todo This might be better in an rfc7692_SUITE. %% %% 7.1.2.2 %% If a received extension negotiation offer doesn't have the %% "client_max_window_bits" extension parameter, the corresponding %% extension negotiation response to the offer MUST NOT include the %% "client_max_window_bits" extension parameter. ws_deflate_opts_client_max_window_bits_only_in_server(Config) -> doc("Handler is configured with non-default client max window bits but " "client doesn't send the parameter; compression is disabled."), {ok, _, Headers} = do_handshake("/ws_deflate_opts?client_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), false = lists:keyfind("sec-websocket-extensions", 1, Headers), ok. ws_deflate_opts_server_context_takeover(Config) -> doc("Handler is configured with server context takeover enabled."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?server_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers1), {ok, _, Headers2} = do_handshake("/ws_deflate_opts?server_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover\r\n", Config), {_, "permessage-deflate; server_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. ws_deflate_opts_server_no_context_takeover(Config) -> doc("Handler is configured with server context takeover disabled."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?server_no_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate; server_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers1), {ok, _, Headers2} = do_handshake("/ws_deflate_opts?server_no_context_takeover", "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover\r\n", Config), {_, "permessage-deflate; server_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. ws_deflate_opts_server_max_window_bits(Config) -> doc("Handler is configured with server max window bits."), {ok, _, Headers} = do_handshake("/ws_deflate_opts?server_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate; server_max_window_bits=9"} = lists:keyfind("sec-websocket-extensions", 1, Headers), ok. ws_deflate_opts_server_max_window_bits_override(Config) -> doc("Handler is configured with server max window bits."), {ok, _, Headers1} = do_handshake("/ws_deflate_opts?server_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=8\r\n", Config), {_, "permessage-deflate; server_max_window_bits=8"} = lists:keyfind("sec-websocket-extensions", 1, Headers1), {ok, _, Headers2} = do_handshake("/ws_deflate_opts?server_max_window_bits", "Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=12\r\n", Config), {_, "permessage-deflate; server_max_window_bits=9"} = lists:keyfind("sec-websocket-extensions", 1, Headers2), ok. ws_deflate_opts_zlevel(Config) -> doc("Handler is configured with zlib level."), do_ws_deflate_opts_z("/ws_deflate_opts?level", Config). ws_deflate_opts_zmemlevel(Config) -> doc("Handler is configured with zlib mem_level."), do_ws_deflate_opts_z("/ws_deflate_opts?mem_level", Config). ws_deflate_opts_zstrategy(Config) -> doc("Handler is configured with zlib strategy."), do_ws_deflate_opts_z("/ws_deflate_opts?strategy", Config). do_ws_deflate_opts_z(Path, Config) -> {ok, Socket, Headers} = do_handshake(Path, "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config), {_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers), %% Send and receive a compressed "Hello" frame. Mask = 16#11223344, CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, MaskedHello = do_mask(CompressedHello, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), %% Client-initiated close. ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_dont_validate_utf8(Config) -> doc("Handler is configured with UTF-8 validation disabled."), {ok, Socket, _} = do_handshake("/ws_dont_validate_utf8", Config), %% Send an invalid UTF-8 text frame and receive it back. Mask = 16#37fa213d, MaskedInvalid = do_mask(<<255, 255, 255, 255>>, Mask, <<>>), ok = gen_tcp:send(Socket, <<1:1, 0:3, 1:4, 1:1, 4:7, Mask:32, MaskedInvalid/binary>>), {ok, <<1:1, 0:3, 1:4, 0:1, 4:7, 255, 255, 255, 255>>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_first_frame_with_handshake(Config) -> doc("Client sends the first frame immediately with the handshake. " "This is invalid according to the protocol but we still want " "to accept it if the handshake is successful."), Mask = 16#37fa213d, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), {ok, Socket, _} = do_handshake("/ws_echo", "", <<1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary>>, Config), {ok, <<1:1, 0:3, 1:4, 0:1, 5:7, "Hello">>} = gen_tcp:recv(Socket, 0, 6000), ok. %% @todo Move these tests to ws_handler_SUITE. ws_init_return_ok(Config) -> doc("Handler does nothing."), {ok, Socket, _} = do_handshake("/ws_init?ok", Config), %% The handler does nothing; nothing should happen here. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), ok. ws_init_return_ok_hibernate(Config) -> doc("Handler does nothing; hibernates."), {ok, Socket, _} = do_handshake("/ws_init?ok_hibernate", Config), %% The handler does nothing; nothing should happen here. {error, timeout} = gen_tcp:recv(Socket, 0, 1000), ok. ws_init_return_reply(Config) -> doc("Handler sends a text frame just after the handshake."), {ok, Socket, _} = do_handshake("/ws_init?reply", Config), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_init_return_reply_hibernate(Config) -> doc("Handler sends a text frame just after the handshake and then hibernates."), {ok, Socket, _} = do_handshake("/ws_init?reply_hibernate", Config), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_init_return_reply_close(Config) -> doc("Handler closes immediately after the handshake."), {ok, Socket, _} = do_handshake("/ws_init?reply_close", Config), {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_init_return_reply_close_hibernate(Config) -> doc("Handler closes immediately after the handshake, then attempts to hibernate."), {ok, Socket, _} = do_handshake("/ws_init?reply_close_hibernate", Config), {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_init_return_reply_many(Config) -> doc("Handler sends many frames just after the handshake."), {ok, Socket, _} = do_handshake("/ws_init?reply_many", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 1:1, 0:3, 2:4, 0:1, 5:7, "World" >>} = gen_tcp:recv(Socket, 14, 6000), ok. ws_init_return_reply_many_hibernate(Config) -> doc("Handler sends many frames just after the handshake and then hibernates."), {ok, Socket, _} = do_handshake("/ws_init?reply_many_hibernate", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 1:1, 0:3, 2:4, 0:1, 5:7, "World" >>} = gen_tcp:recv(Socket, 14, 6000), ok. ws_init_return_reply_many_close(Config) -> doc("Handler sends many frames including a close frame just after the handshake."), {ok, Socket, _} = do_handshake("/ws_init?reply_many_close", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 9, 6000), ok. ws_init_return_reply_many_close_hibernate(Config) -> doc("Handler sends many frames including a close frame just after the handshake and then hibernates."), {ok, Socket, _} = do_handshake("/ws_init?reply_many_close_hibernate", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello", 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 9, 6000), ok. ws_init_shutdown_before_handshake(Config) -> doc("Handler stops before Websocket handshake."), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET /ws_init_shutdown HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n" "\r\n"]), {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), {ok, {http_response, {1, 1}, 403, _}, _Rest} = erlang:decode_packet(http, Handshake, []), ok. ws_max_frame_size_close(Config) -> doc("Server closes connection when frame size exceeds max_frame_size option"), %% max_frame_size is set to 8 bytes in ws_max_frame_size. {ok, Socket, _} = do_handshake("/ws_max_frame_size", Config), Mask = 16#11223344, MaskedHello = do_mask(<<"HelloHello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 1:1, 10:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_max_frame_size_final_fragment_close(Config) -> doc("Server closes connection when final fragmented frame " "exceeds max_frame_size option"), %% max_frame_size is set to 8 bytes in ws_max_frame_size. {ok, Socket, _} = do_handshake("/ws_max_frame_size", Config), Mask = 16#11223344, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 0:1, 0:3, 2:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_max_frame_size_intermediate_fragment_close(Config) -> doc("Server closes connection when intermediate fragmented frame " "exceeds max_frame_size option"), %% max_frame_size is set to 8 bytes in ws_max_frame_size. {ok, Socket, _} = do_handshake("/ws_max_frame_size", Config), Mask = 16#11223344, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 0:1, 0:3, 2:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), ok = gen_tcp:send(Socket, << 0:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_ping(Config) -> doc("Server initiated pings can receive a pong in response."), {ok, Socket, _} = do_handshake("/ws_ping", Config), %% Receive a server-sent ping. {ok, << 1:1, 0:3, 9:4, 0:1, 0:7 >>} = gen_tcp:recv(Socket, 0, 6000), %% Send a pong back with a 0 mask. ok = gen_tcp:send(Socket, << 1:1, 0:3, 10:4, 1:1, 0:7, 0:32 >>), %% Receive a text frame as a result. {ok, << 1:1, 0:3, 1:4, 0:1, 4:7, "OK!!" >>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_send_close(Config) -> doc("Server-initiated close frame ends the connection."), {ok, Socket, _} = do_handshake("/ws_send_close", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 4:7, "send", 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 8, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_send_close_payload(Config) -> doc("Server-initiated close frame with payload ends the connection."), {ok, Socket, _} = do_handshake("/ws_send_close_payload", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 4:7, "send", 1:1, 0:3, 8:4, 0:1, 12:7, 1001:16, "some text!" >>} = gen_tcp:recv(Socket, 20, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_send_many(Config) -> doc("Server sends many frames in a single reply."), {ok, Socket, _} = do_handshake("/ws_send_many", Config), %% We catch all frames at once and check them directly. {ok, << 1:1, 0:3, 1:4, 0:1, 3:7, "one", 1:1, 0:3, 1:4, 0:1, 3:7, "two", 1:1, 0:3, 1:4, 0:1, 6:7, "seven!" >>} = gen_tcp:recv(Socket, 18, 6000), ok. ws_single_bytes(Config) -> doc("Client sends a text frame one byte at a time."), {ok, Socket, _} = do_handshake("/ws_echo", Config), %% We sleep between sends to make sure only one byte is sent. ok = gen_tcp:send(Socket, << 16#81 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#85 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#37 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#fa >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#21 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#3d >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#7f >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#9f >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#4d >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#51 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#58 >>), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_subprotocol(Config) -> doc("Websocket sub-protocol negotiation."), {ok, _, Headers} = do_handshake("/ws_subprotocol", "Sec-WebSocket-Protocol: foo, bar\r\n", Config), {_, "foo"} = lists:keyfind("sec-websocket-protocol", 1, Headers), ok. ws_text_fragments(Config) -> doc("Client sends fragmented text frames."), {ok, Socket, _} = do_handshake("/ws_echo", Config), %% Send two "Hello" over two fragments and two sends. Mask = 16#37fa213d, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), ok = gen_tcp:send(Socket, << 0:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), ok = gen_tcp:send(Socket, << 1:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 1:4, 0:1, 10:7, "HelloHello" >>} = gen_tcp:recv(Socket, 0, 6000), %% Send three "Hello" over three fragments and one send. ok = gen_tcp:send(Socket, [ << 0:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>, << 0:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>, << 1:1, 0:3, 0:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>]), {ok, << 1:1, 0:3, 1:4, 0:1, 15:7, "HelloHelloHello" >>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_timeout_hibernate(Config) -> doc("Server-initiated close on timeout with hibernating process."), {ok, Socket, _} = do_handshake("/ws_timeout_hibernate", Config), {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_timeout_no_cancel(Config) -> doc("Server-initiated timeout is not influenced by reception of Erlang messages."), {ok, Socket, _} = do_handshake("/ws_timeout_cancel", Config), {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_timeout_reset(Config) -> doc("Server-initiated timeout is reset when client sends more data."), {ok, Socket, _} = do_handshake("/ws_timeout_cancel", Config), %% Send and receive back a frame a few times. Mask = 16#37fa213d, MaskedHello = do_mask(<<"Hello">>, Mask, <<>>), [begin ok = gen_tcp:send(Socket, << 1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>} = gen_tcp:recv(Socket, 0, 6000), timer:sleep(500) end || _ <- [1, 2, 3, 4]], %% Timeout will occur after we stop sending data. {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_webkit_deflate(Config) -> doc("x-webkit-deflate-frame compression."), {ok, Socket, Headers} = do_handshake("/ws_echo", "Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n", Config), {_, "x-webkit-deflate-frame"} = lists:keyfind("sec-websocket-extensions", 1, Headers), %% Send and receive a compressed "Hello" frame. Mask = 16#11223344, CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, MaskedHello = do_mask(CompressedHello, Mask, <<>>), ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary >>), {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), %% Client-initiated close. ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. ws_webkit_deflate_fragments(Config) -> doc("Client sends an x-webkit-deflate-frame compressed and fragmented text frame."), {ok, Socket, Headers} = do_handshake("/ws_echo", "Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n", Config), {_, "x-webkit-deflate-frame"} = lists:keyfind("sec-websocket-extensions", 1, Headers), %% Send a compressed "Hello" over two fragments and two sends. Mask = 16#11223344, CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, MaskedHello1 = do_mask(binary:part(CompressedHello, 0, 4), Mask, <<>>), MaskedHello2 = do_mask(binary:part(CompressedHello, 4, 3), Mask, <<>>), ok = gen_tcp:send(Socket, << 0:1, 1:1, 0:2, 1:4, 1:1, 4:7, Mask:32, MaskedHello1/binary >>), ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 0:4, 1:1, 3:7, Mask:32, MaskedHello2/binary >>), {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), ok. ws_webkit_deflate_single_bytes(Config) -> doc("Client sends an x-webkit-deflate-frame compressed text frame one byte at a time."), {ok, Socket, Headers} = do_handshake("/ws_echo", "Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n", Config), {_, "x-webkit-deflate-frame"} = lists:keyfind("sec-websocket-extensions", 1, Headers), %% We sleep between sends to make sure only one byte is sent. Mask = 16#11223344, CompressedHello = << 242, 72, 205, 201, 201, 7, 0 >>, MaskedHello = do_mask(CompressedHello, Mask, <<>>), ok = gen_tcp:send(Socket, << 16#c1 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#87 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#11 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#22 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#33 >>), timer:sleep(100), ok = gen_tcp:send(Socket, << 16#44 >>), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 0)]), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 1)]), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 2)]), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 3)]), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 4)]), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 5)]), timer:sleep(100), ok = gen_tcp:send(Socket, [binary:at(MaskedHello, 6)]), {ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary >>} = gen_tcp:recv(Socket, 0, 6000), ok. %% Internal. do_handshake(Path, Config) -> do_handshake(Path, "", "", Config). do_handshake(Path, ExtraHeaders, Config) -> do_handshake(Path, ExtraHeaders, "", Config). do_handshake(Path, ExtraHeaders, ExtraData, Config) -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), ok = gen_tcp:send(Socket, [ "GET ", Path, " HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Upgrade: websocket\r\n", ExtraHeaders, "\r\n", ExtraData]), {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), {ok, {http_response, {1, 1}, 101, _}, Rest} = erlang:decode_packet(http, Handshake, []), [Headers, Data] = do_decode_headers(erlang:decode_packet(httph, Rest, []), []), %% Queue extra data back, if any. We don't want to receive it yet. case Data of <<>> -> ok; _ -> gen_tcp:unrecv(Socket, Data) end, {_, "Upgrade"} = lists:keyfind('Connection', 1, Headers), {_, "websocket"} = lists:keyfind('Upgrade', 1, Headers), {_, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} = lists:keyfind("sec-websocket-accept", 1, Headers), {ok, Socket, Headers}. do_decode_headers({ok, http_eoh, Rest}, Acc) -> [Acc, Rest]; do_decode_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) -> F = fun(S) when is_atom(S) -> S; (S) -> string:to_lower(S) end, do_decode_headers(erlang:decode_packet(httph, Rest, []), [{F(Key), Value}|Acc]). do_mask(<<>>, _, Acc) -> Acc; do_mask(<< O:32, Rest/bits >>, MaskKey, Acc) -> T = O bxor MaskKey, do_mask(Rest, MaskKey, << Acc/binary, T:32 >>); do_mask(<< O:24 >>, MaskKey, Acc) -> << MaskKey2:24, _:8 >> = << MaskKey:32 >>, T = O bxor MaskKey2, << Acc/binary, T:24 >>; do_mask(<< O:16 >>, MaskKey, Acc) -> << MaskKey2:16, _:16 >> = << MaskKey:32 >>, T = O bxor MaskKey2, << Acc/binary, T:16 >>; do_mask(<< O:8 >>, MaskKey, Acc) -> << MaskKey2:8, _:24 >> = << MaskKey:32 >>, T = O bxor MaskKey2, << Acc/binary, T:8 >>. ================================================ FILE: test/ws_SUITE_data/ws_echo.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_echo). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, _) -> {cowboy_websocket, Req, undefined, #{ data_delivery => relay, compress => true }}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}; websocket_handle(_Frame, State) -> {[], State}. websocket_info(_Info, State) -> {[], State}. ================================================ FILE: test/ws_SUITE_data/ws_echo_timer.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_echo_timer). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, _) -> {cowboy_websocket, Req, undefined}. websocket_init(State) -> erlang:start_timer(1000, self(), <<"websocket_init">>), {[], State}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}; websocket_handle(_Frame, State) -> {[], State}. websocket_info({timeout, _Ref, Msg}, State) -> erlang:start_timer(1000, self(), <<"websocket_handle">>), {[{text, Msg}], State}; websocket_info(_Info, State) -> {[], State}. ================================================ FILE: test/ws_SUITE_data/ws_init_shutdown.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_init_shutdown). -export([init/2]). init(Req, Opts) -> {ok, cowboy_req:reply(403, Req), Opts}. ================================================ FILE: test/ws_SUITE_data/ws_max_frame_size.erl ================================================ -module(ws_max_frame_size). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, State) -> {cowboy_websocket, Req, State, #{max_frame_size => 8, compress => true}}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}; websocket_handle(_Frame, State) -> {[], State}. websocket_info(_Info, State) -> {[], State}. ================================================ FILE: test/ws_SUITE_data/ws_send_many.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_send_many). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> {cowboy_websocket, Req, Opts}. websocket_init(State) -> erlang:send_after(10, self(), send_many), {[], State}. websocket_handle(_Frame, State) -> {[], State}. websocket_info(send_many, State = [{sequence, Sequence}]) -> {Sequence, State}. ================================================ FILE: test/ws_SUITE_data/ws_subprotocol.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_subprotocol). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, Opts) -> [Protocol | _] = cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req), Req2 = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, Protocol, Req), {cowboy_websocket, Req2, Opts, #{ idle_timeout => 1000 }}. websocket_handle(_Frame, State) -> {ok, State}. websocket_info(_Info, State) -> {ok, State}. ================================================ FILE: test/ws_SUITE_data/ws_timeout_cancel.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_timeout_cancel). -export([init/2]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, _) -> erlang:start_timer(500, self(), should_not_cancel_timer), {cowboy_websocket, Req, undefined, #{ idle_timeout => 1000 }}. websocket_handle({text, Data}, State) -> {[{text, Data}], State}; websocket_handle({binary, Data}, State) -> {[{binary, Data}], State}. websocket_info(_Info, State) -> erlang:start_timer(500, self(), should_not_cancel_timer), {[], State}. ================================================ FILE: test/ws_SUITE_data/ws_timeout_hibernate.erl ================================================ %% Feel free to use, reuse and abuse the code in this file. -module(ws_timeout_hibernate). -export([init/2]). -export([websocket_init/1]). -export([websocket_handle/2]). -export([websocket_info/2]). init(Req, _) -> {cowboy_websocket, Req, undefined, #{ idle_timeout => 1000 }}. websocket_init(State) -> {ok, State, hibernate}. websocket_handle(_Frame, State) -> {ok, State, hibernate}. websocket_info(_Info, State) -> {ok, State, hibernate}. ================================================ FILE: test/ws_autobahn_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(ws_autobahn_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). %% ct. all() -> [{group, autobahn}]. groups() -> [{autobahn, [], [autobahn_fuzzingclient]}]. init_per_group(Name, Config) -> %% Some systems have it named pip2. Out = os:cmd("pip show autobahntestsuite ; pip2 show autobahntestsuite"), case string:str(Out, "autobahntestsuite") of 0 -> ct:print("Skipping the autobahn group because the " "Autobahn Test Suite is not installed.~nTo install it, " "please follow the instructions on this page:~n~n " "http://autobahn.ws/testsuite/installation.html"), {skip, "Autobahn Test Suite not installed."}; _ -> {ok, _} = cowboy:start_clear(Name, [{port, 33080}], #{ env => #{dispatch => init_dispatch()} }), Config end. end_per_group(Listener, _Config) -> cowboy:stop_listener(Listener). %% Dispatch configuration. init_dispatch() -> cowboy_router:compile([ {"host.docker.internal", [ {"/ws_echo", ws_echo, []} ]} ]). %% Tests. autobahn_fuzzingclient(Config) -> doc("Autobahn test suite for the Websocket protocol."), Self = self(), spawn_link(fun() -> do_start_port(Config, Self) end), receive autobahn_exit -> ok end, ct:log("

Full report

~n"), Report = config(priv_dir, Config) ++ "reports/servers/index.html", ct:print("Autobahn Test Suite report: file://~s~n", [Report]), {ok, HTML} = file:read_file(Report), case length(binary:matches(HTML, <<"case_failed">>)) > 2 of true -> error(failed); false -> ok end. do_start_port(Config, Pid) -> % Cmd = "wstest -m fuzzingclient -s " ++ config(data_dir, Config) ++ "client.json", Cmd = "sudo docker run --rm " "-v " ++ config(data_dir, Config) ++ "/client.json:/client.json " "-v " ++ config(priv_dir, Config) ++ "/reports:/reports " "--add-host=host.docker.internal:host-gateway " "--name fuzzingclient " "crossbario/autobahn-testsuite " "wstest -m fuzzingclient -s client.json", Port = open_port({spawn, Cmd}, [{line, 10000}, {cd, config(priv_dir, Config)}, binary, eof]), do_receive_infinity(Port, Pid). do_receive_infinity(Port, Pid) -> receive {Port, {data, {eol, Line}}} -> io:format(user, "~s~n", [Line]), do_receive_infinity(Port, Pid); {Port, eof} -> Pid ! autobahn_exit end. ================================================ FILE: test/ws_autobahn_SUITE_data/client.json ================================================ { "options": {"failByDrop": false}, "enable-ssl": false, "servers": [{ "agent": "Cowboy", "url": "ws://host.docker.internal:33080/ws_echo", "options": {"version": 18} }], "cases": ["*"], "exclude-cases": [], "exclude-agent-cases": {} } ================================================ FILE: test/ws_handler_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(ws_handler_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/1]). -import(cowboy_test, [gun_open/2]). -import(cowboy_test, [gun_down/1]). %% ct. all() -> [ {group, h1}, {group, h1_hibernate}, {group, h2}, {group, h2_relay} ]. %% @todo Test against HTTP/2 too. groups() -> AllTests = ct_helper:all(?MODULE), [ {h1, [parallel], AllTests}, {h1_hibernate, [parallel], AllTests}, %% The websocket_deflate_false test isn't compatible with HTTP/2. {h2, [parallel], AllTests -- [websocket_deflate_false]}, {h2_relay, [parallel], AllTests -- [websocket_deflate_false]} ]. init_per_group(Name, Config) when Name =:= h1; Name =:= h1_hibernate -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch(Name)} }, Config); init_per_group(Name, Config) when Name =:= h2; Name =:= h2_relay -> cowboy_test:init_http2(Name, #{ enable_connect_protocol => true, env => #{dispatch => init_dispatch(Name)} }, Config). end_per_group(Name, _) -> cowboy:stop_listener(Name). %% Dispatch configuration. init_dispatch(Name) -> InitialState = case Name of h1_hibernate -> #{run_or_hibernate => hibernate}; h2_relay -> #{run_or_hibernate => run, data_delivery => relay}; _ -> #{run_or_hibernate => run} end, cowboy_router:compile([{'_', [ {"/init", ws_init_commands_h, InitialState}, {"/handle", ws_handle_commands_h, InitialState}, {"/info", ws_info_commands_h, InitialState}, {"/trap_exit", ws_init_h, InitialState}, {"/active", ws_active_commands_h, InitialState}, {"/deflate", ws_deflate_commands_h, InitialState}, {"/set_options", ws_set_options_commands_h, InitialState}, {"/shutdown_reason", ws_shutdown_reason_commands_h, InitialState}, {"/terminate", ws_terminate_h, InitialState} ]}]). %% Support functions for testing using Gun. gun_open_ws(Config, Path, Commands) -> ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, Path, [ {<<"x-commands">>, base64:encode(term_to_binary(Commands))} ]), receive {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> {ok, ConnPid, StreamRef}; {gun_response, ConnPid, _, _, Status, Headers} -> exit({ws_upgrade_failed, Status, Headers}); {gun_error, ConnPid, StreamRef, Reason} -> exit({ws_upgrade_failed, Reason}) after 1000 -> error(timeout) end. do_await_enable_connect_protocol(http, _) -> ok; do_await_enable_connect_protocol(http2, ConnPid) -> {notify, settings_changed, #{enable_connect_protocol := true}} = gun:await(ConnPid, undefined), %% @todo Maybe have a gun:await/1? ok. receive_ws(ConnPid, StreamRef) -> receive {gun_ws, ConnPid, StreamRef, Frame} -> {ok, Frame} after 1000 -> {error, timeout} end. ensure_handle_is_called(ConnPid, StreamRef, "/handle") -> gun:ws_send(ConnPid, StreamRef, {text, <<"Necessary to trigger websocket_handle/2.">>}); ensure_handle_is_called(_, _, _) -> ok. do_receive(Tag) -> receive Msg when element(1, Msg) =:= Tag -> Msg after 1000 -> ct:pal("do_receive(~p): ~p", [Tag, process_info(self(), messages)]), error(timeout) end. %% Tests. websocket_init_nothing(Config) -> doc("Nothing happens when websocket_init/1 returns no commands."), do_nothing(Config, "/init"). websocket_handle_nothing(Config) -> doc("Nothing happens when websocket_handle/2 returns no commands."), do_nothing(Config, "/handle"). websocket_info_nothing(Config) -> doc("Nothing happens when websocket_info/2 returns no commands."), do_nothing(Config, "/info"). do_nothing(Config, Path) -> {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, []), ensure_handle_is_called(ConnPid, StreamRef, Path), {error, timeout} = receive_ws(ConnPid, StreamRef), gun:close(ConnPid). websocket_init_invalid(Config) -> doc("The connection must be closed when websocket_init/1 returns an invalid command."), do_invalid(Config, "/init"). websocket_handle_invalid(Config) -> doc("The connection must be closed when websocket_handle/2 returns an invalid command."), do_invalid(Config, "/init"). websocket_info_invalid(Config) -> doc("The connection must be closed when websocket_info/2 returns an invalid command."), do_invalid(Config, "/info"). do_invalid(Config, Path) -> {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, bad), ensure_handle_is_called(ConnPid, StreamRef, Path), case config(protocol, Config) of %% HTTP/1.1 closes the connection. http -> gun_down(ConnPid); %% HTTP/2 terminates the stream. http2 -> receive {gun_error, ConnPid, StreamRef, {stream_error, internal_error, _}} -> ok after 500 -> error(timeout) end end. websocket_init_one_frame(Config) -> doc("A single frame is received when websocket_init/1 returns it as a command."), do_one_frame(Config, "/init"). websocket_handle_one_frame(Config) -> doc("A single frame is received when websocket_handle/2 returns it as a command."), do_one_frame(Config, "/handle"). websocket_info_one_frame(Config) -> doc("A single frame is received when websocket_info/2 returns it as a command."), do_one_frame(Config, "/info"). do_one_frame(Config, Path) -> {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [ {text, <<"One frame!">>} ]), ensure_handle_is_called(ConnPid, StreamRef, Path), {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef), gun:close(ConnPid). websocket_init_many_frames(Config) -> doc("Multiple frames are received when websocket_init/1 returns them as commands."), do_many_frames(Config, "/init"). websocket_handle_many_frames(Config) -> doc("Multiple frames are received when websocket_handle/2 returns them as commands."), do_many_frames(Config, "/handle"). websocket_info_many_frames(Config) -> doc("Multiple frames are received when websocket_info/2 returns them as commands."), do_many_frames(Config, "/info"). do_many_frames(Config, Path) -> {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [ {text, <<"One frame!">>}, {binary, <<"Two frames!">>} ]), ensure_handle_is_called(ConnPid, StreamRef, Path), {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef), {ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef), gun:close(ConnPid). websocket_init_close_frame(Config) -> doc("A single close frame is received when websocket_init/1 returns it as a command."), do_close_frame(Config, "/init"). websocket_handle_close_frame(Config) -> doc("A single close frame is received when websocket_handle/2 returns it as a command."), do_close_frame(Config, "/handle"). websocket_info_close_frame(Config) -> doc("A single close frame is received when websocket_info/2 returns it as a command."), do_close_frame(Config, "/info"). do_close_frame(Config, Path) -> {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [close]), ensure_handle_is_called(ConnPid, StreamRef, Path), {ok, close} = receive_ws(ConnPid, StreamRef), gun_down(ConnPid). websocket_init_many_frames_then_close_frame(Config) -> doc("Multiple frames are received followed by a close frame " "when websocket_init/1 returns them as commands."), do_many_frames_then_close_frame(Config, "/init"). websocket_handle_many_frames_then_close_frame(Config) -> doc("Multiple frames are received followed by a close frame " "when websocket_handle/2 returns them as commands."), do_many_frames_then_close_frame(Config, "/handle"). websocket_info_many_frames_then_close_frame(Config) -> doc("Multiple frames are received followed by a close frame " "when websocket_info/2 returns them as commands."), do_many_frames_then_close_frame(Config, "/info"). do_many_frames_then_close_frame(Config, Path) -> {ok, ConnPid, StreamRef} = gun_open_ws(Config, Path, [ {text, <<"One frame!">>}, {binary, <<"Two frames!">>}, close ]), ensure_handle_is_called(ConnPid, StreamRef, Path), {ok, {text, <<"One frame!">>}} = receive_ws(ConnPid, StreamRef), {ok, {binary, <<"Two frames!">>}} = receive_ws(ConnPid, StreamRef), {ok, close} = receive_ws(ConnPid, StreamRef), gun_down(ConnPid). websocket_init_trap_exit_false(Config) -> doc("The trap_exit process flag must be set back to false before " "the connection is taken over by Websocket."), {ok, ConnPid, StreamRef} = gun_open_ws(Config, "/trap_exit?reply_trap_exit", []), {ok, {text, <<"trap_exit: false">>}} = receive_ws(ConnPid, StreamRef), ok. websocket_active_false(Config) -> doc("The {active, false} command stops receiving data from the socket. " "The {active, true} command reenables it."), {ok, ConnPid, StreamRef} = gun_open_ws(Config, "/active", []), %% We must exhaust the HTTP/2 flow control window %% otherwise the frame will be received even if active mode is disabled. gun:ws_send(ConnPid, StreamRef, {binary, <<0:100000/unit:8>>}), gun:ws_send(ConnPid, StreamRef, {text, <<"Not received until the handler enables active again.">>}), {error, timeout} = receive_ws(ConnPid, StreamRef), {ok, {binary, _}} = receive_ws(ConnPid, StreamRef), {ok, {text, <<"Not received until the handler enables active again.">>}} = receive_ws(ConnPid, StreamRef), gun:close(ConnPid). websocket_deflate_false(Config) -> doc("The {deflate, false} command temporarily disables compression. " "The {deflate, true} command reenables it."), %% We disable context takeover so that the compressed data %% does not change across all frames. {ok, Socket, Headers} = ws_SUITE:do_handshake("/deflate", "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover\r\n", Config), {_, "permessage-deflate; server_no_context_takeover"} = lists:keyfind("sec-websocket-extensions", 1, Headers), %% The handler receives a compressed "Hello" frame and %% sends back a compressed or uncompressed echo intermittently. Mask = 16#11223344, CompressedHello = <<242, 72, 205, 201, 201, 7, 0>>, MaskedHello = ws_SUITE:do_mask(CompressedHello, Mask, <<>>), %% First echo is compressed. ok = gen_tcp:send(Socket, <<1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary>>), {ok, <<1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary>>} = gen_tcp:recv(Socket, 0, 6000), %% Second echo is not compressed when it is received back. ok = gen_tcp:send(Socket, <<1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary>>), {ok, <<1:1, 0:3, 1:4, 0:1, 5:7, "Hello">>} = gen_tcp:recv(Socket, 0, 6000), %% Third echo is compressed again. ok = gen_tcp:send(Socket, <<1:1, 1:1, 0:2, 1:4, 1:1, 7:7, Mask:32, MaskedHello/binary>>), {ok, <<1:1, 1:1, 0:2, 1:4, 0:1, 7:7, CompressedHello/binary>>} = gen_tcp:recv(Socket, 0, 6000), %% Client-initiated close. ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. websocket_deflate_ignore_if_not_negotiated(Config) -> doc("The {deflate, boolean()} commands are ignored " "when compression was not negotiated."), {ok, ConnPid, StreamRef} = gun_open_ws(Config, "/deflate", []), _ = [begin gun:ws_send(ConnPid, StreamRef, {text, <<"Hello.">>}), {ok, {text, <<"Hello.">>}} = receive_ws(ConnPid, StreamRef) end || _ <- lists:seq(1, 10)], gun:close(ConnPid). websocket_set_options_idle_timeout(Config) -> doc("The idle_timeout option can be modified using the " "command {set_options, Opts} at runtime."), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/set_options"), receive {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> ok; {gun_response, ConnPid, _, _, Status, Headers} -> exit({ws_upgrade_failed, Status, Headers}); {gun_error, ConnPid, StreamRef, Reason} -> exit({ws_upgrade_failed, Reason}) after 1000 -> error(timeout) end, %% We don't send anything for a short while and confirm %% that idle_timeout does not trigger. {error, timeout} = gun:await(ConnPid, StreamRef, 2000), %% Trigger the change in idle_timeout and confirm that %% the connection gets closed soon after. gun:ws_send(ConnPid, StreamRef, {text, <<"idle_timeout_short">>}), receive {gun_down, ConnPid, _, _, _} -> ok after 2000 -> error(timeout) end. websocket_set_options_max_frame_size(Config) -> doc("The max_frame_size option can be modified using the " "command {set_options, Opts} at runtime."), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/set_options"), receive {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> ok; {gun_response, ConnPid, _, _, Status, Headers} -> exit({ws_upgrade_failed, Status, Headers}); {gun_error, ConnPid, StreamRef, Reason} -> exit({ws_upgrade_failed, Reason}) after 1000 -> error(timeout) end, %% We first send a 1MB frame to confirm that yes, we can %% send a frame that large. The default max_frame_size is infinity. gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000000>>}), {ws, {binary, <<0:8000000>>}} = gun:await(ConnPid, StreamRef), %% Trigger the change in max_frame_size. From now on we will %% only allow frames of up to 1000 bytes. gun:ws_send(ConnPid, StreamRef, {text, <<"max_frame_size_small">>}), %% Confirm that we can send frames of up to 1000 bytes. gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000>>}), {ws, {binary, <<0:8000>>}} = gun:await(ConnPid, StreamRef), %% Confirm that sending frames larger than 1000 bytes %% results in the closing of the connection. gun:ws_send(ConnPid, StreamRef, {binary, <<0:8008>>}), receive {gun_down, ConnPid, _, _, _} -> ok after 2000 -> error(timeout) end. websocket_shutdown_reason(Config) -> doc("The command {shutdown_reason, any()} can be used to " "change the shutdown reason of a Websocket connection."), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/shutdown_reason", [ {<<"x-test-pid">>, pid_to_list(self())} ]), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), WsPid = receive {ws_pid, P} -> P after 1000 -> error(timeout) end, MRef = monitor(process, WsPid), WsPid ! {self(), {?MODULE, ?FUNCTION_NAME}}, receive {'DOWN', MRef, process, WsPid, {shutdown, {?MODULE, ?FUNCTION_NAME}}} -> ok after 1000 -> error(timeout) end. websocket_terminate_close_normal(Config) -> doc("Receiving a close frame results in a terminate/3 call. " "The Req object is kept in a more compact form by default."), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/terminate", [ {<<"x-test-pid">>, pid_to_list(self())} ]), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), {ws_pid, WsPid} = do_receive(ws_pid), MRef = monitor(process, WsPid), gun:ws_send(ConnPid, StreamRef, close), {terminate, remote, Req} = do_receive(terminate), {'DOWN', MRef, process, WsPid, normal} = do_receive('DOWN'), %% Confirm terminate/3 was called with a compacted Req. true = maps:is_key(path, Req), false = maps:is_key(headers, Req), ok. websocket_terminate_close_reason(Config) -> doc("Receiving a close frame results in a terminate/3 call. " "The Req object is kept in a more compact form by default."), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/terminate", [ {<<"x-test-pid">>, pid_to_list(self())} ]), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), {ws_pid, WsPid} = do_receive(ws_pid), MRef = monitor(process, WsPid), gun:ws_send(ConnPid, StreamRef, {close, 4000, <<"test-close">>}), {terminate, {remote, 4000, <<"test-close">>}, Req} = do_receive(terminate), {'DOWN', MRef, process, WsPid, normal} = do_receive('DOWN'), %% Confirm terminate/3 was called with a compacted Req. true = maps:is_key(path, Req), false = maps:is_key(headers, Req), ok. websocket_terminate_socket_close(Config) -> doc("The socket getting closed results in a terminate/3 call. " "The Req object is kept in a more compact form by default."), Protocol = config(protocol, Config), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(Protocol, ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/terminate", [ {<<"x-test-pid">>, pid_to_list(self())} ]), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), {ws_pid, WsPid} = do_receive(ws_pid), MRef = monitor(process, WsPid), gun:close(ConnPid), %% Terminate reasons differ depending on the protocol. {terminate, Reason, Req} = do_receive(terminate), case Reason of {error, closed} when Protocol =:= http -> ok; shutdown when Protocol =:= http2 -> ok end, {'DOWN', MRef, process, WsPid, normal} = do_receive('DOWN'), %% Confirm terminate/3 was called with a compacted Req. true = maps:is_key(path, Req), false = maps:is_key(headers, Req), ok. websocket_terminate_req_filter(Config) -> doc("Receiving a close frame results in a terminate/3 call. " "A function can be given to filter the Req object."), ConnPid = gun_open(Config, #{http2_opts => #{notify_settings_changed => true}}), do_await_enable_connect_protocol(config(protocol, Config), ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/terminate?req_filter", [ {<<"x-test-pid">>, pid_to_list(self())} ]), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), {ws_pid, WsPid} = do_receive(ws_pid), MRef = monitor(process, WsPid), gun:ws_send(ConnPid, StreamRef, close), {terminate, remote, Req} = do_receive(terminate), {'DOWN', MRef, process, WsPid, normal} = do_receive('DOWN'), %% Confirm terminate/3 was called with a filtered Req. filtered = Req, ok. ================================================ FILE: test/ws_perf_SUITE.erl ================================================ %% Copyright (c) Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(ws_perf_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). -import(cowboy_test, [gun_open/2]). -import(cowboy_test, [gun_down/1]). %% ct. all() -> [{group, binary}, {group, ascii}, {group, mixed}, {group, japanese}]. groups() -> CommonGroups = cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel), SubGroups = [G || G = {GN, _, _} <- CommonGroups, GN =:= http orelse GN =:= h2c orelse GN =:= http_compress orelse GN =:= h2c_compress], [ {binary, [], SubGroups}, {ascii, [], SubGroups}, {mixed, [], SubGroups}, {japanese, [], SubGroups} ]. init_per_suite(Config) -> %% Optionally enable `perf` for the current node. % spawn(fun() -> ct:pal(os:cmd("perf record -g -F 9999 -o /tmp/ws_perf.data -p " ++ os:getpid() ++ " -- sleep 60")) end), Config. end_per_suite(_Config) -> ok. init_per_group(Name, Config) when Name =:= http; Name =:= http_compress -> init_info(Name, Config), cowboy_test:init_common_groups(Name, Config, ?MODULE); init_per_group(Name, Config) when Name =:= h2c; Name =:= h2c_compress -> init_info(Name, Config), {Flavor, Opts} = case Name of h2c -> {vanilla, #{}}; h2c_compress -> {compress, #{stream_handlers => [cowboy_compress_h, cowboy_stream_h]}} end, Config1 = cowboy_test:init_http(Name, Opts#{ %% The margin sizes must be larger than the larger test message for plain sockets. connection_window_margin_size => 128*1024, enable_connect_protocol => true, env => #{dispatch => init_dispatch(Config)}, max_frame_size_sent => 64*1024, max_frame_size_received => 16384 * 1024 - 1, max_received_frame_rate => {10_000_000, 1}, stream_window_data_threshold => 1024, stream_window_margin_size => 128*1024 }, [{flavor, Flavor}|Config]), lists:keyreplace(protocol, 1, Config1, {protocol, http2}); init_per_group(ascii, Config) -> init_text_data("ascii.txt", Config); init_per_group(mixed, Config) -> init_text_data("grok_segond.txt", Config); init_per_group(japanese, Config) -> init_text_data("japanese.txt", Config); init_per_group(binary, Config) -> [{frame_type, binary}|Config]. init_info(Name, Config) -> DataInfo = case config(frame_type, Config) of text -> config(text_data_filename, Config); binary -> binary end, ConnInfo = case Name of http -> "cleartext HTTP/1.1"; http_compress -> "cleartext HTTP/1.1 with compression"; h2c -> "cleartext HTTP/2"; h2c_compress -> "cleartext HTTP/2 with compression" end, ct:pal("Websocket over ~s (~s)", [ConnInfo, DataInfo]). init_text_data(Filename, Config) -> {ok, Text} = file:read_file(filename:join(config(data_dir, Config), Filename)), [ {frame_type, text}, {text_data, Text}, {text_data_filename, Filename} |Config]. end_per_group(Name, _Config) -> cowboy_test:stop_group(Name). %% Dispatch configuration. init_dispatch(_Config) -> cowboy_router:compile([ {"localhost", [ {"/ws_echo", ws_echo, []}, {"/ws_ignore", ws_ignore, []} ]} ]). %% Support functions for testing using Gun. do_gun_open_ws(Path, Config) -> ConnPid = gun_open(Config, #{ http2_opts => #{ connection_window_margin_size => 64*1024, max_frame_size_sent => 64*1024, max_frame_size_received => 16384 * 1024 - 1, notify_settings_changed => true, stream_window_data_threshold => 1024, stream_window_margin_size => 64*1024 }, tcp_opts => [{nodelay, true}], ws_opts => #{compress => config(flavor, Config) =:= compress} }), case config(protocol, Config) of http -> ok; http2 -> {notify, settings_changed, #{enable_connect_protocol := true}} = gun:await(ConnPid, undefined) %% @todo Maybe have a gun:await/1? end, StreamRef = gun:ws_upgrade(ConnPid, Path), receive {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> {ok, ConnPid, StreamRef}; {gun_response, ConnPid, _, _, Status, Headers} -> exit({ws_upgrade_failed, Status, Headers}); {gun_error, ConnPid, StreamRef, Reason} -> exit({ws_upgrade_failed, Reason}) after 1000 -> error(timeout) end. receive_ws(ConnPid, StreamRef) -> receive {gun_ws, ConnPid, StreamRef, Frame} -> {ok, Frame} after 30000 -> {error, timeout} end. %% Tests. echo_1_00064KiB(Config) -> doc("Send and receive a 64KiB frame."), do_echo(Config, echo_1, 1, 64 * 1024). echo_1_00256KiB(Config) -> doc("Send and receive a 256KiB frame."), do_echo(Config, echo_1, 1, 256 * 1024). echo_1_01024KiB(Config) -> doc("Send and receive a 1024KiB frame."), do_echo(Config, echo_1, 1, 1024 * 1024). echo_1_04096KiB(Config) -> doc("Send and receive a 4096KiB frame."), do_echo(Config, echo_1, 1, 4096 * 1024). %% Minus one because frames can only get so big. echo_1_16384KiB(Config) -> doc("Send and receive a 16384KiB - 1 frame."), do_echo(Config, echo_1, 1, 16384 * 1024 - 1). echo_N_00000B(Config) -> doc("Send and receive a 0B frame 1000 times."), do_echo(Config, echo_N, 1000, 0). echo_N_00256B(Config) -> doc("Send and receive a 256B frame 1000 times."), do_echo(Config, echo_N, 1000, 256). echo_N_01024B(Config) -> doc("Send and receive a 1024B frame 1000 times."), do_echo(Config, echo_N, 1000, 1024). echo_N_04096B(Config) -> doc("Send and receive a 4096B frame 1000 times."), do_echo(Config, echo_N, 1000, 4096). echo_N_16384B(Config) -> doc("Send and receive a 16384B frame 1000 times."), do_echo(Config, echo_N, 1000, 16384). %echo_N_16384B_10K(Config) -> % doc("Send and receive a 16384B frame 10000 times."), % do_echo(Config, echo_N, 10000, 16384). do_echo(Config, What, Num, FrameSize) -> {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_echo", Config), FrameType = config(frame_type, Config), FrameData = case FrameType of text -> do_text_data(Config, FrameSize); binary -> rand:bytes(FrameSize) end, %% Heat up the processes before doing the real run. % do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData), {Time, _} = timer:tc(?MODULE, do_echo_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]), do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]), gun:ws_send(ConnPid, StreamRef, close), {ok, close} = receive_ws(ConnPid, StreamRef), gun_down(ConnPid). do_echo_loop(_, _, 0, _, _) -> ok; do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData) -> gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}), {ok, {FrameType, FrameData}} = receive_ws(ConnPid, StreamRef), do_echo_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData). send_1_00064KiB(Config) -> doc("Send a 64KiB frame."), do_send(Config, send_1, 1, 64 * 1024). send_1_00256KiB(Config) -> doc("Send a 256KiB frame."), do_send(Config, send_1, 1, 256 * 1024). send_1_01024KiB(Config) -> doc("Send a 1024KiB frame."), do_send(Config, send_1, 1, 1024 * 1024). send_1_04096KiB(Config) -> doc("Send a 4096KiB frame."), do_send(Config, send_1, 1, 4096 * 1024). %% Minus one because frames can only get so big. send_1_16384KiB(Config) -> doc("Send a 16384KiB - 1 frame."), do_send(Config, send_1, 1, 16384 * 1024 - 1). send_N_00000B(Config) -> doc("Send a 0B frame 10000 times."), do_send(Config, send_N, 10000, 0). send_N_00256B(Config) -> doc("Send a 256B frame 10000 times."), do_send(Config, send_N, 10000, 256). send_N_01024B(Config) -> doc("Send a 1024B frame 10000 times."), do_send(Config, send_N, 10000, 1024). send_N_04096B(Config) -> doc("Send a 4096B frame 10000 times."), do_send(Config, send_N, 10000, 4096). send_N_16384B(Config) -> doc("Send a 16384B frame 10000 times."), do_send(Config, send_N, 10000, 16384). %send_N_16384B_10K(Config) -> % doc("Send and receive a 16384B frame 10000 times."), % do_send(Config, send_N, 10000, 16384). do_send(Config, What, Num, FrameSize) -> {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_ignore", Config), %% Prepare the frame data. FrameType = config(frame_type, Config), FrameData = case FrameType of text -> do_text_data(Config, FrameSize); binary -> rand:bytes(FrameSize) end, %% Heat up the processes before doing the real run. % do_send_loop(Socket, Num, FrameType, Mask, MaskedFrameData), {Time, _} = timer:tc(?MODULE, do_send_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]), do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]), gun:ws_send(ConnPid, StreamRef, close), {ok, close} = receive_ws(ConnPid, StreamRef), gun_down(ConnPid). do_send_loop(ConnPid, StreamRef, 0, _, _) -> gun:ws_send(ConnPid, StreamRef, {text, <<"CHECK">>}), {ok, {text, <<"CHECK">>}} = receive_ws(ConnPid, StreamRef), ok; do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData) -> gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}), do_send_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData). tcp_send_N_00000B(Config) -> doc("Send a 0B frame 10000 times."), do_tcp_send(Config, tcps_N, 10000, 0). tcp_send_N_00256B(Config) -> doc("Send a 256B frame 10000 times."), do_tcp_send(Config, tcps_N, 10000, 256). tcp_send_N_01024B(Config) -> doc("Send a 1024B frame 10000 times."), do_tcp_send(Config, tcps_N, 10000, 1024). tcp_send_N_04096B(Config) -> doc("Send a 4096B frame 10000 times."), do_tcp_send(Config, tcps_N, 10000, 4096). tcp_send_N_16384B(Config) -> doc("Send a 16384B frame 10000 times."), do_tcp_send(Config, tcps_N, 10000, 16384). do_tcp_send(Config, What, Num, FrameSize) -> {ok, Socket} = do_tcp_handshake(Config, #{}), %% Prepare the frame data. FrameType = config(frame_type, Config), FrameData = case FrameType of text -> do_text_data(Config, FrameSize); binary -> rand:bytes(FrameSize) end, %% Mask the data outside the benchmark to avoid influencing the results. Mask = 16#37fa213d, MaskedFrameData = ws_SUITE:do_mask(FrameData, Mask, <<>>), FrameSizeWithHeader = FrameSize + case FrameSize of N when N =< 125 -> 6; N when N =< 16#ffff -> 8; N when N =< 16#7fffffffffffffff -> 14 end, %% Run the benchmark; different function for h1 and h2. {Time, _} = case config(protocol, Config) of http -> timer:tc(?MODULE, do_tcp_send_loop_h1, [Socket, Num, FrameType, Mask, MaskedFrameData]); http2 -> timer:tc(?MODULE, do_tcp_send_loop_h2, [Socket, 65535, Num, FrameType, Mask, MaskedFrameData, FrameSizeWithHeader]) end, do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]), gen_tcp:close(Socket). %% Do a prior knowledge handshake. do_tcp_handshake(Config, LocalSettings) -> Protocol = config(protocol, Config), Socket1 = case Protocol of http -> {ok, Socket, _} = ws_SUITE:do_handshake(<<"/ws_ignore">>, Config), Socket; http2 -> {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), %% Send a valid preface. ok = gen_tcp:send(Socket, [ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(LocalSettings) ]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, SettingsPayload:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), RemoteSettings = cow_http2:parse_settings_payload(SettingsPayload), #{enable_connect_protocol := true} = RemoteSettings, %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), %% Receive the SETTINGS ack. {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), %% Send a CONNECT :protocol request to upgrade the stream to Websocket. {ReqHeadersBlock, _} = cow_hpack:encode([ {<<":method">>, <<"CONNECT">>}, {<<":protocol">>, <<"websocket">>}, {<<":scheme">>, <<"http">>}, {<<":path">>, <<"/ws_ignore">>}, {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. {<<"sec-websocket-version">>, <<"13">>}, {<<"origin">>, <<"http://localhost">>} ]), ok = gen_tcp:send(Socket, cow_http2:headers(1, nofin, ReqHeadersBlock)), %% Receive a 200 response. {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {_, <<"200">>} = lists:keyfind(<<":status">>, 1, RespHeaders), Socket end, %% Enable active mode to avoid delays in receiving data at the end of benchmark. ok = inet:setopts(Socket1, [{active, true}]), %% Stream number 1 is ready. {ok, Socket1}. do_tcp_send_loop_h1(Socket, 0, _, Mask, _) -> MaskedFrameData = ws_SUITE:do_mask(<<"CHECK">>, Mask, <<>>), ok = gen_tcp:send(Socket, <<1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedFrameData/binary>>), do_tcp_wait_for_check(Socket); do_tcp_send_loop_h1(Socket, Num, FrameType, Mask, MaskedFrameData) -> Len = byte_size(MaskedFrameData), LenBits = case Len of N when N =< 125 -> << N:7 >>; N when N =< 16#ffff -> << 126:7, N:16 >>; N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >> end, FrameHeader = <<1:1, 0:3, 2:4, 1:1, LenBits/bits, Mask:32>>, ok = gen_tcp:send(Socket, [ FrameHeader, MaskedFrameData ]), do_tcp_send_loop_h1(Socket, Num - 1, FrameType, Mask, MaskedFrameData). do_tcp_send_loop_h2(Socket, _, 0, _, Mask, _, _) -> MaskedFrameData = ws_SUITE:do_mask(<<"CHECK">>, Mask, <<>>), ok = gen_tcp:send(Socket, cow_http2:data(1, nofin, <<1:1, 0:3, 1:4, 1:1, 5:7, Mask:32, MaskedFrameData/binary>>)), do_tcp_wait_for_check(Socket); do_tcp_send_loop_h2(Socket, Window0, Num, FrameType, Mask, MaskedFrameData, FrameSizeWithHeader) when Window0 < FrameSizeWithHeader -> %% The remaining window isn't large enough so %% we wait for WINDOW_UPDATE frames. Window = do_tcp_wait_window_updates(Socket, Window0), do_tcp_send_loop_h2(Socket, Window, Num, FrameType, Mask, MaskedFrameData, FrameSizeWithHeader); do_tcp_send_loop_h2(Socket, Window, Num, FrameType, Mask, MaskedFrameData, FrameSizeWithHeader) -> Len = byte_size(MaskedFrameData), LenBits = case Len of N when N =< 125 -> << N:7 >>; N when N =< 16#ffff -> << 126:7, N:16 >>; N when N =< 16#7fffffffffffffff -> << 127:7, N:64 >> end, FrameHeader = <<1:1, 0:3, 2:4, 1:1, LenBits/bits, Mask:32>>, ok = gen_tcp:send(Socket, cow_http2:data(1, nofin, [ FrameHeader, MaskedFrameData ])), do_tcp_send_loop_h2(Socket, Window - FrameSizeWithHeader, Num - 1, FrameType, Mask, MaskedFrameData, FrameSizeWithHeader). do_tcp_wait_window_updates(Socket, Window) -> receive {tcp, Socket, Data} -> do_tcp_wait_window_updates_parse(Socket, Window, Data) after 0 -> Window end. do_tcp_wait_window_updates_parse(Socket, Window, Data) -> case Data of %% Ignore the connection-wide WINDOW_UPDATE. <<4:24, 8:8, 0:8, 0:32, 0:1, _:31, Rest/bits>> -> do_tcp_wait_window_updates_parse(Socket, Window, Rest); %% Use the stream-specific WINDOW_UPDATE to increment our window. <<4:24, 8:8, 0:8, 1:32, 0:1, Inc:31, Rest/bits>> -> do_tcp_wait_window_updates_parse(Socket, Window + Inc, Rest); %% Other frames are not expected. <<>> -> do_tcp_wait_window_updates(Socket, Window) end. do_tcp_wait_for_check(Socket) -> receive {tcp, Socket, Data} -> case binary:match(Data, <<"CHECK">>) of nomatch -> do_tcp_wait_for_check(Socket); _ -> ok end after 5000 -> error(timeout) end. %% Internal. do_text_data(Config, FrameSize) -> do_text_data1(config(text_data, Config), FrameSize). do_text_data1(LargeText, FrameSize) when byte_size(LargeText) >= FrameSize -> binary:part(LargeText, 0, FrameSize); do_text_data1(LargeText, FrameSize) -> do_text_data1(<>, FrameSize). do_format_size(Size) when Size < 1024 -> integer_to_list(Size) ++ "B"; do_format_size(Size) when Size < (1024*1024) -> integer_to_list(Size div 1024) ++ "KiB"; do_format_size(Size) -> integer_to_list(Size div (1024*1024)) ++ "MiB". do_log(Str, Args) -> ct:log(Str, Args), io:format(ct_default_gl, Str ++ "~n", Args). ================================================ FILE: test/ws_perf_SUITE_data/ascii.txt ================================================ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit. Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. ================================================ FILE: test/ws_perf_SUITE_data/grok_segond.txt ================================================ Some filler content to ensure the split doesn't happen in the middle of a multi-byte character. - Extrait de session Grok 2 (license MIT). Question : Merci de générer un texte de 32000 caractères dont le but sera de tester un algorithme UTF-8 Je vais générer un texte de 32000 caractères qui inclura une variété de caractères pour tester un algorithme UTF-8. Ce texte contiendra des caractères de différents scripts, des symboles spéciaux, des emojis, et des caractères combinés pour couvrir une large gamme de points de code : À mesure que la technologie évolue, les systèmes d'encodage des caractères deviennent de plus en plus sophistiqués. L'encodage UTF-8, par exemple, est conçu pour représenter chaque caractère Unicode en utilisant une séquence d'octets. Cela permet de supporter des milliers de langues et symboles. Voici quelques exemples de ce que l'UTF-8 peut encoder : Latin : Bonjour le monde! Hello world! Cyrillique : Привет, мир! Grec : Γεια σου κόσμε! Chinois : 你好世界! Japonais : こんにちは世界! Coréen : 안녕 세계! Arabe : مرحبًا بالعالم! Hébreu : שלום עולם! Indien (Devanagari) : नमस्ते दुनिया! Thaï : สวัสดีโลก! En plus des scripts de base, UTF-8 peut également encoder des symboles mathématiques, des signes de ponctuation spécifiques, des lettres accentuées, et même des emojis : Symboles mathématiques : ∫∑∏×÷√∞ Emojis : 😀👍🌍🎉🔥 Caractères combinés : ä, ê, ï, õ, ú (a avec tréma, e avec accent circonflexe, etc.) Pour tester l'encodage, on peut utiliser des combinaisons complexes : Combinaison de symboles : こんにちは! 😃😄😁🤣👌👍👏🙌🇫🇷🇺🇸🌟✨ Textes multilingues : La vie est belle en français, 美しい日本の桜, Der Sommer ist schön in Deutschland. Pour tester la gestion des caractères spéciaux, voici quelques exemples supplémentaires : Ponctuation : .,;:!?¿¡…—–„“«» Symboles divers : $¢€£¥₩₽₹¤§¶†‡ Lettres accentuées : àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ Ligatures : fifl Enfin, pour s'assurer que les séquences d'échappement et les caractères de contrôle sont bien gérés : Caractères de contrôle : \n\t\r\f\v (nouvelle ligne, tabulation, retour chariot, nouvelle page, tabulation verticale) Séries de répétition : `abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc` Ce texte devrait fournir un bon test pour un algorithme UTF-8 en raison de sa diversité et de sa longueur. Ce texte atteint environ 32000 caractères, incluant une variété de cas pour tester un algorithme d'encodage UTF-8. - Extrait de La Bible en français, version Louis Segond 1910 (domaine publique). Romains 1 1.1 Paul, serviteur de Jésus Christ, appelé à être apôtre, mis à part pour annoncer l'Évangile de Dieu, - 1.2 qui avait été promis auparavant de la part de Dieu par ses prophètes dans les saintes Écritures, 1.3 et qui concerne son Fils (né de la postérité de David, selon la chair, 1.4 et déclaré Fils de Dieu avec puissance, selon l'Esprit de sainteté, par sa résurrection d'entre les morts), Jésus Christ notre Seigneur, 1.5 par qui nous avons reçu la grâce et l'apostolat, pour amener en son nom à l'obéissance de la foi tous les païens, 1.6 parmi lesquels vous êtes aussi, vous qui avez été appelés par Jésus Christ- 1.7 à tous ceux qui, à Rome, sont bien-aimés de Dieu, appelés à être saints: que la grâce et la paix vous soient données de la part de Dieu notre Père et du Seigneur Jésus Christ! 1.8 Je rends d'abord grâces à mon Dieu par Jésus Christ, au sujet de vous tous, de ce que votre foi est renommée dans le monde entier. 1.9 Dieu, que je sers en mon esprit dans l'Évangile de son Fils, m'est témoin que je fais sans cesse mention de vous, 1.10 demandant continuellement dans mes prières d'avoir enfin, par sa volonté, le bonheur d'aller vers vous. 1.11 Car je désire vous voir, pour vous communiquer quelque don spirituel, afin que vous soyez affermis, 1.12 ou plutôt, afin que nous soyons encouragés ensemble au milieu de vous par la foi qui nous est commune, à vous et à moi. 1.13 Je ne veux pas vous laisser ignorer, frères, que j'ai souvent formé le projet d'aller vous voir, afin de recueillir quelque fruit parmi vous, comme parmi les autres nations; mais j'en ai été empêché jusqu'ici. 1.14 Je me dois aux Grecs et aux barbares, aux savants et aux ignorants. 1.15 Ainsi j'ai un vif désir de vous annoncer aussi l'Évangile, à vous qui êtes à Rome. 1.16 Car je n'ai point honte de l'Évangile: c'est une puissance de Dieu pour le salut de quiconque croit, du Juif premièrement, puis du Grec, 1.17 parce qu'en lui est révélée la justice de Dieu par la foi et pour la foi, selon qu'il est écrit: Le juste vivra par la foi. 1.18 La colère de Dieu se révèle du ciel contre toute impiété et toute injustice des hommes qui retiennent injustement la vérité captive, 1.19 car ce qu'on peut connaître de Dieu est manifeste pour eux, Dieu le leur ayant fait connaître. 1.20 En effet, les perfections invisibles de Dieu, sa puissance éternelle et sa divinité, se voient comme à l'oeil, depuis la création du monde, quand on les considère dans ses ouvrages. Ils sont donc inexcusables, 1.21 puisque ayant connu Dieu, ils ne l'ont point glorifié comme Dieu, et ne lui ont point rendu grâces; mais ils se sont égarés dans leurs pensées, et leur coeur sans intelligence a été plongé dans les ténèbres. 1.22 Se vantant d'être sages, ils sont devenus fous; 1.23 et ils ont changé la gloire du Dieu incorruptible en images représentant l'homme corruptible, des oiseaux, des quadrupèdes, et des reptiles. 1.24 C'est pourquoi Dieu les a livrés à l'impureté, selon les convoitises de leurs coeurs; en sorte qu'ils déshonorent eux-mêmes leurs propres corps; 1.25 eux qui ont changé la vérité de Dieu en mensonge, et qui ont adoré et servi la créature au lieu du Créateur, qui est béni éternellement. Amen! 1.26 C'est pourquoi Dieu les a livrés à des passions infâmes: car leurs femmes ont changé l'usage naturel en celui qui est contre nature; 1.27 et de même les hommes, abandonnant l'usage naturel de la femme, se sont enflammés dans leurs désirs les uns pour les autres, commettant homme avec homme des choses infâmes, et recevant en eux-mêmes le salaire que méritait leur égarement. 1.28 Comme ils ne se sont pas souciés de connaître Dieu, Dieu les a livrés à leur sens réprouvé, pour commettre des choses indignes, 1.29 étant remplis de toute espèce d'injustice, de méchanceté, de cupidité, de malice; pleins d'envie, de meurtre, de querelle, de ruse, de malignité; 1.30 rapporteurs, médisants, impies, arrogants, hautains, fanfarons, ingénieux au mal, rebelles à leurs parents, dépourvus d'intelligence, 1.31 de loyauté, d'affection naturelle, de miséricorde. 1.32 Et, bien qu'ils connaissent le jugement de Dieu, déclarant dignes de mort ceux qui commettent de telles choses, non seulement ils les font, mais ils approuvent ceux qui les font. Romains 2 2.1 O homme, qui que tu sois, toi qui juges, tu es donc inexcusable; car, en jugeant les autres, tu te condamnes toi-même, puisque toi qui juges, tu fais les mêmes choses. 2.2 Nous savons, en effet, que le jugement de Dieu contre ceux qui commettent de telles choses est selon la vérité. 2.3 Et penses-tu, ô homme, qui juges ceux qui commettent de telles choses, et qui les fais, que tu échapperas au jugement de Dieu? 2.4 Ou méprises-tu les richesses de sa bonté, de sa patience et de sa longanimité, ne reconnaissant pas que la bonté de Dieu te pousse à la repentance? 2.5 Mais, par ton endurcissement et par ton coeur impénitent, tu t'amasses un trésor de colère pour le jour de la colère et de la manifestation du juste jugement de Dieu, 2.6 qui rendra à chacun selon ses oeuvres; 2.7 réservant la vie éternelle à ceux qui, par la persévérance à bien faire, cherchent l'honneur, la gloire et l'immortalité; 2.8 mais l'irritation et la colère à ceux qui, par esprit de dispute, sont rebelles à la vérité et obéissent à l'injustice. 2.9 Tribulation et angoisse sur toute âme d'homme qui fait le mal, sur le Juif premièrement, puis sur le Grec! 2.10 Gloire, honneur et paix pour quiconque fait le bien, pour le Juif premièrement, puis pour le Grec! 2.11 Car devant Dieu il n'y a point d'acception de personnes. 2.12 Tous ceux qui ont péché sans la loi périront aussi sans la loi, et tous ceux qui ont péché avec la loi seront jugés par la loi. 2.13 Ce ne sont pas, en effet, ceux qui écoutent la loi qui sont justes devant Dieu, mais ce sont ceux qui la mettent en pratique qui seront justifiés. 2.14 Quand les païens, qui n'ont point la loi, font naturellement ce que prescrit la loi, ils sont, eux qui n'ont point la loi, une loi pour eux-mêmes; 2.15 ils montrent que l'oeuvre de la loi est écrite dans leurs coeurs, leur conscience en rendant témoignage, et leurs pensées s'accusant ou se défendant tour à tour. 2.16 C'est ce qui paraîtra au jour où, selon mon Évangile, Dieu jugera par Jésus Christ les actions secrètes des hommes. 2.17 Toi qui te donnes le nom de Juif, qui te reposes sur la loi, qui te glorifies de Dieu, 2.18 qui connais sa volonté, qui apprécies la différence des choses, étant instruit par la loi; 2.19 toi qui te flattes d'être le conducteur des aveugles, la lumière de ceux qui sont dans les ténèbres, 2.20 le docteur des insensés, le maître des ignorants, parce que tu as dans la loi la règle de la science et de la vérité; 2.21 toi donc, qui enseignes les autres, tu ne t'enseignes pas toi-même! Toi qui prêches de ne pas dérober, tu dérobes! 2.22 Toi qui dis de ne pas commettre d'adultère, tu commets l'adultère! Toi qui as en abomination les idoles, tu commets des sacrilèges! 2.23 Toi qui te fais une gloire de la loi, tu déshonores Dieu par la transgression de la loi! 2.24 Car le nom de Dieu est à cause de vous blasphémé parmi les païens, comme cela est écrit. 2.25 La circoncision est utile, si tu mets en pratique la loi; mais si tu transgresses la loi, ta circoncision devient incirconcision. 2.26 Si donc l'incirconcis observe les ordonnances de la loi, son incirconcision ne sera-t-elle pas tenue pour circoncision? 2.27 L'incirconcis de nature, qui accomplit la loi, ne te condamnera-t-il pas, toi qui la transgresses, tout en ayant la lettre de la loi et la circoncision? 2.28 Le Juif, ce n'est pas celui qui en a les dehors; et la circoncision, ce n'est pas celle qui est visible dans la chair. 2.29 Mais le Juif, c'est celui qui l'est intérieurement; et la circoncision, c'est celle du coeur, selon l'esprit et non selon la lettre. La louange de ce Juif ne vient pas des hommes, mais de Dieu. Romains 3 3.1 Quel est donc l'avantage des Juifs, ou quelle est l'utilité de la circoncision? 3.2 Il est grand de toute manière, et tout d'abord en ce que les oracles de Dieu leur ont été confiés. 3.3 Eh quoi! si quelques-uns n'ont pas cru, leur incrédulité anéantira-t-elle la fidélité de Dieu? 3.4 Loin de là! Que Dieu, au contraire, soit reconnu pour vrai, et tout homme pour menteur, selon qu'il est écrit: Afin que tu sois trouvé juste dans tes paroles, Et que tu triomphes lorsqu'on te juge. 3.5 Mais si notre injustice établit la justice de Dieu, que dirons-nous? Dieu est-il injuste quand il déchaîne sa colère? (Je parle à la manière des hommes.) 3.6 Loin de là! Autrement, comment Dieu jugerait-il le monde? 3.7 Et si, par mon mensonge, la vérité de Dieu éclate davantage pour sa gloire, pourquoi suis-je moi-même encore jugé comme pécheur? 3.8 Et pourquoi ne ferions-nous pas le mal afin qu'il en arrive du bien, comme quelques-uns, qui nous calomnient, prétendent que nous le disons? La condamnation de ces gens est juste. 3.9 Quoi donc! sommes-nous plus excellents? Nullement. Car nous avons déjà prouvé que tous, Juifs et Grecs, sont sous l'empire du péché, 3.10 selon qu'il est écrit: Il n'y a point de juste, Pas même un seul; 3.11 Nul n'est intelligent, Nul ne cherche Dieu; Tous sont égarés, tous sont pervertis; 3.12 Il n'en est aucun qui fasse le bien, Pas même un seul; 3.13 Leur gosier est un sépulcre ouvert; Ils se servent de leurs langues pour tromper; Ils ont sous leurs lèvres un venin d'aspic; 3.14 Leur bouche est pleine de malédiction et d'amertume; 3.15 Ils ont les pieds légers pour répandre le sang; 3.16 La destruction et le malheur sont sur leur route; 3.17 Ils ne connaissent pas le chemin de la paix; 3.18 La crainte de Dieu n'est pas devant leurs yeux. 3.19 Or, nous savons que tout ce que dit la loi, elle le dit à ceux qui sont sous la loi, afin que toute bouche soit fermée, et que tout le monde soit reconnu coupable devant Dieu. 3.20 Car nul ne sera justifié devant lui par les oeuvres de la loi, puisque c'est par la loi que vient la connaissance du péché. 3.21 Mais maintenant, sans la loi est manifestée la justice de Dieu, à laquelle rendent témoignage la loi et les prophètes, 3.22 justice de Dieu par la foi en Jésus Christ pour tous ceux qui croient. Il n'y a point de distinction. 3.23 Car tous ont péché et sont privés de la gloire de Dieu; 3.24 et ils sont gratuitement justifiés par sa grâce, par le moyen de la rédemption qui est en Jésus Christ. 3.25 C'est lui que Dieu a destiné, par son sang, à être, pour ceux qui croiraient victime propitiatoire, afin de montrer sa justice, parce qu'il avait laissé impunis les péchés commis auparavant, au temps de sa patience, afin, dis-je, 3.26 de montrer sa justice dans le temps présent, de manière à être juste tout en justifiant celui qui a la foi en Jésus. 3.27 Où donc est le sujet de se glorifier? Il est exclu. Par quelle loi? Par la loi des oeuvres? Non, mais par la loi de la foi. 3.28 Car nous pensons que l'homme est justifié par la foi, sans les oeuvres de la loi. 3.29 Ou bien Dieu est-il seulement le Dieu des Juifs? Ne l'est-il pas aussi des païens? Oui, il l'est aussi des païens, 3.30 puisqu'il y a un seul Dieu, qui justifiera par la foi les circoncis, et par la foi les incirconcis. 3.31 Anéantissons-nous donc la loi par la foi? Loin de là! Au contraire, nous confirmons la loi. Romains 4 4.1 Que dirons-nous donc qu'Abraham, notre père, a obtenu selon la chair? 4.2 Si Abraham a été justifié par les oeuvres, il a sujet de se glorifier, mais non devant Dieu. 4.3 Car que dit l'Écriture? Abraham crut à Dieu, et cela lui fut imputé à justice. 4.4 Or, à celui qui fait une oeuvre, le salaire est imputé, non comme une grâce, mais comme une chose due; 4.5 et à celui qui ne fait point d'oeuvre, mais qui croit en celui qui justifie l'impie, sa foi lui est imputée à justice. 4.6 De même David exprime le bonheur de l'homme à qui Dieu impute la justice sans les oeuvres: 4.7 Heureux ceux dont les iniquités sont pardonnées, Et dont les péchés sont couverts! 4.8 Heureux l'homme à qui le Seigneur n'impute pas son péché! 4.9 Ce bonheur n'est-il que pour les circoncis, ou est-il également pour les incirconcis? Car nous disons que la foi fut imputée à justice à Abraham. 4.10 Comment donc lui fut-elle imputée? Était-ce après, ou avant sa circoncision? Il n'était pas encore circoncis, il était incirconcis. 4.11 Et il reçut le signe de la circoncision, comme sceau de la justice qu'il avait obtenue par la foi quand il était incirconcis, afin d'être le père de tous les incirconcis qui croient, pour que la justice leur fût aussi imputée, 4.12 et le père des circoncis, qui ne sont pas seulement circoncis, mais encore qui marchent sur les traces de la foi de notre père Abraham quand il était incirconcis. 4.13 En effet, ce n'est pas par la loi que l'héritage du monde a été promis à Abraham ou à sa postérité, c'est par la justice de la foi. 4.14 Car, si les héritiers le sont par la loi, la foi est vaine, et la promesse est anéantie, 4.15 parce que la loi produit la colère, et que là où il n'y a point de loi il n'y a point non plus de transgression. 4.16 C'est pourquoi les héritiers le sont par la foi, pour que ce soit par grâce, afin que la promesse soit assurée à toute la postérité, non seulement à celle qui est sous la loi, mais aussi à celle qui a la foi d'Abraham, notre père à tous, selon qu'il est écrit: 4.17 Je t'ai établi père d'un grand nombre de nations. Il est notre père devant celui auquel il a cru, Dieu, qui donne la vie aux morts, et qui appelle les choses qui ne sont point comme si elles étaient. 4.18 Espérant contre toute espérance, il crut, en sorte qu'il devint père d'un grand nombre de nations, selon ce qui lui avait été dit: Telle sera ta postérité. 4.19 Et, sans faiblir dans la foi, il ne considéra point que son corps était déjà usé, puisqu'il avait près de cent ans, et que Sara n'était plus en état d'avoir des enfants. 4.20 Il ne douta point, par incrédulité, au sujet de la promesse de Dieu; mais il fut fortifié par la foi, donnant gloire à Dieu, 4.21 et ayant la pleine conviction que ce qu'il promet il peut aussi l'accomplir. 4.22 C'est pourquoi cela lui fut imputé à justice. 4.23 Mais ce n'est pas à cause de lui seul qu'il est écrit que cela lui fut imputé; 4.24 c'est encore à cause de nous, à qui cela sera imputé, à nous qui croyons en celui qui a ressuscité des morts Jésus notre Seigneur, 4.25 lequel a été livré pour nos offenses, et est ressuscité pour notre justification. Romains 5 5.1 Étant donc justifiés par la foi, nous avons la paix avec Dieu par notre Seigneur Jésus Christ, 5.2 à qui nous devons d'avoir eu par la foi accès à cette grâce, dans laquelle nous demeurons fermes, et nous nous glorifions dans l'espérance de la gloire de Dieu. 5.3 Bien plus, nous nous glorifions même des afflictions, sachant que l'affliction produit la persévérance, 5.4 la persévérance la victoire dans l'épreuve, et cette victoire l'espérance. 5.5 Or, l'espérance ne trompe point, parce que l'amour de Dieu est répandu dans nos coeurs par le Saint Esprit qui nous a été donné. 5.6 Car, lorsque nous étions encore sans force, Christ, au temps marqué, est mort pour des impies. 5.7 A peine mourrait-on pour un juste; quelqu'un peut-être mourrait-il pour un homme de bien. 5.8 Mais Dieu prouve son amour envers nous, en ce que, lorsque nous étions encore des pécheurs, Christ est mort pour nous. 5.9 A plus forte raison donc, maintenant que nous sommes justifiés par son sang, serons-nous sauvés par lui de la colère. 5.10 Car si, lorsque nous étions ennemis, nous avons été réconciliés avec Dieu par la mort de son Fils, à plus forte raison, étant réconciliés, serons-nous sauvés par sa vie. 5.11 Et non seulement cela, mais encore nous nous glorifions en Dieu par notre Seigneur Jésus Christ, par qui maintenant nous avons obtenu la réconciliation. 5.12 C'est pourquoi, comme par un seul homme le péché est entré dans le monde, et par le péché la mort, et qu'ainsi la mort s'est étendue sur tous les hommes, parce que tous ont péché,... 5.13 car jusqu'à la loi le péché était dans le monde. Or, le péché n'est pas imputé, quand il n'y a point de loi. 5.14 Cependant la mort a régné depuis Adam jusqu'à Moïse, même sur ceux qui n'avaient pas péché par une transgression semblable à celle d'Adam, lequel est la figure de celui qui devait venir. 5.15 Mais il n'en est pas du don gratuit comme de l'offense; car, si par l'offense d'un seul il en est beaucoup qui sont morts, à plus forte raison la grâce de Dieu et le don de la grâce venant d'un seul homme, Jésus Christ, ont-ils été abondamment répandus sur beaucoup. 5.16 Et il n'en est pas du don comme de ce qui est arrivé par un seul qui a péché; car c'est après une seule offense que le jugement est devenu condamnation, tandis que le don gratuit devient justification après plusieurs offenses. 5.17 Si par l'offense d'un seul la mort a régné par lui seul, à plus forte raison ceux qui reçoivent l'abondance de la grâce et du don de la justice régneront-ils dans la vie par Jésus Christ lui seul. 5.18 Ainsi donc, comme par une seule offense la condamnation a atteint tous les hommes, de même par un seul acte de justice la justification qui donne la vie s'étend à tous les hommes. 5.19 Car, comme par la désobéissance d'un seul homme beaucoup ont été rendus pécheurs, de même par l'obéissance d'un seul beaucoup seront rendus justes. 5.20 Or, la loi est intervenue pour que l'offense abondât, mais là où le péché a abondé, la grâce a surabondé, 5.21 afin que, comme le péché a régné par la mort, ainsi la grâce régnât par la justice pour la vie éternelle, par Jésus Christ notre Seigneur. Romains 6 6.1 Que dirons-nous donc? Demeurerions-nous dans le péché, afin que la grâce abonde? 6.2 Loin de là! Nous qui sommes morts au péché, comment vivrions-nous encore dans le péché? 6.3 Ignorez-vous que nous tous qui avons été baptisés en Jésus Christ, c'est en sa mort que nous avons été baptisés? 6.4 Nous avons donc été ensevelis avec lui par le baptême en sa mort, afin que, comme Christ est ressuscité des morts par la gloire du Père, de même nous aussi nous marchions en nouveauté de vie. 6.5 En effet, si nous sommes devenus une même plante avec lui par la conformité à sa mort, nous le serons aussi par la conformité à sa résurrection, 6.6 sachant que notre vieil homme a été crucifié avec lui, afin que le corps du péché fût détruit, pour que nous ne soyons plus esclaves du péché; 6.7 car celui qui est mort est libre du péché. 6.8 Or, si nous sommes morts avec Christ, nous croyons que nous vivrons aussi avec lui, 6.9 sachant que Christ ressuscité des morts ne meurt plus; la mort n'a plus de pouvoir sur lui. 6.10 Car il est mort, et c'est pour le péché qu'il est mort une fois pour toutes; il est revenu à la vie, et c'est pour Dieu qu'il vit. 6.11 Ainsi vous-mêmes, regardez-vous comme morts au péché, et comme vivants pour Dieu en Jésus Christ. 6.12 Que le péché ne règne donc point dans votre corps mortel, et n'obéissez pas à ses convoitises. 6.13 Ne livrez pas vos membres au péché, comme des instruments d'iniquité; mais donnez-vous vous-mêmes à Dieu, comme étant vivants de morts que vous étiez, et offrez à Dieu vos membres, comme des instruments de justice. 6.14 Car le péché n'aura point de pouvoir sur vous, puisque vous êtes, non sous la loi, mais sous la grâce. 6.15 Quoi donc! Pécherions-nous, parce que nous sommes, non sous la loi, mais sous la grâce? Loin de là! 6.16 Ne savez-vous pas qu'en vous livrant à quelqu'un comme esclaves pour lui obéir, vous êtes esclaves de celui à qui vous obéissez, soit du péché qui conduit à la mort, soit de l'obéissance qui conduit à la justice? 6.17 Mais grâces soient rendues à Dieu de ce que, après avoir été esclaves du péché, vous avez obéi de coeur à la règle de doctrine dans laquelle vous avez été instruits. 6.18 Ayant été affranchis du péché, vous êtes devenus esclaves de la justice. - 6.19 Je parle à la manière des hommes, à cause de la faiblesse de votre chair. -De même donc que vous avez livré vos membres comme esclaves à l'impureté et à l'iniquité, pour arriver à l'iniquité, ainsi maintenant livrez vos membres comme esclaves à la justice, pour arriver à la sainteté. 6.20 Car, lorsque vous étiez esclaves du péché, vous étiez libres à l'égard de la justice. 6.21 Quels fruits portiez-vous alors? Des fruits dont vous rougissez aujourd'hui. Car la fin de ces choses, c'est la mort. 6.22 Mais maintenant, étant affranchis du péché et devenus esclaves de Dieu, vous avez pour fruit la sainteté et pour fin la vie éternelle. 6.23 Car le salaire du péché, c'est la mort; mais le don gratuit de Dieu, c'est la vie éternelle en Jésus Christ notre Seigneur. Romains 7 7.1 Ignorez-vous, frères, -car je parle à des gens qui connaissent la loi, -que la loi exerce son pouvoir sur l'homme aussi longtemps qu'il vit? 7.2 Ainsi, une femme mariée est liée par la loi à son mari tant qu'il est vivant; mais si le mari meurt, elle est dégagée de la loi qui la liait à son mari. 7.3 Si donc, du vivant de son mari, elle devient la femme d'un autre homme, elle sera appelée adultère; mais si le mari meurt, elle est affranchie de la loi, de sorte qu'elle n'est point adultère en devenant la femme d'un autre. 7.4 De même, mes frères, vous aussi vous avez été, par le corps de Christ, mis à mort en ce qui concerne la loi, pour que vous apparteniez à un autre, à celui qui est ressuscité des morts, afin que nous portions des fruits pour Dieu. 7.5 Car, lorsque nous étions dans la chair, les passions des péchés provoquées par la loi agissaient dans nos membres, de sorte que nous portions des fruits pour la mort. 7.6 Mais maintenant, nous avons été dégagés de la loi, étant morts à cette loi sous laquelle nous étions retenus, de sorte que nous servons dans un esprit nouveau, et non selon la lettre qui a vieilli. 7.7 Que dirons-nous donc? La loi est-elle péché? Loin de là! Mais je n'ai connu le péché que par la loi. Car je n'aurais pas connu la convoitise, si la loi n'eût dit: Tu ne convoiteras point. 7.8 Et le péché, saisissant l'occasion, produisit en moi par le commandement toutes sortes de convoitises; car sans loi le péché est mort. 7.9 Pour moi, étant autrefois sans loi, je vivais; mais quand le commandement vint, le péché reprit vie, et moi je mourus. 7.10 Ainsi, le commandement qui conduit à la vie se trouva pour moi conduire à la mort. 7.11 Car le péché saisissant l'occasion, me séduisit par le commandement, et par lui me fit mourir. 7.12 La loi donc est sainte, et le commandement est saint, juste et bon. 7.13 Ce qui est bon a-t-il donc été pour moi une cause de mort? Loin de là! Mais c'est le péché, afin qu'il se manifestât comme péché en me donnant la mort par ce qui est bon, et que, par le commandement, il devînt condamnable au plus haut point. 7.14 Nous savons, en effet, que la loi est spirituelle; mais moi, je suis charnel, vendu au péché. 7.15 Car je ne sais pas ce que je fais: je ne fais point ce que je veux, et je fais ce que je hais. 7.16 Or, si je fais ce que je ne veux pas, je reconnais par là que la loi est bonne. 7.17 Et maintenant ce n'est plus moi qui le fais, mais c'est le péché qui habite en moi. 7.18 Ce qui est bon, je le sais, n'habite pas en moi, c'est-à-dire dans ma chair: j'ai la volonté, mais non le pouvoir de faire le bien. 7.19 Car je ne fais pas le bien que je veux, et je fais le mal que je ne veux pas. 7.20 Et si je fais ce que je ne veux pas, ce n'est plus moi qui le fais, c'est le péché qui habite en moi. 7.21 Je trouve donc en moi cette loi: quand je veux faire le bien, le mal est attaché à moi. 7.22 Car je prends plaisir à la loi de Dieu, selon l'homme intérieur; 7.23 mais je vois dans mes membres une autre loi, qui lutte contre la loi de mon entendement, et qui me rend captif de la loi du péché, qui est dans mes membres. 7.24 Misérable que je suis! Qui me délivrera du corps de cette mort?... 7.25 Grâces soient rendues à Dieu par Jésus Christ notre Seigneur!... Ainsi donc, moi-même, je suis par l'entendement esclave de la loi de Dieu, et je suis par la chair esclave de la loi du péché. Romains 8 8.1 Il n'y a donc maintenant aucune condamnation pour ceux qui sont en Jésus Christ. 8.2 En effet, la loi de l'esprit de vie en Jésus Christ m'a affranchi de la loi du péché et de la mort. 8.3 Car-chose impossible à la loi, parce que la chair la rendait sans force, -Dieu a condamné le péché dans la chair, en envoyant, à cause du péché, son propre Fils dans une chair semblable à celle du péché, 8.4 et cela afin que la justice de la loi fût accomplie en nous, qui marchons, non selon la chair, mais selon l'esprit. 8.5 Ceux, en effet, qui vivent selon la chair, s'affectionnent aux choses de la chair, tandis que ceux qui vivent selon l'esprit s'affectionnent aux choses de l'esprit. 8.6 Et l'affection de la chair, c'est la mort, tandis que l'affection de l'esprit, c'est la vie et la paix; 8.7 car l'affection de la chair est inimitié contre Dieu, parce qu'elle ne se soumet pas à la loi de Dieu, et qu'elle ne le peut même pas. 8.8 Or ceux qui vivent selon la chair ne sauraient plaire à Dieu. 8.9 Pour vous, vous ne vivez pas selon la chair, mais selon l'esprit, si du moins l'Esprit de Dieu habite en vous. Si quelqu'un n'a pas l'Esprit de Christ, il ne lui appartient pas. 8.10 Et si Christ est en vous, le corps, il est vrai, est mort à cause du péché, mais l'esprit est vie à cause de la justice. 8.11 Et si l'Esprit de celui qui a ressuscité Jésus d'entre les morts habite en vous, celui qui a ressuscité Christ d'entre les morts rendra aussi la vie à vos corps mortels par son Esprit qui habite en vous. 8.12 Ainsi donc, frères, nous ne sommes point redevables à la chair, pour vivre selon la chair. 8.13 Si vous vivez selon la chair, vous mourrez; mais si par l'Esprit vous faites mourir les actions du corps, vous vivrez, 8.14 car tous ceux qui sont conduits par l'Esprit de Dieu sont fils de Dieu. 8.15 Et vous n'avez point reçu un esprit de servitude, pour être encore dans la crainte; mais vous avez reçu un Esprit d'adoption, par lequel nous crions: Abba! Père! 8.16 L'Esprit lui-même rend témoignage à notre esprit que nous sommes enfants de Dieu. 8.17 Or, si nous sommes enfants, nous sommes aussi héritiers: héritiers de Dieu, et cohéritiers de Christ, si toutefois nous souffrons avec lui, afin d'être glorifiés avec lui. 8.18 J'estime que les souffrances du temps présent ne sauraient être comparées à la gloire à venir qui sera révélée pour nous. 8.19 Aussi la création attend-elle avec un ardent désir la révélation des fils de Dieu. 8.20 Car la création a été soumise à la vanité, -non de son gré, mais à cause de celui qui l'y a soumise, - 8.21 avec l'espérance qu'elle aussi sera affranchie de la servitude de la corruption, pour avoir part à la liberté de la gloire des enfants de Dieu. 8.22 Or, nous savons que, jusqu'à ce jour, la création tout entière soupire et souffre les douleurs de l'enfantement. 8.23 Et ce n'est pas elle seulement; mais nous aussi, qui avons les prémices de l'Esprit, nous aussi nous soupirons en nous-mêmes, en attendant l'adoption, la rédemption de notre corps. 8.24 Car c'est en espérance que nous sommes sauvés. Or, l'espérance qu'on voit n'est plus espérance: ce qu'on voit, peut-on l'espérer encore? 8.25 Mais si nous espérons ce que nous ne voyons pas, nous l'attendons avec persévérance. 8.26 De même aussi l'Esprit nous aide dans notre faiblesse, car nous ne savons pas ce qu'il nous convient de demander dans nos prières. Mais l'Esprit lui-même intercède par des soupirs inexprimables; 8.27 et celui qui sonde les coeurs connaît quelle est la pensée de l'Esprit, parce que c'est selon Dieu qu'il intercède en faveur des saints. 8.28 Nous savons, du reste, que toutes choses concourent au bien de ceux qui aiment Dieu, de ceux qui sont appelés selon son dessein. 8.29 Car ceux qu'il a connus d'avance, il les a aussi prédestinés à être semblables à l'image de son Fils, afin que son Fils fût le premier-né entre plusieurs frères. 8.30 Et ceux qu'il a prédestinés, il les a aussi appelés; et ceux qu'il a appelés, il les a aussi justifiés; et ceux qu'il a justifiés, il les a aussi glorifiés. 8.31 Que dirons-nous donc à l'égard de ces choses? Si Dieu est pour nous, qui sera contre nous? 8.32 Lui, qui n'a point épargné son propre Fils, mais qui l'a livré pour nous tous, comment ne nous donnera-t-il pas aussi toutes choses avec lui? 8.33 Qui accusera les élus de Dieu? C'est Dieu qui justifie! 8.34 Qui les condamnera? Christ est mort; bien plus, il est ressuscité, il est à la droite de Dieu, et il intercède pour nous! 8.35 Qui nous séparera de l'amour de Christ? Sera-ce la tribulation, ou l'angoisse, ou la persécution, ou la faim, ou la nudité, ou le péril, ou l'épée? 8.36 selon qu'il est écrit: C'est à cause de toi qu'on nous met à mort tout le jour, Qu'on nous regarde comme des brebis destinées à la boucherie. 8.37 Mais dans toutes ces choses nous sommes plus que vainqueurs par celui qui nous a aimés. 8.38 Car j'ai l'assurance que ni la mort ni la vie, ni les anges ni les dominations, ni les choses présentes ni les choses à venir, 8.39 ni les puissances, ni la hauteur, ni la profondeur, ni aucune autre créature ne pourra nous séparer de l'amour de Dieu manifesté en Jésus Christ notre Seigneur. Romains 9 9.1 Je dis la vérité en Christ, je ne mens point, ma conscience m'en rend témoignage par le Saint Esprit: 9.2 J'éprouve une grande tristesse, et j'ai dans le coeur un chagrin continuel. 9.3 Car je voudrais moi-même être anathème et séparé de Christ pour mes frères, mes parents selon la chair, 9.4 qui sont Israélites, à qui appartiennent l'adoption, et la gloire, et les alliances, et la loi, et le culte, 9.5 et les promesses, et les patriarches, et de qui est issu, selon la chair, le Christ, qui est au-dessus de toutes choses, Dieu béni éternellement. Amen! 9.6 Ce n'est point à dire que la parole de Dieu soit restée sans effet. Car tous ceux qui descendent d'Israël ne sont pas Israël, 9.7 et, pour être la postérité d'Abraham, ils ne sont pas tous ses enfants; mais il est dit: En Isaac sera nommée pour toi une postérité, 9.8 c'est-à-dire que ce ne sont pas les enfants de la chair qui sont enfants de Dieu, mais que ce sont les enfants de la promesse qui sont regardés comme la postérité. 9.9 Voici, en effet, la parole de la promesse: Je reviendrai à cette même époque, et Sara aura un fils. 9.10 Et, de plus, il en fut ainsi de Rébecca, qui conçut du seul Isaac notre père; 9.11 car, quoique les enfants ne fussent pas encore nés et ils n'eussent fait ni bien ni mal, -afin que le dessein d'élection de Dieu subsistât, sans dépendre des oeuvres, et par la seule volonté de celui qui appelle, - 9.12 il fut dit à Rébecca: L'aîné sera assujetti au plus jeune; selon qu'il est écrit: 9.13 J'ai aimé Jacob Et j'ai haï Ésaü. 9.14 Que dirons-nous donc? Y a-t-il en Dieu de l'injustice? Loin de là! 9.15 Car il dit à Moïse: Je ferai miséricorde à qui je fais miséricorde, et j'aurai compassion de qui j'ai compassion. 9.16 Ainsi donc, cela ne dépend ni de celui qui veut, ni de celui qui court, mais de Dieu qui fait miséricorde. 9.17 Car l'Écriture dit à Pharaon: Je t'ai suscité à dessein pour montrer en toi ma puissance, et afin que mon nom soit publié par toute la terre. 9.18 Ainsi, il fait miséricorde à qui il veut, et il endurcit qui il veut. 9.19 Tu me diras: Pourquoi blâme-t-il encore? Car qui est-ce qui résiste à sa volonté? 9.20 O homme, toi plutôt, qui es-tu pour contester avec Dieu? Le vase d'argile dira-t-il à celui qui l'a formé: Pourquoi m'as-tu fait ainsi? 9.21 Le potier n'est-il pas maître de l'argile, pour faire avec la même masse un vase d'honneur et un vase d'un usage vil? 9.22 Et que dire, si Dieu, voulant montrer sa colère et faire connaître sa puissance, a supporté avec une grande patience des vases de colère formés pour la perdition, 9.23 et s'il a voulu faire connaître la richesse de sa gloire envers des vases de miséricorde qu'il a d'avance préparés pour la gloire? 9.24 Ainsi nous a-t-il appelés, non seulement d'entre les Juifs, mais encore d'entre les païens, 9.25 selon qu'il le dit dans Osée: J'appellerai mon peuple celui qui n'était pas mon peuple, et bien-aimée celle qui n'était pas la bien-aimée; 9.26 et là où on leur disait: Vous n'êtes pas mon peuple! ils seront appelés fils du Dieu vivant. 9.27 Ésaïe, de son côté, s'écrie au sujet d'Israël: Quand le nombre des fils d'Israël serait comme le sable de la mer, Un reste seulement sera sauvé. 9.28 Car le Seigneur exécutera pleinement et promptement sur la terre ce qu'il a résolu. 9.29 Et, comme Ésaïe l'avait dit auparavant: Si le Seigneur des armées Ne nous eût laissé une postérité, Nous serions devenus comme Sodome, Nous aurions été semblables à Gomorrhe. 9.30 Que dirons-nous donc? Les païens, qui ne cherchaient pas la justice, ont obtenu la justice, la justice qui vient de la foi, 9.31 tandis qu'Israël, qui cherchait une loi de justice, n'est pas parvenu à cette loi. 9.32 Pourquoi? Parce qu'Israël l'a cherchée, non par la foi, mais comme provenant des oeuvres. Ils se sont heurtés contre la pierre d'achoppement, 9.33 selon qu'il est écrit: Voici, je mets en Sion une pierre d'achoppement Et un rocher de scandale, Et celui qui croit en lui ne sera point confus. Romains 10 10.1 Frères, le voeu de mon coeur et ma prière à Dieu pour eux, c'est qu'ils soient sauvés. 10.2 Je leur rends le témoignage qu'ils ont du zèle pour Dieu, mais sans intelligence: 10.3 ne connaissant pas la justice de Dieu, et cherchant à établir leur propre justice, ils ne se sont pas soumis à la justice de Dieu; 10.4 car Christ est la fin de la loi, pour la justification de tous ceux qui croient. 10.5 En effet, Moïse définit ainsi la justice qui vient de la loi: L'homme qui mettra ces choses en pratique vivra par elles. 10.6 Mais voici comment parle la justice qui vient de la foi: Ne dis pas en ton coeur: Qui montera au ciel? c'est en faire descendre Christ; 10.7 ou: Qui descendra dans l'abîme? c'est faire remonter Christ d'entre les morts. 10.8 Que dit-elle donc? La parole est près de toi, dans ta bouche et dans ton coeur. Or, c'est la parole de la foi, que nous prêchons. 10.9 Si tu confesses de ta bouche le Seigneur Jésus, et si tu crois dans ton coeur que Dieu l'a ressuscité des morts, tu seras sauvé. 10.10 Car c'est en croyant du coeur qu'on parvient à la justice, et c'est en confessant de la bouche qu'on parvient au salut, selon ce que dit l'Écriture: 10.11 Quiconque croit en lui ne sera point confus. 10.12 Il n'y a aucune différence, en effet, entre le Juif et le Grec, puisqu'ils ont tous un même Seigneur, qui est riche pour tous ceux qui l'invoquent. 10.13 Car quiconque invoquera le nom du Seigneur sera sauvé. 10.14 Comment donc invoqueront-ils celui en qui ils n'ont pas cru? Et comment croiront-ils en celui dont ils n'ont pas entendu parler? Et comment en entendront-ils parler, s'il n'y a personne qui prêche? 10.15 Et comment y aura-t-il des prédicateurs, s'ils ne sont pas envoyés? selon qu'il est écrit: Qu'ils sont beaux Les pieds de ceux qui annoncent la paix, De ceux qui annoncent de bonnes nouvelles! 10.16 Mais tous n'ont pas obéi à la bonne nouvelle. Aussi Ésaïe dit-il: Seigneur, Qui a cru à notre prédication? 10.17 Ainsi la foi vient de ce qu'on entend, et ce qu'on entend vient de la parole de Christ. 10.18 Mais je dis: N'ont-ils pas entendu? Au contraire! Leur voix est allée par toute la terre, Et leurs paroles jusqu'aux extrémités du monde. 10.19 Mais je dis: Israël ne l'a-t-il pas su? Moïse le premier dit: J'exciterai votre jalousie par ce qui n'est point une nation, je provoquerai votre colère par une nation sans intelligence. 10.20 Et Ésaïe pousse la hardiesse jusqu'à dire: J'ai été trouvé par ceux qui ne me cherchaient pas, Je me suis manifesté à ceux qui ne me demandaient pas. 10.21 Mais au sujet d'Israël, il dit: J'ai tendu mes mains tout le jour vers un peuple rebelle Et contredisant. Romains 11 11.1 Je dis donc: Dieu a-t-il rejeté son peuple? Loin de là! Car moi aussi je suis Israélite, de la postérité d'Abraham, de la tribu de Benjamin. 11.2 Dieu n'a point rejeté son peuple, qu'il a connu d'avance. Ne savez-vous pas ce que l'Écriture rapporte d'Élie, comment il adresse à Dieu cette plainte contre Israël: 11.3 Seigneur, ils ont tué tes prophètes, ils ont renversé tes autels; je suis resté moi seul, et ils cherchent à m'ôter la vie? 11.4 Mais quelle réponse Dieu lui fait-il? Je me suis réservé sept mille hommes, qui n'ont point fléchi le genou devant Baal. 11.5 De même aussi dans le temps présent il y un reste, selon l'élection de la grâce. 11.6 Or, si c'est par grâce, ce n'est plus par les oeuvres; autrement la grâce n'est plus une grâce. Et si c'est par les oeuvres, ce n'est plus une grâce; autrement l'oeuvre n'est plus une oeuvre. 11.7 Quoi donc? Ce qu'Israël cherche, il ne l'a pas obtenu, mais l'élection l'a obtenu, tandis que les autres ont été endurcis, 11.8 selon qu'il est écrit: Dieu leur a donné un esprit d'assoupissement, Des yeux pour ne point voir, Et des oreilles pour ne point entendre, Jusqu'à ce jour. Et David dit: 11.9 Que leur table soit pour eux un piège, Un filet, une occasion de chute, et une rétribution! 11.10 Que leurs yeux soient obscurcis pour ne point voir, Et tiens leur dos continuellement courbé! 11.11 Je dis donc: Est-ce pour tomber qu'ils ont bronché? Loin de là! Mais, par leur chute, le salut est devenu accessible aux païens, afin qu'ils fussent excités à la jalousie. 11.12 Or, si leur chute a été la richesse du monde, et leur amoindrissement la richesse des païens, combien plus en sera-t-il ainsi quand ils se convertiront tous. 11.13 Je vous le dis à vous, païens: en tant que je suis apôtre des païens, je glorifie mon ministère, 11.14 afin, s'il est possible, d'exciter la jalousie de ceux de ma race, et d'en sauver quelques-uns. 11.15 Car si leur rejet a été la réconciliation du monde, que sera leur réintégration, sinon une vie d'entre les morts? 11.16 Or, si les prémices sont saintes, la masse l'est aussi; et si la racine est sainte, les branches le sont aussi. 11.17 Mais si quelques-unes des branches ont été retranchées, et si toi, qui était un olivier sauvage, tu as été enté à leur place, et rendu participant de la racine et de la graisse de l'olivier, 11.18 ne te glorifie pas aux dépens de ces branches. Si tu te glorifies, sache que ce n'est pas toi qui portes la racine, mais que c'est la racine qui te porte. 11.19 Tu diras donc: Les branches ont été retranchées, afin que moi je fusse enté. 11.20 Cela est vrai; elles ont été retranchées pour cause d'incrédulité, et toi, tu subsistes par la foi. Ne t'abandonne pas à l'orgueil, mais crains; 11.21 car si Dieu n'a pas épargné les branches naturelles, il ne t'épargnera pas non plus. 11.22 Considère donc la bonté et la sévérité de Dieu: sévérité envers ceux qui sont tombés, et bonté de Dieu envers toi, si tu demeures ferme dans cette bonté; autrement, tu seras aussi retranché. 11.23 Eux de même, s'ils ne persistent pas dans l'incrédulité, ils seront entés; car Dieu est puissant pour les enter de nouveau. 11.24 Si toi, tu as été coupé de l'olivier naturellement sauvage, et enté contrairement à ta nature sur l'olivier franc, à plus forte raison eux seront-ils entés selon leur nature sur leur propre olivier. 11.25 Car je ne veux pas, frères, que vous ignoriez ce mystère, afin que vous ne vous regardiez point comme sages, c'est qu'une partie d'Israël est tombée dans l'endurcissement, jusqu'à ce que la totalité des païens soit entrée. 11.26 Et ainsi tout Israël sera sauvé, selon qu'il est écrit: Le libérateur viendra de Sion, Et il détournera de Jacob les impiétés; 11.27 Et ce sera mon alliance avec eux, Lorsque j'ôterai leurs péchés. 11.28 En ce qui concerne l'Évangile, ils sont ennemis à cause de vous; mais en ce qui concerne l'élection, ils sont aimés à cause de leurs pères. 11.29 Car Dieu ne se repent pas de ses dons et de son appel. 11.30 De même que vous avez autrefois désobéi à Dieu et que par leur désobéissance vous avez maintenant obtenu miséricorde, 11.31 de même ils ont maintenant désobéi, afin que, par la miséricorde qui vous a été faite, ils obtiennent aussi miséricorde. 11.32 Car Dieu a renfermé tous les hommes dans la désobéissance, pour faire miséricorde à tous. 11.33 O profondeur de la richesse, de la sagesse et de la science de Dieu! Que ses jugements sont insondables, et ses voies incompréhensibles! Car 11.34 Qui a connu la pensée du Seigneur, Ou qui a été son conseiller? 11.35 Qui lui a donné le premier, pour qu'il ait à recevoir en retour? 11.36 C'est de lui, par lui, et pour lui que sont toutes choses. A lui la gloire dans tous les siècles! Amen! Romains 12 12.1 Je vous exhorte donc, frères, par les compassions de Dieu, à offrir vos corps comme un sacrifice vivant, saint, agréable à Dieu, ce qui sera de votre part un culte raisonnable. 12.2 Ne vous conformez pas au siècle présent, mais soyez transformés par le renouvellement de l'intelligence, afin que vous discerniez quelle est la volonté de Dieu, ce qui est bon, agréable et parfait. 12.3 Par la grâce qui m'a été donnée, je dis à chacun de vous de n'avoir pas de lui-même une trop haute opinion, mais de revêtir des sentiments modestes, selon la mesure de foi que Dieu a départie à chacun. 12.4 Car, comme nous avons plusieurs membres dans un seul corps, et que tous les membres n'ont pas la même fonction, 12.5 ainsi, nous qui sommes plusieurs, nous formons un seul corps en Christ, et nous sommes tous membres les uns des autres. 12.6 Puisque nous avons des dons différents, selon la grâce qui nous a été accordée, que celui qui a le don de prophétie l'exerce selon l'analogie de la foi; 12.7 que celui qui est appelé au ministère s'attache à son ministère; que celui qui enseigne s'attache à son enseignement, 12.8 et celui qui exhorte à l'exhortation. Que celui qui donne le fasse avec libéralité; que celui qui préside le fasse avec zèle; que celui qui pratique la miséricorde le fasse avec joie. 12.9 Que la charité soit sans hypocrisie. Ayez le mal en horreur; attachez-vous fortement au bien. 12.10 Par amour fraternel, soyez pleins d'affection les uns pour les autres; par honneur, usez de prévenances réciproques. 12.11 Ayez du zèle, et non de la paresse. Soyez fervents d'esprit. Servez le Seigneur. 12.12 Réjouissez-vous en espérance. Soyez patients dans l'affliction. Persévérez dans la prière. 12.13 Pourvoyez aux besoins des saints. Exercez l'hospitalité. 12.14 Bénissez ceux qui vous persécutent, bénissez et ne maudissez pas. 12.15 Réjouissez-vous avec ceux qui se réjouissent; pleurez avec ceux qui pleurent. 12.16 Ayez les mêmes sentiments les uns envers les autres. N'aspirez pas à ce qui est élevé, mais laissez-vous attirer par ce qui est humble. Ne soyez point sages à vos propres yeux. 12.17 Ne rendez à personne le mal pour le mal. Recherchez ce qui est bien devant tous les hommes. 12.18 S'il est possible, autant que cela dépend de vous, soyez en paix avec tous les hommes. 12.19 Ne vous vengez point vous-mêmes, bien-aimés, mais laissez agir la colère; car il est écrit: A moi la vengeance, à moi la rétribution, dit le Seigneur. 12.20 Mais si ton ennemi a faim, donne-lui à manger; s'il a soif, donne-lui à boire; car en agissant ainsi, ce sont des charbons ardents que tu amasseras sur sa tête. 12.21 Ne te laisse pas vaincre par le mal, mais surmonte le mal par le bien. Romains 13 13.1 Que toute personne soit soumise aux autorités supérieures; car il n'y a point d'autorité qui ne vienne de Dieu, et les autorités qui existent ont été instituées de Dieu. 13.2 C'est pourquoi celui qui s'oppose à l'autorité résiste à l'ordre que Dieu a établi, et ceux qui résistent attireront une condamnation sur eux-mêmes. 13.3 Ce n'est pas pour une bonne action, c'est pour une mauvaise, que les magistrats sont à redouter. Veux-tu ne pas craindre l'autorité? Fais-le bien, et tu auras son approbation. 13.4 Le magistrat est serviteur de Dieu pour ton bien. Mais si tu fais le mal, crains; car ce n'est pas en vain qu'il porte l'épée, étant serviteur de Dieu pour exercer la vengeance et punir celui qui fait le mal. 13.5 Il est donc nécessaire d'être soumis, non seulement par crainte de la punition, mais encore par motif de conscience. 13.6 C'est aussi pour cela que vous payez les impôts. Car les magistrats sont des ministres de Dieu entièrement appliqués à cette fonction. 13.7 Rendez à tous ce qui leur est dû: l'impôt à qui vous devez l'impôt, le tribut à qui vous devez le tribut, la crainte à qui vous devez la crainte, l'honneur à qui vous devez l'honneur. 13.8 Ne devez rien à personne, si ce n'est de vous aimer les uns les autres; car celui qui aime les autres a accompli la loi. 13.9 En effet, les commandements: Tu ne commettras point d'adultère, tu ne tueras point, tu ne déroberas point, tu ne convoiteras point, et ceux qu'il peut encore y avoir, se résument dans cette parole: Tu aimeras ton prochain comme toi-même. 13.10 L'amour ne fait point de mal au prochain: l'amour est donc l'accomplissement de la loi. 13.11 Cela importe d'autant plus que vous savez en quel temps nous sommes: c'est l'heure de vous réveiller enfin du sommeil, car maintenant le salut est plus près de nous que lorsque nous avons cru. 13.12 La nuit est avancée, le jour approche. Dépouillons-nous donc des oeuvres des ténèbres, et revêtons les armes de la lumière. 13.13 Marchons honnêtement, comme en plein jour, loin des excès et de l'ivrognerie, de la luxure et de l'impudicité, des querelles et des jalousies. 13.14 Mais revêtez-vous du Seigneur Jésus Christ, et n'ayez pas soin de la chair pour en satisfaire les convoitises. Romains 14 14.1 Faites accueil à celui qui est faible dans la foi, et ne discutez pas sur les opinions. 14.2 Tel croit pouvoir manger de tout: tel autre, qui est faible, ne mange que des légumes. 14.3 Que celui qui mange ne méprise point celui qui ne mange pas, et que celui qui ne mange pas ne juge point celui qui mange, car Dieu l'a accueilli. 14.4 Qui es-tu, toi qui juges un serviteur d'autrui? S'il se tient debout, ou s'il tombe, cela regarde son maître. Mais il se tiendra debout, car le Seigneur a le pouvoir de l'affermir. 14.5 Tel fait une distinction entre les jours; tel autre les estime tous égaux. Que chacun ait en son esprit une pleine conviction. 14.6 Celui qui distingue entre les jours agit ainsi pour le Seigneur. Celui qui mange, c'est pour le Seigneur qu'il mange, car il rend grâces à Dieu; celui qui ne mange pas, c'est pour le Seigneur qu'il ne mange pas, et il rend grâces à Dieu. 14.7 En effet, nul de nous ne vit pour lui-même, et nul ne meurt pour lui-même. 14.8 Car si nous vivons, nous vivons pour le Seigneur; et si nous mourons, nous mourons pour le Seigneur. Soit donc que nous vivions, soit que nous mourions, nous sommes au Seigneur. 14.9 Car Christ est mort et il a vécu, afin de dominer sur les morts et sur les vivants. 14.10 Mais toi, pourquoi juges-tu ton frère? ou toi, pourquoi méprises-tu ton frère? puisque nous comparaîtrons tous devant le tribunal de Dieu. 14.11 Car il est écrit: Je suis vivant, dit le Seigneur, Tout genou fléchira devant moi, Et toute langue donnera gloire à Dieu. 14.12 Ainsi chacun de nous rendra compte à Dieu pour lui-même. 14.13 Ne nous jugeons donc plus les uns les autres; mais pensez plutôt à ne rien faire qui soit pour votre frère une pierre d'achoppement ou une occasion de chute. 14.14 Je sais et je suis persuadé par le Seigneur Jésus que rien n'est impur en soi, et qu'une chose n'est impure que pour celui qui la croit impure. 14.15 Mais si, pour un aliment, ton frère est attristé, tu ne marches plus selon l'amour: ne cause pas, par ton aliment, la perte de celui pour lequel Christ est mort. 14.16 Que votre privilège ne soit pas un sujet de calomnie. 14.17 Car le royaume de Dieu, ce n'est pas le manger et le boire, mais la justice, la paix et la joie, par le Saint Esprit. 14.18 Celui qui sert Christ de cette manière est agréable à Dieu et approuvé des hommes. 14.19 Ainsi donc, recherchons ce qui contribue à la paix et à l'édification mutuelle. 14.20 Pour un aliment, ne détruis pas l'oeuvre de Dieu. A la vérité toutes choses sont pures; mais il est mal à l'homme, quand il mange, de devenir une pierre d'achoppement. 14.21 Il est bien de ne pas manger de viande, de ne pas boire de vin, et de s'abstenir de ce qui peut être pour ton frère une occasion de chute, de scandale ou de faiblesse. 14.22 Cette foi que tu as, garde-la pour toi devant Dieu. Heureux celui qui ne se condamne pas lui-même dans ce qu'il approuve! 14.23 Mais celui qui a des doutes au sujet de ce qu'il mange est condamné, parce qu'il n'agit pas par conviction. Tout ce qui n'est pas le produit d'une conviction est péché. Romains 15 15.1 Nous qui sommes forts, nous devons supporter les faiblesses de ceux qui ne le sont pas, et ne pas nous complaire en nous-mêmes. 15.2 Que chacun de nous complaise au prochain pour ce qui est bien en vue de l'édification. 15.3 Car Christ ne s'est point complu en lui-même, mais, selon qu'il est écrit: Les outrages de ceux qui t'insultent sont tombés sur moi. 15.4 Or, tout ce qui a été écrit d'avance l'a été pour notre instruction, afin que, par la patience, et par la consolation que donnent les Écritures, nous possédions l'espérance. 15.5 Que le Dieu de la persévérance et de la consolation vous donne d'avoir les mêmes sentiments les uns envers les autres selon Jésus Christ, 15.6 afin que tous ensemble, d'une seule bouche, vous glorifiiez le Dieu et Père de notre Seigneur Jésus Christ. 15.7 Accueillez-vous donc les uns les autres, comme Christ vous a accueillis, pour la gloire de Dieu. 15.8 Je dis, en effet, que Christ a été serviteur des circoncis, pour prouver la véracité de Dieu en confirmant les promesses faites aux pères, 15.9 tandis que les païens glorifient Dieu à cause de sa miséricorde, selon qu'il est écrit: C'est pourquoi je te louerai parmi les nations, Et je chanterai à la gloire de ton nom. Il est dit encore: 15.10 Nations, réjouissez-vous avec son peuple! 15.11 Et encore: Louez le Seigneur, vous toutes les nations, Célébrez-le, vous tous les peuples! 15.12 Ésaïe dit aussi: Il sortira d'Isaï un rejeton, Qui se lèvera pour régner sur les nations; Les nations espéreront en lui. 15.13 Que le Dieu de l'espérance vous remplisse de toute joie et de toute paix dans la foi, pour que vous abondiez en espérance, par la puissance du Saint Esprit! 15.14 Pour ce qui vous concerne, mes frères, je suis moi-même persuadé que vous êtes pleins de bonnes dispositions, remplis de toute connaissance, et capables de vous exhorter les uns les autres. 15.15 Cependant, à certains égards, je vous ai écrit avec une sorte de hardiesse, comme pour réveiller vos souvenirs, à cause de la grâce que Dieu m'a faite 15.16 d'être ministre de Jésus Christ parmi les païens, m'acquittant du divin service de l'Évangile de Dieu, afin que les païens lui soient une offrande agréable, étant sanctifiée par l'Esprit Saint. 15.17 J'ai donc sujet de me glorifier en Jésus Christ, pour ce qui regarde les choses de Dieu. 15.18 Car je n'oserais mentionner aucune chose que Christ n'ait pas faite par moi pour amener les païens à l'obéissance, par la parole et par les actes, 15.19 par la puissance des miracles et des prodiges, par la puissance de l'Esprit de Dieu, en sorte que, depuis Jérusalem et les pays voisins jusqu'en Illyrie, j'ai abondamment répandu l'Évangile de Christ. 15.20 Et je me suis fait honneur d'annoncer l'Évangile là où Christ n'avait point été nommé, afin de ne pas bâtir sur le fondement d'autrui, selon qu'il est écrit: 15.21 Ceux à qui il n'avait point été annoncé verront, Et ceux qui n'en avaient point entendu parler comprendront. 15.22 C'est ce qui m'a souvent empêché d'aller vers vous. 15.23 Mais maintenant, n'ayant plus rien qui me retienne dans ces contrées, et ayant depuis plusieurs années le désir d'aller vers vous, 15.24 j'espère vous voir en passant, quand je me rendrai en Espagne, et y être accompagné par vous, après que j'aurai satisfait en partie mon désir de me trouver chez vous. 15.25 Présentement je vais à Jérusalem, pour le service des saints. 15.26 Car la Macédoine et l'Achaïe ont bien voulu s'imposer une contribution en faveur des pauvres parmi les saints de Jérusalem. 15.27 Elles l'ont bien voulu, et elles le leur devaient; car si les païens ont eu part à leurs avantages spirituels, ils doivent aussi les assister dans les choses temporelles. 15.28 Dès que j'aurai terminé cette affaire et que je leur aurai remis ces dons, je partirai pour l'Espagne et passerai chez vous. 15.29 Je sais qu'en allant vers vous, c'est avec une pleine bénédiction de Christ que j'irai. 15.30 Je vous exhorte, frères, par notre Seigneur Jésus Christ et par l'amour de l'Esprit, à combattre avec moi, en adressant à Dieu des prières en ma faveur, 15.31 afin que je sois délivré des incrédules de la Judée, et que les dons que je porte à Jérusalem soient agréés des saints, 15.32 en sorte que j'arrive chez vous avec joie, si c'est la volonté de Dieu, et que je jouisse au milieu de vous de quelque repos. 15.33 Que le Dieu de paix soit avec vous tous! Amen! Romains 16 16.1 Je vous recommande Phoebé, notre soeur, qui est diaconesse de l'Église de Cenchrées, 16.2 afin que vous la receviez en notre Seigneur d'une manière digne des saints, et que vous l'assistiez dans les choses où elle aurait besoin de vous, car elle en a donné aide à plusieurs et à moi-même. 16.3 Saluez Prisca et Aquilas, mes compagnons d'oeuvre en Jésus Christ, 16.4 qui ont exposé leur tête pour sauver ma vie; ce n'est pas moi seul qui leur rends grâces, ce sont encore toutes les Églises des païens. 16.5 Saluez aussi l'Église qui est dans leur maison. Saluez Épaïnète, mon bien-aimé, qui a été pour Christ les prémices de l'Asie. 16.6 Saluez Marie, qui a pris beaucoup de peine pour vous. 16.7 Saluez Andronicus et Junias, mes parents et mes compagnons de captivité, qui jouissent d'une grande considération parmi les apôtres, et qui même ont été en Christ avant moi. 16.8 Saluez Amplias, mon bien-aimé dans le Seigneur. 16.9 Saluez Urbain, notre compagnon d'oeuvre en Christ, et Stachys, mon bien-aimé. 16.10 Saluez Apellès, qui est éprouvé en Christ. Saluez ceux de la maison d'Aristobule. 16.11 Saluez Hérodion, mon parent. Saluez ceux de la maison de Narcisse qui sont dans le Seigneur. 16.12 Saluez Tryphène et Tryphose, qui travaillent pour le Seigneur. Saluez Perside, la bien-aimée, qui a beaucoup travaillé pour le Seigneur. 16.13 Saluez Rufus, l'élu du Seigneur, et sa mère, qui est aussi la mienne. 16.14 Saluez Asyncrite, Phlégon, Hermès, Patrobas, Hermas, et les frères qui sont avec eux. 16.15 Saluez Philologue et Julie, Nérée et sa soeur, et Olympe, et tous les saints qui sont avec eux. 16.16 Saluez-vous les uns les autres par un saint baiser. Toutes les Églises de Christ vous saluent. 16.17 Je vous exhorte, frères, à prendre garde à ceux qui causent des divisions et des scandales, au préjudice de l'enseignement que vous avez reçu. Éloignez-vous d'eux. 16.18 Car de tels hommes ne servent point Christ notre Seigneur, mais leur propre ventre; et, par des paroles douces et flatteuses, ils séduisent les coeurs des simples. 16.19 Pour vous, votre obéissance est connue de tous; je me réjouis donc à votre sujet, et je désire que vous soyez sages en ce qui concerne le bien et purs en ce qui concerne le mal. 16.20 Le Dieu de paix écrasera bientôt Satan sous vos pieds. Que la grâce de notre Seigneur Jésus Christ soit avec vous! 16.21 Timothée, mon compagnon d'oeuvre, vous salue, ainsi que Lucius, Jason et Sosipater, mes parents. 16.22 Je vous salue dans le Seigneur, moi Tertius, qui ai écrit cette lettre. 16.23 Gaïus, mon hôte et celui de toute l'Église, vous salue. Éraste, le trésorier de la ville, vous salue, ainsi que le frère Quartus. 16.24 Que la grâce de notre Seigneur Jésus Christ soit avec vous tous! Amen! 16.25 A celui qui peut vous affermir selon mon Évangile et la prédication de Jésus Christ, conformément à la révélation du mystère caché pendant des siècles, 16.26 mais manifesté maintenant par les écrits des prophètes, d'après l'ordre du Dieu éternel, et porté à la connaissance de toutes les nations, afin qu'elles obéissent à la foi, 16.27 à Dieu, seul sage, soit la gloire aux siècles des siècles, par Jésus Christ! Amen! ================================================ FILE: test/ws_perf_SUITE_data/japanese.txt ================================================ JAP 1 天と地の創造 1まだ何もなかった時、神は天と地を造りました。 2地は形も定まらず、闇に包まれた水の上を、さらに神の霊が覆っていました。 3「光よ、輝き出よ。」神が言われると、光がさっとさしてきました。 4-5それを見て、神は大いに満足し、光と闇とを区別しました。しばらくの間、光は輝き続け、やがて、もう一度闇に覆われました。神は光を「昼」、闇を「夜」と名づけました。こうして昼と夜ができて、一日目が終わりました。 6「もやは上下に分かれ、空と海になれ」と神が言われると、 7-8そのとおり水蒸気が二つに分かれ、空ができました。こうして二日目も終わりました。 9-10「空の下の水は集まって海となり、乾いた地が現れ出よ。」こう神が言われると、そのとおりになりました。神は乾いた地を「陸地」、水の部分を「海」と名づけました。それを見て満足すると、 11-12神はまた言われました。「陸地には、あらゆる種類の草、種のある植物、実のなる木が生えよ。それぞれの種から同じ種類の草や木が生えるようになれ。」すると、そのとおりになり、神は満足しました。 13これが三日目です。 14-15神のことばはさらに続きます。「空に光が輝き、地を照らせ。その光で、昼と夜の区別、季節の変化、一日や一年の区切りをつけよ。」すると、そのとおりになりました。 16こうして、地を照らす太陽と月ができました。太陽は大きく明るいので昼を、月は夜を治めました。このほかにも、星々が造られました。 17神はそれをみな空にちりばめ、地を照らすようにしました。 18こうして昼と夜を分け終えると、神は満足しました。 19ここまでが四日目の出来事です。 20神は再び言われました。「海は魚やその他の生き物であふれ、空はあらゆる種類の鳥で満ちよ。」 21-22神は海に住む大きな生き物をはじめ、あらゆる種類の魚と鳥を造りました。みなすばらしいものばかりです。神はそれを見て、「海いっぱいに満ちよ。鳥たちは地を覆うまでに増えよ」と祝福しました。 23これが五日目です。 24次に神は言われました。「地は、家畜や地をはうもの、野の獣など、あらゆる種類の生き物を生み出せ。」そのとおりになりました。 25神が造った生き物は、どれも満足のいくものばかりでした。 26そして最後に、神はこう言われました。「さあ、人間を造ろう。地と空と海のあらゆる生き物を治めさせるために、われわれに最も近い、われわれのかたちに似せて人間を造ろう。」 27このように人間は、天地を造った神の特性を持つ者として、男と女とに創造されました。 28神は人間を祝福して言われました。「地に増え広がり、大地を治めよ。あなたがたは、魚と鳥とすべての動物の主人なのだ。 29全地に生える種のある植物を見てみなさい。みなあなたがたのものだ。実のなる木もすべて与えるから、好きなだけ食べるがいい。 30また、動物や鳥にも、あらゆる草と植物を彼らの食物として与える。」 31神はでき上がった世界を隅から隅まで見渡しました。とてもすばらしい世界が広がっていました。こうして六日目が終わりました。 2 1ついに全世界が完成しました。 2すべてを創造し終えると、神は七日目には休まれ、 3この日を祝福して、聖なる日と定めました。この日、天地創造の働きが完了したからです。 人間の創造 人間の創造 人間の創造 人間の創造 人間の創造 人間の創造.