0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Merge branch 'master' into return-allow-header

This commit is contained in:
Edigleysson Silva (Edy) 2024-07-15 11:29:13 -03:00 committed by GitHub
commit 75a8f98490
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 7050 additions and 719 deletions

View file

@ -4,11 +4,16 @@ name: Check Cowboy
on: on:
push: push:
branches:
- master
pull_request: pull_request:
schedule: schedule:
## Every Monday at 2am. ## Every Monday at 2am.
- cron: 0 2 * * 1 - cron: 0 2 * * 1
env:
CI_ERLANG_MK: 1
jobs: jobs:
cleanup-master: cleanup-master:
name: Cleanup master build name: Cleanup master build
@ -29,3 +34,37 @@ jobs:
name: Cowboy name: Cowboy
needs: cleanup-master needs: cleanup-master
uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master
# The examples test suite is nice to run but typically not
# important. So we run them after we are done with the other
# test suites. At this point we know that Erlang was built
# so we can just use the latest version.
examples:
name: Check examples
needs: check
runs-on: 'ubuntu-latest'
if: ${{ !cancelled() }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Output latest Erlang/OTP version
id: latest_version
run: |
{
echo "latest<<EOF"
make ci-list | grep -v rc | grep -v master | tail -n1
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Restore CI cache
uses: actions/cache/restore@v4
with:
path: |
~/erlang/
key: ${{ runner.os }}-${{ runner.arch }}-Erlang-${{ steps.latest_version.outputs.latest }}
- name: Run ct-examples
run: make ct-examples LATEST_ERLANG_OTP=1

View file

@ -1,4 +1,4 @@
Copyright (c) 2011-2022, Loïc Hoguin <essen@ninenines.eu> Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View file

@ -2,7 +2,7 @@
PROJECT = cowboy PROJECT = cowboy
PROJECT_DESCRIPTION = Small, fast, modern HTTP server. PROJECT_DESCRIPTION = Small, fast, modern HTTP server.
PROJECT_VERSION = 2.10.0 PROJECT_VERSION = 2.12.0
PROJECT_REGISTERED = cowboy_clock PROJECT_REGISTERED = cowboy_clock
# Options. # Options.
@ -15,9 +15,14 @@ CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl
LOCAL_DEPS = crypto LOCAL_DEPS = crypto
DEPS = cowlib ranch DEPS = cowlib ranch
dep_cowlib = git https://github.com/ninenines/cowlib 2.12.1 dep_cowlib = git https://github.com/ninenines/cowlib master
dep_ranch = git https://github.com/ninenines/ranch 1.8.0 dep_ranch = git https://github.com/ninenines/ranch 1.8.0
ifeq ($(COWBOY_QUICER),1)
DEPS += quicer
dep_quicer = git https://github.com/emqx/quic main
endif
DOC_DEPS = asciideck DOC_DEPS = asciideck
TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper gun TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper gun
@ -38,8 +43,8 @@ define HEX_TARBALL_EXTRA_METADATA
#{ #{
licenses => [<<"ISC">>], licenses => [<<"ISC">>],
links => #{ links => #{
<<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.10/guide/">>, <<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.12/guide/">>,
<<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.10/manual/">>, <<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.12/manual/">>,
<<"GitHub">> => <<"https://github.com/ninenines/cowboy">>, <<"GitHub">> => <<"https://github.com/ninenines/cowboy">>,
<<"Sponsor">> => <<"https://github.com/sponsors/essen">> <<"Sponsor">> => <<"https://github.com/sponsors/essen">>
} }
@ -50,21 +55,37 @@ endef
include erlang.mk include erlang.mk
# Don't run the examples test suite by default. # Don't run the examples/autobahn test suites by default.
ifndef FULL ifndef FULL
CT_SUITES := $(filter-out examples ws_autobahn,$(CT_SUITES)) CT_SUITES := $(filter-out examples ws_autobahn,$(CT_SUITES))
endif 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. # Compile options.
ERLC_OPTS += +warn_missing_spec +warn_untyped_record # +bin_opt_info ERLC_OPTS += +warn_missing_spec +warn_untyped_record # +bin_opt_info
TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}' 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. # Generate rebar.config on build.
app:: rebar.config 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. # Dialyze the tests.
#DIALYZER_OPTS += --src -r test #DIALYZER_OPTS += --src -r test
@ -101,8 +122,11 @@ prepare_tag:
$(verbose) echo -n "GUIDE: " $(verbose) echo -n "GUIDE: "
$(verbose) grep -h dep_$(PROJECT)_commit doc/src/guide/*.asciidoc || true $(verbose) grep -h dep_$(PROJECT)_commit doc/src/guide/*.asciidoc || true
$(verbose) echo $(verbose) echo
$(verbose) echo "Links in the README:"
$(verbose) grep http.*:// README.asciidoc
$(verbose) echo
$(verbose) echo "Titles in most recent CHANGELOG:" $(verbose) echo "Titles in most recent CHANGELOG:"
$(verbose) for f in `ls -r doc/src/guide/migrating_from_*.asciidoc | head -n1`; do \ $(verbose) for f in `ls -rv doc/src/guide/migrating_from_*.asciidoc | head -n1`; do \
echo $$f:; \ echo $$f:; \
grep == $$f; \ grep == $$f; \
done done

View file

@ -18,8 +18,8 @@ Cowboy is *clean* and *well tested* Erlang code.
== Online documentation == Online documentation
* https://ninenines.eu/docs/en/cowboy/2.6/guide[User guide] * https://ninenines.eu/docs/en/cowboy/2.12/guide[User guide]
* https://ninenines.eu/docs/en/cowboy/2.6/manual[Function reference] * https://ninenines.eu/docs/en/cowboy/2.12/manual[Function reference]
== Offline documentation == Offline documentation

View file

@ -75,6 +75,10 @@ include::performance.asciidoc[Performance]
= Additional information = Additional information
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.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.8.asciidoc[Migrating from Cowboy 2.8 to 2.9]

View file

@ -62,14 +62,16 @@ handler.
=== Cowboy setup === Cowboy setup
We will modify the 'Makefile' to tell the build system it needs to We will modify the 'Makefile' to tell the build system it needs to
fetch and compile Cowboy: fetch and compile Cowboy, and that we will use releases:
[source,makefile] [source,makefile]
---- ----
PROJECT = hello_erlang PROJECT = hello_erlang
DEPS = cowboy DEPS = cowboy
dep_cowboy_commit = 2.10.0 dep_cowboy_commit = 2.11.0
REL_DEPS = relx
DEP_PLUGINS = cowboy DEP_PLUGINS = cowboy
@ -80,6 +82,9 @@ The `DEP_PLUGINS` line tells the build system to load the plugins
Cowboy provides. These include predefined templates that we will Cowboy provides. These include predefined templates that we will
use soon. 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 If you do `make run` now, Cowboy will be included in the release
and started automatically. This is not enough however, as Cowboy and started automatically. This is not enough however, as Cowboy
doesn't do anything by default. We still need to tell Cowboy to doesn't do anything by default. We still need to tell Cowboy to

View file

@ -42,7 +42,7 @@ Cowboy is developed for Erlang/OTP 22.0 and newer.
Cowboy uses the ISC License. Cowboy uses the ISC License.
---- ----
Copyright (c) 2011-2019, Loïc Hoguin <essen@ninenines.eu> Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View file

@ -0,0 +1,139 @@
[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.

View file

@ -0,0 +1,15 @@
[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.

View file

@ -144,6 +144,16 @@ never be called.
Implement the `languages_provided` or `charsets_provided` Implement the `languages_provided` or `charsets_provided`
callbacks if applicable. 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 Is there any other header that may make the representation
of the resource vary? Implement the `variances` callback. of the resource vary? Implement the `variances` callback.
@ -191,10 +201,15 @@ the `options` method.
=== GET and HEAD methods === GET and HEAD methods
If you implement the methods GET and/or HEAD, you must If you implement the methods GET and/or HEAD, you must
implement one `ProvideResource` callback for each implement one `ProvideCallback` callback for each
content-type returned by the `content_types_provided` content-type returned by the `content_types_provided`
callback. 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 === PUT, POST and PATCH methods
If you implement the methods PUT, POST and/or PATCH, If you implement the methods PUT, POST and/or PATCH,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Before After
Before After

View file

@ -2,24 +2,23 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="744.09448819" width="744.09448819"
height="1052.3622047" height="1052.3622047"
id="svg2" id="svg2"
version="1.1" version="1.1"
inkscape:version="0.48.4 r9939" inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="rest_conneg.svg" sodipodi:docname="rest_conneg.svg"
inkscape:export-filename="/home/essen/Dropbox/Public/drawing.png" inkscape:export-filename="/home/essen/Dropbox/Public/drawing.png"
inkscape:export-xdpi="90" inkscape:export-xdpi="90"
inkscape:export-ydpi="90"> inkscape:export-ydpi="90"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs <defs
id="defs4"> id="defs4">
<linearGradient <linearGradient
@ -50,7 +49,7 @@
</linearGradient> </linearGradient>
<linearGradient <linearGradient
id="linearGradient5233" id="linearGradient5233"
osb:paint="solid"> inkscape:swatch="solid">
<stop <stop
style="stop-color:#69d2e7;stop-opacity:1;" style="stop-color:#69d2e7;stop-opacity:1;"
offset="0" offset="0"
@ -64,26 +63,34 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="1" inkscape:pageopacity="1"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1.4142136" inkscape:zoom="0.65304847"
inkscape:cx="222.80947" inkscape:cx="259.55194"
inkscape:cy="634.56615" inkscape:cy="483.11881"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
inkscape:window-width="2560" inkscape:window-width="1440"
inkscape:window-height="1402" inkscape:window-height="900"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="38" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:snap-global="true" inkscape:snap-global="true"
showguides="true"> showguides="true"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid <inkscape:grid
type="xygrid" type="xygrid"
id="grid5357" id="grid5357"
empspacing="5" empspacing="5"
visible="true" visible="false"
enabled="true" enabled="true"
snapvisiblegridlinesonly="true" /> snapvisiblegridlinesonly="true"
originx="0"
originy="0"
spacingy="1"
spacingx="1"
units="px" />
</sodipodi:namedview> </sodipodi:namedview>
<metadata <metadata
id="metadata7"> id="metadata7">
@ -93,7 +100,7 @@
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:type <dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title> <dc:title />
</cc:Work> </cc:Work>
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
@ -112,7 +119,7 @@
<g <g
transform="translate(303.92143,-296.03137)" transform="translate(303.92143,-296.03137)"
id="g5650-7" id="g5650-7"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="rest_conneg.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"> inkscape:export-ydpi="89.926643">
<path <path
@ -557,149 +564,149 @@
inkscape:export-ydpi="89.926643" /> inkscape:export-ydpi="89.926643" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#77823c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#77823c;fill-opacity:1;stroke:none"
x="-58.692513" x="-58.692513"
y="114.39204" y="114.39204"
id="text5371" id="text5371"><tspan
sodipodi:linespacing="125%"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373" id="tspan5373"
x="-58.692513" x="-58.692513"
y="114.39204">some text</tspan></text> y="114.39204"
style="font-size:16px;line-height:1.25;font-family:sans-serif">some text</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="-58.692513" x="-58.692513"
y="53.112247" y="53.112247"
id="text5371-2" id="text5371-2"><tspan
sodipodi:linespacing="125%"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6" id="tspan5373-6"
x="-58.692513" x="-58.692513"
y="53.112247">some text</tspan></text> y="53.112247"
style="font-size:16px;line-height:1.25;font-family:sans-serif">some text</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.18575" x="246.18575"
y="310.19913" y="310.19913"
id="text5371-2-3" id="text5371-2-3"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-7" id="tspan5373-6-7"
x="246.18575" x="246.18575"
y="310.19913">has accept-language?</tspan></text> y="310.19913"
style="font-size:16px;line-height:1.25;font-family:sans-serif">has accept-language?</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="245.60762" x="245.60762"
y="477.47531" y="477.47531"
id="text5371-2-3-0" id="text5371-2-3-0"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-7-3" id="tspan5373-6-7-3"
x="245.60762" x="245.60762"
y="477.47531">has accept-charset?</tspan></text> y="477.47531"
style="font-size:16px;line-height:1.25;font-family:sans-serif">has accept-charset?</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="-58.692513" x="-58.692513"
y="236.95154" y="236.95154"
id="text5371-4" id="text5371-4"><tspan
sodipodi:linespacing="125%"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9" id="tspan5373-9"
x="-58.692513" x="-58.692513"
y="236.95154">some text</tspan></text> y="236.95154"
style="font-size:16px;line-height:1.25;font-family:sans-serif">some text</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="245.00391" x="245.00391"
y="60.912468" y="60.912468"
id="text5371-4-0" id="text5371-4-0"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
x="245.00391" x="245.00391"
y="60.912468" y="60.912468"
id="tspan17171">start</tspan></text> id="tspan17171"
style="font-size:16px;line-height:1.25;font-family:sans-serif">start</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.11153" x="246.11153"
y="561.14258" y="561.14258"
id="text5371-2-9" id="text5371-2-9"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-8" id="tspan5373-6-8"
x="246.11153" x="246.11153"
y="561.14258">charsets_provided</tspan></text> y="561.14258"
style="font-size:16px;line-height:1.25;font-family:sans-serif">charsets_provided</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.64278" x="246.64278"
y="646.58331" y="646.58331"
id="text5371-2-7" id="text5371-2-7"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-2" id="tspan5373-6-2"
x="246.64278" x="246.64278"
y="646.58331">variances</tspan></text> y="646.58331"
style="font-size:16px;line-height:1.25;font-family:sans-serif">ranges_provided</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.13106" x="246.13106"
y="142.80627" y="142.80627"
id="text5371-2-95" id="text5371-2-95"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-80" id="tspan5373-6-80"
x="246.13106" x="246.13106"
y="142.80627">has accept?</tspan></text> y="142.80627"
style="font-size:16px;line-height:1.25;font-family:sans-serif">has accept?</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="245.80684" x="245.80684"
y="226.4736" y="226.4736"
id="text5371-2-32" id="text5371-2-32"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-27" id="tspan5373-6-27"
x="245.80684" x="245.80684"
y="226.4736">content_types_provided</tspan></text> y="226.4736"
style="font-size:16px;line-height:1.25;font-family:sans-serif">content_types_provided</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.05293" x="246.05293"
y="393.80801" y="393.80801"
id="text5371-2-74" id="text5371-2-74"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-18" id="tspan5373-6-18"
x="246.05293" x="246.05293"
y="393.80801">languages_provided</tspan></text> y="393.80801"
style="font-size:16px;line-height:1.25;font-family:sans-serif">languages_provided</tspan></text>
<rect <rect
style="fill:#d1f2a5;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" style="fill:#d1f2a5;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect5273-1-2" id="rect5273-1-2"
@ -710,88 +717,88 @@
rx="15" /> rx="15" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none"
x="262.26562" x="262.26562"
y="185.95248" y="185.95248"
id="text5371-2-391" id="text5371-2-391"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-63" id="tspan5373-6-63"
x="262.26562" x="262.26562"
y="185.95248">true</tspan></text> y="185.95248"
style="font-size:16px;line-height:1.25;font-family:sans-serif">true</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none"
x="262.26562" x="262.26562"
y="269.61978" y="269.61978"
id="text5371-2-954" id="text5371-2-954"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-808" id="tspan5373-6-808"
x="262.26562" x="262.26562"
y="269.61978">provided*</tspan></text> y="269.61978"
style="font-size:16px;line-height:1.25;font-family:sans-serif">provided*</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none"
x="262.26562" x="262.26562"
y="353.28702" y="353.28702"
id="text5371-2-4" id="text5371-2-4"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-11" id="tspan5373-6-11"
x="262.26562" x="262.26562"
y="353.28702">true</tspan></text> y="353.28702"
style="font-size:16px;line-height:1.25;font-family:sans-serif">true</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none"
x="262.26562" x="262.26562"
y="436.95425" y="436.95425"
id="text5371-2-92" id="text5371-2-92"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-4" id="tspan5373-6-4"
x="262.26562" x="262.26562"
y="436.95425">provided*</tspan></text> y="436.95425"
style="font-size:16px;line-height:1.25;font-family:sans-serif">provided*</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none"
x="262.26562" x="262.26562"
y="520.62152" y="520.62152"
id="text5371-2-739" id="text5371-2-739"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-0" id="tspan5373-6-0"
x="262.26562" x="262.26562"
y="520.62152">true</tspan></text> y="520.62152"
style="font-size:16px;line-height:1.25;font-family:sans-serif">true</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#6d8e41;fill-opacity:1;stroke:none"
x="262.26562" x="262.26562"
y="604.28876" y="604.28876"
id="text5371-2-8" id="text5371-2-8"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-3" id="tspan5373-6-3"
x="262.26562" x="262.26562"
y="604.28876">provided*</tspan></text> y="604.28876"
style="font-size:16px;line-height:1.25;font-family:sans-serif">provided*</tspan></text>
<g <g
transform="matrix(0,-1,1,0,-513.31414,353.05561)" transform="matrix(0,-1,1,0,-513.31414,353.05561)"
id="g5650-2"> id="g5650-2">
@ -820,75 +827,75 @@
</g> </g>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="76.761719" x="76.761719"
y="227.88033" y="227.88033"
id="text5371-4-6" id="text5371-4-6"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9-0" id="tspan5373-9-0"
x="76.761719" x="76.761719"
y="227.88033">false</tspan></text> y="227.88033"
style="font-size:16px;line-height:1.25;font-family:sans-serif">false</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="76.761719" x="76.761719"
y="395.20209" y="395.20209"
id="text5371-4-2" id="text5371-4-2"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9-01" id="tspan5373-9-01"
x="76.761719" x="76.761719"
y="395.20209">false</tspan></text> y="395.20209"
style="font-size:16px;line-height:1.25;font-family:sans-serif">false</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="413.26172" x="413.26172"
y="374.19577" y="374.19577"
id="text5371-4-3" id="text5371-4-3"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9-62" id="tspan5373-9-62"
x="413.26172" x="413.26172"
y="374.19577">not provided*</tspan></text> y="374.19577"
style="font-size:16px;line-height:1.25;font-family:sans-serif">not provided*</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="76.761719" x="76.761719"
y="562.52386" y="562.52386"
id="text5371-4-4" id="text5371-4-4"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9-2" id="tspan5373-9-2"
x="76.761719" x="76.761719"
y="562.52386">false</tspan></text> y="562.52386"
style="font-size:16px;line-height:1.25;font-family:sans-serif">false</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="-8.8034744" x="-8.8034744"
y="663.24762" y="663.24762"
id="text5371-4-5" id="text5371-4-5"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643" inkscape:export-ydpi="89.926643"
transform="matrix(0.7410941,-0.67140117,0.67140117,0.7410941,0,0)"><tspan transform="rotate(-42.1753)"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9-09" id="tspan5373-9-09"
x="-8.8034744" x="-8.8034744"
y="663.24762">not provided*</tspan></text> y="663.24762"
style="font-size:16px;line-height:1.25;font-family:sans-serif">not provided*</tspan></text>
<rect <rect
style="fill:#effab4;fill-opacity:1;fill-rule:nonzero;stroke:#c7d28c;stroke-width:2.73499846;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" style="fill:#effab4;fill-opacity:1;fill-rule:nonzero;stroke:#c7d28c;stroke-width:2.73499846;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect5273-7-3-1" id="rect5273-7-3-1"
@ -931,18 +938,18 @@
</g> </g>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#77823c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#77823c;fill-opacity:1;stroke:none"
x="599.20062" x="599.20062"
y="394.09869" y="394.09869"
id="text5371-43" id="text5371-43"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-3" id="tspan5373-3"
x="599.20062" x="599.20062"
y="394.09869">406 not acceptable</tspan></text> y="394.09869"
style="font-size:16px;line-height:1.25;font-family:sans-serif">406 not acceptable</tspan></text>
<rect <rect
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:2.44279909;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:2.44279909;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect5367" id="rect5367"
@ -956,19 +963,19 @@
inkscape:export-ydpi="89.926643" /> inkscape:export-ydpi="89.926643" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="-544.69421" x="-544.69421"
y="-354.17184" y="-354.17184"
id="text5371-2-3-0-7" id="text5371-2-3-0-7"
sodipodi:linespacing="125%" transform="rotate(-90)"
transform="matrix(0,-1,1,0,0,0)"
inkscape:export-filename="/home/essen/extend/cowboy/guide/http_req_resp.png" inkscape:export-filename="/home/essen/extend/cowboy/guide/http_req_resp.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-7-3-9" id="tspan5373-6-7-3-9"
x="-544.69421" x="-544.69421"
y="-354.17184">middlewares</tspan></text> y="-354.17184"
style="font-size:16px;line-height:1.25;font-family:sans-serif">middlewares</tspan></text>
<g <g
transform="matrix(0,-1,1,0,-508.93096,565.23553)" transform="matrix(0,-1,1,0,-508.93096,565.23553)"
id="g5650-2-0-4" id="g5650-2-0-4"
@ -1093,19 +1100,48 @@
</g> </g>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#9b3b1c;fill-opacity:1;stroke:none"
x="509.41452" x="509.41452"
y="-106.16136" y="-106.16136"
id="text5371-4-5-9" id="text5371-4-5-9"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643" inkscape:export-ydpi="89.926643"
transform="matrix(0.69480867,0.71919462,-0.71919462,0.69480867,0,0)"><tspan transform="rotate(45.988027)"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-9-09-1" id="tspan5373-9-09-1"
x="509.41452" x="509.41452"
y="-106.16136">not provided*</tspan></text> y="-106.16136"
style="font-size:16px;line-height:1.25;font-family:sans-serif">not provided*</tspan></text>
<g
transform="translate(303.92156,372.14538)"
id="g5650-6-2-7"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643">
<path
inkscape:connector-curvature="0"
id="path5570-78-4-5"
d="m -57.78256,351.41962 v 52.3259"
style="opacity:0.8;fill:none;stroke:#6d8e41;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(0.58787746,0,0,0.58787746,73.160466,163.35774)"
inkscape:transform-center-y="2.1823437"
d="m -222.73865,430.10821 -12.85982,-22.27386 25.71964,0 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="2.6179939"
sodipodi:arg1="1.5707963"
sodipodi:r2="7.4246211"
sodipodi:r1="14.849242"
sodipodi:cy="415.25897"
sodipodi:cx="-222.73865"
sodipodi:sides="3"
id="path5576-9-2-3"
style="opacity:0.8;fill:#6d8e41;fill-opacity:1;fill-rule:nonzero;stroke:#6d8e41;stroke-width:0;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
sodipodi:type="star" />
</g>
<rect <rect
style="fill:#d1f2a5;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" style="fill:#d1f2a5;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="rect5273-1-41-0" id="rect5273-1-41-0"
@ -1119,17 +1155,42 @@
inkscape:export-ydpi="89.926643" /> inkscape:export-ydpi="89.926643" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:16px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.15048" x="246.15048"
y="725.27777" y="730.10156"
id="text5371-2-7-9" id="text5371-2-7-9"
sodipodi:linespacing="125%"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png" inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643" inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line" sodipodi:role="line"
id="tspan5373-6-2-1" id="tspan5373-6-2-1"
x="246.15048" x="246.15048"
y="725.27777">...</tspan></text> y="730.10156"
style="font-size:16px;line-height:1.25;font-family:sans-serif">variances</tspan></text>
<rect
style="fill:#d1f2a5;fill-opacity:1;fill-rule:nonzero;stroke:#a9ca7d;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5273-1-41-0-2"
width="210.17955"
height="35.209244"
x="141.049"
y="789.44257"
rx="15"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:Sans;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#6d8e41;fill-opacity:1;stroke:none"
x="246.15047"
y="808.03937"
id="text5371-2-7-9-9"
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
inkscape:export-xdpi="89.926643"
inkscape:export-ydpi="89.926643"><tspan
sodipodi:role="line"
id="tspan5373-6-2-1-1"
x="246.15047"
y="808.03937"
style="font-size:16px;line-height:1.25;font-family:sans-serif">...</tspan></text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

View file

@ -95,6 +95,11 @@ callback will only be called at the end of the
"GET and HEAD methods" diagram, when all conditions "GET and HEAD methods" diagram, when all conditions
have been met. 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 The selected content-type, language and charset are
saved as meta values in the Req object. You *should* saved as meta values in the Req object. You *should*
use the appropriate representation if you set a use the appropriate representation if you set a
@ -121,11 +126,18 @@ succeed, the resource can be retrieved.
Cowboy prepares the response by first retrieving Cowboy prepares the response by first retrieving
metadata about the representation, then by calling metadata about the representation, then by calling
the `ProvideResource` callback. This is the callback the `ProvideCallback` callback. This is the callback
you defined for each content-types you returned from you defined for each content-types you returned from
`content_types_provided`. This callback returns the body `content_types_provided`. This callback returns the body
that will be sent back to the client, or a fun if the that will be sent back to the client.
body must be streamed.
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 When the resource does not exist, Cowboy will figure out
whether the resource existed previously, and if so whether whether the resource existed previously, and if so whether

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Before After
Before After

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Before After
Before After

View file

@ -84,6 +84,8 @@ if it is undefined, moving directly to the next step. Similarly,
| multiple_choices | `false` | multiple_choices | `false`
| options | `ok` | options | `ok`
| previously_existed | `false` | previously_existed | `false`
| ranges_provided | skip
| range_satisfiable | `true`
| rate_limited | `false` | rate_limited | `false`
| resource_exists | `true` | resource_exists | `true`
| service_available | `true` | service_available | `true`
@ -97,8 +99,9 @@ As you can see, Cowboy tries to move on with the request whenever
possible by using well thought out default values. possible by using well thought out default values.
In addition to these, there can be any number of user-defined In addition to these, there can be any number of user-defined
callbacks that are specified through `content_types_accepted/2` callbacks that are specified through `content_types_accepted/2`,
and `content_types_provided/2`. They can take any name, however `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 it is recommended to use a separate prefix for the callbacks of
each function. For example, `from_html` and `to_html` indicate each function. For example, `from_html` and `to_html` indicate
in the first case that we're accepting a resource given as HTML, in the first case that we're accepting a resource given as HTML,
@ -113,9 +116,10 @@ Req object directly. The values are defined in the following table:
[cols="<,<",options="header"] [cols="<,<",options="header"]
|=== |===
| Key | Details | Key | Details
| media_type | The content-type negotiated for the response entity. | media_type | The content-type negotiated for the response entity
| language | The language negotiated for the response entity. | language | The language negotiated for the response entity
| charset | The charset 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 They can be used to send a proper body with the response to a
@ -129,11 +133,16 @@ of the REST code. They are listed in the following table.
[cols="<,<",options="header"] [cols="<,<",options="header"]
|=== |===
| Header name | Details | 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-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 | content-type | Media type and charset of the response body
| etag | Etag of the resource | etag | Etag of the resource
| expires | Expiration date of the resource | expires | Expiration date of the resource
| last-modified | Last modification date for the resource | last-modified | Last modification date for the resource
| location | Relative or absolute URI to the requested 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 | vary | List of headers that may change the representation of the resource
| www-authenticate | Authentication information to access the resource
|=== |===

View file

@ -36,6 +36,7 @@ opts() :: #{
max_skip_body_length => non_neg_integer(), max_skip_body_length => non_neg_integer(),
proxy_header => boolean(), proxy_header => boolean(),
request_timeout => timeout(), request_timeout => timeout(),
reset_idle_timeout_on_send => boolean(),
sendfile => boolean(), sendfile => boolean(),
stream_handlers => [module()] stream_handlers => [module()]
} }
@ -148,6 +149,11 @@ request_timeout (5000)::
Time in ms with no requests before Cowboy closes the connection. 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):: sendfile (true)::
Whether the sendfile syscall may be used. It can be useful to disable Whether the sendfile syscall may be used. It can be useful to disable
@ -160,6 +166,7 @@ Ordered list of stream handlers that will handle all stream events.
== Changelog == Changelog
* *2.11*: The `reset_idle_timeout_on_send` option was added.
* *2.8*: The `active_n` option was added. * *2.8*: The `active_n` option was added.
* *2.7*: The `initial_stream_flow_size` and `logger` options were 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.6*: The `chunked`, `http10_keepalive`, `proxy_header` and `sendfile` options were added.

View file

@ -35,6 +35,7 @@ opts() :: #{
max_connection_window_size => 0..16#7fffffff, max_connection_window_size => 0..16#7fffffff,
max_decode_table_size => non_neg_integer(), max_decode_table_size => non_neg_integer(),
max_encode_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_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity, max_frame_size_sent => 16384..16777215 | infinity,
max_received_frame_rate => {pos_integer(), timeout()}, max_received_frame_rate => {pos_integer(), timeout()},
@ -44,6 +45,7 @@ opts() :: #{
max_stream_window_size => 0..16#7fffffff, max_stream_window_size => 0..16#7fffffff,
preface_timeout => timeout(), preface_timeout => timeout(),
proxy_header => boolean(), proxy_header => boolean(),
reset_idle_timeout_on_send => boolean(),
sendfile => boolean(), sendfile => boolean(),
settings_timeout => timeout(), settings_timeout => timeout(),
stream_handlers => [module()], stream_handlers => [module()],
@ -93,7 +95,10 @@ enable_connect_protocol (false)::
Whether to enable the extended CONNECT method to allow Whether to enable the extended CONNECT method to allow
protocols like Websocket to be used over an HTTP/2 stream. protocols like Websocket to be used over an HTTP/2 stream.
This option is experimental and disabled by default. +
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):: goaway_initial_timeout (1000)::
@ -168,6 +173,14 @@ 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 compare this value to what the client advertises and choose the smallest
one as the encoder's header table size. 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):: max_frame_size_received (16384)::
Maximum size in bytes of the frames received by the server. This value is Maximum size in bytes of the frames received by the server. This value is
@ -235,6 +248,11 @@ Whether incoming connections have a PROXY protocol header. The
proxy information will be passed forward via the `proxy_header` proxy information will be passed forward via the `proxy_header`
key of the Req object. 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):: sendfile (true)::
Whether the sendfile syscall may be used. It can be useful to disable Whether the sendfile syscall may be used. It can be useful to disable
@ -271,6 +289,8 @@ too many `WINDOW_UPDATE` frames.
== Changelog == Changelog
* *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 * *2.11*: Add the option `max_cancel_stream_rate` to protect
against another flood scenario. against another flood scenario.
* *2.9*: The `goaway_initial_timeout` and `goaway_complete_timeout` * *2.9*: The `goaway_initial_timeout` and `goaway_complete_timeout`
@ -300,7 +320,7 @@ too many `WINDOW_UPDATE` frames.
`max_frame_size_received`, `max_frame_size_sent` `max_frame_size_received`, `max_frame_size_sent`
and `settings_timeout` to configure HTTP/2 SETTINGS and `settings_timeout` to configure HTTP/2 SETTINGS
and related behavior. and related behavior.
* *2.4*: Add the experimental option `enable_connect_protocol`. * *2.4*: Add the option `enable_connect_protocol`.
* *2.0*: Protocol introduced. * *2.0*: Protocol introduced.
== See also == See also

View file

@ -605,17 +605,139 @@ The response body can be provided either as the actual data
to be sent or a tuple indicating which file to send. to be sent or a tuple indicating which file to send.
This function is called for both GET and HEAD requests. For This function is called for both GET and HEAD requests. For
the latter the body is not sent, however. 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 // @todo Perhaps we can optimize HEAD requests and just
// allow calculating the length instead of returning the // allow calculating the length instead of returning the
// whole thing. // whole thing.
Note that there used to be a way to stream the response body. It is possible to stream the response body either by manually
It was temporarily removed and will be added back in a later sending the response and returning a `stop` value; or by
release. 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.
// @todo Add a way to switch to loop handler for streaming the body. == 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 === rate_limited
@ -625,7 +747,7 @@ rate_limited(Req, State) -> {Result, Req, State}
Result :: false | {true, RetryAfter} Result :: false | {true, RetryAfter}
RetryAfter :: non_neg_integer() | calendar:datetime() RetryAfter :: non_neg_integer() | calendar:datetime()
Default - false Default :: false
---- ----
Return whether the user is rate limited. Return whether the user is rate limited.
@ -734,6 +856,8 @@ listed here, like the authorization header.
== Changelog == Changelog
* *2.11*: The `ranges_provided`, `range_satisfiable` and
the `RangeCallback` callbacks have been added.
* *2.11*: The `generate_etag` callback can now return * *2.11*: The `generate_etag` callback can now return
`undefined` to conditionally avoid generating `undefined` to conditionally avoid generating
an etag. an etag.

View file

@ -129,6 +129,8 @@ when it fails to detect a file's MIME type.
== Changelog == 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. * *2.6*: The `charset` extra option was added.
* *1.0*: Handler introduced. * *1.0*: Handler introduced.

View file

@ -285,6 +285,7 @@ normal circumstances if necessary.
== Changelog == Changelog
* *2.11*: Websocket over HTTP/2 is now considered stable.
* *2.11*: HTTP/1.1 Websocket no longer traps exits by default. * *2.11*: HTTP/1.1 Websocket no longer traps exits by default.
* *2.8*: The `active_n` option was added. * *2.8*: The `active_n` option was added.
* *2.7*: The commands based interface has been documented. * *2.7*: The commands based interface has been documented.

View file

@ -1,7 +1,7 @@
{application, 'cowboy', [ {application, 'cowboy', [
{description, "Small, fast, modern HTTP server."}, {description, "Small, fast, modern HTTP server."},
{vsn, "2.10.0"}, {vsn, "2.12.0"},
{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_loop','cowboy_metrics_h','cowboy_middleware','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']}, {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']},
{registered, [cowboy_sup,cowboy_clock]}, {registered, [cowboy_sup,cowboy_clock]},
{applications, [kernel,stdlib,crypto,cowlib,ranch]}, {applications, [kernel,stdlib,crypto,cowlib,ranch]},
{optional_applications, []}, {optional_applications, []},

5
erlang.mk vendored
View file

@ -17,7 +17,7 @@
ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST)))
export ERLANG_MK_FILENAME export ERLANG_MK_FILENAME
ERLANG_MK_VERSION = bb811a8 ERLANG_MK_VERSION = 16d60fa
ERLANG_MK_WITHOUT = ERLANG_MK_WITHOUT =
# Make 3.81 and 3.82 are deprecated. # Make 3.81 and 3.82 are deprecated.
@ -3565,7 +3565,7 @@ REBAR_DEPS_DIR = $(DEPS_DIR)
export REBAR_DEPS_DIR export REBAR_DEPS_DIR
REBAR3_GIT ?= https://github.com/erlang/rebar3 REBAR3_GIT ?= https://github.com/erlang/rebar3
REBAR3_COMMIT ?= 3f563feaf1091a1980241adefa83a32dd2eebf7c # 3.20.0 REBAR3_COMMIT ?= 06aaecd51b0ce828b66bb65a74d3c1fd7833a4ba # 3.22.1 + OTP-27 fixes
CACHE_DEPS ?= 0 CACHE_DEPS ?= 0
@ -4665,7 +4665,6 @@ define makedep.erl
end, end,
MakeDepend = fun MakeDepend = fun
(F, Fd, Mod, StartLocation) -> (F, Fd, Mod, StartLocation) ->
{ok, Filename} = file:pid2name(Fd),
case io:parse_erl_form(Fd, undefined, StartLocation) of case io:parse_erl_form(Fd, undefined, StartLocation) of
{ok, AbsData, EndLocation} -> {ok, AbsData, EndLocation} ->
case AbsData of case AbsData of

View file

@ -9,8 +9,9 @@ $ make run
Then point your browser to https://localhost:8443 Then point your browser to https://localhost:8443
You will need to temporarily trust the root certificate authority, You will be greeted by a security message. You can ask for more
which can also be found in `priv/ssl/cowboy-ca.crt`. 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 Recent browsers will communicate using HTTP/2. Older browsers
will use HTTP/1.1. will use HTTP/1.1.
@ -19,7 +20,7 @@ will use HTTP/1.1.
[source,bash] [source,bash]
---- ----
$ curl --cacert priv/ssl/cowboy-ca.crt -i https://localhost:8443 $ curl -k -i https://localhost:8443
HTTP/1.1 200 OK HTTP/1.1 200 OK
connection: keep-alive connection: keep-alive
server: Cowboy server: Cowboy

View file

@ -0,0 +1,20 @@
-----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-----

View file

@ -1,16 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICeDCCAeGgAwIBAgIJAOvpU0y2e5J4MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczETMBEGA1UECgwKTmluZSBOaW5lczEPMA0G
A1UECwwGQ293Ym95MRAwDgYDVQQDDAdST09UIENBMB4XDTEzMDIyODA1MTAwMVoX
DTMzMDIyMzA1MTAwMVowVTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRMw
EQYDVQQKDApOaW5lIE5pbmVzMQ8wDQYDVQQLDAZDb3dib3kxEDAOBgNVBAMMB1JP
T1QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMzmY7Us06yjyUbpqwPx
Iv+xh/g3V7we07ClC9GEYnvr3OQvdA1jFEHccMBUUjRoQ8DPd6uSyK5UkixABs08
Tt5B3VsnGKr0DIN+IO4SN2PkmBqIU/BN3KdcwN65YNr3iM0KsKWeFtAZdYx4CakX
7REbO0wjK20AH3xSBn3uFGiBAgMBAAGjUDBOMB0GA1UdDgQWBBRKfZ8KF2jlLBDm
NL6IuEuGY0pdbzAfBgNVHSMEGDAWgBRKfZ8KF2jlLBDmNL6IuEuGY0pdbzAMBgNV
HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAG1I0kBxXiLkM1b7rl2zPLizREYg
1m+ajb6rWzPOBg6TXjv58Be+H4tqoHIL/M/crixew5emftBkuAGjiKMhbIokjvan
aPTCV8U6HHvNvz9c68HpESWbd+56cHqfsS5XCKp1OpW5tbL2UQYpFKMP4qmbv3Ea
pBfPPmSFMBb1i2AI
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----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-----

View file

@ -1,17 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICpTCCAg6gAwIBAgIJAOvpU0y2e5J5MA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczETMBEGA1UECgwKTmluZSBOaW5lczEPMA0G
A1UECwwGQ293Ym95MRAwDgYDVQQDDAdST09UIENBMB4XDTEzMDIyODA1MjMzNFoX
DTMzMDIyMzA1MjMzNFowVzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRMw
EQYDVQQKDApOaW5lIE5pbmVzMQ8wDQYDVQQLDAZDb3dib3kxEjAQBgNVBAMMCWxv
Y2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzbW1GjECzHUc/WST
qLiAGqjCNccR5saVS+yoz2SPRhpoyf0/qBrX5BY0tzmgozoTiRfE4wCiVD99Cc+D
rp/FM49r4EpZdocIovprmOmv/gwkoj95zaA6PKNn1OdmDp2hwJsX2Zm3kpbGUZTx
jDkkccmgUb4EjL7qNHq7saQtivUCAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB
hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE
FB6jTEIWI8T1ckORA4GezbyYxtbvMB8GA1UdIwQYMBaAFEp9nwoXaOUsEOY0voi4
S4ZjSl1vMA0GCSqGSIb3DQEBBQUAA4GBACMboVQjrx8u/fk3gl/sR0tbA0Wf/NcS
2Dzsy2czndgVUAG4Sqb+hfgn0dqAyUKghRrj3JDcYxYksGPIklDfPzZb7yJ39l16
6x5ZiIzhp8CAVdPvRxRznw5rZwaXesryXu1jVSZxTr3MYZdkG6KaAM0t90+YlGLZ
UG8fAicx0Bf+
-----END CERTIFICATE-----

View file

@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNtbUaMQLMdRz9ZJOouIAaqMI1xxHmxpVL7KjPZI9GGmjJ/T+o
GtfkFjS3OaCjOhOJF8TjAKJUP30Jz4Oun8Uzj2vgSll2hwii+muY6a/+DCSiP3nN
oDo8o2fU52YOnaHAmxfZmbeSlsZRlPGMOSRxyaBRvgSMvuo0eruxpC2K9QIDAQAB
AoGAaD85c/h6bpq7Aj7CBbLaWKhFI3OqwsTITB22vsM7SE+B4zsP02UnG1OVi3UM
zytTUxpUkKV1njQ+bYZYOVqGWF4Up8tTqUglHn0FTPok1AIemELWtz3sXvdSHC1T
lqvFBAZ9kibn13qGyVOiyCFaMwfOM/05RvV7p3jfUMTWnNECQQDs7yCJZ8Ol8MyH
TGZzvkjoN2zg1KwmTbSD1hkP6QAJtPdRuqFbjlEru0/PefgOXsWLRIa3/3v0qw2G
xGkV6AXTAkEA3kNbFisqUydjPnZIYv/P6SvPdUimHJEjXbAbfNfzS9dzszrOVJd2
XqGH7z5yzjoH3IyaIMW8GnubVzGDSjrHFwJAKSU5vELlygpwKkrNO+pelN0TLlQg
dSJnZ8GlZorq88SWcn37iX/EftivenNO7YftvEqxLoDSkOGnnrC7Iw/A+wJBAIEe
L/QY72WPJCBNJpAce/PA96vyoE1II3txqwZDjZspdpVQPDz4IFOpEwbxCFC1dYuy
Qnd3Z2cbF4r3wIWGz9ECQQCJGNhUNtY+Om1ELdqPcquxE2VRV/pucnvJSTKwyo2C
Rvm6H7kFDwPDuN23YnTOlTiho0zzCkclcIukhIVJ+dKz
-----END RSA PRIVATE KEY-----

View file

@ -19,9 +19,8 @@ start(_Type, _Args) ->
PrivDir = code:priv_dir(ssl_hello_world), PrivDir = code:priv_dir(ssl_hello_world),
{ok, _} = cowboy:start_tls(https, [ {ok, _} = cowboy:start_tls(https, [
{port, 8443}, {port, 8443},
{cacertfile, PrivDir ++ "/ssl/cowboy-ca.crt"}, {certfile, PrivDir ++ "/ssl/cert.pem"},
{certfile, PrivDir ++ "/ssl/server.crt"}, {keyfile, PrivDir ++ "/ssl/key.pem"}
{keyfile, PrivDir ++ "/ssl/server.key"}
], #{env => #{dispatch => Dispatch}}), ], #{env => #{dispatch => Dispatch}}),
ssl_hello_world_sup:start_link(). ssl_hello_world_sup:start_link().

View file

@ -1,4 +1,4 @@
{deps, [ {deps, [
{cowlib,".*",{git,"https://github.com/ninenines/cowlib","2.12.1"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}} {cowlib,".*",{git,"https://github.com/ninenines/cowlib","master"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}}
]}. ]}.
{erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}. {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}.

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -16,6 +16,7 @@
-export([start_clear/3]). -export([start_clear/3]).
-export([start_tls/3]). -export([start_tls/3]).
-export([start_quic/3]).
-export([stop_listener/1]). -export([stop_listener/1]).
-export([get_env/2]). -export([get_env/2]).
-export([get_env/3]). -export([get_env/3]).
@ -25,6 +26,9 @@
-export([log/2]). -export([log/2]).
-export([log/4]). -export([log/4]).
%% Don't warn about the bad quicer specs.
-dialyzer([{nowarn_function, start_quic/3}]).
-type opts() :: cowboy_http:opts() | cowboy_http2:opts(). -type opts() :: cowboy_http:opts() | cowboy_http2:opts().
-export_type([opts/0]). -export_type([opts/0]).
@ -44,6 +48,7 @@
-spec start_clear(ranch:ref(), ranch:opts(), opts()) -spec start_clear(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}. -> {ok, pid()} | {error, any()}.
start_clear(Ref, TransOpts0, ProtoOpts0) -> start_clear(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0), TransOpts1 = ranch:normalize_opts(TransOpts0),
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts1), {TransOpts, ConnectionType} = ensure_connection_type(TransOpts1),
@ -52,6 +57,7 @@ start_clear(Ref, TransOpts0, ProtoOpts0) ->
-spec start_tls(ranch:ref(), ranch:opts(), opts()) -spec start_tls(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}. -> {ok, pid()} | {error, any()}.
start_tls(Ref, TransOpts0, ProtoOpts0) -> start_tls(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0), TransOpts1 = ranch:normalize_opts(TransOpts0),
SocketOpts = maps:get(socket_opts, TransOpts1, []), SocketOpts = maps:get(socket_opts, TransOpts1, []),
@ -62,28 +68,103 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType}, ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts). ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).
%% @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()}.
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?
{peer_unidi_stream_count, 3}, %% We only need control and QPACK enc/dec.
{peer_bidi_stream_count, 100}
|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.
ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) -> ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) ->
{TransOpts, ConnectionType}; {TransOpts, ConnectionType};
ensure_connection_type(TransOpts) -> ensure_connection_type(TransOpts) ->
{TransOpts#{connection_type => supervisor}, supervisor}. {TransOpts#{connection_type => supervisor}, supervisor}.
-spec stop_listener(ranch:ref()) -> ok | {error, not_found}. -spec stop_listener(ranch:ref()) -> ok | {error, not_found}.
stop_listener(Ref) -> stop_listener(Ref) ->
ranch:stop_listener(Ref). ranch:stop_listener(Ref).
-spec get_env(ranch:ref(), atom()) -> ok. -spec get_env(ranch:ref(), atom()) -> ok.
get_env(Ref, Name) -> get_env(Ref, Name) ->
Opts = ranch:get_protocol_options(Ref), Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}), Env = maps:get(env, Opts, #{}),
maps:get(Name, Env). maps:get(Name, Env).
-spec get_env(ranch:ref(), atom(), any()) -> ok. -spec get_env(ranch:ref(), atom(), any()) -> ok.
get_env(Ref, Name, Default) -> get_env(Ref, Name, Default) ->
Opts = ranch:get_protocol_options(Ref), Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}), Env = maps:get(env, Opts, #{}),
maps:get(Name, Env, Default). maps:get(Name, Env, Default).
-spec set_env(ranch:ref(), atom(), any()) -> ok. -spec set_env(ranch:ref(), atom(), any()) -> ok.
set_env(Ref, Name, Value) -> set_env(Ref, Name, Value) ->
Opts = ranch:get_protocol_options(Ref), Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}), Env = maps:get(env, Opts, #{}),
@ -93,10 +174,12 @@ set_env(Ref, Name, Value) ->
%% Internal. %% Internal.
-spec log({log, logger:level(), io:format(), list()}, opts()) -> ok. -spec log({log, logger:level(), io:format(), list()}, opts()) -> ok.
log({log, Level, Format, Args}, Opts) -> log({log, Level, Format, Args}, Opts) ->
log(Level, Format, Args, Opts). log(Level, Format, Args, Opts).
-spec log(logger:level(), io:format(), list(), opts()) -> ok. -spec log(logger:level(), io:format(), list(), opts()) -> ok.
log(Level, Format, Args, #{logger := Logger}) log(Level, Format, Args, #{logger := Logger})
when Logger =/= error_logger -> when Logger =/= error_logger ->
_ = Logger:Level(Format, Args), _ = Logger:Level(Format, Args),

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2014-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2014-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -12,6 +12,8 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% 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). -module(cowboy_http).
-export([init/6]). -export([init/6]).
@ -1531,7 +1533,7 @@ maybe_socket_error(_, Result = {ok, _}, _) ->
maybe_socket_error(State, {error, Reason}, Human) -> maybe_socket_error(State, {error, Reason}, Human) ->
terminate(State, {socket_error, Reason, Human}). terminate(State, {socket_error, Reason, Human}).
-spec terminate(_, _) -> no_return(). -spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) -> terminate(undefined, Reason) ->
exit({shutdown, Reason}); exit({shutdown, Reason});
terminate(State=#state{streams=Streams, children=Children}, Reason) -> terminate(State=#state{streams=Streams, children=Children}, Reason) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2015-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -44,6 +44,7 @@
max_connection_window_size => 0..16#7fffffff, max_connection_window_size => 0..16#7fffffff,
max_decode_table_size => non_neg_integer(), max_decode_table_size => non_neg_integer(),
max_encode_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_received => 16384..16777215,
max_frame_size_sent => 16384..16777215 | infinity, max_frame_size_sent => 16384..16777215 | infinity,
max_received_frame_rate => {pos_integer(), timeout()}, max_received_frame_rate => {pos_integer(), timeout()},
@ -1138,7 +1139,7 @@ maybe_socket_error(_, Result = {ok, _}, _) ->
maybe_socket_error(State, {error, Reason}, Human) -> maybe_socket_error(State, {error, Reason}, Human) ->
terminate(State, {socket_error, Reason, Human}). terminate(State, {socket_error, Reason, Human}).
-spec terminate(#state{}, _) -> no_return(). -spec terminate(#state{} | undefined, _) -> no_return().
terminate(undefined, Reason) -> terminate(undefined, Reason) ->
exit({shutdown, Reason}); exit({shutdown, Reason});
terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status, terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,

973
src/cowboy_http3.erl Normal file
View file

@ -0,0 +1,973 @@
%% Copyright (c) 2023-2024, Loïc Hoguin <essen@ninenines.eu>
%%
%% 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).
-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]).
-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,
%% 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));
%% 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);
{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);
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, <<Buffer/binary, Data/binary>>, 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;
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 ->
<<Data1:Len/binary, Rest/bits>> = Data,
FrameIsFin = is_fin(IsFin, Rest),
parse(frame(State, Stream#stream{status=normal}, {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{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);
{more, Frame = {data, _}, Len} ->
%% We're at the end of the data so FrameIsFin is equivalent to IsFin.
case IsFin of
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;
{ok, push, _} ->
terminate(State0, {connection_error, h3_stream_creation_error,
'Only servers can push. (RFC9114 6.2.2)'});
%% 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))
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=#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).
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.
%% 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, _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.
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_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
StreamID, StreamType) ->
{HTTP3Machine, Status} = case StreamType of
unidi ->
{cow_http3_machine:init_unidi_stream(StreamID, unidi_remote, HTTP3Machine0),
header};
bidi ->
{cow_http3_machine:init_bidi_stream(StreamID, HTTP3Machine0),
normal}
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
{#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).

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2013-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

231
src/cowboy_quicer.erl Normal file
View file

@ -0,0 +1,231 @@
%% Copyright (c) 2023, Loïc Hoguin <essen@ninenines.eu>
%%
%% 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).
%% Connection.
-export([peername/1]).
-export([sockname/1]).
-export([peercert/1]).
-export([shutdown/2]).
%% Streams.
-export([start_unidi_stream/2]).
-export([send/3]).
-export([send/4]).
-export([shutdown_stream/4]).
%% Messages.
-export([handle/1]).
-ifndef(COWBOY_QUICER).
-spec peername(_) -> no_return().
peername(_) -> no_quicer().
-spec sockname(_) -> no_return().
sockname(_) -> no_quicer().
-spec peercert(_) -> no_return().
peercert(_) -> no_quicer().
-spec shutdown(_, _) -> no_return().
shutdown(_, _) -> no_quicer().
-spec start_unidi_stream(_, _) -> no_return().
start_unidi_stream(_, _) -> no_quicer().
-spec send(_, _, _) -> no_return().
send(_, _, _) -> no_quicer().
-spec send(_, _, _, _) -> no_return().
send(_, _, _, _) -> no_quicer().
-spec shutdown_stream(_, _, _, _) -> no_return().
shutdown_stream(_, _, _, _) -> no_quicer().
-spec handle(_) -> no_return().
handle(_) -> no_quicer().
no_quicer() ->
error({no_quicer,
"Cowboy must be compiled with environment variable COWBOY_QUICER=1 "
"or with compilation flag -D COWBOY_QUICER=1 in order to enable "
"QUIC support using the emqx/quic NIF"}).
-else.
%% @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_unidi_stream(quicer_connection_handle(), iodata())
-> {ok, cow_http3:stream_id()}
| {error, any()}.
start_unidi_stream(Conn, HeaderData) ->
case quicer:start_stream(Conn, #{
active => true,
open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}) of
{ok, StreamRef} ->
case quicer:send(StreamRef, HeaderData) 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 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 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.
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()}
| {stream_started, cow_http3:stream_id(), unidi | bidi}
| {stream_closed, cow_http3:stream_id(), quicer_app_errno()}
| closed
| 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};
%% 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;
handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) ->
ok;
handle({quic, shutdown, _Conn, success}) ->
ok;
handle(_Msg) ->
unknown.
-endif.

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu> %% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
@ -1024,7 +1024,12 @@ filter([], Map, Errors) ->
_ -> {error, Errors} _ -> {error, Errors}
end; end;
filter([{Key, Constraints}|Tail], Map, Errors) -> filter([{Key, Constraints}|Tail], Map, Errors) ->
filter_constraints(Tail, Map, Errors, Key, maps:get(Key, Map), Constraints); 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) -> filter([{Key, Constraints, Default}|Tail], Map, Errors) ->
case maps:find(Key, Map) of case maps:find(Key, Map) of
{ok, Value} -> {ok, Value} ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -1189,6 +1189,7 @@ if_range(Req=#{headers := #{<<"if-range">> := _, <<"range">> := _}},
if_range(Req, State) -> if_range(Req, State) ->
range(Req, State). range(Req, State).
%% @todo This can probably be moved to if_range directly.
range(Req, State=#state{ranges_a=[]}) -> range(Req, State=#state{ranges_a=[]}) ->
set_resp_body(Req, State); set_resp_body(Req, State);
range(Req, State) -> range(Req, State) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2013-2024, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com> %% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2015-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -151,6 +151,11 @@ info(StreamID, Exit={'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref, p
[Ref, self(), StreamID, Pid, Reason, Stacktrace]} [Ref, self(), StreamID, Pid, Reason, Stacktrace]}
|Commands0] |Commands0]
end, 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, [ do_info(StreamID, Exit, [
{error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>} {error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>}
|Commands], State); |Commands], State);

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2013-2024, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2013, James Fish <james@fishcakez.com> %% Copyright (c) 2013, James Fish <james@fishcakez.com>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2015-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -103,7 +103,8 @@
%% is trying to upgrade to the Websocket protocol. %% is trying to upgrade to the Websocket protocol.
-spec is_upgrade_request(cowboy_req:req()) -> boolean(). -spec is_upgrade_request(cowboy_req:req()) -> boolean().
is_upgrade_request(#{version := 'HTTP/2', method := <<"CONNECT">>, protocol := Protocol}) -> is_upgrade_request(#{version := Version, method := <<"CONNECT">>, protocol := Protocol})
when Version =:= 'HTTP/2'; Version =:= 'HTTP/3' ->
<<"websocket">> =:= cowboy_bstr:to_lower(Protocol); <<"websocket">> =:= cowboy_bstr:to_lower(Protocol);
is_upgrade_request(Req=#{version := 'HTTP/1.1', method := <<"GET">>}) -> is_upgrade_request(Req=#{version := 'HTTP/1.1', method := <<"GET">>}) ->
ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []), ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []),
@ -148,13 +149,13 @@ upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) ->
<<"connection">> => <<"upgrade">>, <<"connection">> => <<"upgrade">>,
<<"upgrade">> => <<"websocket">> <<"upgrade">> => <<"websocket">>
}, Req0), Env}; }, Req0), Env};
%% Use a generic 400 error for HTTP/2. %% Use 501 Not Implemented for HTTP/2 and HTTP/3 as recommended
%% by RFC9220 3 (WebSockets Upgrade over HTTP/3).
{error, upgrade_required} -> {error, upgrade_required} ->
{ok, cowboy_req:reply(400, Req0), Env} {ok, cowboy_req:reply(501, Req0), Env}
catch _:_ -> catch _:_ ->
%% @todo Probably log something here? %% @todo Probably log something here?
%% @todo Test that we can have 2 /ws 400 status code in a row on the same connection. %% @todo Test that we can have 2 /ws 400 status code in a row on the same connection.
%% @todo Does this even work?
{ok, cowboy_req:reply(400, Req0), Env} {ok, cowboy_req:reply(400, Req0), Env}
end. end.
@ -286,9 +287,12 @@ websocket_handshake(State, Req=#{ref := Ref, pid := Pid, streamid := StreamID},
module() | undefined, any(), binary(), module() | undefined, any(), binary(),
{#state{}, any()}) -> no_return(). {#state{}, any()}) -> no_return().
takeover(Parent, Ref, Socket, Transport, _Opts, Buffer, takeover(Parent, Ref, Socket, Transport, _Opts, Buffer,
{State0=#state{handler=Handler}, HandlerState}) -> {State0=#state{handler=Handler, req=Req}, HandlerState}) ->
case Req of
#{version := 'HTTP/3'} -> ok;
%% @todo We should have an option to disable this behavior. %% @todo We should have an option to disable this behavior.
ranch:remove_connection(Ref), _ -> ranch:remove_connection(Ref)
end,
Messages = case Transport of Messages = case Transport of
undefined -> undefined; undefined -> undefined;
_ -> Transport:messages() _ -> Transport:messages()

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -23,12 +23,20 @@
%% ct. %% ct.
all() -> all() ->
[ All = [
{group, http_compress}, {group, http_compress},
{group, https_compress}, {group, https_compress},
{group, h2_compress}, {group, h2_compress},
{group, h2c_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() -> groups() ->
cowboy_test:common_groups(ct_helper:all(?MODULE)). cowboy_test:common_groups(ct_helper:all(?MODULE)).
@ -37,7 +45,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Routes. %% Routes.

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2014-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2014-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2014-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2014-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -37,35 +37,82 @@ init_http2(Ref, ProtoOpts, Config) ->
Port = ranch:get_port(Ref), Port = ranch:get_port(Ref),
[{ref, Ref}, {type, ssl}, {protocol, http2}, {port, Port}, {opts, Opts}|Config]. [{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 group of listeners used by most suites.
common_all() -> common_all() ->
[ All = [
{group, http}, {group, http},
{group, https}, {group, https},
{group, h2}, {group, h2},
{group, h2c}, {group, h2c},
{group, h3},
{group, http_compress}, {group, http_compress},
{group, https_compress}, {group, https_compress},
{group, h2_compress}, {group, h2_compress},
{group, h2c_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) -> common_groups(Tests) ->
Opts = case os:getenv("NO_PARALLEL") of Opts = case os:getenv("NO_PARALLEL") of
false -> [parallel]; false -> [parallel];
_ -> [] _ -> []
end, end,
[ Groups = [
{http, Opts, Tests}, {http, Opts, Tests},
{https, Opts, Tests}, {https, Opts, Tests},
{h2, Opts, Tests}, {h2, Opts, Tests},
{h2c, Opts, Tests}, {h2c, Opts, Tests},
{h3, Opts, Tests},
{http_compress, Opts, Tests}, {http_compress, Opts, Tests},
{https_compress, Opts, Tests}, {https_compress, Opts, Tests},
{h2_compress, Opts, Tests}, {h2_compress, Opts, Tests},
{h2c_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 = http, Config, Mod) -> init_common_groups(Name = http, Config, Mod) ->
init_http(Name, #{ init_http(Name, #{
@ -84,6 +131,10 @@ init_common_groups(Name = h2c, Config, Mod) ->
env => #{dispatch => Mod:init_dispatch(Config)} env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]), }, [{flavor, vanilla}|Config]),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}); lists:keyreplace(protocol, 1, Config1, {protocol, http2});
init_common_groups(Name = h3, Config, Mod) ->
init_http3(Name, #{
env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]);
init_common_groups(Name = http_compress, Config, Mod) -> init_common_groups(Name = http_compress, Config, Mod) ->
init_http(Name, #{ init_http(Name, #{
env => #{dispatch => Mod:init_dispatch(Config)}, env => #{dispatch => Mod:init_dispatch(Config)},
@ -104,7 +155,12 @@ init_common_groups(Name = h2c_compress, Config, Mod) ->
env => #{dispatch => Mod:init_dispatch(Config)}, env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h] stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]), }, [{flavor, compress}|Config]),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}). lists:keyreplace(protocol, 1, Config1, {protocol, http2});
init_common_groups(Name = h3_compress, Config, Mod) ->
init_http3(Name, #{
env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]).
%% Support functions for testing using Gun. %% Support functions for testing using Gun.
@ -114,7 +170,7 @@ gun_open(Config) ->
gun_open(Config, Opts) -> gun_open(Config, Opts) ->
TlsOpts = case proplists:get_value(no_cert, Config, false) of TlsOpts = case proplists:get_value(no_cert, Config, false) of
true -> [{verify, verify_none}]; true -> [{verify, verify_none}];
false -> ct_helper:get_certs_from_ets() false -> ct_helper:get_certs_from_ets() %% @todo Wrong in current quicer.
end, end,
{ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{ {ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{
retry => 0, retry => 0,

View file

@ -1,4 +1,5 @@
%% Copyright (c) 2024, jdamanalo <joshuadavid.agustin@manalo.ph> %% Copyright (c) 2024, jdamanalo <joshuadavid.agustin@manalo.ph>
%% Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -37,6 +38,8 @@ init_per_group(Name = h2, Config) ->
init_per_group(Name = h2c, Config) -> init_per_group(Name = h2c, Config) ->
Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config), Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}); 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) -> init_per_group(Name = http_compress, Config) ->
cowboy_test:init_http(Name, init_compress_opts(Config), Config); cowboy_test:init_http(Name, init_compress_opts(Config), Config);
init_per_group(Name = https_compress, Config) -> init_per_group(Name = https_compress, Config) ->
@ -45,7 +48,9 @@ init_per_group(Name = h2_compress, Config) ->
cowboy_test:init_http2(Name, init_compress_opts(Config), Config); cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
init_per_group(Name = h2c_compress, Config) -> init_per_group(Name = h2c_compress, Config) ->
Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config), Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}). 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, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy:stop_listener(Name).

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -86,6 +86,7 @@ echo(<<"match">>, Req, Opts) ->
Fields = [binary_to_atom(F, latin1) || F <- Fields0], Fields = [binary_to_atom(F, latin1) || F <- Fields0],
Value = case Type of Value = case Type of
<<"qs">> -> cowboy_req:match_qs(Fields, Req); <<"qs">> -> cowboy_req:match_qs(Fields, Req);
<<"qs_with_constraints">> -> cowboy_req:match_qs([{id, integer}], Req);
<<"cookies">> -> cowboy_req:match_cookies(Fields, Req); <<"cookies">> -> cowboy_req:match_cookies(Fields, Req);
<<"body_qs">> -> <<"body_qs">> ->
%% Note that the Req should not be discarded but for the %% Note that the Req should not be discarded but for the

View file

@ -182,6 +182,7 @@ do(<<"reply2">>, Req0, Opts) ->
<<"twice">> -> <<"twice">> ->
ct_helper:ignore(cowboy_req, reply, 4), ct_helper:ignore(cowboy_req, reply, 4),
Req1 = cowboy_req:reply(200, Req0), Req1 = cowboy_req:reply(200, Req0),
timer:sleep(100),
cowboy_req:reply(200, Req1); cowboy_req:reply(200, Req1);
Status -> Status ->
cowboy_req:reply(binary_to_integer(Status), Req0) cowboy_req:reply(binary_to_integer(Status), Req0)
@ -245,6 +246,7 @@ do(<<"stream_reply2">>, Req0, Opts) ->
<<"twice">> -> <<"twice">> ->
ct_helper:ignore(cowboy_req, stream_reply, 3), ct_helper:ignore(cowboy_req, stream_reply, 3),
Req1 = cowboy_req:stream_reply(200, Req0), Req1 = cowboy_req:stream_reply(200, Req0),
timer:sleep(100),
%% We will crash here so the body shouldn't be sent. %% We will crash here so the body shouldn't be sent.
Req = cowboy_req:stream_reply(200, Req1), Req = cowboy_req:stream_reply(200, Req1),
stream_body(Req), stream_body(Req),

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -90,7 +90,7 @@ chunked_one_byte_at_a_time(Config) ->
"Transfer-encoding: chunked\r\n\r\n"), "Transfer-encoding: chunked\r\n\r\n"),
_ = [begin _ = [begin
raw_send(Client, <<C>>), raw_send(Client, <<C>>),
timer:sleep(10) timer:sleep(1)
end || <<C>> <= ChunkedBody], end || <<C>> <= ChunkedBody],
Rest = case catch raw_recv_head(Client) of Rest = case catch raw_recv_head(Client) of
{'EXIT', _} -> error(closed); {'EXIT', _} -> error(closed);
@ -329,6 +329,7 @@ do_idle_timeout_on_send(Config, Protocol) ->
try try
ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]), ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]),
{ok, Protocol} = gun:await_up(ConnPid), {ok, Protocol} = gun:await_up(ConnPid),
timer:sleep(500),
#{socket := Socket} = gun:info(ConnPid), #{socket := Socket} = gun:info(ConnPid),
Pid = get_remote_pid_tcp(Socket), Pid = get_remote_pid_tcp(Socket),
StreamRef = gun:get(ConnPid, "/streamed_result/10/250"), StreamRef = gun:get(ConnPid, "/streamed_result/10/250"),
@ -359,6 +360,7 @@ do_idle_timeout_reset_on_send(Config, Protocol) ->
try try
ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]), ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]),
{ok, Protocol} = gun:await_up(ConnPid), {ok, Protocol} = gun:await_up(ConnPid),
timer:sleep(500),
#{socket := Socket} = gun:info(ConnPid), #{socket := Socket} = gun:info(ConnPid),
Pid = get_remote_pid_tcp(Socket), Pid = get_remote_pid_tcp(Socket),
StreamRef = gun:get(ConnPid, "/streamed_result/10/250"), StreamRef = gun:get(ConnPid, "/streamed_result/10/250"),

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -32,7 +32,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Dispatch configuration. %% Dispatch configuration.

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -44,6 +44,8 @@ init_per_group(Name = h2, Config) ->
init_per_group(Name = h2c, Config) -> init_per_group(Name = h2c, Config) ->
Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config), Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}); 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) -> init_per_group(Name = http_compress, Config) ->
cowboy_test:init_http(Name, init_compress_opts(Config), Config); cowboy_test:init_http(Name, init_compress_opts(Config), Config);
init_per_group(Name = https_compress, Config) -> init_per_group(Name = https_compress, Config) ->
@ -52,10 +54,12 @@ init_per_group(Name = h2_compress, Config) ->
cowboy_test:init_http2(Name, init_compress_opts(Config), Config); cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
init_per_group(Name = h2c_compress, Config) -> init_per_group(Name = h2c_compress, Config) ->
Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config), Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}). 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, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
init_plain_opts(Config) -> init_plain_opts(Config) ->
#{ #{
@ -157,16 +161,24 @@ do_get(Path, UserData, Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := normal, reason := normal, %% @todo Getting h3_no_error here.
req := #{}, req := #{},
informational := [], informational := [],
user_data := UserData user_data := UserData
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
%% All good! %% All good!
gun:close(ConnPid) gun:close(ConnPid)
end. end.
do_check_streamid(StreamID, Config) ->
case config(protocol, Config) of
http -> 1 = StreamID;
http2 -> 1 = StreamID;
http3 -> 0 = StreamID
end.
post_body(Config) -> post_body(Config) ->
doc("Confirm metrics are correct for a normal POST request."), doc("Confirm metrics are correct for a normal POST request."),
%% Perform a POST request. %% Perform a POST request.
@ -218,12 +230,13 @@ post_body(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := normal, reason := normal,
req := #{}, req := #{},
informational := [], informational := [],
user_data := #{} user_data := #{}
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
%% All good! %% All good!
gun:close(ConnPid) gun:close(ConnPid)
end. end.
@ -273,12 +286,13 @@ no_resp_body(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := normal, reason := normal,
req := #{}, req := #{},
informational := [], informational := [],
user_data := #{} user_data := #{}
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
%% All good! %% All good!
gun:close(ConnPid) gun:close(ConnPid)
end. end.
@ -291,7 +305,8 @@ early_error(Config) ->
%% reason in both protocols. %% reason in both protocols.
{Method, Headers, Status, Error} = case config(protocol, Config) of {Method, Headers, Status, Error} = case config(protocol, Config) of
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error}; http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
http2 -> {<<"TRACE">>, [], 501, no_error} http2 -> {<<"TRACE">>, [], 501, no_error};
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
end, end,
Ref = gun:request(ConnPid, Method, "/", [ Ref = gun:request(ConnPid, Method, "/", [
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
@ -305,7 +320,7 @@ early_error(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := {stream_error, Error, _}, reason := {stream_error, Error, _},
partial_req := #{}, partial_req := #{},
resp_status := Status, resp_status := Status,
@ -313,6 +328,7 @@ early_error(Config) ->
early_error_time := _, early_error_time := _,
resp_body_length := 0 resp_body_length := 0
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
ExpectedRespHeaders = maps:from_list(RespHeaders), ExpectedRespHeaders = maps:from_list(RespHeaders),
%% All good! %% All good!
gun:close(ConnPid) gun:close(ConnPid)
@ -321,7 +337,8 @@ early_error(Config) ->
early_error_request_line(Config) -> early_error_request_line(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_early_error_request_line(Config); http -> do_early_error_request_line(Config);
http2 -> doc("There are no request lines in HTTP/2.") http2 -> doc("There are no request lines in HTTP/2.");
http3 -> doc("There are no request lines in HTTP/3.")
end. end.
do_early_error_request_line(Config) -> do_early_error_request_line(Config) ->
@ -341,7 +358,7 @@ do_early_error_request_line(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := {connection_error, protocol_error, _}, reason := {connection_error, protocol_error, _},
partial_req := #{}, partial_req := #{},
resp_status := 400, resp_status := 400,
@ -349,6 +366,7 @@ do_early_error_request_line(Config) ->
early_error_time := _, early_error_time := _,
resp_body_length := 0 resp_body_length := 0
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
ExpectedRespHeaders = maps:from_list(RespHeaders), ExpectedRespHeaders = maps:from_list(RespHeaders),
%% All good! %% All good!
ok ok
@ -362,7 +380,9 @@ stream_reply(Config) ->
ws(Config) -> ws(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_ws(Config); http -> do_ws(Config);
http2 -> doc("It is not currently possible to switch to Websocket over HTTP/2.") %% @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. end.
do_ws(Config) -> do_ws(Config) ->
@ -405,7 +425,7 @@ do_ws(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := switch_protocol, reason := switch_protocol,
req := #{}, req := #{},
%% A 101 upgrade response was sent. %% A 101 upgrade response was sent.
@ -420,6 +440,7 @@ do_ws(Config) ->
}], }],
user_data := #{} user_data := #{}
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
%% All good! %% All good!
ok ok
end, end,
@ -438,7 +459,15 @@ error_response(Config) ->
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
{<<"x-test-pid">>, pid_to_list(self())} {<<"x-test-pid">>, pid_to_list(self())}
]), ]),
{response, fin, 500, RespHeaders} = gun:await(ConnPid, Ref, infinity), 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), timer:sleep(100),
%% Receive the metrics and validate them. %% Receive the metrics and validate them.
receive receive
@ -463,7 +492,14 @@ error_response(Config) ->
resp_headers := ExpectedRespHeaders, resp_headers := ExpectedRespHeaders,
resp_body_length := 0 resp_body_length := 0
} = Metrics, } = Metrics,
ExpectedRespHeaders = maps:from_list(RespHeaders), 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. %% The request process executed normally.
#{procs := Procs} = Metrics, #{procs := Procs} = Metrics,
[{_, #{ [{_, #{
@ -476,12 +512,13 @@ error_response(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'}, reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'},
req := #{}, req := #{},
informational := [], informational := [],
user_data := #{} user_data := #{}
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
%% All good! %% All good!
gun:close(ConnPid) gun:close(ConnPid)
end. end.
@ -495,7 +532,15 @@ error_response_after_reply(Config) ->
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
{<<"x-test-pid">>, pid_to_list(self())} {<<"x-test-pid">>, pid_to_list(self())}
]), ]),
{response, fin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity), 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), timer:sleep(100),
%% Receive the metrics and validate them. %% Receive the metrics and validate them.
receive receive
@ -520,7 +565,14 @@ error_response_after_reply(Config) ->
resp_headers := ExpectedRespHeaders, resp_headers := ExpectedRespHeaders,
resp_body_length := 0 resp_body_length := 0
} = Metrics, } = Metrics,
ExpectedRespHeaders = maps:from_list(RespHeaders), 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. %% The request process executed normally.
#{procs := Procs} = Metrics, #{procs := Procs} = Metrics,
[{_, #{ [{_, #{
@ -533,12 +585,13 @@ error_response_after_reply(Config) ->
#{ #{
ref := _, ref := _,
pid := From, pid := From,
streamid := 1, streamid := StreamID,
reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'}, reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'},
req := #{}, req := #{},
informational := [], informational := [],
user_data := #{} user_data := #{}
} = Metrics, } = Metrics,
do_check_streamid(StreamID, Config),
%% All good! %% All good!
gun:close(ConnPid) gun:close(ConnPid)
end. end.

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -43,7 +43,7 @@ init_per_group(Name, Config) ->
end_per_group(env, _) -> end_per_group(env, _) ->
ok; ok;
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
init_dispatch(_) -> init_dispatch(_) ->
cowboy_router:compile([{"localhost", [ cowboy_router:compile([{"localhost", [

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -39,7 +39,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Routes. %% Routes.
@ -58,8 +58,15 @@ crash_after_reply(Config) ->
Ref = gun:get(ConnPid, "/crash/reply", [ Ref = gun:get(ConnPid, "/crash/reply", [
{<<"accept-encoding">>, <<"gzip">>} {<<"accept-encoding">>, <<"gzip">>}
]), ]),
{response, fin, 200, _} = gun:await(ConnPid, Ref), Protocol = config(protocol, Config),
{error, timeout} = gun:await(ConnPid, Ref, 1000), _ = 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). gun:close(ConnPid).
crash_before_reply(Config) -> crash_before_reply(Config) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -46,7 +46,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Routes. %% Routes.
@ -57,7 +57,8 @@ init_dispatch(Config) ->
{"/resp/:key[/:arg]", resp_h, []}, {"/resp/:key[/:arg]", resp_h, []},
{"/multipart[/:key]", multipart_h, []}, {"/multipart[/:key]", multipart_h, []},
{"/args/:key/:arg[/:default]", echo_h, []}, {"/args/:key/:arg[/:default]", echo_h, []},
{"/crash/:key/period", echo_h, #{length => 999999999, period => 1000, crash => true}}, {"/crash/:key/period", echo_h,
#{length => 999999999, period => 1000, timeout => 5000, crash => true}},
{"/no-opts/:key", echo_h, #{crash => true}}, {"/no-opts/:key", echo_h, #{crash => true}},
{"/opts/:key/length", echo_h, #{length => 1000}}, {"/opts/:key/length", echo_h, #{length => 1000}},
{"/opts/:key/period", echo_h, #{length => 999999999, period => 2000}}, {"/opts/:key/period", echo_h, #{length => 999999999, period => 2000}},
@ -106,13 +107,17 @@ do_get(Path, Config) ->
do_get(Path, Headers, Config) -> do_get(Path, Headers, Config) ->
ConnPid = gun_open(Config), ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]), Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]),
{response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity), case gun:await(ConnPid, Ref, infinity) of
{response, IsFin, Status, RespHeaders} ->
{ok, RespBody} = case IsFin of {ok, RespBody} = case IsFin of
nofin -> gun:await_body(ConnPid, Ref, infinity); nofin -> gun:await_body(ConnPid, Ref, infinity);
fin -> {ok, <<>>} fin -> {ok, <<>>}
end, end,
gun:close(ConnPid), gun:close(ConnPid),
{Status, RespHeaders, do_decode(RespHeaders, RespBody)}. {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, [], Config). do_get_body(Path, [], Config).
@ -141,7 +146,9 @@ do_get_inform(Path, Config) ->
fin -> {ok, <<>>} fin -> {ok, <<>>}
end, end,
gun:close(ConnPid), gun:close(ConnPid),
{InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)} {InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)};
{error, {stream_error, Error}} ->
Error
end. end.
do_decode(Headers, Body) -> do_decode(Headers, Body) ->
@ -183,7 +190,8 @@ bindings(Config) ->
cert(Config) -> cert(Config) ->
case config(type, Config) of case config(type, Config) of
tcp -> doc("TLS certificates can only be provided over TLS."); tcp -> doc("TLS certificates can only be provided over TLS.");
ssl -> do_cert(Config) ssl -> do_cert(Config);
quic -> do_cert(Config)
end. end.
do_cert(Config) -> do_cert(Config) ->
@ -265,6 +273,7 @@ match_qs(Config) ->
end, end,
%% Ensure match errors result in a 400 response. %% Ensure match errors result in a 400 response.
{400, _, _} = do_get("/match/qs/a/c?a=b", [], Config), {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. %% This function is tested more extensively through unit tests.
ok. ok.
@ -384,7 +393,8 @@ port(Config) ->
Port = do_get_body("/direct/port", Config), Port = do_get_body("/direct/port", Config),
ExpectedPort = case config(type, Config) of ExpectedPort = case config(type, Config) of
tcp -> <<"80">>; tcp -> <<"80">>;
ssl -> <<"443">> ssl -> <<"443">>;
quic -> <<"443">>
end, end,
ExpectedPort = do_get_body("/port", [{<<"host">>, <<"localhost">>}], Config), ExpectedPort = do_get_body("/port", [{<<"host">>, <<"localhost">>}], Config),
ExpectedPort = do_get_body("/direct/port", [{<<"host">>, <<"localhost">>}], Config), ExpectedPort = do_get_body("/direct/port", [{<<"host">>, <<"localhost">>}], Config),
@ -410,7 +420,8 @@ do_scheme(Path, Config) ->
Transport = config(type, Config), Transport = config(type, Config),
case do_get_body(Path, Config) of case do_get_body(Path, Config) of
<<"http">> when Transport =:= tcp -> ok; <<"http">> when Transport =:= tcp -> ok;
<<"https">> when Transport =:= ssl -> ok <<"https">> when Transport =:= ssl -> ok;
<<"https">> when Transport =:= quic -> ok
end. end.
sock(Config) -> sock(Config) ->
@ -423,7 +434,8 @@ uri(Config) ->
doc("Request URI building/modification."), doc("Request URI building/modification."),
Scheme = case config(type, Config) of Scheme = case config(type, Config) of
tcp -> <<"http">>; tcp -> <<"http">>;
ssl -> <<"https">> ssl -> <<"https">>;
quic -> <<"https">>
end, end,
SLen = byte_size(Scheme), SLen = byte_size(Scheme),
Port = integer_to_binary(config(port, Config)), Port = integer_to_binary(config(port, Config)),
@ -457,7 +469,8 @@ do_version(Path, Config) ->
Protocol = config(protocol, Config), Protocol = config(protocol, Config),
case do_get_body(Path, Config) of case do_get_body(Path, Config) of
<<"HTTP/1.1">> when Protocol =:= http -> ok; <<"HTTP/1.1">> when Protocol =:= http -> ok;
<<"HTTP/2">> when Protocol =:= http2 -> ok <<"HTTP/2">> when Protocol =:= http2 -> ok;
<<"HTTP/3">> when Protocol =:= http3 -> ok
end. end.
%% Tests: Request body. %% Tests: Request body.
@ -511,11 +524,19 @@ read_body_period(Config) ->
%% for 2 seconds. The test succeeds if we get some of the data back %% for 2 seconds. The test succeeds if we get some of the data back
%% (meaning the function will have returned after the period ends). %% (meaning the function will have returned after the period ends).
gun:data(ConnPid, Ref, nofin, Body), gun:data(ConnPid, Ref, nofin, Body),
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity), Response = gun:await(ConnPid, Ref, infinity),
case Response of
{response, nofin, 200, _} ->
{data, _, Data} = gun:await(ConnPid, Ref, infinity), {data, _, Data} = gun:await(ConnPid, Ref, infinity),
%% We expect to read at least some data. %% We expect to read at least some data.
true = Data =/= <<>>, true = Data =/= <<>>,
gun:close(ConnPid). 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. %% We expect a crash.
do_read_body_timeout(Path, Body, Config) -> do_read_body_timeout(Path, Body, Config) ->
@ -523,7 +544,13 @@ do_read_body_timeout(Path, Body, Config) ->
Ref = gun:headers(ConnPid, "POST", Path, [ Ref = gun:headers(ConnPid, "POST", Path, [
{<<"content-length">>, integer_to_binary(byte_size(Body))} {<<"content-length">>, integer_to_binary(byte_size(Body))}
]), ]),
{response, _, 500, _} = gun:await(ConnPid, Ref, infinity), 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). gun:close(ConnPid).
read_body_auto(Config) -> read_body_auto(Config) ->
@ -590,8 +617,20 @@ do_read_urlencoded_body_too_large(Path, Body, Config) ->
{<<"content-length">>, integer_to_binary(iolist_size(Body))} {<<"content-length">>, integer_to_binary(iolist_size(Body))}
]), ]),
gun:data(ConnPid, Ref, fin, Body), gun:data(ConnPid, Ref, fin, Body),
{response, _, 413, _} = gun:await(ConnPid, Ref, infinity), Response = gun:await(ConnPid, Ref, infinity),
gun:close(ConnPid). 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) -> read_urlencoded_body_too_long(Config) ->
doc("application/x-www-form-urlencoded request body sent too slow. " doc("application/x-www-form-urlencoded request body sent too slow. "
@ -606,15 +645,19 @@ do_read_urlencoded_body_too_long(Path, Body, Config) ->
{<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)}
]), ]),
gun:data(ConnPid, Ref, nofin, Body), gun:data(ConnPid, Ref, nofin, Body),
{response, _, 408, RespHeaders} = gun:await(ConnPid, Ref, infinity), Protocol = config(protocol, Config),
_ = case config(protocol, Config) of case gun:await(ConnPid, Ref, infinity) of
http -> {response, _, 408, RespHeaders} when Protocol =:= http ->
%% 408 error responses should close HTTP/1.1 connections. %% 408 error responses should close HTTP/1.1 connections.
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders); {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders),
http2 -> gun:close(ConnPid);
ok {response, _, 408, _} when Protocol =:= http2; Protocol =:= http3 ->
end, gun:close(ConnPid);
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) -> read_and_match_urlencoded_body(Config) ->
doc("Read and match an application/x-www-form-urlencoded request body."), doc("Read and match an application/x-www-form-urlencoded request body."),
@ -810,7 +853,7 @@ set_resp_header(Config) ->
{200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config), {200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config),
true = lists:keymember(<<"content-type">>, 1, Headers), true = lists:keymember(<<"content-type">>, 1, Headers),
%% The set-cookie header is special. set_resp_cookie must be used. %% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/set_resp_header_cookie", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_header_cookie", Config)),
ok. ok.
set_resp_headers(Config) -> set_resp_headers(Config) ->
@ -819,7 +862,7 @@ set_resp_headers(Config) ->
true = lists:keymember(<<"content-type">>, 1, Headers), true = lists:keymember(<<"content-type">>, 1, Headers),
true = lists:keymember(<<"content-encoding">>, 1, Headers), true = lists:keymember(<<"content-encoding">>, 1, Headers),
%% The set-cookie header is special. set_resp_cookie must be used. %% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/set_resp_headers_cookie", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)),
ok. ok.
resp_header(Config) -> resp_header(Config) ->
@ -881,28 +924,52 @@ delete_resp_header(Config) ->
false = lists:keymember(<<"content-type">>, 1, Headers), false = lists:keymember(<<"content-type">>, 1, Headers),
ok. 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) -> inform2(Config) ->
doc("Informational response(s) without headers, followed by the real response."), 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/102", Config),
{102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config), {102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config),
{500, _} = do_get_inform("/resp/inform2/error", Config), {500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform2/error", Config)),
{102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config), {102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config),
%% @todo How to test this properly? This isn't enough. %% With HTTP/1.1 and HTTP/2 we will not get an error.
{200, _} = do_get_inform("/resp/inform2/after_reply", Config), %% With HTTP/3 however the stream will occasionally
ok. %% 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) -> inform3(Config) ->
doc("Informational response(s) with headers, followed by the real response."), doc("Informational response(s) with headers, followed by the real response."),
Headers = [{<<"ext-header">>, <<"ext-value">>}], Headers = [{<<"ext-header">>, <<"ext-value">>}],
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config), {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config),
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config), {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config),
{500, _} = do_get_inform("/resp/inform3/error", 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. %% The set-cookie header is special. set_resp_cookie must be used.
{500, _} = do_get_inform("/resp/inform3/set_cookie", Config), {500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/set_cookie", Config)),
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config), {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config),
%% @todo How to test this properly? This isn't enough. %% With HTTP/1.1 and HTTP/2 we will not get an error.
{200, _} = do_get_inform("/resp/inform3/after_reply", Config), %% With HTTP/3 however the stream will occasionally
ok. %% 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) -> reply2(Config) ->
doc("Response with default headers and no body."), doc("Response with default headers and no body."),
@ -910,7 +977,7 @@ reply2(Config) ->
{201, _, _} = do_get("/resp/reply2/201", Config), {201, _, _} = do_get("/resp/reply2/201", Config),
{404, _, _} = do_get("/resp/reply2/404", Config), {404, _, _} = do_get("/resp/reply2/404", Config),
{200, _, _} = do_get("/resp/reply2/binary", Config), {200, _, _} = do_get("/resp/reply2/binary", Config),
{500, _, _} = do_get("/resp/reply2/error", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply2/error", Config)),
%% @todo How to test this properly? This isn't enough. %% @todo How to test this properly? This isn't enough.
{200, _, _} = do_get("/resp/reply2/twice", Config), {200, _, _} = do_get("/resp/reply2/twice", Config),
ok. ok.
@ -923,9 +990,9 @@ reply3(Config) ->
true = lists:keymember(<<"content-type">>, 1, Headers2), true = lists:keymember(<<"content-type">>, 1, Headers2),
{404, Headers3, _} = do_get("/resp/reply3/404", Config), {404, Headers3, _} = do_get("/resp/reply3/404", Config),
true = lists:keymember(<<"content-type">>, 1, Headers3), true = lists:keymember(<<"content-type">>, 1, Headers3),
{500, _, _} = do_get("/resp/reply3/error", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used. %% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/reply3/set_cookie", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/set_cookie", Config)),
ok. ok.
reply4(Config) -> reply4(Config) ->
@ -933,9 +1000,9 @@ reply4(Config) ->
{200, _, <<"OK">>} = do_get("/resp/reply4/200", Config), {200, _, <<"OK">>} = do_get("/resp/reply4/200", Config),
{201, _, <<"OK">>} = do_get("/resp/reply4/201", Config), {201, _, <<"OK">>} = do_get("/resp/reply4/201", Config),
{404, _, <<"OK">>} = do_get("/resp/reply4/404", Config), {404, _, <<"OK">>} = do_get("/resp/reply4/404", Config),
{500, _, _} = do_get("/resp/reply4/error", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used. %% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/reply4/set_cookie", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/set_cookie", Config)),
ok. ok.
stream_reply2(Config) -> stream_reply2(Config) ->
@ -945,12 +1012,11 @@ stream_reply2(Config) ->
{201, _, Body} = do_get("/resp/stream_reply2/201", Config), {201, _, Body} = do_get("/resp/stream_reply2/201", Config),
{404, _, Body} = do_get("/resp/stream_reply2/404", Config), {404, _, Body} = do_get("/resp/stream_reply2/404", Config),
{200, _, Body} = do_get("/resp/stream_reply2/binary", Config), {200, _, Body} = do_get("/resp/stream_reply2/binary", Config),
{500, _, _} = do_get("/resp/stream_reply2/error", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply2/error", Config)),
ok. ok.
stream_reply2_twice(Config) -> stream_reply2_twice(Config) ->
doc("Attempting to stream a response twice results in a crash. " doc("Attempting to stream a response twice results in a crash."),
"This crash can only be properly detected in HTTP/2."),
ConnPid = gun_open(Config), ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, "/resp/stream_reply2/twice", Ref = gun:get(ConnPid, "/resp/stream_reply2/twice",
[{<<"accept-encoding">>, <<"gzip">>}]), [{<<"accept-encoding">>, <<"gzip">>}]),
@ -969,8 +1035,10 @@ stream_reply2_twice(Config) ->
zlib:inflateInit(Z, 31), zlib:inflateInit(Z, 31),
0 = iolist_size(zlib:inflate(Z, Data)), 0 = iolist_size(zlib:inflate(Z, Data)),
ok; ok;
%% In HTTP/2 the stream gets reset with an appropriate error. %% In HTTP/2 and HTTP/3 the stream gets reset with an appropriate error.
{http2, _, {error, {stream_error, {stream_error, internal_error, _}}}} -> {http2, _, {error, {stream_error, {stream_error, internal_error, _}}}} ->
ok;
{http3, _, {error, {stream_error, {stream_error, h3_internal_error, _}}}} ->
ok ok
end, end,
gun:close(ConnPid). gun:close(ConnPid).
@ -984,9 +1052,9 @@ stream_reply3(Config) ->
true = lists:keymember(<<"content-type">>, 1, Headers2), true = lists:keymember(<<"content-type">>, 1, Headers2),
{404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config), {404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config),
true = lists:keymember(<<"content-type">>, 1, Headers3), true = lists:keymember(<<"content-type">>, 1, Headers3),
{500, _, _} = do_get("/resp/stream_reply3/error", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used. %% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/stream_reply3/set_cookie", Config), {500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/set_cookie", Config)),
ok. ok.
stream_body_fin0(Config) -> stream_body_fin0(Config) ->
@ -1070,8 +1138,11 @@ stream_body_content_length_nofin_error(Config) ->
end end
end; end;
http2 -> http2 ->
%% @todo HTTP2 should have the same content-length checks %% @todo HTTP/2 should have the same content-length checks.
ok {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. end.
stream_body_concurrent(Config) -> stream_body_concurrent(Config) ->
@ -1173,16 +1244,24 @@ stream_trailers_set_cookie(Config) ->
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
{<<"te">>, <<"trailers">>} {<<"te">>, <<"trailers">>}
]), ]),
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity), Protocol = config(protocol, Config),
case config(protocol, Config) of case gun:await(ConnPid, Ref, infinity) of
http -> {response, nofin, 200, _} when Protocol =:= http ->
%% Trailers are not sent because of the stream error. %% Trailers are not sent because of the stream error.
{ok, _Body} = gun:await_body(ConnPid, Ref, infinity), {ok, _Body} = gun:await_body(ConnPid, Ref, infinity),
{error, timeout} = gun:await_body(ConnPid, Ref, 1000), {error, timeout} = gun:await_body(ConnPid, Ref, 1000),
ok; ok;
http2 -> {response, nofin, 200, _} when Protocol =:= http2 ->
{error, {stream_error, {stream_error, internal_error, _}}} {error, {stream_error, {stream_error, internal_error, _}}}
= gun:await_body(ConnPid, Ref, infinity), = 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 ok
end, end,
gun:close(ConnPid). gun:close(ConnPid).
@ -1210,34 +1289,45 @@ do_trailers(Path, Config) ->
push(Config) -> push(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_push_http("/resp/push", Config); http -> do_push_http("/resp/push", Config);
http2 -> do_push_http2(Config) http2 -> do_push_http2(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end. end.
push_after_reply(Config) -> push_after_reply(Config) ->
doc("Trying to push a response after the final response results in a crash."), doc("Trying to push a response after the final response results in a crash."),
ConnPid = gun_open(Config), ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, "/resp/push/after_reply", []), Ref = gun:get(ConnPid, "/resp/push/after_reply", []),
%% @todo How to test this properly? This isn't enough. %% With HTTP/1.1 and HTTP/2 we will not get an error.
{response, fin, 200, _} = gun:await(ConnPid, Ref, infinity), %% 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). gun:close(ConnPid).
push_method(Config) -> push_method(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_push_http("/resp/push/method", Config); http -> do_push_http("/resp/push/method", Config);
http2 -> do_push_http2_method(Config) http2 -> do_push_http2_method(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end. end.
push_origin(Config) -> push_origin(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_push_http("/resp/push/origin", Config); http -> do_push_http("/resp/push/origin", Config);
http2 -> do_push_http2_origin(Config) http2 -> do_push_http2_origin(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end. end.
push_qs(Config) -> push_qs(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_push_http("/resp/push/qs", Config); http -> do_push_http("/resp/push/qs", Config);
http2 -> do_push_http2_qs(Config) http2 -> do_push_http2_qs(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end. end.
do_push_http(Path, Config) -> do_push_http(Path, Config) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -32,7 +32,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Dispatch configuration. %% Dispatch configuration.
@ -85,7 +85,7 @@ accept_callback_missing(Config) ->
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
{<<"content-type">>, <<"text/plain">>} {<<"content-type">>, <<"text/plain">>}
], <<"Missing!">>), ], <<"Missing!">>),
{response, fin, 500, _} = gun:await(ConnPid, Ref), {response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
ok. ok.
accept_callback_patch_false(Config) -> accept_callback_patch_false(Config) ->
@ -472,7 +472,8 @@ delete_resource_missing(Config) ->
Ref = gun:delete(ConnPid, "/delete_resource?missing", [ Ref = gun:delete(ConnPid, "/delete_resource?missing", [
{<<"accept-encoding">>, <<"gzip">>} {<<"accept-encoding">>, <<"gzip">>}
]), ]),
{response, _, 500, Headers} = gun:await(ConnPid, Ref),
{response, _, 500, Headers} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
{_, <<"DELETE">>} = lists:keyfind(<<"allow">>, 1, Headers), {_, <<"DELETE">>} = lists:keyfind(<<"allow">>, 1, Headers),
ok. ok.
@ -653,10 +654,16 @@ do_generate_etag(Config, Qs, ReqHeaders, Status, Etag) ->
{<<"accept-encoding">>, <<"gzip">>} {<<"accept-encoding">>, <<"gzip">>}
|ReqHeaders |ReqHeaders
]), ]),
{response, _, Status, RespHeaders} = gun:await(ConnPid, Ref), {response, _, Status, RespHeaders} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
Etag = lists:keyfind(<<"etag">>, 1, RespHeaders), Etag = lists:keyfind(<<"etag">>, 1, RespHeaders),
ok. 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) -> if_range_etag_equal(Config) ->
doc("When the if-range header matches, a 206 partial content " doc("When the if-range header matches, a 206 partial content "
"response is expected for an otherwise valid range request. (RFC7233 3.2)"), "response is expected for an otherwise valid range request. (RFC7233 3.2)"),
@ -821,7 +828,7 @@ provide_callback_missing(Config) ->
doc("A 500 response must be sent when the ProvideCallback can't be called."), doc("A 500 response must be sent when the ProvideCallback can't be called."),
ConnPid = gun_open(Config), ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, "/provide_callback_missing", [{<<"accept-encoding">>, <<"gzip">>}]), Ref = gun:get(ConnPid, "/provide_callback_missing", [{<<"accept-encoding">>, <<"gzip">>}]),
{response, fin, 500, _} = gun:await(ConnPid, Ref), {response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
ok. ok.
provide_range_callback(Config) -> provide_range_callback(Config) ->
@ -977,7 +984,7 @@ provide_range_callback_missing(Config) ->
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
{<<"range">>, <<"bytes=0-">>} {<<"range">>, <<"bytes=0-">>}
]), ]),
{response, fin, 500, _} = gun:await(ConnPid, Ref), {response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
ok. ok.
range_ignore_unknown_unit(Config) -> range_ignore_unknown_unit(Config) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
init_dispatch(_) -> init_dispatch(_) ->
cowboy_router:compile([{"[...]", [ cowboy_router:compile([{"[...]", [

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2015-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -35,7 +35,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
init_dispatch(_) -> init_dispatch(_) ->
cowboy_router:compile([{"[...]", [ cowboy_router:compile([{"[...]", [
@ -237,6 +237,8 @@ http10_expect(Config) ->
http -> http ->
do_http10_expect(Config); do_http10_expect(Config);
http2 -> http2 ->
expect(Config);
http3 ->
expect(Config) expect(Config)
end. end.
@ -303,6 +305,9 @@ expect_discard_body_close(Config) ->
do_expect_discard_body_close(Config); do_expect_discard_body_close(Config);
http2 -> http2 ->
doc("There's no reason to close the connection when using HTTP/2, " 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.") "even if a stream body is too large. We just cancel the stream.")
end. end.
@ -424,8 +429,10 @@ http10_status_code_100(Config) ->
http -> http ->
doc("The 100 Continue status code must not " doc("The 100 Continue status code must not "
"be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"), "be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"),
do_http10_status_code_1xx(100, Config); do_unsupported_status_code_1xx(100, Config);
http2 -> http2 ->
status_code_100(Config);
http3 ->
status_code_100(Config) status_code_100(Config)
end. end.
@ -434,12 +441,16 @@ http10_status_code_101(Config) ->
http -> http ->
doc("The 101 Switching Protocols status code must not " doc("The 101 Switching Protocols status code must not "
"be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"), "be sent to HTTP/1.0 endpoints. (RFC7231 6.2)"),
do_http10_status_code_1xx(101, Config); do_unsupported_status_code_1xx(101, Config);
http2 -> 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) status_code_101(Config)
end. end.
do_http10_status_code_1xx(StatusCode, Config) -> do_unsupported_status_code_1xx(StatusCode, Config) ->
ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}), ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}),
Ref = gun:get(ConnPid, "/resp/inform2/" ++ integer_to_list(StatusCode), [ Ref = gun:get(ConnPid, "/resp/inform2/" ++ integer_to_list(StatusCode), [
{<<"accept-encoding">>, <<"gzip">>} {<<"accept-encoding">>, <<"gzip">>}
@ -653,7 +664,9 @@ status_code_408_connection_close(Config) ->
http -> http ->
do_http11_status_code_408_connection_close(Config); do_http11_status_code_408_connection_close(Config);
http2 -> http2 ->
doc("HTTP/2 connections are not closed on 408 responses.") doc("HTTP/2 connections are not closed on 408 responses.");
http3 ->
doc("HTTP/3 connections are not closed on 408 responses.")
end. end.
do_http11_status_code_408_connection_close(Config) -> do_http11_status_code_408_connection_close(Config) ->
@ -744,7 +757,9 @@ status_code_426_upgrade_header(Config) ->
http -> http ->
do_status_code_426_upgrade_header(Config); do_status_code_426_upgrade_header(Config);
http2 -> http2 ->
doc("HTTP/2 does not support the HTTP/1.1 Upgrade mechanism.") 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. end.
do_status_code_426_upgrade_header(Config) -> do_status_code_426_upgrade_header(Config) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
init_dispatch(_) -> init_dispatch(_) ->
cowboy_router:compile([{"[...]", [ cowboy_router:compile([{"[...]", [

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -34,9 +34,9 @@
all() -> [{group, clear}, {group, tls}]. all() -> [{group, clear}, {group, tls}].
groups() -> groups() ->
Modules = ct_helper:all(?MODULE), Tests = ct_helper:all(?MODULE),
Clear = [M || M <- Modules, lists:sublist(atom_to_list(M), 4) =/= "alpn"] -- [prior_knowledge_reject_tls], Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- [prior_knowledge_reject_tls],
TLS = [M || M <- Modules, lists:sublist(atom_to_list(M), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls], TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls],
[{clear, [parallel], Clear}, {tls, [parallel], TLS}]. [{clear, [parallel], Clear}, {tls, [parallel], TLS}].
init_per_group(Name = clear, Config) -> init_per_group(Name = clear, Config) ->
@ -3893,6 +3893,7 @@ accept_host_header_on_missing_pseudo_header_authority(Config) ->
%% When both :authority and host headers are received, the current behavior %% When both :authority and host headers are received, the current behavior
%% is to favor :authority and ignore the host header. The specification does %% is to favor :authority and ignore the host header. The specification does
%% not describe the correct behavior to follow in that case. %% 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) -> reject_many_pseudo_header_authority(Config) ->
doc("A request containing more than one authority component must be rejected " doc("A request containing more than one authority component must be rejected "

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -30,7 +30,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
init_dispatch(_) -> init_dispatch(_) ->
cowboy_router:compile([{"[...]", [ cowboy_router:compile([{"[...]", [

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -126,6 +126,7 @@ reject_handshake_disabled_by_default(Config0) ->
% The Extended CONNECT Method. % The Extended CONNECT Method.
%% @todo Refer to RFC9110 7.8 about the case insensitive comparison.
accept_uppercase_pseudo_header_protocol(Config) -> accept_uppercase_pseudo_header_protocol(Config) ->
doc("The :protocol pseudo header is case insensitive. (draft-01 4)"), doc("The :protocol pseudo header is case insensitive. (draft-01 4)"),
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
@ -172,6 +173,7 @@ reject_many_pseudo_header_protocol(Config) ->
ok. ok.
reject_unknown_pseudo_header_protocol(Config) -> 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 " doc("An extended CONNECT request with an unknown protocol must be rejected "
"with a 400 error. (draft-01 4)"), "with a 400 error. (draft-01 4)"),
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
@ -192,10 +194,11 @@ reject_unknown_pseudo_header_protocol(Config) ->
{ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
{ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000),
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
{_, <<"400">>} = lists:keyfind(<<":status">>, 1, RespHeaders), {_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
ok. ok.
reject_invalid_pseudo_header_protocol(Config) -> 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 " doc("An extended CONNECT request with an invalid protocol must be rejected "
"with a 400 error. (draft-01 4)"), "with a 400 error. (draft-01 4)"),
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
@ -216,7 +219,7 @@ reject_invalid_pseudo_header_protocol(Config) ->
{ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), {ok, << Len1:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
{ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000), {ok, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000),
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock), {RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
{_, <<"400">>} = lists:keyfind(<<":status">>, 1, RespHeaders), {_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
ok. ok.
reject_missing_pseudo_header_scheme(Config) -> reject_missing_pseudo_header_scheme(Config) ->
@ -293,7 +296,7 @@ reject_missing_pseudo_header_protocol(Config) ->
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
{ok, Socket, Settings} = do_handshake(Config), {ok, Socket, Settings} = do_handshake(Config),
#{enable_connect_protocol := true} = Settings, #{enable_connect_protocol := true} = Settings,
%% Send an extended CONNECT request without a :scheme pseudo-header. %% Send an extended CONNECT request without a :protocol pseudo-header.
{ReqHeadersBlock, _} = cow_hpack:encode([ {ReqHeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"CONNECT">>}, {<<":method">>, <<"CONNECT">>},
{<<":scheme">>, <<"http">>}, {<<":scheme">>, <<"http">>},
@ -317,7 +320,7 @@ reject_connection_header(Config) ->
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
{ok, Socket, Settings} = do_handshake(Config), {ok, Socket, Settings} = do_handshake(Config),
#{enable_connect_protocol := true} = Settings, #{enable_connect_protocol := true} = Settings,
%% Send an extended CONNECT request without a :scheme pseudo-header. %% Send an extended CONNECT request with a connection header.
{ReqHeadersBlock, _} = cow_hpack:encode([ {ReqHeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"CONNECT">>}, {<<":method">>, <<"CONNECT">>},
{<<":protocol">>, <<"websocket">>}, {<<":protocol">>, <<"websocket">>},
@ -339,7 +342,7 @@ reject_upgrade_header(Config) ->
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1. %% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
{ok, Socket, Settings} = do_handshake(Config), {ok, Socket, Settings} = do_handshake(Config),
#{enable_connect_protocol := true} = Settings, #{enable_connect_protocol := true} = Settings,
%% Send an extended CONNECT request without a :scheme pseudo-header. %% Send an extended CONNECT request with a upgrade header.
{ReqHeadersBlock, _} = cow_hpack:encode([ {ReqHeadersBlock, _} = cow_hpack:encode([
{<<":method">>, <<"CONNECT">>}, {<<":method">>, <<"CONNECT">>},
{<<":protocol">>, <<"websocket">>}, {<<":protocol">>, <<"websocket">>},

2426
test/rfc9114_SUITE.erl Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVJakPYfQA1Hr6Gnq
GYmpMfXpxUi2QwDBrZfw8dBcVqKhRANCAAQDHeeAvjwD7p+Mg1F+G9FBNy+7Wcms
HEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx0l77vFjrUc6X1al4+ZM5
-----END PRIVATE KEY-----

View file

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBtTCCAVugAwIBAgIUeAPi9oyMIE/KRpsRdukfx2eMuuswCgYIKoZIzj0EAwIw
IDELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9EWUFCMB4XDTIzMDcwNTEwMjIy
MloXDTI0MTExNjEwMjIyMlowMTELMAkGA1UEBhMCU0UxETAPBgNVBAoMCE5PQk9E
WUFCMQ8wDQYDVQQDDAZjbGllbnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQD
HeeAvjwD7p+Mg1F+G9FBNy+7WcmsHEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx
0l77vFjrUc6X1al4+ZM5o2IwYDALBgNVHQ8EBAMCA4gwEQYDVR0RBAowCIIGY2xp
ZW50MB0GA1UdDgQWBBTnhPpO+rSIFAxvkwVjlkKOO2jOeDAfBgNVHSMEGDAWgBSD
Hw8A4XXG3jB1Atrqux7AUsf+KjAKBggqhkjOPQQDAgNIADBFAiEA2qf29EBp2hcL
sEO7MM0ZLm4gnaMdcxtyneF3+c7Lg3cCIBFTVP8xHlhCJyb8ESV7S052VU0bKQFN
ioyoYtcycxuZ
-----END CERTIFICATE-----

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvykUYMOS2gW8XTTh
HgmeJM36NT8GGTNXzzt4sIs0o9ahRANCAATnQOMkKbLFQCZY/cxf8otEJG2tVuG6
QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBMB4ZfLA/PdSiO+KrOeOcj
-----END PRIVATE KEY-----

View file

@ -0,0 +1,12 @@
-----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-----

357
test/rfc9204_SUITE.erl Normal file
View file

@ -0,0 +1,357 @@
%% Copyright (c) 2024, Loïc Hoguin <essen@ninenines.eu>
%%
%% 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, <<TypeValue>>, 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.

485
test/rfc9220_SUITE.erl Normal file
View file

@ -0,0 +1,485 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% 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/2 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

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -33,13 +33,14 @@ groups() ->
Tests = [nc_rand, nc_zero], Tests = [nc_rand, nc_zero],
H1Tests = [slowloris, slowloris_chunks], H1Tests = [slowloris, slowloris_chunks],
H2CTests = [ H2CTests = [
http2_cancel_flood,
http2_data_dribble, http2_data_dribble,
http2_empty_frame_flooding_data, http2_empty_frame_flooding_data,
http2_empty_frame_flooding_headers_continuation, http2_empty_frame_flooding_headers_continuation,
http2_empty_frame_flooding_push_promise, http2_empty_frame_flooding_push_promise,
http2_infinite_continuations,
http2_ping_flood, http2_ping_flood,
http2_reset_flood, http2_reset_flood,
http2_cancel_flood,
http2_settings_flood, http2_settings_flood,
http2_zero_length_header_leak http2_zero_length_header_leak
], ],
@ -48,10 +49,12 @@ groups() ->
{https, [parallel], Tests ++ H1Tests}, {https, [parallel], Tests ++ H1Tests},
{h2, [parallel], Tests}, {h2, [parallel], Tests},
{h2c, [parallel], Tests ++ H2CTests}, {h2c, [parallel], Tests ++ H2CTests},
{h3, [], Tests},
{http_compress, [parallel], Tests ++ H1Tests}, {http_compress, [parallel], Tests ++ H1Tests},
{https_compress, [parallel], Tests ++ H1Tests}, {https_compress, [parallel], Tests ++ H1Tests},
{h2_compress, [parallel], Tests}, {h2_compress, [parallel], Tests},
{h2c_compress, [parallel], Tests ++ H2CTests} {h2c_compress, [parallel], Tests ++ H2CTests},
{h3_compress, [], Tests}
]. ].
init_per_suite(Config) -> init_per_suite(Config) ->
@ -65,7 +68,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE). cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Routes. %% Routes.
@ -219,6 +222,38 @@ http2_empty_frame_flooding_push_promise(Config) ->
{ok, <<_:24, 7:8, _:72, 1:32>>} = gen_tcp:recv(Socket, 17, 6000), {ok, <<_:24, 7:8, _:72, 1:32>>} = gen_tcp:recv(Socket, 17, 6000),
ok. 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. %% @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, " % 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)"), % "but do not attempt to read from the socket. (CVE-2019-9517)"),

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2016-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -20,6 +20,12 @@
-import(ct_helper, [doc/1]). -import(ct_helper, [doc/1]).
-import(cowboy_test, [gun_open/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. %% ct.
all() -> all() ->
@ -39,16 +45,22 @@ groups() ->
{dir, [parallel], DirTests}, {dir, [parallel], DirTests},
{priv_dir, [parallel], DirTests} {priv_dir, [parallel], DirTests}
], ],
GroupTestsNoParallel = OtherTests ++ [
{dir, [], DirTests},
{priv_dir, [], DirTests}
],
[ [
{http, [parallel], GroupTests}, {http, [parallel], GroupTests},
{https, [parallel], GroupTests}, {https, [parallel], GroupTests},
{h2, [parallel], GroupTests}, {h2, [parallel], GroupTests},
{h2c, [parallel], GroupTests}, {h2c, [parallel], GroupTests},
{h3, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
{http_compress, [parallel], GroupTests}, {http_compress, [parallel], GroupTests},
{https_compress, [parallel], GroupTests}, {https_compress, [parallel], GroupTests},
{h2_compress, [parallel], GroupTests}, {h2_compress, [parallel], GroupTests},
{h2c_compress, [parallel], GroupTests}, {h2c_compress, [parallel], GroupTests},
%% No real need to test sendfile disabled against https or h2. {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}, {http_no_sendfile, [parallel], GroupTests},
{h2c_no_sendfile, [parallel], GroupTests} {h2c_no_sendfile, [parallel], GroupTests}
]. ].
@ -116,6 +128,17 @@ init_per_group(Name=h2c_no_sendfile, Config) ->
sendfile => false sendfile => false
}, [{flavor, vanilla}|Config]), }, [{flavor, vanilla}|Config]),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}); 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) -> init_per_group(Name, Config) ->
Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE), Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE),
Opts = ranch:get_protocol_options(Name), Opts = ranch:get_protocol_options(Name),
@ -129,7 +152,7 @@ end_per_group(dir, _) ->
end_per_group(priv_dir, _) -> end_per_group(priv_dir, _) ->
ok; ok;
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(Name). cowboy_test:stop_group(Name).
%% Large file. %% Large file.
@ -248,25 +271,11 @@ do_mime_custom(Path) ->
_ -> {<<"application">>, <<"octet-stream">>, []} _ -> {<<"application">>, <<"octet-stream">>, []}
end. end.
do_get(Path, Config) ->
do_get(Path, [], Config).
do_get(Path, ReqHeaders, Config) ->
ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|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. %% Tests.
bad(Config) -> bad(Config) ->
doc("Bad cowboy_static options: not a tuple."), doc("Bad cowboy_static options: not a tuple."),
{500, _, _} = do_get("/bad", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad", Config)),
ok. ok.
bad_dir_path(Config) -> bad_dir_path(Config) ->
@ -276,7 +285,7 @@ bad_dir_path(Config) ->
bad_dir_route(Config) -> bad_dir_route(Config) ->
doc("Bad cowboy_static options: missing [...] in route."), doc("Bad cowboy_static options: missing [...] in route."),
{500, _, _} = do_get("/bad/dir/route", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/dir/route", Config)),
ok. ok.
bad_file_in_priv_dir_in_ez_archive(Config) -> bad_file_in_priv_dir_in_ez_archive(Config) ->
@ -291,27 +300,27 @@ bad_file_path(Config) ->
bad_options(Config) -> bad_options(Config) ->
doc("Bad cowboy_static extra options: not a list."), doc("Bad cowboy_static extra options: not a list."),
{500, _, _} = do_get("/bad/options", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options", Config)),
ok. ok.
bad_options_charset(Config) -> bad_options_charset(Config) ->
doc("Bad cowboy_static extra options: invalid charset option."), doc("Bad cowboy_static extra options: invalid charset option."),
{500, _, _} = do_get("/bad/options/charset", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options/charset", Config)),
ok. ok.
bad_options_etag(Config) -> bad_options_etag(Config) ->
doc("Bad cowboy_static extra options: invalid etag option."), doc("Bad cowboy_static extra options: invalid etag option."),
{500, _, _} = do_get("/bad/options/etag", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options/etag", Config)),
ok. ok.
bad_options_mime(Config) -> bad_options_mime(Config) ->
doc("Bad cowboy_static extra options: invalid mimetypes option."), doc("Bad cowboy_static extra options: invalid mimetypes option."),
{500, _, _} = do_get("/bad/options/mime", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/options/mime", Config)),
ok. ok.
bad_priv_dir_app(Config) -> bad_priv_dir_app(Config) ->
doc("Bad cowboy_static options: wrong application name."), doc("Bad cowboy_static options: wrong application name."),
{500, _, _} = do_get("/bad/priv_dir/app/style.css", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_dir/app/style.css", Config)),
ok. ok.
bad_priv_dir_in_ez_archive(Config) -> bad_priv_dir_in_ez_archive(Config) ->
@ -331,12 +340,12 @@ bad_priv_dir_path(Config) ->
bad_priv_dir_route(Config) -> bad_priv_dir_route(Config) ->
doc("Bad cowboy_static options: missing [...] in route."), doc("Bad cowboy_static options: missing [...] in route."),
{500, _, _} = do_get("/bad/priv_dir/route", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_dir/route", Config)),
ok. ok.
bad_priv_file_app(Config) -> bad_priv_file_app(Config) ->
doc("Bad cowboy_static options: wrong application name."), doc("Bad cowboy_static options: wrong application name."),
{500, _, _} = do_get("/bad/priv_file/app", Config), {500, _, _} = do_maybe_h3_error3(do_get("/bad/priv_file/app", Config)),
ok. ok.
bad_priv_file_in_ez_archive(Config) -> bad_priv_file_in_ez_archive(Config) ->
@ -535,7 +544,7 @@ dir_unknown(Config) ->
etag_crash(Config) -> etag_crash(Config) ->
doc("Get a file with a crashing etag function."), doc("Get a file with a crashing etag function."),
{500, _, _} = do_get("/etag/crash", Config), {500, _, _} = do_maybe_h3_error3(do_get("/etag/crash", Config)),
ok. ok.
etag_custom(Config) -> etag_custom(Config) ->
@ -813,7 +822,7 @@ mime_all_uppercase(Config) ->
mime_crash(Config) -> mime_crash(Config) ->
doc("Get a file with a crashing mimetype function."), doc("Get a file with a crashing mimetype function."),
{500, _, _} = do_get("/mime/crash/style.css", Config), {500, _, _} = do_maybe_h3_error3(do_get("/mime/crash/style.css", Config)),
ok. ok.
mime_custom_cowboy(Config) -> mime_custom_cowboy(Config) ->
@ -848,7 +857,7 @@ mime_hardcode_tuple(Config) ->
charset_crash(Config) -> charset_crash(Config) ->
doc("Get a file with a crashing charset function."), doc("Get a file with a crashing charset function."),
{500, _, _} = do_get("/charset/crash/style.css", Config), {500, _, _} = do_maybe_h3_error3(do_get("/charset/crash/style.css", Config)),
ok. ok.
charset_custom_cowboy(Config) -> charset_custom_cowboy(Config) ->
@ -933,7 +942,8 @@ unicode_basic_error(Config) ->
%% # and ? indicate fragment and query components %% # and ? indicate fragment and query components
%% and are therefore not part of the path. %% and are therefore not part of the path.
http -> "\r\s#?"; http -> "\r\s#?";
http2 -> "#?" http2 -> "#?";
http3 -> "#?"
end, end,
_ = [case do_get("/char/" ++ [C], Config) of _ = [case do_get("/char/" ++ [C], Config) of
{400, _, _} -> ok; {400, _, _} -> ok;

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -31,50 +31,42 @@ groups() ->
%% We set this module as a logger in order to silence expected errors. %% We set this module as a logger in order to silence expected errors.
init_per_group(Name = http, Config) -> init_per_group(Name = http, Config) ->
cowboy_test:init_http(Name, #{ cowboy_test:init_http(Name, init_plain_opts(), Config);
logger => ?MODULE,
stream_handlers => [stream_handler_h]
}, Config);
init_per_group(Name = https, Config) -> init_per_group(Name = https, Config) ->
cowboy_test:init_https(Name, #{ cowboy_test:init_https(Name, init_plain_opts(), Config);
logger => ?MODULE,
stream_handlers => [stream_handler_h]
}, Config);
init_per_group(Name = h2, Config) -> init_per_group(Name = h2, Config) ->
cowboy_test:init_http2(Name, #{ cowboy_test:init_http2(Name, init_plain_opts(), Config);
logger => ?MODULE,
stream_handlers => [stream_handler_h]
}, Config);
init_per_group(Name = h2c, Config) -> init_per_group(Name = h2c, Config) ->
Config1 = cowboy_test:init_http(Name, #{ Config1 = cowboy_test:init_http(Name, init_plain_opts(), Config),
logger => ?MODULE,
stream_handlers => [stream_handler_h]
}, Config),
lists:keyreplace(protocol, 1, Config1, {protocol, http2}); 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) -> init_per_group(Name = http_compress, Config) ->
cowboy_test:init_http(Name, #{ cowboy_test:init_http(Name, init_compress_opts(), Config);
logger => ?MODULE,
stream_handlers => [cowboy_compress_h, stream_handler_h]
}, Config);
init_per_group(Name = https_compress, Config) -> init_per_group(Name = https_compress, Config) ->
cowboy_test:init_https(Name, #{ cowboy_test:init_https(Name, init_compress_opts(), Config);
logger => ?MODULE,
stream_handlers => [cowboy_compress_h, stream_handler_h]
}, Config);
init_per_group(Name = h2_compress, Config) -> init_per_group(Name = h2_compress, Config) ->
cowboy_test:init_http2(Name, #{ cowboy_test:init_http2(Name, init_compress_opts(), Config);
logger => ?MODULE,
stream_handlers => [cowboy_compress_h, stream_handler_h]
}, Config);
init_per_group(Name = h2c_compress, Config) -> init_per_group(Name = h2c_compress, Config) ->
Config1 = cowboy_test:init_http(Name, #{ Config1 = cowboy_test:init_http(Name, init_compress_opts(), Config),
logger => ?MODULE, lists:keyreplace(protocol, 1, Config1, {protocol, http2});
stream_handlers => [cowboy_compress_h, stream_handler_h] init_per_group(Name = h3_compress, Config) ->
}, Config), cowboy_test:init_http3(Name, init_compress_opts(), Config).
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
end_per_group(Name, _) -> end_per_group(Name, _) ->
cowboy:stop_listener(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. %% Logger function silencing the expected crashes.
@ -99,15 +91,20 @@ crash_in_init(Config) ->
%% Confirm terminate/3 is NOT called. We have no state to give to it. %% Confirm terminate/3 is NOT called. We have no state to give to it.
receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end, receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end,
%% Confirm early_error/5 is called in HTTP/1.1's case. %% Confirm early_error/5 is called in HTTP/1.1's case.
%% HTTP/2 does not send a response back so there is no early_error call. %% HTTP/2 and HTTP/3 do not send a response back so there is no early_error call.
case config(protocol, Config) of case config(protocol, Config) of
http -> receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end; http -> receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end;
http2 -> ok http2 -> ok;
http3 -> ok
end, end,
%% Receive a 500 error response. do_await_internal_error(ConnPid, Ref, Config).
case gun:await(ConnPid, Ref) of
{response, fin, 500, _} -> ok; do_await_internal_error(ConnPid, Ref, Config) ->
{error, {stream_error, {stream_error, internal_error, _}}} -> ok 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. end.
crash_in_data(Config) -> crash_in_data(Config) ->
@ -126,11 +123,7 @@ crash_in_data(Config) ->
gun:data(ConnPid, Ref, fin, <<"Hello!">>), gun:data(ConnPid, Ref, fin, <<"Hello!">>),
%% Confirm terminate/3 is called, indicating the stream ended. %% Confirm terminate/3 is called, indicating the stream ended.
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
%% Receive a 500 error response. do_await_internal_error(ConnPid, Ref, Config).
case gun:await(ConnPid, Ref) of
{response, fin, 500, _} -> ok;
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
end.
crash_in_info(Config) -> crash_in_info(Config) ->
doc("Confirm an error is sent when a stream handler crashes in info/3."), doc("Confirm an error is sent when a stream handler crashes in info/3."),
@ -144,14 +137,14 @@ crash_in_info(Config) ->
%% Confirm init/3 is called. %% Confirm init/3 is called.
Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end, Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end,
%% Send a message to make the stream handler crash. %% Send a message to make the stream handler crash.
Pid ! {{Pid, 1}, crash}, StreamID = case config(protocol, Config) of
http3 -> 0;
_ -> 1
end,
Pid ! {{Pid, StreamID}, crash},
%% Confirm terminate/3 is called, indicating the stream ended. %% Confirm terminate/3 is called, indicating the stream ended.
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
%% Receive a 500 error response. do_await_internal_error(ConnPid, Ref, Config).
case gun:await(ConnPid, Ref) of
{response, fin, 500, _} -> ok;
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
end.
crash_in_terminate(Config) -> crash_in_terminate(Config) ->
doc("Confirm the state is correct when a stream handler crashes in terminate/3."), doc("Confirm the state is correct when a stream handler crashes in terminate/3."),
@ -185,10 +178,12 @@ crash_in_terminate(Config) ->
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref2), {ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref2),
ok. ok.
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
crash_in_early_error(Config) -> crash_in_early_error(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_crash_in_early_error(Config); http -> do_crash_in_early_error(Config);
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.") 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. end.
do_crash_in_early_error(Config) -> do_crash_in_early_error(Config) ->
@ -225,10 +220,12 @@ do_crash_in_early_error(Config) ->
{response, fin, 500, _} = gun:await(ConnPid, Ref2), {response, fin, 500, _} = gun:await(ConnPid, Ref2),
ok. ok.
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
crash_in_early_error_fatal(Config) -> crash_in_early_error_fatal(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_crash_in_early_error_fatal(Config); http -> do_crash_in_early_error_fatal(Config);
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.") 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. end.
do_crash_in_early_error_fatal(Config) -> do_crash_in_early_error_fatal(Config) ->
@ -262,7 +259,8 @@ early_error_stream_error_reason(Config) ->
%% reason in both protocols. %% reason in both protocols.
{Method, Headers, Status, Error} = case config(protocol, Config) of {Method, Headers, Status, Error} = case config(protocol, Config) of
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error}; http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
http2 -> {<<"TRACE">>, [], 501, no_error} http2 -> {<<"TRACE">>, [], 501, no_error};
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
end, end,
Ref = gun:request(ConnPid, Method, "/long_polling", [ Ref = gun:request(ConnPid, Method, "/long_polling", [
{<<"accept-encoding">>, <<"gzip">>}, {<<"accept-encoding">>, <<"gzip">>},
@ -355,11 +353,20 @@ shutdown_on_socket_close(Config) ->
Spawn ! {Self, ready}, Spawn ! {Self, ready},
%% Close the socket. %% Close the socket.
ok = gun:close(ConnPid), ok = gun:close(ConnPid),
Protocol = config(protocol, Config),
try
%% Confirm terminate/3 is called, indicating the stream ended. %% Confirm terminate/3 is called, indicating the stream ended.
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
%% Confirm we receive a DOWN message for the child process. %% Confirm we receive a DOWN message for the child process.
receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end, receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end,
ok. 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) -> shutdown_timeout_on_stream_stop(Config) ->
doc("Confirm supervised processes are killed " doc("Confirm supervised processes are killed "
@ -406,33 +413,45 @@ shutdown_timeout_on_socket_close(Config) ->
Spawn ! {Self, ready}, Spawn ! {Self, ready},
%% Close the socket. %% Close the socket.
ok = gun:close(ConnPid), ok = gun:close(ConnPid),
Protocol = config(protocol, Config),
try
%% Confirm terminate/3 is called, indicating the stream ended. %% Confirm terminate/3 is called, indicating the stream ended.
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
%% We should NOT receive a DOWN message immediately. %% We should NOT receive a DOWN message immediately.
receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end, receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end,
%% We should receive it now. %% We should receive it now.
receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end, receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end,
ok. 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) -> switch_protocol_after_headers(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_switch_protocol_after_response( http -> do_switch_protocol_after_response(
<<"switch_protocol_after_headers">>, Config); <<"switch_protocol_after_headers">>, Config);
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.") 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. end.
switch_protocol_after_headers_data(Config) -> switch_protocol_after_headers_data(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_switch_protocol_after_response( http -> do_switch_protocol_after_response(
<<"switch_protocol_after_headers_data">>, Config); <<"switch_protocol_after_headers_data">>, Config);
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.") 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. end.
switch_protocol_after_response(Config) -> switch_protocol_after_response(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_switch_protocol_after_response( http -> do_switch_protocol_after_response(
<<"switch_protocol_after_response">>, Config); <<"switch_protocol_after_response">>, Config);
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.") 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. end.
do_switch_protocol_after_response(TestCase, Config) -> do_switch_protocol_after_response(TestCase, Config) ->
@ -502,7 +521,12 @@ terminate_on_stop(Config) ->
{response, fin, 204, _} = gun:await(ConnPid, Ref), {response, fin, 204, _} = gun:await(ConnPid, Ref),
%% Confirm the stream is still alive even though we %% Confirm the stream is still alive even though we
%% received the response fully, and tell it to stop. %% received the response fully, and tell it to stop.
Pid ! {{Pid, 1}, please_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, receive {Self, Pid, info, _, please_stop, _} -> ok after 1000 -> error(timeout) end,
%% Confirm terminate/3 is called. %% Confirm terminate/3 is called.
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end, receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
@ -511,7 +535,8 @@ terminate_on_stop(Config) ->
terminate_on_switch_protocol(Config) -> terminate_on_switch_protocol(Config) ->
case config(protocol, Config) of case config(protocol, Config) of
http -> do_terminate_on_switch_protocol(Config); http -> do_terminate_on_switch_protocol(Config);
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.") 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. end.
do_terminate_on_switch_protocol(Config) -> do_terminate_on_switch_protocol(Config) ->

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2017-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above
@ -29,7 +29,8 @@ suite() ->
%% We initialize trace patterns here. Appropriate would be in %% We initialize trace patterns here. Appropriate would be in
%% init_per_suite/1, but this works just as well. %% init_per_suite/1, but this works just as well.
all() -> all() ->
cowboy_test:common_all(). %% @todo Implement these tests for HTTP/3.
cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}].
init_per_suite(Config) -> init_per_suite(Config) ->
cowboy_tracer_h:set_trace_patterns(), cowboy_tracer_h:set_trace_patterns(),

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2011-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above

View file

@ -1,4 +1,4 @@
%% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu> %% Copyright (c) 2018-2024, Loïc Hoguin <essen@ninenines.eu>
%% %%
%% Permission to use, copy, modify, and/or distribute this software for any %% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above %% purpose with or without fee is hereby granted, provided that the above