Merge branch 'master' into return-allow-header
39
.github/workflows/ci.yaml
vendored
|
@ -4,11 +4,16 @@ name: Check Cowboy
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
schedule:
|
||||
## Every Monday at 2am.
|
||||
- cron: 0 2 * * 1
|
||||
|
||||
env:
|
||||
CI_ERLANG_MK: 1
|
||||
|
||||
jobs:
|
||||
cleanup-master:
|
||||
name: Cleanup master build
|
||||
|
@ -29,3 +34,37 @@ jobs:
|
|||
name: Cowboy
|
||||
needs: cleanup-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
|
||||
|
|
2
LICENSE
|
@ -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
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
36
Makefile
|
@ -2,7 +2,7 @@
|
|||
|
||||
PROJECT = cowboy
|
||||
PROJECT_DESCRIPTION = Small, fast, modern HTTP server.
|
||||
PROJECT_VERSION = 2.10.0
|
||||
PROJECT_VERSION = 2.12.0
|
||||
PROJECT_REGISTERED = cowboy_clock
|
||||
|
||||
# Options.
|
||||
|
@ -15,9 +15,14 @@ CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl
|
|||
LOCAL_DEPS = crypto
|
||||
|
||||
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
|
||||
|
||||
ifeq ($(COWBOY_QUICER),1)
|
||||
DEPS += quicer
|
||||
dep_quicer = git https://github.com/emqx/quic main
|
||||
endif
|
||||
|
||||
DOC_DEPS = asciideck
|
||||
|
||||
TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper gun
|
||||
|
@ -38,8 +43,8 @@ define HEX_TARBALL_EXTRA_METADATA
|
|||
#{
|
||||
licenses => [<<"ISC">>],
|
||||
links => #{
|
||||
<<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.10/guide/">>,
|
||||
<<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.10/manual/">>,
|
||||
<<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.12/guide/">>,
|
||||
<<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.12/manual/">>,
|
||||
<<"GitHub">> => <<"https://github.com/ninenines/cowboy">>,
|
||||
<<"Sponsor">> => <<"https://github.com/sponsors/essen">>
|
||||
}
|
||||
|
@ -50,21 +55,37 @@ endef
|
|||
|
||||
include erlang.mk
|
||||
|
||||
# Don't run the examples test suite by default.
|
||||
# Don't run the examples/autobahn test suites by default.
|
||||
|
||||
ifndef FULL
|
||||
CT_SUITES := $(filter-out examples ws_autobahn,$(CT_SUITES))
|
||||
endif
|
||||
|
||||
# Don't run HTTP/3 test suites on Windows.
|
||||
|
||||
ifeq ($(PLATFORM),msys2)
|
||||
CT_SUITES := $(filter-out rfc9114 rfc9204 rfc9220,$(CT_SUITES))
|
||||
endif
|
||||
|
||||
# Compile options.
|
||||
|
||||
ERLC_OPTS += +warn_missing_spec +warn_untyped_record # +bin_opt_info
|
||||
TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}'
|
||||
|
||||
ifeq ($(COWBOY_QUICER),1)
|
||||
ERLC_OPTS += -D COWBOY_QUICER=1
|
||||
TEST_ERLC_OPTS += -D COWBOY_QUICER=1
|
||||
endif
|
||||
|
||||
# Generate rebar.config on build.
|
||||
|
||||
app:: rebar.config
|
||||
|
||||
# Fix quicer compilation for HTTP/3.
|
||||
|
||||
autopatch-quicer::
|
||||
$(verbose) printf "%s\n" "all: ;" > $(DEPS_DIR)/quicer/c_src/Makefile.erlang.mk
|
||||
|
||||
# Dialyze the tests.
|
||||
|
||||
#DIALYZER_OPTS += --src -r test
|
||||
|
@ -101,8 +122,11 @@ prepare_tag:
|
|||
$(verbose) echo -n "GUIDE: "
|
||||
$(verbose) grep -h dep_$(PROJECT)_commit doc/src/guide/*.asciidoc || true
|
||||
$(verbose) echo
|
||||
$(verbose) echo "Links in the README:"
|
||||
$(verbose) grep http.*:// README.asciidoc
|
||||
$(verbose) echo
|
||||
$(verbose) echo "Titles in most recent CHANGELOG:"
|
||||
$(verbose) for f in `ls -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:; \
|
||||
grep == $$f; \
|
||||
done
|
||||
|
|
|
@ -18,8 +18,8 @@ Cowboy is *clean* and *well tested* Erlang code.
|
|||
|
||||
== Online documentation
|
||||
|
||||
* https://ninenines.eu/docs/en/cowboy/2.6/guide[User guide]
|
||||
* https://ninenines.eu/docs/en/cowboy/2.6/manual[Function reference]
|
||||
* https://ninenines.eu/docs/en/cowboy/2.12/guide[User guide]
|
||||
* https://ninenines.eu/docs/en/cowboy/2.12/manual[Function reference]
|
||||
|
||||
== Offline documentation
|
||||
|
||||
|
|
|
@ -75,6 +75,10 @@ include::performance.asciidoc[Performance]
|
|||
|
||||
= 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.8.asciidoc[Migrating from Cowboy 2.8 to 2.9]
|
||||
|
|
|
@ -62,14 +62,16 @@ handler.
|
|||
=== Cowboy setup
|
||||
|
||||
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]
|
||||
----
|
||||
PROJECT = hello_erlang
|
||||
|
||||
DEPS = cowboy
|
||||
dep_cowboy_commit = 2.10.0
|
||||
dep_cowboy_commit = 2.11.0
|
||||
|
||||
REL_DEPS = relx
|
||||
|
||||
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
|
||||
use soon.
|
||||
|
||||
The `REL_DEPS` line tells the build system to fetch and build
|
||||
`relx`, the library that will create the release.
|
||||
|
||||
If you do `make run` now, Cowboy will be included in the release
|
||||
and started automatically. This is not enough however, as Cowboy
|
||||
doesn't do anything by default. We still need to tell Cowboy to
|
||||
|
|
|
@ -42,7 +42,7 @@ Cowboy is developed for Erlang/OTP 22.0 and newer.
|
|||
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
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
139
doc/src/guide/migrating_from_2.10.asciidoc
Normal 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.
|
15
doc/src/guide/migrating_from_2.11.asciidoc
Normal 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.
|
|
@ -144,6 +144,16 @@ never be called.
|
|||
Implement the `languages_provided` or `charsets_provided`
|
||||
callbacks if applicable.
|
||||
|
||||
Does the resource accept ranged requests? If it does,
|
||||
implement the `ranges_provided` callback. Resources that
|
||||
only accept `bytes` units can use the callback name
|
||||
`auto` and let Cowboy automatically do ranged responses.
|
||||
Other callbacks should have a name prefix of `ranged_`
|
||||
for clarity. For example, `ranged_bytes` or `ranged_pages`.
|
||||
If the resource needs to perform additional checks before
|
||||
accepting to do a ranged responses, implement the
|
||||
`range_satisfiable` callback.
|
||||
|
||||
Is there any other header that may make the representation
|
||||
of the resource vary? Implement the `variances` callback.
|
||||
|
||||
|
@ -191,10 +201,15 @@ the `options` method.
|
|||
=== GET and HEAD methods
|
||||
|
||||
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`
|
||||
callback.
|
||||
|
||||
When range requests are accepted, you must implement one
|
||||
`RangeCallback` for each range unit returned by
|
||||
`ranges_provided` (unless `auto` was used). This is
|
||||
in addition to the `ProvideCallback` callback.
|
||||
|
||||
=== PUT, POST and PATCH methods
|
||||
|
||||
If you implement the methods PUT, POST and/or PATCH,
|
||||
|
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 74 KiB |
|
@ -2,24 +2,23 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<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"
|
||||
height="1052.3622047"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
sodipodi:docname="rest_conneg.svg"
|
||||
inkscape:export-filename="/home/essen/Dropbox/Public/drawing.png"
|
||||
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
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
|
@ -50,7 +49,7 @@
|
|||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient5233"
|
||||
osb:paint="solid">
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#69d2e7;stop-opacity:1;"
|
||||
offset="0"
|
||||
|
@ -64,26 +63,34 @@
|
|||
borderopacity="1.0"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="222.80947"
|
||||
inkscape:cy="634.56615"
|
||||
inkscape:zoom="0.65304847"
|
||||
inkscape:cx="259.55194"
|
||||
inkscape:cy="483.11881"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1402"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="900"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:snap-global="true"
|
||||
showguides="true">
|
||||
showguides="true"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid5357"
|
||||
empspacing="5"
|
||||
visible="true"
|
||||
visible="false"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true" />
|
||||
snapvisiblegridlinesonly="true"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingy="1"
|
||||
spacingx="1"
|
||||
units="px" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
|
@ -93,7 +100,7 @@
|
|||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
|
@ -112,7 +119,7 @@
|
|||
<g
|
||||
transform="translate(303.92143,-296.03137)"
|
||||
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-ydpi="89.926643">
|
||||
<path
|
||||
|
@ -557,149 +564,149 @@
|
|||
inkscape:export-ydpi="89.926643" />
|
||||
<text
|
||||
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"
|
||||
y="114.39204"
|
||||
id="text5371"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
id="text5371"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5373"
|
||||
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
|
||||
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"
|
||||
y="53.112247"
|
||||
id="text5371-2"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
id="text5371-2"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5373-6"
|
||||
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
|
||||
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"
|
||||
y="310.19913"
|
||||
id="text5371-2-3"
|
||||
sodipodi:linespacing="125%"
|
||||
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-7"
|
||||
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
|
||||
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"
|
||||
y="477.47531"
|
||||
id="text5371-2-3-0"
|
||||
sodipodi:linespacing="125%"
|
||||
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-7-3"
|
||||
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
|
||||
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"
|
||||
y="236.95154"
|
||||
id="text5371-4"
|
||||
sodipodi:linespacing="125%"><tspan
|
||||
id="text5371-4"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5373-9"
|
||||
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
|
||||
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"
|
||||
y="60.912468"
|
||||
id="text5371-4-0"
|
||||
sodipodi:linespacing="125%"
|
||||
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"
|
||||
x="245.00391"
|
||||
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
|
||||
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"
|
||||
y="561.14258"
|
||||
id="text5371-2-9"
|
||||
sodipodi:linespacing="125%"
|
||||
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-8"
|
||||
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
|
||||
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"
|
||||
y="646.58331"
|
||||
id="text5371-2-7"
|
||||
sodipodi:linespacing="125%"
|
||||
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"
|
||||
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
|
||||
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"
|
||||
y="142.80627"
|
||||
id="text5371-2-95"
|
||||
sodipodi:linespacing="125%"
|
||||
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-80"
|
||||
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
|
||||
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"
|
||||
y="226.4736"
|
||||
id="text5371-2-32"
|
||||
sodipodi:linespacing="125%"
|
||||
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-27"
|
||||
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
|
||||
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"
|
||||
y="393.80801"
|
||||
id="text5371-2-74"
|
||||
sodipodi:linespacing="125%"
|
||||
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-18"
|
||||
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
|
||||
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"
|
||||
|
@ -710,88 +717,88 @@
|
|||
rx="15" />
|
||||
<text
|
||||
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"
|
||||
y="185.95248"
|
||||
id="text5371-2-391"
|
||||
sodipodi:linespacing="125%"
|
||||
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-63"
|
||||
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
|
||||
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"
|
||||
y="269.61978"
|
||||
id="text5371-2-954"
|
||||
sodipodi:linespacing="125%"
|
||||
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-808"
|
||||
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
|
||||
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"
|
||||
y="353.28702"
|
||||
id="text5371-2-4"
|
||||
sodipodi:linespacing="125%"
|
||||
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-11"
|
||||
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
|
||||
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"
|
||||
y="436.95425"
|
||||
id="text5371-2-92"
|
||||
sodipodi:linespacing="125%"
|
||||
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-4"
|
||||
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
|
||||
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"
|
||||
y="520.62152"
|
||||
id="text5371-2-739"
|
||||
sodipodi:linespacing="125%"
|
||||
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-0"
|
||||
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
|
||||
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"
|
||||
y="604.28876"
|
||||
id="text5371-2-8"
|
||||
sodipodi:linespacing="125%"
|
||||
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-3"
|
||||
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
|
||||
transform="matrix(0,-1,1,0,-513.31414,353.05561)"
|
||||
id="g5650-2">
|
||||
|
@ -820,75 +827,75 @@
|
|||
</g>
|
||||
<text
|
||||
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"
|
||||
y="227.88033"
|
||||
id="text5371-4-6"
|
||||
sodipodi:linespacing="125%"
|
||||
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-9-0"
|
||||
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
|
||||
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"
|
||||
y="395.20209"
|
||||
id="text5371-4-2"
|
||||
sodipodi:linespacing="125%"
|
||||
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-9-01"
|
||||
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
|
||||
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"
|
||||
y="374.19577"
|
||||
id="text5371-4-3"
|
||||
sodipodi:linespacing="125%"
|
||||
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-9-62"
|
||||
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
|
||||
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"
|
||||
y="562.52386"
|
||||
id="text5371-4-4"
|
||||
sodipodi:linespacing="125%"
|
||||
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-9-2"
|
||||
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
|
||||
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"
|
||||
y="663.24762"
|
||||
id="text5371-4-5"
|
||||
sodipodi:linespacing="125%"
|
||||
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
|
||||
inkscape:export-xdpi="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"
|
||||
id="tspan5373-9-09"
|
||||
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
|
||||
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"
|
||||
|
@ -931,18 +938,18 @@
|
|||
</g>
|
||||
<text
|
||||
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"
|
||||
y="394.09869"
|
||||
id="text5371-43"
|
||||
sodipodi:linespacing="125%"
|
||||
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-3"
|
||||
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
|
||||
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"
|
||||
|
@ -956,19 +963,19 @@
|
|||
inkscape:export-ydpi="89.926643" />
|
||||
<text
|
||||
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"
|
||||
y="-354.17184"
|
||||
id="text5371-2-3-0-7"
|
||||
sodipodi:linespacing="125%"
|
||||
transform="matrix(0,-1,1,0,0,0)"
|
||||
transform="rotate(-90)"
|
||||
inkscape:export-filename="/home/essen/extend/cowboy/guide/http_req_resp.png"
|
||||
inkscape:export-xdpi="89.926643"
|
||||
inkscape:export-ydpi="89.926643"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5373-6-7-3-9"
|
||||
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
|
||||
transform="matrix(0,-1,1,0,-508.93096,565.23553)"
|
||||
id="g5650-2-0-4"
|
||||
|
@ -1093,19 +1100,48 @@
|
|||
</g>
|
||||
<text
|
||||
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"
|
||||
y="-106.16136"
|
||||
id="text5371-4-5-9"
|
||||
sodipodi:linespacing="125%"
|
||||
inkscape:export-filename="/home/essen/ninenines/cowboy/guide/rest_options.png"
|
||||
inkscape:export-xdpi="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"
|
||||
id="tspan5373-9-09-1"
|
||||
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
|
||||
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"
|
||||
|
@ -1119,17 +1155,42 @@
|
|||
inkscape:export-ydpi="89.926643" />
|
||||
<text
|
||||
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"
|
||||
y="725.27777"
|
||||
y="730.10156"
|
||||
id="text5371-2-7-9"
|
||||
sodipodi:linespacing="125%"
|
||||
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"
|
||||
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>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 55 KiB |
|
@ -95,6 +95,11 @@ callback will only be called at the end of the
|
|||
"GET and HEAD methods" diagram, when all conditions
|
||||
have been met.
|
||||
|
||||
Optionally, the `ranges_provided` also returns the
|
||||
name of a callback for every range unit it accepts. This
|
||||
will be called at the end of the "GET and HEAD methods"
|
||||
diagram in the case of ranged requests.
|
||||
|
||||
The selected content-type, language and charset are
|
||||
saved as meta values in the Req object. You *should*
|
||||
use the appropriate representation if you set a
|
||||
|
@ -121,11 +126,18 @@ succeed, the resource can be retrieved.
|
|||
|
||||
Cowboy prepares the response by first retrieving
|
||||
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
|
||||
`content_types_provided`. This callback returns the body
|
||||
that will be sent back to the client, or a fun if the
|
||||
body must be streamed.
|
||||
that will be sent back to the client.
|
||||
|
||||
For ranged requests, but only when the `ranges_provided`
|
||||
callback was defined earlier, Cowboy will add the selected
|
||||
`range` information to the Req object and call the
|
||||
`range_satisfiable` callback. After confirming that the
|
||||
range can be provided, Cowboy calls the `RangeResource`
|
||||
callback and produces a ranged response using the
|
||||
ranged data from the callback.
|
||||
|
||||
When the resource does not exist, Cowboy will figure out
|
||||
whether the resource existed previously, and if so whether
|
||||
|
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 115 KiB |
|
@ -84,6 +84,8 @@ if it is undefined, moving directly to the next step. Similarly,
|
|||
| multiple_choices | `false`
|
||||
| options | `ok`
|
||||
| previously_existed | `false`
|
||||
| ranges_provided | skip
|
||||
| range_satisfiable | `true`
|
||||
| rate_limited | `false`
|
||||
| resource_exists | `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.
|
||||
|
||||
In addition to these, there can be any number of user-defined
|
||||
callbacks that are specified through `content_types_accepted/2`
|
||||
and `content_types_provided/2`. They can take any name, however
|
||||
callbacks that are specified through `content_types_accepted/2`,
|
||||
`content_types_provided/2` or `ranges_provided/2`. They can take
|
||||
any name (except `auto` for range callbacks), however
|
||||
it is recommended to use a separate prefix for the callbacks of
|
||||
each function. For example, `from_html` and `to_html` indicate
|
||||
in the first case that we're accepting a resource given as HTML,
|
||||
|
@ -113,9 +116,10 @@ Req object directly. The values are defined in the following table:
|
|||
[cols="<,<",options="header"]
|
||||
|===
|
||||
| Key | Details
|
||||
| media_type | The content-type negotiated for the response entity.
|
||||
| language | The language negotiated for the response entity.
|
||||
| charset | The charset negotiated for the response entity.
|
||||
| media_type | The content-type negotiated for the response entity
|
||||
| language | The language negotiated for the response entity
|
||||
| charset | The charset negotiated for the response entity
|
||||
| range | The range selected for the ranged response
|
||||
|===
|
||||
|
||||
They can be used to send a proper body with the response to a
|
||||
|
@ -129,11 +133,16 @@ of the REST code. They are listed in the following table.
|
|||
[cols="<,<",options="header"]
|
||||
|===
|
||||
| Header name | Details
|
||||
| accept-ranges | Range units accepted by the resource
|
||||
| allow | HTTP methods allowed by the resource
|
||||
| content-language | Language used in the response body
|
||||
| content-range | Range of the content found in the response
|
||||
| content-type | Media type and charset of the response body
|
||||
| etag | Etag of the resource
|
||||
| expires | Expiration date of the resource
|
||||
| last-modified | Last modification date for the resource
|
||||
| location | Relative or absolute URI to the requested resource
|
||||
| retry-after | Delay or time the client should wait before accessing the resource
|
||||
| vary | List of headers that may change the representation of the resource
|
||||
| www-authenticate | Authentication information to access the resource
|
||||
|===
|
||||
|
|
|
@ -36,6 +36,7 @@ opts() :: #{
|
|||
max_skip_body_length => non_neg_integer(),
|
||||
proxy_header => boolean(),
|
||||
request_timeout => timeout(),
|
||||
reset_idle_timeout_on_send => boolean(),
|
||||
sendfile => boolean(),
|
||||
stream_handlers => [module()]
|
||||
}
|
||||
|
@ -148,6 +149,11 @@ request_timeout (5000)::
|
|||
|
||||
Time in ms with no requests before Cowboy closes the connection.
|
||||
|
||||
reset_idle_timeout_on_send (false)::
|
||||
|
||||
Whether the `idle_timeout` gets reset when sending data
|
||||
to the socket.
|
||||
|
||||
sendfile (true)::
|
||||
|
||||
Whether the sendfile syscall may be used. It can be useful to disable
|
||||
|
@ -160,6 +166,7 @@ Ordered list of stream handlers that will handle all stream events.
|
|||
|
||||
== Changelog
|
||||
|
||||
* *2.11*: The `reset_idle_timeout_on_send` option was added.
|
||||
* *2.8*: The `active_n` option was added.
|
||||
* *2.7*: The `initial_stream_flow_size` and `logger` options were added.
|
||||
* *2.6*: The `chunked`, `http10_keepalive`, `proxy_header` and `sendfile` options were added.
|
||||
|
|
|
@ -35,6 +35,7 @@ opts() :: #{
|
|||
max_connection_window_size => 0..16#7fffffff,
|
||||
max_decode_table_size => non_neg_integer(),
|
||||
max_encode_table_size => non_neg_integer(),
|
||||
max_fragmented_header_block_size => 16384..16#7fffffff,
|
||||
max_frame_size_received => 16384..16777215,
|
||||
max_frame_size_sent => 16384..16777215 | infinity,
|
||||
max_received_frame_rate => {pos_integer(), timeout()},
|
||||
|
@ -44,6 +45,7 @@ opts() :: #{
|
|||
max_stream_window_size => 0..16#7fffffff,
|
||||
preface_timeout => timeout(),
|
||||
proxy_header => boolean(),
|
||||
reset_idle_timeout_on_send => boolean(),
|
||||
sendfile => boolean(),
|
||||
settings_timeout => timeout(),
|
||||
stream_handlers => [module()],
|
||||
|
@ -93,7 +95,10 @@ enable_connect_protocol (false)::
|
|||
|
||||
Whether to enable the extended CONNECT method to allow
|
||||
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)::
|
||||
|
||||
|
@ -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
|
||||
one as the encoder's header table size.
|
||||
|
||||
max_fragmented_header_block_size (32768)::
|
||||
|
||||
Maximum header block size when headers are split over multiple HEADERS
|
||||
and CONTINUATION frames. Clients that attempt to send header blocks
|
||||
larger than this value will receive an ENHANCE_YOUR_CALM connection
|
||||
error. Note that this value is not advertised and should be large
|
||||
enough for legitimate requests.
|
||||
|
||||
max_frame_size_received (16384)::
|
||||
|
||||
Maximum size in bytes of the frames received by the server. This value is
|
||||
|
@ -235,6 +248,11 @@ Whether incoming connections have a PROXY protocol header. The
|
|||
proxy information will be passed forward via the `proxy_header`
|
||||
key of the Req object.
|
||||
|
||||
reset_idle_timeout_on_send (false)::
|
||||
|
||||
Whether the `idle_timeout` gets reset when sending data
|
||||
to the socket.
|
||||
|
||||
sendfile (true)::
|
||||
|
||||
Whether the sendfile syscall may be used. It can be useful to disable
|
||||
|
@ -271,6 +289,8 @@ too many `WINDOW_UPDATE` frames.
|
|||
|
||||
== 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
|
||||
against another flood scenario.
|
||||
* *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`
|
||||
and `settings_timeout` to configure HTTP/2 SETTINGS
|
||||
and related behavior.
|
||||
* *2.4*: Add the experimental option `enable_connect_protocol`.
|
||||
* *2.4*: Add the option `enable_connect_protocol`.
|
||||
* *2.0*: Protocol introduced.
|
||||
|
||||
== See also
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
// allow calculating the length instead of returning the
|
||||
// whole thing.
|
||||
|
||||
Note that there used to be a way to stream the response body.
|
||||
It was temporarily removed and will be added back in a later
|
||||
release.
|
||||
It is possible to stream the response body either by manually
|
||||
sending the response and returning a `stop` value; or by
|
||||
switching to a different handler (for example a loop handler)
|
||||
and manually sending the response. All headers already set
|
||||
by Cowboy will also be included in the response.
|
||||
|
||||
// @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
|
||||
|
||||
|
@ -625,7 +747,7 @@ rate_limited(Req, State) -> {Result, Req, State}
|
|||
|
||||
Result :: false | {true, RetryAfter}
|
||||
RetryAfter :: non_neg_integer() | calendar:datetime()
|
||||
Default - false
|
||||
Default :: false
|
||||
----
|
||||
|
||||
Return whether the user is rate limited.
|
||||
|
@ -734,6 +856,8 @@ listed here, like the authorization header.
|
|||
|
||||
== Changelog
|
||||
|
||||
* *2.11*: The `ranges_provided`, `range_satisfiable` and
|
||||
the `RangeCallback` callbacks have been added.
|
||||
* *2.11*: The `generate_etag` callback can now return
|
||||
`undefined` to conditionally avoid generating
|
||||
an etag.
|
||||
|
|
|
@ -129,6 +129,8 @@ when it fails to detect a file's MIME type.
|
|||
|
||||
== Changelog
|
||||
|
||||
* *2.11*: Support for range requests was added in 2.6 and
|
||||
is now considered stable.
|
||||
* *2.6*: The `charset` extra option was added.
|
||||
* *1.0*: Handler introduced.
|
||||
|
||||
|
|
|
@ -285,6 +285,7 @@ normal circumstances if necessary.
|
|||
|
||||
== Changelog
|
||||
|
||||
* *2.11*: Websocket over HTTP/2 is now considered stable.
|
||||
* *2.11*: HTTP/1.1 Websocket no longer traps exits by default.
|
||||
* *2.8*: The `active_n` option was added.
|
||||
* *2.7*: The commands based interface has been documented.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{application, 'cowboy', [
|
||||
{description, "Small, fast, modern HTTP server."},
|
||||
{vsn, "2.10.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']},
|
||||
{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_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]},
|
||||
{applications, [kernel,stdlib,crypto,cowlib,ranch]},
|
||||
{optional_applications, []},
|
||||
|
|
5
erlang.mk
vendored
|
@ -17,7 +17,7 @@
|
|||
ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
export ERLANG_MK_FILENAME
|
||||
|
||||
ERLANG_MK_VERSION = bb811a8
|
||||
ERLANG_MK_VERSION = 16d60fa
|
||||
ERLANG_MK_WITHOUT =
|
||||
|
||||
# Make 3.81 and 3.82 are deprecated.
|
||||
|
@ -3565,7 +3565,7 @@ REBAR_DEPS_DIR = $(DEPS_DIR)
|
|||
export REBAR_DEPS_DIR
|
||||
|
||||
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
|
||||
|
||||
|
@ -4665,7 +4665,6 @@ define makedep.erl
|
|||
end,
|
||||
MakeDepend = fun
|
||||
(F, Fd, Mod, StartLocation) ->
|
||||
{ok, Filename} = file:pid2name(Fd),
|
||||
case io:parse_erl_form(Fd, undefined, StartLocation) of
|
||||
{ok, AbsData, EndLocation} ->
|
||||
case AbsData of
|
||||
|
|
|
@ -9,8 +9,9 @@ $ make run
|
|||
|
||||
Then point your browser to https://localhost:8443
|
||||
|
||||
You will need to temporarily trust the root certificate authority,
|
||||
which can also be found in `priv/ssl/cowboy-ca.crt`.
|
||||
You will be greeted by a security message. You can ask for more
|
||||
information and ultimately accept to access localhost. This is
|
||||
due to the example using a self-signed certificate.
|
||||
|
||||
Recent browsers will communicate using HTTP/2. Older browsers
|
||||
will use HTTP/1.1.
|
||||
|
@ -19,7 +20,7 @@ will use HTTP/1.1.
|
|||
|
||||
[source,bash]
|
||||
----
|
||||
$ curl --cacert priv/ssl/cowboy-ca.crt -i https://localhost:8443
|
||||
$ curl -k -i https://localhost:8443
|
||||
HTTP/1.1 200 OK
|
||||
connection: keep-alive
|
||||
server: Cowboy
|
||||
|
|
20
examples/ssl_hello_world/priv/ssl/cert.pem
Normal 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-----
|
|
@ -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-----
|
28
examples/ssl_hello_world/priv/ssl/key.pem
Normal 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-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -19,9 +19,8 @@ start(_Type, _Args) ->
|
|||
PrivDir = code:priv_dir(ssl_hello_world),
|
||||
{ok, _} = cowboy:start_tls(https, [
|
||||
{port, 8443},
|
||||
{cacertfile, PrivDir ++ "/ssl/cowboy-ca.crt"},
|
||||
{certfile, PrivDir ++ "/ssl/server.crt"},
|
||||
{keyfile, PrivDir ++ "/ssl/server.key"}
|
||||
{certfile, PrivDir ++ "/ssl/cert.pem"},
|
||||
{keyfile, PrivDir ++ "/ssl/key.pem"}
|
||||
], #{env => #{dispatch => Dispatch}}),
|
||||
ssl_hello_world_sup:start_link().
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{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]}.
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -16,6 +16,7 @@
|
|||
|
||||
-export([start_clear/3]).
|
||||
-export([start_tls/3]).
|
||||
-export([start_quic/3]).
|
||||
-export([stop_listener/1]).
|
||||
-export([get_env/2]).
|
||||
-export([get_env/3]).
|
||||
|
@ -25,6 +26,9 @@
|
|||
-export([log/2]).
|
||||
-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().
|
||||
-export_type([opts/0]).
|
||||
|
||||
|
@ -44,6 +48,7 @@
|
|||
|
||||
-spec start_clear(ranch:ref(), ranch:opts(), opts())
|
||||
-> {ok, pid()} | {error, any()}.
|
||||
|
||||
start_clear(Ref, TransOpts0, ProtoOpts0) ->
|
||||
TransOpts1 = ranch:normalize_opts(TransOpts0),
|
||||
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts1),
|
||||
|
@ -52,6 +57,7 @@ start_clear(Ref, TransOpts0, ProtoOpts0) ->
|
|||
|
||||
-spec start_tls(ranch:ref(), ranch:opts(), opts())
|
||||
-> {ok, pid()} | {error, any()}.
|
||||
|
||||
start_tls(Ref, TransOpts0, ProtoOpts0) ->
|
||||
TransOpts1 = ranch:normalize_opts(TransOpts0),
|
||||
SocketOpts = maps:get(socket_opts, TransOpts1, []),
|
||||
|
@ -62,28 +68,103 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
|
|||
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
|
||||
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}) ->
|
||||
{TransOpts, ConnectionType};
|
||||
ensure_connection_type(TransOpts) ->
|
||||
{TransOpts#{connection_type => supervisor}, supervisor}.
|
||||
|
||||
-spec stop_listener(ranch:ref()) -> ok | {error, not_found}.
|
||||
|
||||
stop_listener(Ref) ->
|
||||
ranch:stop_listener(Ref).
|
||||
|
||||
-spec get_env(ranch:ref(), atom()) -> ok.
|
||||
|
||||
get_env(Ref, Name) ->
|
||||
Opts = ranch:get_protocol_options(Ref),
|
||||
Env = maps:get(env, Opts, #{}),
|
||||
maps:get(Name, Env).
|
||||
|
||||
-spec get_env(ranch:ref(), atom(), any()) -> ok.
|
||||
|
||||
get_env(Ref, Name, Default) ->
|
||||
Opts = ranch:get_protocol_options(Ref),
|
||||
Env = maps:get(env, Opts, #{}),
|
||||
maps:get(Name, Env, Default).
|
||||
|
||||
-spec set_env(ranch:ref(), atom(), any()) -> ok.
|
||||
|
||||
set_env(Ref, Name, Value) ->
|
||||
Opts = ranch:get_protocol_options(Ref),
|
||||
Env = maps:get(env, Opts, #{}),
|
||||
|
@ -93,10 +174,12 @@ set_env(Ref, Name, Value) ->
|
|||
%% Internal.
|
||||
|
||||
-spec log({log, logger:level(), io:format(), list()}, opts()) -> ok.
|
||||
|
||||
log({log, Level, Format, Args}, Opts) ->
|
||||
log(Level, Format, Args, Opts).
|
||||
|
||||
-spec log(logger:level(), io:format(), list(), opts()) -> ok.
|
||||
|
||||
log(Level, Format, Args, #{logger := Logger})
|
||||
when Logger =/= error_logger ->
|
||||
_ = Logger:Level(Format, Args),
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% @todo Worth renaming to cowboy_http1.
|
||||
%% @todo Change use of cow_http to cow_http1 where appropriate.
|
||||
-module(cowboy_http).
|
||||
|
||||
-export([init/6]).
|
||||
|
@ -1531,7 +1533,7 @@ maybe_socket_error(_, Result = {ok, _}, _) ->
|
|||
maybe_socket_error(State, {error, Reason}, Human) ->
|
||||
terminate(State, {socket_error, Reason, Human}).
|
||||
|
||||
-spec terminate(_, _) -> no_return().
|
||||
-spec terminate(#state{} | undefined, _) -> no_return().
|
||||
terminate(undefined, Reason) ->
|
||||
exit({shutdown, Reason});
|
||||
terminate(State=#state{streams=Streams, children=Children}, Reason) ->
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -44,6 +44,7 @@
|
|||
max_connection_window_size => 0..16#7fffffff,
|
||||
max_decode_table_size => non_neg_integer(),
|
||||
max_encode_table_size => non_neg_integer(),
|
||||
max_fragmented_header_block_size => 16384..16#7fffffff,
|
||||
max_frame_size_received => 16384..16777215,
|
||||
max_frame_size_sent => 16384..16777215 | infinity,
|
||||
max_received_frame_rate => {pos_integer(), timeout()},
|
||||
|
@ -1138,7 +1139,7 @@ maybe_socket_error(_, Result = {ok, _}, _) ->
|
|||
maybe_socket_error(State, {error, Reason}, Human) ->
|
||||
terminate(State, {socket_error, Reason, Human}).
|
||||
|
||||
-spec terminate(#state{}, _) -> no_return().
|
||||
-spec terminate(#state{} | undefined, _) -> no_return().
|
||||
terminate(undefined, Reason) ->
|
||||
exit({shutdown, Reason});
|
||||
terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,
|
||||
|
|
973
src/cowboy_http3.erl
Normal 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).
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
231
src/cowboy_quicer.erl
Normal 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.
|
|
@ -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>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
|
@ -1024,7 +1024,12 @@ filter([], Map, Errors) ->
|
|||
_ -> {error, Errors}
|
||||
end;
|
||||
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) ->
|
||||
case maps:find(Key, Map) of
|
||||
{ok, Value} ->
|
||||
|
|
|
@ -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
|
||||
%% 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) ->
|
||||
range(Req, State).
|
||||
|
||||
%% @todo This can probably be moved to if_range directly.
|
||||
range(Req, State=#state{ranges_a=[]}) ->
|
||||
set_resp_body(Req, State);
|
||||
range(Req, State) ->
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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]}
|
||||
|Commands0]
|
||||
end,
|
||||
%% @todo We are trying to send a 500 response before resetting
|
||||
%% the stream. But due to the way the RESET_STREAM frame
|
||||
%% works in QUIC the data may be lost. The problem is
|
||||
%% known and a draft RFC exists at
|
||||
%% https://www.ietf.org/id/draft-ietf-quic-reliable-stream-reset-03.html
|
||||
do_info(StreamID, Exit, [
|
||||
{error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>}
|
||||
|Commands], State);
|
||||
|
|
|
@ -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>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -103,7 +103,8 @@
|
|||
%% is trying to upgrade to the Websocket protocol.
|
||||
|
||||
-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);
|
||||
is_upgrade_request(Req=#{version := 'HTTP/1.1', method := <<"GET">>}) ->
|
||||
ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []),
|
||||
|
@ -148,13 +149,13 @@ upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) ->
|
|||
<<"connection">> => <<"upgrade">>,
|
||||
<<"upgrade">> => <<"websocket">>
|
||||
}, 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} ->
|
||||
{ok, cowboy_req:reply(400, Req0), Env}
|
||||
{ok, cowboy_req:reply(501, Req0), Env}
|
||||
catch _:_ ->
|
||||
%% @todo Probably log something here?
|
||||
%% @todo Test that we can have 2 /ws 400 status code in a row on the same connection.
|
||||
%% @todo Does this even work?
|
||||
{ok, cowboy_req:reply(400, Req0), Env}
|
||||
end.
|
||||
|
||||
|
@ -286,9 +287,12 @@ websocket_handshake(State, Req=#{ref := Ref, pid := Pid, streamid := StreamID},
|
|||
module() | undefined, any(), binary(),
|
||||
{#state{}, any()}) -> no_return().
|
||||
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.
|
||||
ranch:remove_connection(Ref),
|
||||
_ -> ranch:remove_connection(Ref)
|
||||
end,
|
||||
Messages = case Transport of
|
||||
undefined -> undefined;
|
||||
_ -> Transport:messages()
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -23,12 +23,20 @@
|
|||
%% ct.
|
||||
|
||||
all() ->
|
||||
[
|
||||
All = [
|
||||
{group, http_compress},
|
||||
{group, https_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() ->
|
||||
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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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),
|
||||
[{ref, Ref}, {type, ssl}, {protocol, http2}, {port, Port}, {opts, Opts}|Config].
|
||||
|
||||
%% @todo This will probably require TransOpts as argument.
|
||||
init_http3(Ref, ProtoOpts, Config) ->
|
||||
%% @todo Quicer does not currently support non-file cert/key,
|
||||
%% so we use quicer test certificates for now.
|
||||
%% @todo Quicer also does not support cacerts which means
|
||||
%% we currently have no authentication based security.
|
||||
DataDir = filename:dirname(filename:dirname(config(data_dir, Config)))
|
||||
++ "/rfc9114_SUITE_data",
|
||||
TransOpts = #{
|
||||
socket_opts => [
|
||||
{certfile, DataDir ++ "/server.pem"},
|
||||
{keyfile, DataDir ++ "/server.key"}
|
||||
]
|
||||
},
|
||||
{ok, Listener} = cowboy:start_quic(Ref, TransOpts, ProtoOpts),
|
||||
{ok, {_, Port}} = quicer:sockname(Listener),
|
||||
%% @todo Keep listener information around in a better place.
|
||||
persistent_term:put({cowboy_test_quic, Ref}, Listener),
|
||||
[{ref, Ref}, {type, quic}, {protocol, http3}, {port, Port}, {opts, TransOpts}|Config].
|
||||
|
||||
stop_group(Ref) ->
|
||||
case persistent_term:get({cowboy_test_quic, Ref}, undefined) of
|
||||
undefined ->
|
||||
cowboy:stop_listener(Ref);
|
||||
Listener ->
|
||||
quicer:close_listener(Listener)
|
||||
end.
|
||||
|
||||
%% Common group of listeners used by most suites.
|
||||
|
||||
common_all() ->
|
||||
[
|
||||
All = [
|
||||
{group, http},
|
||||
{group, https},
|
||||
{group, h2},
|
||||
{group, h2c},
|
||||
{group, h3},
|
||||
{group, http_compress},
|
||||
{group, https_compress},
|
||||
{group, h2_compress},
|
||||
{group, h2c_compress}
|
||||
].
|
||||
{group, 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) ->
|
||||
Opts = case os:getenv("NO_PARALLEL") of
|
||||
false -> [parallel];
|
||||
_ -> []
|
||||
end,
|
||||
[
|
||||
Groups = [
|
||||
{http, Opts, Tests},
|
||||
{https, Opts, Tests},
|
||||
{h2, Opts, Tests},
|
||||
{h2c, Opts, Tests},
|
||||
{h3, Opts, Tests},
|
||||
{http_compress, Opts, Tests},
|
||||
{https_compress, Opts, Tests},
|
||||
{h2_compress, Opts, Tests},
|
||||
{h2c_compress, Opts, Tests}
|
||||
].
|
||||
{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_http(Name, #{
|
||||
|
@ -84,6 +131,10 @@ init_common_groups(Name = h2c, Config, Mod) ->
|
|||
env => #{dispatch => Mod:init_dispatch(Config)}
|
||||
}, [{flavor, vanilla}|Config]),
|
||||
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_http(Name, #{
|
||||
env => #{dispatch => Mod:init_dispatch(Config)},
|
||||
|
@ -104,7 +155,12 @@ init_common_groups(Name = h2c_compress, Config, Mod) ->
|
|||
env => #{dispatch => Mod:init_dispatch(Config)},
|
||||
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
|
||||
}, [{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.
|
||||
|
||||
|
@ -114,7 +170,7 @@ gun_open(Config) ->
|
|||
gun_open(Config, Opts) ->
|
||||
TlsOpts = case proplists:get_value(no_cert, Config, false) of
|
||||
true -> [{verify, verify_none}];
|
||||
false -> ct_helper:get_certs_from_ets()
|
||||
false -> ct_helper:get_certs_from_ets() %% @todo Wrong in current quicer.
|
||||
end,
|
||||
{ok, ConnPid} = gun:open("localhost", config(port, Config), Opts#{
|
||||
retry => 0,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
%% 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
|
||||
%% 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) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(Config), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
|
@ -45,7 +48,9 @@ init_per_group(Name = h2_compress, Config) ->
|
|||
cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(Config), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -86,6 +86,7 @@ echo(<<"match">>, Req, Opts) ->
|
|||
Fields = [binary_to_atom(F, latin1) || F <- Fields0],
|
||||
Value = case Type of
|
||||
<<"qs">> -> cowboy_req:match_qs(Fields, Req);
|
||||
<<"qs_with_constraints">> -> cowboy_req:match_qs([{id, integer}], Req);
|
||||
<<"cookies">> -> cowboy_req:match_cookies(Fields, Req);
|
||||
<<"body_qs">> ->
|
||||
%% Note that the Req should not be discarded but for the
|
||||
|
|
|
@ -182,6 +182,7 @@ do(<<"reply2">>, Req0, Opts) ->
|
|||
<<"twice">> ->
|
||||
ct_helper:ignore(cowboy_req, reply, 4),
|
||||
Req1 = cowboy_req:reply(200, Req0),
|
||||
timer:sleep(100),
|
||||
cowboy_req:reply(200, Req1);
|
||||
Status ->
|
||||
cowboy_req:reply(binary_to_integer(Status), Req0)
|
||||
|
@ -245,6 +246,7 @@ do(<<"stream_reply2">>, Req0, Opts) ->
|
|||
<<"twice">> ->
|
||||
ct_helper:ignore(cowboy_req, stream_reply, 3),
|
||||
Req1 = cowboy_req:stream_reply(200, Req0),
|
||||
timer:sleep(100),
|
||||
%% We will crash here so the body shouldn't be sent.
|
||||
Req = cowboy_req:stream_reply(200, Req1),
|
||||
stream_body(Req),
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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"),
|
||||
_ = [begin
|
||||
raw_send(Client, <<C>>),
|
||||
timer:sleep(10)
|
||||
timer:sleep(1)
|
||||
end || <<C>> <= ChunkedBody],
|
||||
Rest = case catch raw_recv_head(Client) of
|
||||
{'EXIT', _} -> error(closed);
|
||||
|
@ -329,6 +329,7 @@ do_idle_timeout_on_send(Config, Protocol) ->
|
|||
try
|
||||
ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]),
|
||||
{ok, Protocol} = gun:await_up(ConnPid),
|
||||
timer:sleep(500),
|
||||
#{socket := Socket} = gun:info(ConnPid),
|
||||
Pid = get_remote_pid_tcp(Socket),
|
||||
StreamRef = gun:get(ConnPid, "/streamed_result/10/250"),
|
||||
|
@ -359,6 +360,7 @@ do_idle_timeout_reset_on_send(Config, Protocol) ->
|
|||
try
|
||||
ConnPid = gun_open([{type, tcp}, {protocol, Protocol}, {port, Port}|Config]),
|
||||
{ok, Protocol} = gun:await_up(ConnPid),
|
||||
timer:sleep(500),
|
||||
#{socket := Socket} = gun:info(ConnPid),
|
||||
Pid = get_remote_pid_tcp(Socket),
|
||||
StreamRef = gun:get(ConnPid, "/streamed_result/10/250"),
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Dispatch configuration.
|
||||
|
||||
|
|
|
@ -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
|
||||
%% 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) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(Config), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
|
@ -52,10 +54,12 @@ init_per_group(Name = h2_compress, Config) ->
|
|||
cowboy_test:init_http2(Name, init_compress_opts(Config), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(Config), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(Config), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_plain_opts(Config) ->
|
||||
#{
|
||||
|
@ -157,16 +161,24 @@ do_get(Path, UserData, Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
reason := normal,
|
||||
streamid := StreamID,
|
||||
reason := normal, %% @todo Getting h3_no_error here.
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := UserData
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
||||
do_check_streamid(StreamID, Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> 1 = StreamID;
|
||||
http2 -> 1 = StreamID;
|
||||
http3 -> 0 = StreamID
|
||||
end.
|
||||
|
||||
post_body(Config) ->
|
||||
doc("Confirm metrics are correct for a normal POST request."),
|
||||
%% Perform a POST request.
|
||||
|
@ -218,12 +230,13 @@ post_body(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := normal,
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
@ -273,12 +286,13 @@ no_resp_body(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := normal,
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
@ -291,7 +305,8 @@ early_error(Config) ->
|
|||
%% reason in both protocols.
|
||||
{Method, Headers, Status, Error} = case config(protocol, Config) of
|
||||
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error}
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error};
|
||||
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
|
||||
end,
|
||||
Ref = gun:request(ConnPid, Method, "/", [
|
||||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
|
@ -305,7 +320,7 @@ early_error(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {stream_error, Error, _},
|
||||
partial_req := #{},
|
||||
resp_status := Status,
|
||||
|
@ -313,6 +328,7 @@ early_error(Config) ->
|
|||
early_error_time := _,
|
||||
resp_body_length := 0
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
|
@ -321,7 +337,8 @@ early_error(Config) ->
|
|||
early_error_request_line(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_early_error_request_line(Config);
|
||||
http2 -> doc("There are no request lines in HTTP/2.")
|
||||
http2 -> doc("There are no request lines in HTTP/2.");
|
||||
http3 -> doc("There are no request lines in HTTP/3.")
|
||||
end.
|
||||
|
||||
do_early_error_request_line(Config) ->
|
||||
|
@ -341,7 +358,7 @@ do_early_error_request_line(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {connection_error, protocol_error, _},
|
||||
partial_req := #{},
|
||||
resp_status := 400,
|
||||
|
@ -349,6 +366,7 @@ do_early_error_request_line(Config) ->
|
|||
early_error_time := _,
|
||||
resp_body_length := 0
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
ExpectedRespHeaders = maps:from_list(RespHeaders),
|
||||
%% All good!
|
||||
ok
|
||||
|
@ -362,7 +380,9 @@ stream_reply(Config) ->
|
|||
ws(Config) ->
|
||||
case config(protocol, Config) of
|
||||
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.
|
||||
|
||||
do_ws(Config) ->
|
||||
|
@ -405,7 +425,7 @@ do_ws(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := switch_protocol,
|
||||
req := #{},
|
||||
%% A 101 upgrade response was sent.
|
||||
|
@ -420,6 +440,7 @@ do_ws(Config) ->
|
|||
}],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
ok
|
||||
end,
|
||||
|
@ -438,7 +459,15 @@ error_response(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"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),
|
||||
%% Receive the metrics and validate them.
|
||||
receive
|
||||
|
@ -463,7 +492,14 @@ error_response(Config) ->
|
|||
resp_headers := ExpectedRespHeaders,
|
||||
resp_body_length := 0
|
||||
} = 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.
|
||||
#{procs := Procs} = Metrics,
|
||||
[{_, #{
|
||||
|
@ -476,12 +512,13 @@ error_response(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'},
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
@ -495,7 +532,15 @@ error_response_after_reply(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"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),
|
||||
%% Receive the metrics and validate them.
|
||||
receive
|
||||
|
@ -520,7 +565,14 @@ error_response_after_reply(Config) ->
|
|||
resp_headers := ExpectedRespHeaders,
|
||||
resp_body_length := 0
|
||||
} = 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.
|
||||
#{procs := Procs} = Metrics,
|
||||
[{_, #{
|
||||
|
@ -533,12 +585,13 @@ error_response_after_reply(Config) ->
|
|||
#{
|
||||
ref := _,
|
||||
pid := From,
|
||||
streamid := 1,
|
||||
streamid := StreamID,
|
||||
reason := {internal_error, {'EXIT', _Pid, {crash, StackTrace}}, 'Stream process crashed.'},
|
||||
req := #{},
|
||||
informational := [],
|
||||
user_data := #{}
|
||||
} = Metrics,
|
||||
do_check_streamid(StreamID, Config),
|
||||
%% All good!
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
|
|
@ -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
|
||||
%% 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, _) ->
|
||||
ok;
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"localhost", [
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
@ -58,8 +58,15 @@ crash_after_reply(Config) ->
|
|||
Ref = gun:get(ConnPid, "/crash/reply", [
|
||||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
]),
|
||||
{response, fin, 200, _} = gun:await(ConnPid, Ref),
|
||||
{error, timeout} = gun:await(ConnPid, Ref, 1000),
|
||||
Protocol = config(protocol, Config),
|
||||
_ = case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 200, _} ->
|
||||
{error, timeout} = gun:await(ConnPid, Ref, 1000);
|
||||
%% See maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}}
|
||||
when Protocol =:= http3 ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
||||
crash_before_reply(Config) ->
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Routes.
|
||||
|
||||
|
@ -57,7 +57,8 @@ init_dispatch(Config) ->
|
|||
{"/resp/:key[/:arg]", resp_h, []},
|
||||
{"/multipart[/:key]", multipart_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}},
|
||||
{"/opts/:key/length", echo_h, #{length => 1000}},
|
||||
{"/opts/:key/period", echo_h, #{length => 999999999, period => 2000}},
|
||||
|
@ -106,13 +107,17 @@ do_get(Path, Config) ->
|
|||
do_get(Path, Headers, Config) ->
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]),
|
||||
{response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, IsFin, Status, RespHeaders} ->
|
||||
{ok, RespBody} = case IsFin of
|
||||
nofin -> gun:await_body(ConnPid, Ref, infinity);
|
||||
fin -> {ok, <<>>}
|
||||
end,
|
||||
gun:close(ConnPid),
|
||||
{Status, RespHeaders, do_decode(RespHeaders, RespBody)}.
|
||||
{Status, RespHeaders, do_decode(RespHeaders, RespBody)};
|
||||
{error, {stream_error, Error}} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_get_body(Path, Config) ->
|
||||
do_get_body(Path, [], Config).
|
||||
|
@ -141,7 +146,9 @@ do_get_inform(Path, Config) ->
|
|||
fin -> {ok, <<>>}
|
||||
end,
|
||||
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.
|
||||
|
||||
do_decode(Headers, Body) ->
|
||||
|
@ -183,7 +190,8 @@ bindings(Config) ->
|
|||
cert(Config) ->
|
||||
case config(type, Config) of
|
||||
tcp -> doc("TLS certificates can only be provided over TLS.");
|
||||
ssl -> do_cert(Config)
|
||||
ssl -> do_cert(Config);
|
||||
quic -> do_cert(Config)
|
||||
end.
|
||||
|
||||
do_cert(Config) ->
|
||||
|
@ -265,6 +273,7 @@ match_qs(Config) ->
|
|||
end,
|
||||
%% Ensure match errors result in a 400 response.
|
||||
{400, _, _} = do_get("/match/qs/a/c?a=b", [], Config),
|
||||
{400, _, _} = do_get("/match/qs_with_constraints", [], Config),
|
||||
%% This function is tested more extensively through unit tests.
|
||||
ok.
|
||||
|
||||
|
@ -384,7 +393,8 @@ port(Config) ->
|
|||
Port = do_get_body("/direct/port", Config),
|
||||
ExpectedPort = case config(type, Config) of
|
||||
tcp -> <<"80">>;
|
||||
ssl -> <<"443">>
|
||||
ssl -> <<"443">>;
|
||||
quic -> <<"443">>
|
||||
end,
|
||||
ExpectedPort = do_get_body("/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),
|
||||
case do_get_body(Path, Config) of
|
||||
<<"http">> when Transport =:= tcp -> ok;
|
||||
<<"https">> when Transport =:= ssl -> ok
|
||||
<<"https">> when Transport =:= ssl -> ok;
|
||||
<<"https">> when Transport =:= quic -> ok
|
||||
end.
|
||||
|
||||
sock(Config) ->
|
||||
|
@ -423,7 +434,8 @@ uri(Config) ->
|
|||
doc("Request URI building/modification."),
|
||||
Scheme = case config(type, Config) of
|
||||
tcp -> <<"http">>;
|
||||
ssl -> <<"https">>
|
||||
ssl -> <<"https">>;
|
||||
quic -> <<"https">>
|
||||
end,
|
||||
SLen = byte_size(Scheme),
|
||||
Port = integer_to_binary(config(port, Config)),
|
||||
|
@ -457,7 +469,8 @@ do_version(Path, Config) ->
|
|||
Protocol = config(protocol, Config),
|
||||
case do_get_body(Path, Config) of
|
||||
<<"HTTP/1.1">> when Protocol =:= http -> ok;
|
||||
<<"HTTP/2">> when Protocol =:= http2 -> ok
|
||||
<<"HTTP/2">> when Protocol =:= http2 -> ok;
|
||||
<<"HTTP/3">> when Protocol =:= http3 -> ok
|
||||
end.
|
||||
|
||||
%% 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
|
||||
%% (meaning the function will have returned after the period ends).
|
||||
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),
|
||||
%% We expect to read at least some 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.
|
||||
do_read_body_timeout(Path, Body, Config) ->
|
||||
|
@ -523,7 +544,13 @@ do_read_body_timeout(Path, Body, Config) ->
|
|||
Ref = gun:headers(ConnPid, "POST", Path, [
|
||||
{<<"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).
|
||||
|
||||
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))}
|
||||
]),
|
||||
gun:data(ConnPid, Ref, fin, Body),
|
||||
{response, _, 413, _} = gun:await(ConnPid, Ref, infinity),
|
||||
gun:close(ConnPid).
|
||||
Response = gun:await(ConnPid, Ref, infinity),
|
||||
gun:close(ConnPid),
|
||||
case Response of
|
||||
{response, _, 413, _} ->
|
||||
ok;
|
||||
%% We got the wrong crash, likely because the environment
|
||||
%% was overloaded and the timeout triggered. Try again.
|
||||
{response, _, 408, _} ->
|
||||
do_read_urlencoded_body_too_large(Path, Body, Config);
|
||||
%% Timing issues make it possible for the connection to be
|
||||
%% closed before the data went through. We retry.
|
||||
{error, {stream_error, {closed, {error,closed}}}} ->
|
||||
do_read_urlencoded_body_too_large(Path, Body, Config)
|
||||
end.
|
||||
|
||||
read_urlencoded_body_too_long(Config) ->
|
||||
doc("application/x-www-form-urlencoded request body sent too slow. "
|
||||
|
@ -606,15 +645,19 @@ do_read_urlencoded_body_too_long(Path, Body, Config) ->
|
|||
{<<"content-length">>, integer_to_binary(byte_size(Body) * 2)}
|
||||
]),
|
||||
gun:data(ConnPid, Ref, nofin, Body),
|
||||
{response, _, 408, RespHeaders} = gun:await(ConnPid, Ref, infinity),
|
||||
_ = case config(protocol, Config) of
|
||||
http ->
|
||||
Protocol = config(protocol, Config),
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, _, 408, RespHeaders} when Protocol =:= http ->
|
||||
%% 408 error responses should close HTTP/1.1 connections.
|
||||
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders);
|
||||
http2 ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders),
|
||||
gun:close(ConnPid);
|
||||
{response, _, 408, _} when Protocol =:= http2; Protocol =:= http3 ->
|
||||
gun:close(ConnPid);
|
||||
%% We must have hit the timeout due to busy CI environment. Retry.
|
||||
{response, _, 500, _} ->
|
||||
gun:close(ConnPid),
|
||||
do_read_urlencoded_body_too_long(Path, Body, Config)
|
||||
end.
|
||||
|
||||
read_and_match_urlencoded_body(Config) ->
|
||||
doc("Read and match an application/x-www-form-urlencoded request body."),
|
||||
|
@ -810,7 +853,7 @@ set_resp_header(Config) ->
|
|||
{200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config),
|
||||
true = lists:keymember(<<"content-type">>, 1, Headers),
|
||||
%% The set-cookie header is special. set_resp_cookie must be used.
|
||||
{500, _, _} = do_get("/resp/set_resp_header_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_header_cookie", Config)),
|
||||
ok.
|
||||
|
||||
set_resp_headers(Config) ->
|
||||
|
@ -819,7 +862,7 @@ set_resp_headers(Config) ->
|
|||
true = lists:keymember(<<"content-type">>, 1, Headers),
|
||||
true = lists:keymember(<<"content-encoding">>, 1, Headers),
|
||||
%% 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.
|
||||
|
||||
resp_header(Config) ->
|
||||
|
@ -881,28 +924,52 @@ delete_resp_header(Config) ->
|
|||
false = lists:keymember(<<"content-type">>, 1, Headers),
|
||||
ok.
|
||||
|
||||
%% Data may be lost due to how RESET_STREAM QUIC frame works.
|
||||
%% Because there is ongoing work for a better way to reset streams
|
||||
%% (https://www.ietf.org/archive/id/draft-ietf-quic-reliable-stream-reset-03.html)
|
||||
%% we convert the error to a 500 to keep the tests more explicit
|
||||
%% at what we expect.
|
||||
%% @todo When RESET_STREAM_AT gets added we can remove this function.
|
||||
do_maybe_h3_error2({stream_error, h3_internal_error, _}) -> {500, []};
|
||||
do_maybe_h3_error2(Result) -> Result.
|
||||
|
||||
do_maybe_h3_error3({stream_error, h3_internal_error, _}) -> {500, [], <<>>};
|
||||
do_maybe_h3_error3(Result) -> Result.
|
||||
|
||||
inform2(Config) ->
|
||||
doc("Informational response(s) without headers, followed by the real response."),
|
||||
{102, [], 200, _, _} = do_get_inform("/resp/inform2/102", Config),
|
||||
{102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config),
|
||||
{500, _} = do_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),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{200, _} = do_get_inform("/resp/inform2/after_reply", Config),
|
||||
ok.
|
||||
%% With HTTP/1.1 and HTTP/2 we will not get an error.
|
||||
%% With HTTP/3 however the stream will occasionally
|
||||
%% be reset before Gun receives the response.
|
||||
case do_get_inform("/resp/inform2/after_reply", Config) of
|
||||
{200, _} ->
|
||||
ok;
|
||||
{stream_error, h3_internal_error, _} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
inform3(Config) ->
|
||||
doc("Informational response(s) with headers, followed by the real response."),
|
||||
Headers = [{<<"ext-header">>, <<"ext-value">>}],
|
||||
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config),
|
||||
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config),
|
||||
{500, _} = do_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.
|
||||
{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),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{200, _} = do_get_inform("/resp/inform3/after_reply", Config),
|
||||
ok.
|
||||
%% With HTTP/1.1 and HTTP/2 we will not get an error.
|
||||
%% With HTTP/3 however the stream will occasionally
|
||||
%% be reset before Gun receives the response.
|
||||
case do_get_inform("/resp/inform3/after_reply", Config) of
|
||||
{200, _} ->
|
||||
ok;
|
||||
{stream_error, h3_internal_error, _} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
reply2(Config) ->
|
||||
doc("Response with default headers and no body."),
|
||||
|
@ -910,7 +977,7 @@ reply2(Config) ->
|
|||
{201, _, _} = do_get("/resp/reply2/201", Config),
|
||||
{404, _, _} = do_get("/resp/reply2/404", 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.
|
||||
{200, _, _} = do_get("/resp/reply2/twice", Config),
|
||||
ok.
|
||||
|
@ -923,9 +990,9 @@ reply3(Config) ->
|
|||
true = lists:keymember(<<"content-type">>, 1, Headers2),
|
||||
{404, Headers3, _} = do_get("/resp/reply3/404", Config),
|
||||
true = lists:keymember(<<"content-type">>, 1, Headers3),
|
||||
{500, _, _} = do_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.
|
||||
{500, _, _} = do_get("/resp/reply3/set_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/set_cookie", Config)),
|
||||
ok.
|
||||
|
||||
reply4(Config) ->
|
||||
|
@ -933,9 +1000,9 @@ reply4(Config) ->
|
|||
{200, _, <<"OK">>} = do_get("/resp/reply4/200", Config),
|
||||
{201, _, <<"OK">>} = do_get("/resp/reply4/201", Config),
|
||||
{404, _, <<"OK">>} = do_get("/resp/reply4/404", Config),
|
||||
{500, _, _} = do_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.
|
||||
{500, _, _} = do_get("/resp/reply4/set_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/set_cookie", Config)),
|
||||
ok.
|
||||
|
||||
stream_reply2(Config) ->
|
||||
|
@ -945,12 +1012,11 @@ stream_reply2(Config) ->
|
|||
{201, _, Body} = do_get("/resp/stream_reply2/201", Config),
|
||||
{404, _, Body} = do_get("/resp/stream_reply2/404", Config),
|
||||
{200, _, Body} = do_get("/resp/stream_reply2/binary", Config),
|
||||
{500, _, _} = do_get("/resp/stream_reply2/error", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply2/error", Config)),
|
||||
ok.
|
||||
|
||||
stream_reply2_twice(Config) ->
|
||||
doc("Attempting to stream a response twice results in a crash. "
|
||||
"This crash can only be properly detected in HTTP/2."),
|
||||
doc("Attempting to stream a response twice results in a crash."),
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, "/resp/stream_reply2/twice",
|
||||
[{<<"accept-encoding">>, <<"gzip">>}]),
|
||||
|
@ -969,8 +1035,10 @@ stream_reply2_twice(Config) ->
|
|||
zlib:inflateInit(Z, 31),
|
||||
0 = iolist_size(zlib:inflate(Z, Data)),
|
||||
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, _}}}} ->
|
||||
ok;
|
||||
{http3, _, {error, {stream_error, {stream_error, h3_internal_error, _}}}} ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
@ -984,9 +1052,9 @@ stream_reply3(Config) ->
|
|||
true = lists:keymember(<<"content-type">>, 1, Headers2),
|
||||
{404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config),
|
||||
true = lists:keymember(<<"content-type">>, 1, Headers3),
|
||||
{500, _, _} = do_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.
|
||||
{500, _, _} = do_get("/resp/stream_reply3/set_cookie", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/set_cookie", Config)),
|
||||
ok.
|
||||
|
||||
stream_body_fin0(Config) ->
|
||||
|
@ -1070,8 +1138,11 @@ stream_body_content_length_nofin_error(Config) ->
|
|||
end
|
||||
end;
|
||||
http2 ->
|
||||
%% @todo HTTP2 should have the same content-length checks
|
||||
ok
|
||||
%% @todo HTTP/2 should have the same content-length checks.
|
||||
{skip, "Implement the test for HTTP/2."};
|
||||
http3 ->
|
||||
%% @todo HTTP/3 should have the same content-length checks.
|
||||
{skip, "Implement the test for HTTP/3."}
|
||||
end.
|
||||
|
||||
stream_body_concurrent(Config) ->
|
||||
|
@ -1173,16 +1244,24 @@ stream_trailers_set_cookie(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"te">>, <<"trailers">>}
|
||||
]),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity),
|
||||
case config(protocol, Config) of
|
||||
http ->
|
||||
Protocol = config(protocol, Config),
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, nofin, 200, _} when Protocol =:= http ->
|
||||
%% Trailers are not sent because of the stream error.
|
||||
{ok, _Body} = gun:await_body(ConnPid, Ref, infinity),
|
||||
{error, timeout} = gun:await_body(ConnPid, Ref, 1000),
|
||||
ok;
|
||||
http2 ->
|
||||
{response, nofin, 200, _} when Protocol =:= http2 ->
|
||||
{error, {stream_error, {stream_error, internal_error, _}}}
|
||||
= gun:await_body(ConnPid, Ref, infinity),
|
||||
ok;
|
||||
{response, nofin, 200, _} when Protocol =:= http3 ->
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}}
|
||||
= gun:await_body(ConnPid, Ref, infinity),
|
||||
ok;
|
||||
%% The RST_STREAM arrived before the start of the response.
|
||||
%% See maybe_h3_error comment for details.
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
@ -1210,34 +1289,45 @@ do_trailers(Path, Config) ->
|
|||
push(Config) ->
|
||||
case config(protocol, Config) of
|
||||
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.
|
||||
|
||||
push_after_reply(Config) ->
|
||||
doc("Trying to push a response after the final response results in a crash."),
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, "/resp/push/after_reply", []),
|
||||
%% @todo How to test this properly? This isn't enough.
|
||||
{response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
|
||||
%% With HTTP/1.1 and HTTP/2 we will not get an error.
|
||||
%% With HTTP/3 however the stream will occasionally
|
||||
%% be reset before Gun receives the response.
|
||||
case gun:await(ConnPid, Ref, infinity) of
|
||||
{response, fin, 200, _} ->
|
||||
ok;
|
||||
{error, {stream_error, {stream_error, h3_internal_error, _}}} ->
|
||||
ok
|
||||
end,
|
||||
gun:close(ConnPid).
|
||||
|
||||
push_method(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push/method", Config);
|
||||
http2 -> do_push_http2_method(Config)
|
||||
http2 -> do_push_http2_method(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
|
||||
push_origin(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push/origin", Config);
|
||||
http2 -> do_push_http2_origin(Config)
|
||||
http2 -> do_push_http2_origin(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
push_qs(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_push_http("/resp/push/qs", Config);
|
||||
http2 -> do_push_http2_qs(Config)
|
||||
http2 -> do_push_http2_qs(Config);
|
||||
http3 -> {skip, "Implement server push for HTTP/3."}
|
||||
end.
|
||||
|
||||
do_push_http(Path, Config) ->
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Dispatch configuration.
|
||||
|
||||
|
@ -85,7 +85,7 @@ accept_callback_missing(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"content-type">>, <<"text/plain">>}
|
||||
], <<"Missing!">>),
|
||||
{response, fin, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
accept_callback_patch_false(Config) ->
|
||||
|
@ -472,7 +472,8 @@ delete_resource_missing(Config) ->
|
|||
Ref = gun:delete(ConnPid, "/delete_resource?missing", [
|
||||
{<<"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),
|
||||
ok.
|
||||
|
||||
|
@ -653,10 +654,16 @@ do_generate_etag(Config, Qs, ReqHeaders, Status, Etag) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
|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),
|
||||
ok.
|
||||
|
||||
%% See do_maybe_h3_error2 comment.
|
||||
do_maybe_h3_error({error, {stream_error, {stream_error, h3_internal_error, _}}}) ->
|
||||
{response, fin, 500, []};
|
||||
do_maybe_h3_error(Result) ->
|
||||
Result.
|
||||
|
||||
if_range_etag_equal(Config) ->
|
||||
doc("When the if-range header matches, a 206 partial content "
|
||||
"response is expected for an otherwise valid range request. (RFC7233 3.2)"),
|
||||
|
@ -821,7 +828,7 @@ provide_callback_missing(Config) ->
|
|||
doc("A 500 response must be sent when the ProvideCallback can't be called."),
|
||||
ConnPid = gun_open(Config),
|
||||
Ref = gun:get(ConnPid, "/provide_callback_missing", [{<<"accept-encoding">>, <<"gzip">>}]),
|
||||
{response, fin, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
provide_range_callback(Config) ->
|
||||
|
@ -977,7 +984,7 @@ provide_range_callback_missing(Config) ->
|
|||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
{<<"range">>, <<"bytes=0-">>}
|
||||
]),
|
||||
{response, fin, 500, _} = gun:await(ConnPid, Ref),
|
||||
{response, fin, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
|
||||
ok.
|
||||
|
||||
range_ignore_unknown_unit(Config) ->
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
@ -237,6 +237,8 @@ http10_expect(Config) ->
|
|||
http ->
|
||||
do_http10_expect(Config);
|
||||
http2 ->
|
||||
expect(Config);
|
||||
http3 ->
|
||||
expect(Config)
|
||||
end.
|
||||
|
||||
|
@ -303,6 +305,9 @@ expect_discard_body_close(Config) ->
|
|||
do_expect_discard_body_close(Config);
|
||||
http2 ->
|
||||
doc("There's no reason to close the connection when using HTTP/2, "
|
||||
"even if a stream body is too large. We just cancel the stream.");
|
||||
http3 ->
|
||||
doc("There's no reason to close the connection when using HTTP/3, "
|
||||
"even if a stream body is too large. We just cancel the stream.")
|
||||
end.
|
||||
|
||||
|
@ -424,8 +429,10 @@ http10_status_code_100(Config) ->
|
|||
http ->
|
||||
doc("The 100 Continue status code must not "
|
||||
"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 ->
|
||||
status_code_100(Config);
|
||||
http3 ->
|
||||
status_code_100(Config)
|
||||
end.
|
||||
|
||||
|
@ -434,12 +441,16 @@ http10_status_code_101(Config) ->
|
|||
http ->
|
||||
doc("The 101 Switching Protocols status code must not "
|
||||
"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 ->
|
||||
status_code_101(Config);
|
||||
http3 ->
|
||||
%% While 101 is not supported by HTTP/3, there is no
|
||||
%% wording in RFC9114 that forbids sending it.
|
||||
status_code_101(Config)
|
||||
end.
|
||||
|
||||
do_http10_status_code_1xx(StatusCode, Config) ->
|
||||
do_unsupported_status_code_1xx(StatusCode, Config) ->
|
||||
ConnPid = gun_open(Config, #{http_opts => #{version => 'HTTP/1.0'}}),
|
||||
Ref = gun:get(ConnPid, "/resp/inform2/" ++ integer_to_list(StatusCode), [
|
||||
{<<"accept-encoding">>, <<"gzip">>}
|
||||
|
@ -653,7 +664,9 @@ status_code_408_connection_close(Config) ->
|
|||
http ->
|
||||
do_http11_status_code_408_connection_close(Config);
|
||||
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.
|
||||
|
||||
do_http11_status_code_408_connection_close(Config) ->
|
||||
|
@ -744,7 +757,9 @@ status_code_426_upgrade_header(Config) ->
|
|||
http ->
|
||||
do_status_code_426_upgrade_header(Config);
|
||||
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.
|
||||
|
||||
do_status_code_426_upgrade_header(Config) ->
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -34,9 +34,9 @@
|
|||
all() -> [{group, clear}, {group, tls}].
|
||||
|
||||
groups() ->
|
||||
Modules = ct_helper:all(?MODULE),
|
||||
Clear = [M || M <- Modules, lists:sublist(atom_to_list(M), 4) =/= "alpn"] -- [prior_knowledge_reject_tls],
|
||||
TLS = [M || M <- Modules, lists:sublist(atom_to_list(M), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls],
|
||||
Tests = ct_helper:all(?MODULE),
|
||||
Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 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}].
|
||||
|
||||
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
|
||||
%% is to favor :authority and ignore the host header. The specification does
|
||||
%% not describe the correct behavior to follow in that case.
|
||||
%% @todo The HTTP/3 spec says both values must be identical and non-empty.
|
||||
|
||||
reject_many_pseudo_header_authority(Config) ->
|
||||
doc("A request containing more than one authority component must be rejected "
|
||||
|
|
|
@ -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
|
||||
%% 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).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
init_dispatch(_) ->
|
||||
cowboy_router:compile([{"[...]", [
|
||||
|
|
|
@ -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
|
||||
%% 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.
|
||||
|
||||
%% @todo Refer to RFC9110 7.8 about the case insensitive comparison.
|
||||
accept_uppercase_pseudo_header_protocol(Config) ->
|
||||
doc("The :protocol pseudo header is case insensitive. (draft-01 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
|
@ -172,6 +173,7 @@ reject_many_pseudo_header_protocol(Config) ->
|
|||
ok.
|
||||
|
||||
reject_unknown_pseudo_header_protocol(Config) ->
|
||||
%% @todo This probably shouldn't send 400 but 501 instead based on RFC 9220.
|
||||
doc("An extended CONNECT request with an unknown protocol must be rejected "
|
||||
"with a 400 error. (draft-01 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
|
@ -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, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000),
|
||||
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
|
||||
{_, <<"400">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
{_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
ok.
|
||||
|
||||
reject_invalid_pseudo_header_protocol(Config) ->
|
||||
%% @todo This probably shouldn't send 400 but 501 instead based on RFC 9220.
|
||||
doc("An extended CONNECT request with an invalid protocol must be rejected "
|
||||
"with a 400 error. (draft-01 4)"),
|
||||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
|
@ -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, RespHeadersBlock} = gen_tcp:recv(Socket, Len1, 1000),
|
||||
{RespHeaders, _} = cow_hpack:decode(RespHeadersBlock),
|
||||
{_, <<"400">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
{_, <<"501">>} = lists:keyfind(<<":status">>, 1, RespHeaders),
|
||||
ok.
|
||||
|
||||
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.
|
||||
{ok, Socket, Settings} = do_handshake(Config),
|
||||
#{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([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":scheme">>, <<"http">>},
|
||||
|
@ -317,7 +320,7 @@ reject_connection_header(Config) ->
|
|||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
{ok, Socket, Settings} = do_handshake(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :scheme pseudo-header.
|
||||
%% Send an extended CONNECT request with a connection header.
|
||||
{ReqHeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
|
@ -339,7 +342,7 @@ reject_upgrade_header(Config) ->
|
|||
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
|
||||
{ok, Socket, Settings} = do_handshake(Config),
|
||||
#{enable_connect_protocol := true} = Settings,
|
||||
%% Send an extended CONNECT request without a :scheme pseudo-header.
|
||||
%% Send an extended CONNECT request with a upgrade header.
|
||||
{ReqHeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"CONNECT">>},
|
||||
{<<":protocol">>, <<"websocket">>},
|
||||
|
|
2426
test/rfc9114_SUITE.erl
Normal file
5
test/rfc9114_SUITE_data/client.key
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVJakPYfQA1Hr6Gnq
|
||||
GYmpMfXpxUi2QwDBrZfw8dBcVqKhRANCAAQDHeeAvjwD7p+Mg1F+G9FBNy+7Wcms
|
||||
HEw4sGMzhUL4wjwsqKHpoiuQg3qUXXK0gamx0l77vFjrUc6X1al4+ZM5
|
||||
-----END PRIVATE KEY-----
|
12
test/rfc9114_SUITE_data/client.pem
Normal 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-----
|
5
test/rfc9114_SUITE_data/server.key
Normal file
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgvykUYMOS2gW8XTTh
|
||||
HgmeJM36NT8GGTNXzzt4sIs0o9ahRANCAATnQOMkKbLFQCZY/cxf8otEJG2tVuG6
|
||||
QvLqUdERV2+gzE+4ROGDqbb2Jk1szyz4CfBMB4ZfLA/PdSiO+KrOeOcj
|
||||
-----END PRIVATE KEY-----
|
12
test/rfc9114_SUITE_data/server.pem
Normal 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
|
@ -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
|
@ -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
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -33,13 +33,14 @@ groups() ->
|
|||
Tests = [nc_rand, nc_zero],
|
||||
H1Tests = [slowloris, slowloris_chunks],
|
||||
H2CTests = [
|
||||
http2_cancel_flood,
|
||||
http2_data_dribble,
|
||||
http2_empty_frame_flooding_data,
|
||||
http2_empty_frame_flooding_headers_continuation,
|
||||
http2_empty_frame_flooding_push_promise,
|
||||
http2_infinite_continuations,
|
||||
http2_ping_flood,
|
||||
http2_reset_flood,
|
||||
http2_cancel_flood,
|
||||
http2_settings_flood,
|
||||
http2_zero_length_header_leak
|
||||
],
|
||||
|
@ -48,10 +49,12 @@ groups() ->
|
|||
{https, [parallel], Tests ++ H1Tests},
|
||||
{h2, [parallel], Tests},
|
||||
{h2c, [parallel], Tests ++ H2CTests},
|
||||
{h3, [], Tests},
|
||||
{http_compress, [parallel], Tests ++ H1Tests},
|
||||
{https_compress, [parallel], Tests ++ H1Tests},
|
||||
{h2_compress, [parallel], Tests},
|
||||
{h2c_compress, [parallel], Tests ++ H2CTests}
|
||||
{h2c_compress, [parallel], Tests ++ H2CTests},
|
||||
{h3_compress, [], Tests}
|
||||
].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
|
@ -65,7 +68,7 @@ init_per_group(Name, Config) ->
|
|||
cowboy_test:init_common_groups(Name, Config, ?MODULE).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% 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.
|
||||
|
||||
http2_infinite_continuations(Config) ->
|
||||
doc("Confirm that Cowboy rejects CONTINUATION frames when the "
|
||||
"total size of HEADERS + CONTINUATION(s) exceeds the limit. (VU#421644)"),
|
||||
{ok, Socket} = rfc7540_SUITE:do_handshake(Config),
|
||||
%% Send a HEADERS frame followed by a large number
|
||||
%% of continuation frames.
|
||||
{HeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":method">>, <<"GET">>},
|
||||
{<<":scheme">>, <<"http">>},
|
||||
{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
|
||||
{<<":path">>, <<"/">>}
|
||||
]),
|
||||
HeadersBlockLen = iolist_size(HeadersBlock),
|
||||
ok = gen_tcp:send(Socket, [
|
||||
%% HEADERS frame.
|
||||
<<
|
||||
HeadersBlockLen:24, 1:8, 0:5,
|
||||
0:1, %% END_HEADERS
|
||||
0:1,
|
||||
1:1, %% END_STREAM
|
||||
0:1,
|
||||
1:31 %% Stream ID.
|
||||
>>,
|
||||
HeadersBlock,
|
||||
%% CONTINUATION frames.
|
||||
[<<1024:24, 9:8, 0:8, 0:1, 1:31, 0:1024/unit:8>>
|
||||
|| _ <- lists:seq(1, 100)]
|
||||
]),
|
||||
%% Receive an ENHANCE_YOUR_CALM connection error.
|
||||
{ok, <<_:24, 7:8, _:72, 11:32>>} = gen_tcp:recv(Socket, 17, 6000),
|
||||
ok.
|
||||
|
||||
%% @todo http2_internal_data_buffering(Config) -> I do not know how to test this.
|
||||
% doc("Request many very large responses, with a larger than necessary window size, "
|
||||
% "but do not attempt to read from the socket. (CVE-2019-9517)"),
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
@ -20,6 +20,12 @@
|
|||
-import(ct_helper, [doc/1]).
|
||||
-import(cowboy_test, [gun_open/1]).
|
||||
|
||||
%% Import useful functions from req_SUITE.
|
||||
%% @todo Maybe move these functions to cowboy_test.
|
||||
-import(req_SUITE, [do_get/2]).
|
||||
-import(req_SUITE, [do_get/3]).
|
||||
-import(req_SUITE, [do_maybe_h3_error3/1]).
|
||||
|
||||
%% ct.
|
||||
|
||||
all() ->
|
||||
|
@ -39,16 +45,22 @@ groups() ->
|
|||
{dir, [parallel], DirTests},
|
||||
{priv_dir, [parallel], DirTests}
|
||||
],
|
||||
GroupTestsNoParallel = OtherTests ++ [
|
||||
{dir, [], DirTests},
|
||||
{priv_dir, [], DirTests}
|
||||
],
|
||||
[
|
||||
{http, [parallel], GroupTests},
|
||||
{https, [parallel], GroupTests},
|
||||
{h2, [parallel], GroupTests},
|
||||
{h2c, [parallel], GroupTests},
|
||||
{h3, [], GroupTestsNoParallel}, %% @todo Enable parallel when it works better.
|
||||
{http_compress, [parallel], GroupTests},
|
||||
{https_compress, [parallel], GroupTests},
|
||||
{h2_compress, [parallel], GroupTests},
|
||||
{h2c_compress, [parallel], GroupTests},
|
||||
%% 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},
|
||||
{h2c_no_sendfile, [parallel], GroupTests}
|
||||
].
|
||||
|
@ -116,6 +128,17 @@ init_per_group(Name=h2c_no_sendfile, Config) ->
|
|||
sendfile => false
|
||||
}, [{flavor, vanilla}|Config]),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name=h3, Config) ->
|
||||
cowboy_test:init_http3(Name, #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
middlewares => [?MODULE, cowboy_router, cowboy_handler]
|
||||
}, [{flavor, vanilla}|Config]);
|
||||
init_per_group(Name=h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
middlewares => [?MODULE, cowboy_router, cowboy_handler],
|
||||
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
|
||||
}, [{flavor, vanilla}|Config]);
|
||||
init_per_group(Name, Config) ->
|
||||
Config1 = cowboy_test:init_common_groups(Name, Config, ?MODULE),
|
||||
Opts = ranch:get_protocol_options(Name),
|
||||
|
@ -129,7 +152,7 @@ end_per_group(dir, _) ->
|
|||
end_per_group(priv_dir, _) ->
|
||||
ok;
|
||||
end_per_group(Name, _) ->
|
||||
cowboy:stop_listener(Name).
|
||||
cowboy_test:stop_group(Name).
|
||||
|
||||
%% Large file.
|
||||
|
||||
|
@ -248,25 +271,11 @@ do_mime_custom(Path) ->
|
|||
_ -> {<<"application">>, <<"octet-stream">>, []}
|
||||
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.
|
||||
|
||||
bad(Config) ->
|
||||
doc("Bad cowboy_static options: not a tuple."),
|
||||
{500, _, _} = do_get("/bad", Config),
|
||||
{500, _, _} = do_maybe_h3_error3(do_get("/bad", Config)),
|
||||
ok.
|
||||
|
||||
bad_dir_path(Config) ->
|
||||
|
@ -276,7 +285,7 @@ bad_dir_path(Config) ->
|
|||
|
||||
bad_dir_route(Config) ->
|
||||
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.
|
||||
|
||||
bad_file_in_priv_dir_in_ez_archive(Config) ->
|
||||
|
@ -291,27 +300,27 @@ bad_file_path(Config) ->
|
|||
|
||||
bad_options(Config) ->
|
||||
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.
|
||||
|
||||
bad_options_charset(Config) ->
|
||||
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.
|
||||
|
||||
bad_options_etag(Config) ->
|
||||
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.
|
||||
|
||||
bad_options_mime(Config) ->
|
||||
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.
|
||||
|
||||
bad_priv_dir_app(Config) ->
|
||||
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.
|
||||
|
||||
bad_priv_dir_in_ez_archive(Config) ->
|
||||
|
@ -331,12 +340,12 @@ bad_priv_dir_path(Config) ->
|
|||
|
||||
bad_priv_dir_route(Config) ->
|
||||
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.
|
||||
|
||||
bad_priv_file_app(Config) ->
|
||||
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.
|
||||
|
||||
bad_priv_file_in_ez_archive(Config) ->
|
||||
|
@ -535,7 +544,7 @@ dir_unknown(Config) ->
|
|||
|
||||
etag_crash(Config) ->
|
||||
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.
|
||||
|
||||
etag_custom(Config) ->
|
||||
|
@ -813,7 +822,7 @@ mime_all_uppercase(Config) ->
|
|||
|
||||
mime_crash(Config) ->
|
||||
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.
|
||||
|
||||
mime_custom_cowboy(Config) ->
|
||||
|
@ -848,7 +857,7 @@ mime_hardcode_tuple(Config) ->
|
|||
|
||||
charset_crash(Config) ->
|
||||
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.
|
||||
|
||||
charset_custom_cowboy(Config) ->
|
||||
|
@ -933,7 +942,8 @@ unicode_basic_error(Config) ->
|
|||
%% # and ? indicate fragment and query components
|
||||
%% and are therefore not part of the path.
|
||||
http -> "\r\s#?";
|
||||
http2 -> "#?"
|
||||
http2 -> "#?";
|
||||
http3 -> "#?"
|
||||
end,
|
||||
_ = [case do_get("/char/" ++ [C], Config) of
|
||||
{400, _, _} -> ok;
|
||||
|
|
|
@ -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
|
||||
%% 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.
|
||||
init_per_group(Name = http, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = https, Config) ->
|
||||
cowboy_test:init_https(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_https(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = h2, Config) ->
|
||||
cowboy_test:init_http2(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http2(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = h2c, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [stream_handler_h]
|
||||
}, Config),
|
||||
Config1 = cowboy_test:init_http(Name, init_plain_opts(), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3, Config) ->
|
||||
cowboy_test:init_http3(Name, init_plain_opts(), Config);
|
||||
init_per_group(Name = http_compress, Config) ->
|
||||
cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = https_compress, Config) ->
|
||||
cowboy_test:init_https(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_https(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = h2_compress, Config) ->
|
||||
cowboy_test:init_http2(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config);
|
||||
cowboy_test:init_http2(Name, init_compress_opts(), Config);
|
||||
init_per_group(Name = h2c_compress, Config) ->
|
||||
Config1 = cowboy_test:init_http(Name, #{
|
||||
logger => ?MODULE,
|
||||
stream_handlers => [cowboy_compress_h, stream_handler_h]
|
||||
}, Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2}).
|
||||
Config1 = cowboy_test:init_http(Name, init_compress_opts(), Config),
|
||||
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
|
||||
init_per_group(Name = h3_compress, Config) ->
|
||||
cowboy_test:init_http3(Name, init_compress_opts(), Config).
|
||||
|
||||
end_per_group(Name, _) ->
|
||||
cowboy: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.
|
||||
|
||||
|
@ -99,15 +91,20 @@ crash_in_init(Config) ->
|
|||
%% Confirm terminate/3 is NOT called. We have no state to give to it.
|
||||
receive {Self, Pid, terminate, _, _, _} -> error(terminate) after 1000 -> ok end,
|
||||
%% Confirm early_error/5 is called in HTTP/1.1's case.
|
||||
%% HTTP/2 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
|
||||
http -> receive {Self, Pid, early_error, _, _, _, _, _} -> ok after 1000 -> error(timeout) end;
|
||||
http2 -> ok
|
||||
http2 -> ok;
|
||||
http3 -> ok
|
||||
end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
do_await_internal_error(ConnPid, Ref, Config) ->
|
||||
Protocol = config(protocol, Config),
|
||||
case {Protocol, gun:await(ConnPid, Ref)} of
|
||||
{http, {response, fin, 500, _}} -> ok;
|
||||
{http2, {error, {stream_error, {stream_error, internal_error, _}}}} -> ok;
|
||||
{http3, {error, {stream_error, {stream_error, h3_internal_error, _}}}} -> ok
|
||||
end.
|
||||
|
||||
crash_in_data(Config) ->
|
||||
|
@ -126,11 +123,7 @@ crash_in_data(Config) ->
|
|||
gun:data(ConnPid, Ref, fin, <<"Hello!">>),
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
end.
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
crash_in_info(Config) ->
|
||||
doc("Confirm an error is sent when a stream handler crashes in info/3."),
|
||||
|
@ -144,14 +137,14 @@ crash_in_info(Config) ->
|
|||
%% Confirm init/3 is called.
|
||||
Pid = receive {Self, P, init, _, _, _} -> P after 1000 -> error(timeout) end,
|
||||
%% Send a message to make the stream handler crash.
|
||||
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.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Receive a 500 error response.
|
||||
case gun:await(ConnPid, Ref) of
|
||||
{response, fin, 500, _} -> ok;
|
||||
{error, {stream_error, {stream_error, internal_error, _}}} -> ok
|
||||
end.
|
||||
do_await_internal_error(ConnPid, Ref, Config).
|
||||
|
||||
crash_in_terminate(Config) ->
|
||||
doc("Confirm the state is correct when a stream handler crashes in terminate/3."),
|
||||
|
@ -185,10 +178,12 @@ crash_in_terminate(Config) ->
|
|||
{ok, <<"Hello world!">>} = gun:await_body(ConnPid, Ref2),
|
||||
ok.
|
||||
|
||||
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
|
||||
crash_in_early_error(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_crash_in_early_error(Config);
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.");
|
||||
http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_crash_in_early_error(Config) ->
|
||||
|
@ -225,10 +220,12 @@ do_crash_in_early_error(Config) ->
|
|||
{response, fin, 500, _} = gun:await(ConnPid, Ref2),
|
||||
ok.
|
||||
|
||||
%% @todo The callbacks ARE used for HTTP/2 and HTTP/3 CONNECT/TRACE requests.
|
||||
crash_in_early_error_fatal(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_crash_in_early_error_fatal(Config);
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.")
|
||||
http2 -> doc("The callback early_error/5 is not currently used for HTTP/2.");
|
||||
http3 -> doc("The callback early_error/5 is not currently used for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_crash_in_early_error_fatal(Config) ->
|
||||
|
@ -262,7 +259,8 @@ early_error_stream_error_reason(Config) ->
|
|||
%% reason in both protocols.
|
||||
{Method, Headers, Status, Error} = case config(protocol, Config) of
|
||||
http -> {<<"GET">>, [{<<"host">>, <<"host:port">>}], 400, protocol_error};
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error}
|
||||
http2 -> {<<"TRACE">>, [], 501, no_error};
|
||||
http3 -> {<<"TRACE">>, [], 501, h3_no_error}
|
||||
end,
|
||||
Ref = gun:request(ConnPid, Method, "/long_polling", [
|
||||
{<<"accept-encoding">>, <<"gzip">>},
|
||||
|
@ -355,11 +353,20 @@ shutdown_on_socket_close(Config) ->
|
|||
Spawn ! {Self, ready},
|
||||
%% Close the socket.
|
||||
ok = gun:close(ConnPid),
|
||||
Protocol = config(protocol, Config),
|
||||
try
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% Confirm we receive a DOWN message for the child process.
|
||||
receive {'DOWN', MRef, process, Spawn, shutdown} -> ok after 1000 -> error(timeout) end,
|
||||
ok.
|
||||
ok
|
||||
catch error:timeout when Protocol =:= http3 ->
|
||||
%% @todo Figure out why this happens. Could be a timing issue
|
||||
%% or a legitimate bug. I suspect that the server just
|
||||
%% doesn't receive the GOAWAY frame from Gun because
|
||||
%% Gun is too quick to close the connection.
|
||||
shutdown_on_socket_close(Config)
|
||||
end.
|
||||
|
||||
shutdown_timeout_on_stream_stop(Config) ->
|
||||
doc("Confirm supervised processes are killed "
|
||||
|
@ -406,33 +413,45 @@ shutdown_timeout_on_socket_close(Config) ->
|
|||
Spawn ! {Self, ready},
|
||||
%% Close the socket.
|
||||
ok = gun:close(ConnPid),
|
||||
Protocol = config(protocol, Config),
|
||||
try
|
||||
%% Confirm terminate/3 is called, indicating the stream ended.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
%% We should NOT receive a DOWN message immediately.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> error(killed) after 1500 -> ok end,
|
||||
%% We should receive it now.
|
||||
receive {'DOWN', MRef, process, Spawn, killed} -> ok after 1000 -> error(timeout) end,
|
||||
ok.
|
||||
ok
|
||||
catch error:timeout when Protocol =:= http3 ->
|
||||
%% @todo Figure out why this happens. Could be a timing issue
|
||||
%% or a legitimate bug. I suspect that the server just
|
||||
%% doesn't receive the GOAWAY frame from Gun because
|
||||
%% Gun is too quick to close the connection.
|
||||
shutdown_timeout_on_socket_close(Config)
|
||||
end.
|
||||
|
||||
switch_protocol_after_headers(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_headers">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
switch_protocol_after_headers_data(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_headers_data">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
switch_protocol_after_response(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_switch_protocol_after_response(
|
||||
<<"switch_protocol_after_response">>, Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_switch_protocol_after_response(TestCase, Config) ->
|
||||
|
@ -502,7 +521,12 @@ terminate_on_stop(Config) ->
|
|||
{response, fin, 204, _} = gun:await(ConnPid, Ref),
|
||||
%% Confirm the stream is still alive even though we
|
||||
%% received the response fully, and tell it to stop.
|
||||
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,
|
||||
%% Confirm terminate/3 is called.
|
||||
receive {Self, Pid, terminate, _, _, _} -> ok after 1000 -> error(timeout) end,
|
||||
|
@ -511,7 +535,8 @@ terminate_on_stop(Config) ->
|
|||
terminate_on_switch_protocol(Config) ->
|
||||
case config(protocol, Config) of
|
||||
http -> do_terminate_on_switch_protocol(Config);
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.")
|
||||
http2 -> doc("The switch_protocol command is not currently supported for HTTP/2.");
|
||||
http3 -> doc("The switch_protocol command is not currently supported for HTTP/3.")
|
||||
end.
|
||||
|
||||
do_terminate_on_switch_protocol(Config) ->
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% 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
|
||||
%% init_per_suite/1, but this works just as well.
|
||||
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) ->
|
||||
cowboy_tracer_h:set_trace_patterns(),
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|
|
@ -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
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
|
|