aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LICENSE2
-rw-r--r--Makefile14
-rw-r--r--README.asciidoc4
-rw-r--r--doc/src/guide/book.asciidoc2
-rw-r--r--doc/src/guide/constraints.asciidoc5
-rw-r--r--doc/src/guide/getting_started.asciidoc2
-rw-r--r--doc/src/guide/introduction.asciidoc4
-rw-r--r--doc/src/guide/migrating_from_2.12.asciidoc115
-rw-r--r--doc/src/guide/req.asciidoc11
-rw-r--r--doc/src/manual/cowboy_http.asciidoc39
-rw-r--r--doc/src/manual/cowboy_http2.asciidoc39
-rw-r--r--doc/src/manual/cowboy_req.set_resp_headers.asciidoc15
-rw-r--r--doc/src/manual/cowboy_websocket.asciidoc25
-rw-r--r--ebin/cowboy.app4
-rw-r--r--erlang.mk4210
-rw-r--r--examples/file_server/Makefile2
-rw-r--r--examples/file_server/src/directory_h.erl2
-rw-r--r--rebar.config2
-rw-r--r--src/cowboy.erl55
-rw-r--r--src/cowboy_app.erl2
-rw-r--r--src/cowboy_bstr.erl2
-rw-r--r--src/cowboy_children.erl2
-rw-r--r--src/cowboy_clear.erl6
-rw-r--r--src/cowboy_clock.erl4
-rw-r--r--src/cowboy_compress_h.erl2
-rw-r--r--src/cowboy_constraints.erl2
-rw-r--r--src/cowboy_decompress_h.erl29
-rw-r--r--src/cowboy_dynamic_buffer.hrl80
-rw-r--r--src/cowboy_handler.erl2
-rw-r--r--src/cowboy_http.erl181
-rw-r--r--src/cowboy_http2.erl85
-rw-r--r--src/cowboy_http3.erl306
-rw-r--r--src/cowboy_loop.erl2
-rw-r--r--src/cowboy_metrics_h.erl2
-rw-r--r--src/cowboy_middleware.erl2
-rw-r--r--src/cowboy_quicer.erl64
-rw-r--r--src/cowboy_req.erl24
-rw-r--r--src/cowboy_rest.erl69
-rw-r--r--src/cowboy_router.erl2
-rw-r--r--src/cowboy_static.erl14
-rw-r--r--src/cowboy_stream.erl3
-rw-r--r--src/cowboy_stream_h.erl9
-rw-r--r--src/cowboy_sub_protocol.erl4
-rw-r--r--src/cowboy_sup.erl2
-rw-r--r--src/cowboy_tls.erl8
-rw-r--r--src/cowboy_tracer_h.erl2
-rw-r--r--src/cowboy_websocket.erl60
-rw-r--r--src/cowboy_webtransport.erl292
-rw-r--r--test/compress_SUITE.erl2
-rw-r--r--test/cowboy_ct_hook.erl2
-rw-r--r--test/cowboy_test.erl45
-rw-r--r--test/decompress_SUITE.erl4
-rw-r--r--test/draft_h3_webtransport_SUITE.erl814
-rw-r--r--test/examples_SUITE.erl28
-rw-r--r--test/h2spec_SUITE.erl2
-rw-r--r--test/handlers/content_types_provided_h.erl5
-rw-r--r--test/handlers/crash_h.erl3
-rw-r--r--test/handlers/read_body_h.erl15
-rw-r--r--test/handlers/resp_h.erl15
-rw-r--r--test/handlers/stream_hello_h.erl15
-rw-r--r--test/handlers/ws_ignore.erl20
-rw-r--r--test/handlers/ws_set_options_commands_h.erl19
-rw-r--r--test/handlers/wt_echo_h.erl103
-rw-r--r--test/http2_SUITE.erl25
-rw-r--r--test/http_SUITE.erl124
-rw-r--r--test/http_perf_SUITE.erl220
-rw-r--r--test/loop_handler_SUITE.erl2
-rw-r--r--test/metrics_SUITE.erl2
-rw-r--r--test/misc_SUITE.erl2
-rw-r--r--test/plain_handler_SUITE.erl13
-rw-r--r--test/proxy_header_SUITE.erl2
-rw-r--r--test/req_SUITE.erl15
-rw-r--r--test/rest_handler_SUITE.erl15
-rw-r--r--test/rfc6585_SUITE.erl2
-rw-r--r--test/rfc7230_SUITE.erl2
-rw-r--r--test/rfc7231_SUITE.erl2
-rw-r--r--test/rfc7538_SUITE.erl2
-rw-r--r--test/rfc7540_SUITE.erl25
-rw-r--r--test/rfc8297_SUITE.erl2
-rw-r--r--test/rfc8441_SUITE.erl2
-rw-r--r--test/rfc9114_SUITE.erl2
-rw-r--r--test/rfc9204_SUITE.erl2
-rw-r--r--test/rfc9220_SUITE.erl4
-rw-r--r--test/security_SUITE.erl2
-rw-r--r--test/static_handler_SUITE.erl4
-rw-r--r--test/stream_handler_SUITE.erl2
-rw-r--r--test/sys_SUITE.erl2
-rw-r--r--test/tracer_SUITE.erl2
-rw-r--r--test/ws_SUITE.erl36
-rw-r--r--test/ws_SUITE_data/ws_max_frame_size.erl2
-rw-r--r--test/ws_autobahn_SUITE.erl2
-rw-r--r--test/ws_handler_SUITE.erl37
-rw-r--r--test/ws_perf_SUITE.erl178
93 files changed, 3564 insertions, 4074 deletions
diff --git a/LICENSE b/LICENSE
index efeaf45..9c4406c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+Copyright (c) 2011-2025, Loïc Hoguin <[email protected]>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
diff --git a/Makefile b/Makefile
index 7cf363c..1ac7f2c 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
PROJECT = cowboy
PROJECT_DESCRIPTION = Small, fast, modern HTTP server.
-PROJECT_VERSION = 2.12.0
+PROJECT_VERSION = 2.13.0
PROJECT_REGISTERED = cowboy_clock
# Options.
@@ -17,7 +17,7 @@ LOCAL_DEPS = crypto
DEPS = cowlib ranch
dep_cowlib = git https://github.com/ninenines/cowlib master
-dep_ranch = git https://github.com/ninenines/ranch 1.8.x
+dep_ranch = git https://github.com/ninenines/ranch 1.8.1
ifeq ($(COWBOY_QUICER),1)
DEPS += quicer
@@ -44,14 +44,17 @@ define HEX_TARBALL_EXTRA_METADATA
#{
licenses => [<<"ISC">>],
links => #{
- <<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.12/guide/">>,
- <<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.12/manual/">>,
+ <<"User guide">> => <<"https://ninenines.eu/docs/en/cowboy/2.13/guide/">>,
+ <<"Function reference">> => <<"https://ninenines.eu/docs/en/cowboy/2.13/manual/">>,
<<"GitHub">> => <<"https://github.com/ninenines/cowboy">>,
<<"Sponsor">> => <<"https://github.com/sponsors/essen">>
}
}
endef
+hex_req_ranch = >= 1.8.0 and < 3.0.0
+hex_req_cowlib = >= 2.14.0 and < 3.0.0
+
# Standard targets.
include erlang.mk
@@ -59,7 +62,7 @@ include erlang.mk
# Don't run the examples/autobahn test suites by default.
ifndef FULL
-CT_SUITES := $(filter-out examples ws_autobahn,$(CT_SUITES))
+CT_SUITES := $(filter-out examples http_perf ws_autobahn ws_perf,$(CT_SUITES))
endif
# Don't run HTTP/3 test suites on Windows.
@@ -135,6 +138,7 @@ prepare_tag:
$(verbose) echo "Dependencies:"
$(verbose) grep ^DEPS Makefile || echo "DEPS ="
$(verbose) grep ^dep_ Makefile || true
+ $(verbose) grep ^hex_req_ Makefile || true
$(verbose) echo
$(verbose) echo "rebar.config:"
$(verbose) cat rebar.config || true
diff --git a/README.asciidoc b/README.asciidoc
index afd1fdd..5721a7b 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -18,8 +18,8 @@ Cowboy is *clean* and *well tested* Erlang code.
== Online documentation
-* https://ninenines.eu/docs/en/cowboy/2.12/guide[User guide]
-* https://ninenines.eu/docs/en/cowboy/2.12/manual[Function reference]
+* https://ninenines.eu/docs/en/cowboy/2.13/guide[User guide]
+* https://ninenines.eu/docs/en/cowboy/2.13/manual[Function reference]
== Offline documentation
diff --git a/doc/src/guide/book.asciidoc b/doc/src/guide/book.asciidoc
index cf8c943..58eda34 100644
--- a/doc/src/guide/book.asciidoc
+++ b/doc/src/guide/book.asciidoc
@@ -75,6 +75,8 @@ include::performance.asciidoc[Performance]
= Additional information
+include::migrating_from_2.12.asciidoc[Migrating from Cowboy 2.12 to 2.13]
+
include::migrating_from_2.11.asciidoc[Migrating from Cowboy 2.11 to 2.12]
include::migrating_from_2.10.asciidoc[Migrating from Cowboy 2.10 to 2.11]
diff --git a/doc/src/guide/constraints.asciidoc b/doc/src/guide/constraints.asciidoc
index 6cc1075..4eade8a 100644
--- a/doc/src/guide/constraints.asciidoc
+++ b/doc/src/guide/constraints.asciidoc
@@ -91,6 +91,11 @@ int(forward, Value) ->
The value must be returned even if it is not converted
by the constraint.
+The two other operations are currently experimental. They are
+meant to help implement HATEOAS type services, but proper
+support for HATEOAS is not expected to be available before
+Cowboy 3.0 because of Cowboy's current router's limitations.
+
The `reverse` operation does the opposite: it
takes a converted value and changes it back to what the
user input would have been.
diff --git a/doc/src/guide/getting_started.asciidoc b/doc/src/guide/getting_started.asciidoc
index a26802d..06677ee 100644
--- a/doc/src/guide/getting_started.asciidoc
+++ b/doc/src/guide/getting_started.asciidoc
@@ -69,7 +69,7 @@ fetch and compile Cowboy, and that we will use releases:
PROJECT = hello_erlang
DEPS = cowboy
-dep_cowboy_commit = 2.11.0
+dep_cowboy_commit = 2.13.0
REL_DEPS = relx
diff --git a/doc/src/guide/introduction.asciidoc b/doc/src/guide/introduction.asciidoc
index 519608d..3a03a78 100644
--- a/doc/src/guide/introduction.asciidoc
+++ b/doc/src/guide/introduction.asciidoc
@@ -35,14 +35,14 @@ guarantee that the experience will be safe and smooth. You are advised
to perform the necessary testing and security audits prior to deploying
on other platforms.
-Cowboy is developed for Erlang/OTP 22.0 and newer.
+Cowboy is developed for Erlang/OTP 24.0 and newer.
=== License
Cowboy uses the ISC License.
----
-Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+Copyright (c) 2011-2025, Loïc Hoguin <[email protected]>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
diff --git a/doc/src/guide/migrating_from_2.12.asciidoc b/doc/src/guide/migrating_from_2.12.asciidoc
new file mode 100644
index 0000000..d058db3
--- /dev/null
+++ b/doc/src/guide/migrating_from_2.12.asciidoc
@@ -0,0 +1,115 @@
+[appendix]
+== Migrating from Cowboy 2.12 to 2.13
+
+Cowboy 2.13 focuses on improving the performance of
+Websocket, as well as the HTTP protocols. It also
+contains a variety of new features and bug fixes.
+In addition, Cowboy 2.13 is the first Cowboy version
+that contains the experimental HTTP/3 support.
+
+Cowboy 2.13 requires Erlang/OTP 24.0 or greater.
+
+=== Features added
+
+* The option `dynamic_buffer` has been added. When
+ enabled, Cowboy will dynamically change the
+ `buffer` socket option based on how much data
+ it receives. It will start at 1024 bytes and
+ go up to 131072 bytes by default. This applies
+ to HTTP/1.1, HTTP/2 and Websocket. The performance
+ gains are very important depending on the scenario.
+
+* HTTP/1.1 and HTTP/2 now accept the `hibernate`
+ option. When set the connection process will
+ automatically hibernate to reduce memory usage
+ at a small performance cost.
+
+* The `protocols` and `alpn_default_protocol` protocol
+ options have been added to control exactly which
+ HTTP protocols are allowed over clear and TLS listeners.
+
+* The Websocket `max_frame_size` option can now be
+ set dynamically via the `set_options` command.
+ This allows configuring a smaller max size and
+ increase it after authentication or other checks.
+
+* `cowboy_req:set_resp_headers` now accept lists of
+ headers. This can be used to simplify passing
+ headers coming from client applications such as
+ Gun. Note that the set-cookie header cannot be
+ provided using this function.
+
+* `cowboy_rest` now always sets the allow header.
+
+* Update Ranch to 1.8.1.
+
+* Update Cowlib to 2.14.0.
+
+* When using Hex.pm, version check requirements will
+ now be relaxed. Cowboy will accept any Ranch version
+ from 1.8.0 to 2.2.0 as well as future 2.x versions.
+ Similarly, any Cowlib 2.x version from 2.14.0 will
+ be accepted.
+
+=== Experimental features added
+
+* Experimental support for HTTP/3 has been added,
+ including Websocket over HTTP/3. HTTP/3 support
+ is disabled by default; to enable, the environment
+ variable COWBOY_QUICER must be set at compile-time.
+
+=== Features deprecated
+
+* The `inactivity_timeout` option is now deprecated
+ for all protocols. It is de facto ignored when
+ `hibernate` is enabled.
+
+=== Optimisation-related changes
+
+* The behavior of the `idle_timeout` timer has been
+ changed for HTTP/2 and Websocket. Cowboy used to
+ reset the timer on every data packet received from
+ the socket. Now Cowboy will check periodically
+ whether new data was received in the interval.
+
+* URI and query string hex encoding and decoding has
+ been optimised.
+
+* Websocket UTF-8 validation of text frames has been
+ optimised.
+
+* Websocket unmasking has been optimised.
+
+=== Bugs fixed
+
+* HTTP/1.1 upgrade to HTTP/2 is now disabled over TLS,
+ as HTTP/2 over TLS must be negotiated via ALPN.
+
+* `cowboy_req:filter_cookies` could miss valid cookies.
+ It has been corrected.
+
+* HTTP/1.1 could get to a state where it would stop
+ receiving data from the socket, or buffer the data
+ without processing it, and the connection eventually
+ time out. This has been fixed.
+
+* Websocket did not compress zero-length frames properly.
+ This resulted in decompression errors in the client.
+ This has been corrected.
+
+* Websocket compression will now be disabled when only
+ the server sets `client_max_window_bits`, as otherwise
+ decompression errors will occur.
+
+* Websocket will now apply `max_frame_size` both to
+ compressed frames as well as the uncompressed payload.
+ Cowboy will stop decompressing when the limit is
+ reached.
+
+* Cowboy now properly handles exits of request processes
+ that occurred externally (e.g. via `exit/2`).
+
+* Invalid return values from `content_types_provided`
+ could result in an atom sent to the socket, leading
+ to a cryptic error message. The invalid value will
+ now result in a better error message.
diff --git a/doc/src/guide/req.asciidoc b/doc/src/guide/req.asciidoc
index 754e470..6b95228 100644
--- a/doc/src/guide/req.asciidoc
+++ b/doc/src/guide/req.asciidoc
@@ -258,7 +258,8 @@ contain two parameters of name `key`.
The same is true when trying to use the PHP-style suffix `[]`.
When a query string is `key[]=1&key[]=2`, the list returned will
-contain two parameters of name `key[]`.
+contain two parameters of name `key[]`. Cowboy does not require
+the `[]` suffix to properly handle repeated key names.
When a query string is simply `key`, Cowboy will return the
list `[{<<"key">>, true}]`, using `true` to indicate that the
@@ -291,9 +292,11 @@ If no default is provided and the value is missing, the
query string is deemed invalid and the process will crash.
When the query string is `key=1&key=2`, the value for `key`
-will be the list `[1, 2]`. Parameter names do not need to
-include the PHP-style suffix. Constraints may be used to
-ensure that only one value was passed through.
+will be the list `[<<"1">>, <<"2">>]`. Parameter names do not
+need to include the PHP-style suffix. Constraints may be used
+to ensure that only one value was given. Constraints do not
+automatically look inside the list, a custom constraint must
+be written if that is necessary.
=== Headers
diff --git a/doc/src/manual/cowboy_http.asciidoc b/doc/src/manual/cowboy_http.asciidoc
index 58f0435..96a5585 100644
--- a/doc/src/manual/cowboy_http.asciidoc
+++ b/doc/src/manual/cowboy_http.asciidoc
@@ -18,8 +18,11 @@ as a Ranch protocol.
----
opts() :: #{
active_n => pos_integer(),
+ alpn_default_protocol => http | http2,
chunked => boolean(),
connection_type => worker | supervisor,
+ dynamic_buffer => false | {pos_integer(), pos_integer()},
+ hibernate => boolean(),
http10_keepalive => boolean(),
idle_timeout => timeout(),
inactivity_timeout => timeout(),
@@ -34,6 +37,7 @@ opts() :: #{
max_method_length => non_neg_integer(),
max_request_line_length => non_neg_integer(),
max_skip_body_length => non_neg_integer(),
+ protocols => [http | http2],
proxy_header => boolean(),
request_timeout => timeout(),
reset_idle_timeout_on_send => boolean(),
@@ -53,7 +57,7 @@ Ranch functions `ranch:get_protocol_options/1` and
The default value is given next to the option name:
-active_n (100)::
+active_n (1)::
The number of packets Cowboy will request from the socket at once.
This can be used to tweak the performance of the server. Higher
@@ -61,6 +65,12 @@ values reduce the number of times Cowboy need to request more
packets from the port driver at the expense of potentially
higher memory being used.
+alpn_default_protocol (http)::
+
+Default protocol to use when the client connects over TLS
+without ALPN. Can be set to `http2` to disable HTTP/1.1
+entirely.
+
chunked (true)::
Whether chunked transfer-encoding is enabled for HTTP/1.1 connections.
@@ -75,6 +85,21 @@ connection_type (supervisor)::
Whether the connection process also acts as a supervisor.
+dynamic_buffer ({1024, 131072})::
+
+Cowboy will dynamically change the socket's `buffer` size
+depending on the size of the data it receives from the socket.
+This lets Cowboy use the optimal buffer size for the current
+workload.
++
+The dynamic buffer size functionality can be disabled by
+setting this option to `false`. Cowboy will also disable
+it by default when the `buffer` transport option is configured.
+
+hibernate (false)::
+
+Whether the connection process will hibernate automatically.
+
http10_keepalive (true)::
Whether keep-alive is enabled for HTTP/1.0 connections.
@@ -88,7 +113,7 @@ This option can be updated at any time using the
inactivity_timeout (300000)::
-Time in ms with nothing received at all before Cowboy closes the connection.
+**DEPRECATED** Time in ms with nothing received at all before Cowboy closes the connection.
initial_stream_flow_size (65535)::
@@ -139,6 +164,13 @@ max_skip_body_length (1000000)::
Maximum length Cowboy is willing to skip when the user code did not read the body fully.
When the remaining length is too large or unknown Cowboy will close the connection.
+protocols ([http2, http])::
+
+Protocols that may be used when the client connects over
+cleartext TCP. The default is to allow both HTTP/1.1 and
+HTTP/2. HTTP/1.1 and HTTP/2 can be disabled entirely by
+omitting them from the list.
+
proxy_header (false)::
Whether incoming connections have a PROXY protocol header. The
@@ -166,6 +198,9 @@ Ordered list of stream handlers that will handle all stream events.
== Changelog
+* *2.13*: The `inactivity_timeout` option was deprecated.
+* *2.13*: The `active_n` default value was changed to `1`.
+* *2.13*: The `dynamic_buffer` and `hibernate` options were added.
* *2.11*: The `reset_idle_timeout_on_send` option was added.
* *2.8*: The `active_n` option was added.
* *2.7*: The `initial_stream_flow_size` and `logger` options were added.
diff --git a/doc/src/manual/cowboy_http2.asciidoc b/doc/src/manual/cowboy_http2.asciidoc
index 1d2619c..7b34b88 100644
--- a/doc/src/manual/cowboy_http2.asciidoc
+++ b/doc/src/manual/cowboy_http2.asciidoc
@@ -18,12 +18,15 @@ as a Ranch protocol.
----
opts() :: #{
active_n => pos_integer(),
+ alpn_default_protocol => http | http2,
connection_type => worker | supervisor,
connection_window_margin_size => 0..16#7fffffff,
connection_window_update_threshold => 0..16#7fffffff,
+ dynamic_buffer => false | {pos_integer(), pos_integer()},
enable_connect_protocol => boolean(),
goaway_initial_timeout => timeout(),
goaway_complete_timeout => timeout(),
+ hibernate => boolean(),
idle_timeout => timeout(),
inactivity_timeout => timeout(),
initial_connection_window_size => 65535..16#7fffffff,
@@ -44,6 +47,7 @@ opts() :: #{
max_stream_buffer_size => non_neg_integer(),
max_stream_window_size => 0..16#7fffffff,
preface_timeout => timeout(),
+ protocols => [http | http2],
proxy_header => boolean(),
reset_idle_timeout_on_send => boolean(),
sendfile => boolean(),
@@ -66,7 +70,7 @@ Ranch functions `ranch:get_protocol_options/1` and
The default value is given next to the option name:
-active_n (100)::
+active_n (1)::
The number of packets Cowboy will request from the socket at once.
This can be used to tweak the performance of the server. Higher
@@ -74,6 +78,12 @@ values reduce the number of times Cowboy need to request more
packets from the port driver at the expense of potentially
higher memory being used.
+alpn_default_protocol (http)::
+
+Default protocol to use when the client connects over TLS
+without ALPN. Can be set to `http2` to disable HTTP/1.1
+entirely.
+
connection_type (supervisor)::
Whether the connection process also acts as a supervisor.
@@ -91,6 +101,17 @@ The connection window will only get updated when its size
becomes lower than this threshold, in bytes. This is to
avoid sending too many `WINDOW_UPDATE` frames.
+dynamic_buffer ({1024, 131072})::
+
+Cowboy will dynamically change the socket's `buffer` size
+depending on the size of the data it receives from the socket.
+This lets Cowboy use the optimal buffer size for the current
+workload.
++
+The dynamic buffer size functionality can be disabled by
+setting this option to `false`. Cowboy will also disable
+it by default when the `buffer` transport option is configured.
+
enable_connect_protocol (false)::
Whether to enable the extended CONNECT method to allow
@@ -110,13 +131,17 @@ goaway_complete_timeout (3000)::
Time in ms to wait for ongoing streams to complete before closing the connection
during a graceful shutdown.
+hibernate (false)::
+
+Whether the connection process will hibernate automatically.
+
idle_timeout (60000)::
Time in ms with no data received before Cowboy closes the connection.
inactivity_timeout (300000)::
-Time in ms with nothing received at all before Cowboy closes the connection.
+**DEPRECATED** Time in ms with nothing received at all before Cowboy closes the connection.
initial_connection_window_size (65535)::
@@ -242,6 +267,13 @@ preface_timeout (5000)::
Time in ms Cowboy is willing to wait for the connection preface.
+protocols ([http2, http])::
+
+Protocols that may be used when the client connects over
+cleartext TCP. The default is to allow both HTTP/1.1 and
+HTTP/2. HTTP/1.1 and HTTP/2 can be disabled entirely by
+omitting them from the list.
+
proxy_header (false)::
Whether incoming connections have a PROXY protocol header. The
@@ -289,6 +321,9 @@ too many `WINDOW_UPDATE` frames.
== Changelog
+* *2.13*: The `inactivity_timeout` option was deprecated.
+* *2.13*: The `active_n` default value was changed to `1`.
+* *2.13*: The `dynamic_buffer` and `hibernate` options were added.
* *2.11*: Websocket over HTTP/2 is now considered stable.
* *2.11*: The `reset_idle_timeout_on_send` option was added.
* *2.11*: Add the option `max_cancel_stream_rate` to protect
diff --git a/doc/src/manual/cowboy_req.set_resp_headers.asciidoc b/doc/src/manual/cowboy_req.set_resp_headers.asciidoc
index 63fe424..9ca5901 100644
--- a/doc/src/manual/cowboy_req.set_resp_headers.asciidoc
+++ b/doc/src/manual/cowboy_req.set_resp_headers.asciidoc
@@ -11,7 +11,7 @@ cowboy_req:set_resp_headers - Set several response headers
set_resp_headers(Headers, Req :: cowboy_req:req())
-> Req
-Headers :: cowboy:http_headers()
+Headers :: cowboy:http_headers() | [{binary(), iodata()}]
----
Set several headers to be sent with the response.
@@ -32,8 +32,16 @@ instead of this function to set cookies.
Headers::
-Headers as a map with keys being lowercase binary strings,
-and values as binary strings.
+Headers as a map with names being lowercase binary strings,
+and values as iodata; or as a list with the same requirements
+for names and values.
++
+When a list is given it is converted to its equivalent map,
+with duplicate headers concatenated with a comma inserted
+in-between. Support for lists is meant to simplify using
+data from clients or other applications.
++
+The set-cookie header must not be set using this function.
Req::
@@ -48,6 +56,7 @@ otherwise the headers will not be sent in the response.
== Changelog
+* *2.13*: The function now accepts a list of headers.
* *2.0*: Function introduced.
== Examples
diff --git a/doc/src/manual/cowboy_websocket.asciidoc b/doc/src/manual/cowboy_websocket.asciidoc
index 6d822d9..319dbae 100644
--- a/doc/src/manual/cowboy_websocket.asciidoc
+++ b/doc/src/manual/cowboy_websocket.asciidoc
@@ -138,7 +138,9 @@ commands() :: [Command]
Command :: {active, boolean()}
| {deflate, boolean()}
- | {set_options, #{idle_timeout => timeout()}}
+ | {set_options, #{
+ idle_timeout => timeout(),
+ max_frame_size => non_neg_integer() | infinity}}
| {shutdown_reason, any()}
| Frame :: cow_ws:frame()
----
@@ -159,8 +161,8 @@ effect on connections that did not negotiate compression.
set_options::
-Set Websocket options. Currently only the option `idle_timeout`
-may be updated from a Websocket handler.
+Set Websocket options. Currently only the options `idle_timeout`
+and `max_frame_size` may be updated from a Websocket handler.
shutdown_reason::
@@ -201,6 +203,7 @@ opts() :: #{
active_n => pos_integer(),
compress => boolean(),
deflate_opts => cow_ws:deflate_opts()
+ dynamic_buffer => false | {pos_integer(), pos_integer()},
idle_timeout => timeout(),
max_frame_size => non_neg_integer() | infinity,
req_filter => fun((cowboy_req:req()) -> map()),
@@ -222,7 +225,7 @@ init(Req, State) ->
The default value is given next to the option name:
-active_n (100)::
+active_n (1)::
The number of packets Cowboy will request from the socket at once.
This can be used to tweak the performance of the server. Higher
@@ -246,6 +249,17 @@ options and the zlib compression options. The
defaults optimize the compression at the expense
of some memory and CPU.
+dynamic_buffer ({1024, 131072})::
+
+Cowboy will dynamically change the socket's `buffer` size
+depending on the size of the data it receives from the socket.
+This lets Cowboy use the optimal buffer size for the current
+workload.
++
+The dynamic buffer size functionality can be disabled by
+setting this option to `false`. Cowboy will also disable
+it by default when the `buffer` transport option is configured.
+
idle_timeout (60000)::
Time in milliseconds that Cowboy will keep the
@@ -285,6 +299,9 @@ normal circumstances if necessary.
== Changelog
+* *2.13*: The `active_n` default value was changed to `1`.
+* *2.13*: The `dynamic_buffer` option was added.
+* *2.13*: The `max_frame_size` option can now be set dynamically.
* *2.11*: Websocket over HTTP/2 is now considered stable.
* *2.11*: HTTP/1.1 Websocket no longer traps exits by default.
* *2.8*: The `active_n` option was added.
diff --git a/ebin/cowboy.app b/ebin/cowboy.app
index b5932d9..39be200 100644
--- a/ebin/cowboy.app
+++ b/ebin/cowboy.app
@@ -1,7 +1,7 @@
{application, 'cowboy', [
{description, "Small, fast, modern HTTP server."},
- {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']},
+ {vsn, "2.13.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','cowboy_webtransport']},
{registered, [cowboy_sup,cowboy_clock]},
{applications, [kernel,stdlib,crypto,cowlib,ranch]},
{optional_applications, []},
diff --git a/erlang.mk b/erlang.mk
index 8d0bb5d..600e15f 100644
--- a/erlang.mk
+++ b/erlang.mk
@@ -17,7 +17,7 @@
ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST)))
export ERLANG_MK_FILENAME
-ERLANG_MK_VERSION = d3485e7
+ERLANG_MK_VERSION = 2022.05.31-116-g0206e84-dirty
ERLANG_MK_WITHOUT =
# Make 3.81 and 3.82 are deprecated.
@@ -47,7 +47,7 @@ verbose_0 = @
verbose_2 = set -x;
verbose = $(verbose_$(V))
-ifeq ($(V),3)
+ifeq ($V,3)
SHELL := $(SHELL) -x
endif
@@ -66,7 +66,7 @@ export ERLANG_MK_TMP
# "erl" command.
-ERL = erl +A1 -noinput -boot no_dot_erlang
+ERL = erl -noinput -boot no_dot_erlang -kernel start_distribution false +P 1024 +Q 1024
# Platform detection.
@@ -162,7 +162,7 @@ define newline
endef
define comma_list
-$(subst $(space),$(comma),$(strip $(1)))
+$(subst $(space),$(comma),$(strip $1))
endef
define escape_dquotes
@@ -180,23 +180,23 @@ else
core_native_path = $1
endif
-core_http_get = curl -Lf$(if $(filter-out 0,$(V)),,s)o $(call core_native_path,$1) $2
+core_http_get = curl -Lf$(if $(filter-out 0,$V),,s)o $(call core_native_path,$1) $2
-core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1)))
+core_eq = $(and $(findstring $1,$2),$(findstring $2,$1))
# We skip files that contain spaces because they end up causing issues.
# Files that begin with a dot are already ignored by the wildcard function.
core_find = $(foreach f,$(wildcard $(1:%/=%)/*),$(if $(wildcard $f/.),$(call core_find,$f,$2),$(if $(filter $(subst *,%,$2),$f),$(if $(wildcard $f),$f))))
-core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1)))))))))))))))))))))))))))
+core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1))))))))))))))))))))))))))
-core_ls = $(filter-out $(1),$(shell echo $(1)))
+core_ls = $(filter-out $1,$(shell echo $1))
# @todo Use a solution that does not require using perl.
core_relpath = $(shell perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $1 $2)
define core_render
- printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2)
+ printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $1)))))\n' > $2
endef
# Automated update.
@@ -246,10 +246,10 @@ KERL_MAKEFLAGS ?=
OTP_GIT ?= https://github.com/erlang/otp
define kerl_otp_target
-$(KERL_INSTALL_DIR)/$(1): $(KERL)
+$(KERL_INSTALL_DIR)/$1: $(KERL)
$(verbose) if [ ! -d $$@ ]; then \
- MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1); \
- $(KERL) install $(1) $(KERL_INSTALL_DIR)/$(1); \
+ MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1; \
+ $(KERL) install $1 $(KERL_INSTALL_DIR)/$1; \
fi
endef
@@ -291,54 +291,6 @@ endif
endif
-PACKAGES += aberth
-pkg_aberth_name = aberth
-pkg_aberth_description = Generic BERT-RPC server in Erlang
-pkg_aberth_homepage = https://github.com/a13x/aberth
-pkg_aberth_fetch = git
-pkg_aberth_repo = https://github.com/a13x/aberth
-pkg_aberth_commit = master
-
-PACKAGES += active
-pkg_active_name = active
-pkg_active_description = Active development for Erlang: rebuild and reload source/binary files while the VM is running
-pkg_active_homepage = https://github.com/proger/active
-pkg_active_fetch = git
-pkg_active_repo = https://github.com/proger/active
-pkg_active_commit = master
-
-PACKAGES += aleppo
-pkg_aleppo_name = aleppo
-pkg_aleppo_description = Alternative Erlang Pre-Processor
-pkg_aleppo_homepage = https://github.com/ErlyORM/aleppo
-pkg_aleppo_fetch = git
-pkg_aleppo_repo = https://github.com/ErlyORM/aleppo
-pkg_aleppo_commit = master
-
-PACKAGES += alog
-pkg_alog_name = alog
-pkg_alog_description = Simply the best logging framework for Erlang
-pkg_alog_homepage = https://github.com/siberian-fast-food/alogger
-pkg_alog_fetch = git
-pkg_alog_repo = https://github.com/siberian-fast-food/alogger
-pkg_alog_commit = master
-
-PACKAGES += annotations
-pkg_annotations_name = annotations
-pkg_annotations_description = Simple code instrumentation utilities
-pkg_annotations_homepage = https://github.com/hyperthunk/annotations
-pkg_annotations_fetch = git
-pkg_annotations_repo = https://github.com/hyperthunk/annotations
-pkg_annotations_commit = master
-
-PACKAGES += apns
-pkg_apns_name = apns
-pkg_apns_description = Apple Push Notification Server for Erlang
-pkg_apns_homepage = http://inaka.github.com/apns4erl
-pkg_apns_fetch = git
-pkg_apns_repo = https://github.com/inaka/apns4erl
-pkg_apns_commit = master
-
PACKAGES += asciideck
pkg_asciideck_name = asciideck
pkg_asciideck_description = Asciidoc for Erlang.
@@ -347,421 +299,13 @@ pkg_asciideck_fetch = git
pkg_asciideck_repo = https://github.com/ninenines/asciideck
pkg_asciideck_commit = master
-PACKAGES += backoff
-pkg_backoff_name = backoff
-pkg_backoff_description = Simple exponential backoffs in Erlang
-pkg_backoff_homepage = https://github.com/ferd/backoff
-pkg_backoff_fetch = git
-pkg_backoff_repo = https://github.com/ferd/backoff
-pkg_backoff_commit = master
-
-PACKAGES += barrel_tcp
-pkg_barrel_tcp_name = barrel_tcp
-pkg_barrel_tcp_description = barrel is a generic TCP acceptor pool with low latency in Erlang.
-pkg_barrel_tcp_homepage = https://github.com/benoitc-attic/barrel_tcp
-pkg_barrel_tcp_fetch = git
-pkg_barrel_tcp_repo = https://github.com/benoitc-attic/barrel_tcp
-pkg_barrel_tcp_commit = master
-
-PACKAGES += basho_bench
-pkg_basho_bench_name = basho_bench
-pkg_basho_bench_description = A load-generation and testing tool for basically whatever you can write a returning Erlang function for.
-pkg_basho_bench_homepage = https://github.com/basho/basho_bench
-pkg_basho_bench_fetch = git
-pkg_basho_bench_repo = https://github.com/basho/basho_bench
-pkg_basho_bench_commit = master
-
-PACKAGES += bcrypt
-pkg_bcrypt_name = bcrypt
-pkg_bcrypt_description = Bcrypt Erlang / C library
-pkg_bcrypt_homepage = https://github.com/erlangpack/bcrypt
-pkg_bcrypt_fetch = git
-pkg_bcrypt_repo = https://github.com/erlangpack/bcrypt.git
-pkg_bcrypt_commit = master
-
-PACKAGES += beam
-pkg_beam_name = beam
-pkg_beam_description = BEAM emulator written in Erlang
-pkg_beam_homepage = https://github.com/tonyrog/beam
-pkg_beam_fetch = git
-pkg_beam_repo = https://github.com/tonyrog/beam
-pkg_beam_commit = master
-
-PACKAGES += bear
-pkg_bear_name = bear
-pkg_bear_description = a set of statistics functions for erlang
-pkg_bear_homepage = https://github.com/boundary/bear
-pkg_bear_fetch = git
-pkg_bear_repo = https://github.com/boundary/bear
-pkg_bear_commit = master
-
-PACKAGES += bertconf
-pkg_bertconf_name = bertconf
-pkg_bertconf_description = Make ETS tables out of statc BERT files that are auto-reloaded
-pkg_bertconf_homepage = https://github.com/ferd/bertconf
-pkg_bertconf_fetch = git
-pkg_bertconf_repo = https://github.com/ferd/bertconf
-pkg_bertconf_commit = master
-
-PACKAGES += bifrost
-pkg_bifrost_name = bifrost
-pkg_bifrost_description = Erlang FTP Server Framework
-pkg_bifrost_homepage = https://github.com/thorstadt/bifrost
-pkg_bifrost_fetch = git
-pkg_bifrost_repo = https://github.com/thorstadt/bifrost
-pkg_bifrost_commit = master
-
-PACKAGES += binpp
-pkg_binpp_name = binpp
-pkg_binpp_description = Erlang Binary Pretty Printer
-pkg_binpp_homepage = https://github.com/jtendo/binpp
-pkg_binpp_fetch = git
-pkg_binpp_repo = https://github.com/jtendo/binpp
-pkg_binpp_commit = master
-
-PACKAGES += bisect
-pkg_bisect_name = bisect
-pkg_bisect_description = Ordered fixed-size binary dictionary in Erlang
-pkg_bisect_homepage = https://github.com/knutin/bisect
-pkg_bisect_fetch = git
-pkg_bisect_repo = https://github.com/knutin/bisect
-pkg_bisect_commit = master
-
-PACKAGES += bitcask
-pkg_bitcask_name = bitcask
-pkg_bitcask_description = because you need another a key/value storage engine
-pkg_bitcask_homepage = https://github.com/basho/bitcask
-pkg_bitcask_fetch = git
-pkg_bitcask_repo = https://github.com/basho/bitcask
-pkg_bitcask_commit = develop
-
-PACKAGES += bootstrap
-pkg_bootstrap_name = bootstrap
-pkg_bootstrap_description = A simple, yet powerful Erlang cluster bootstrapping application.
-pkg_bootstrap_homepage = https://github.com/schlagert/bootstrap
-pkg_bootstrap_fetch = git
-pkg_bootstrap_repo = https://github.com/schlagert/bootstrap
-pkg_bootstrap_commit = master
-
-PACKAGES += boss
-pkg_boss_name = boss
-pkg_boss_description = Erlang web MVC, now featuring Comet
-pkg_boss_homepage = https://github.com/ChicagoBoss/ChicagoBoss
-pkg_boss_fetch = git
-pkg_boss_repo = https://github.com/ChicagoBoss/ChicagoBoss
-pkg_boss_commit = master
-
-PACKAGES += boss_db
-pkg_boss_db_name = boss_db
-pkg_boss_db_description = BossDB: a sharded, caching, pooling, evented ORM for Erlang
-pkg_boss_db_homepage = https://github.com/ErlyORM/boss_db
-pkg_boss_db_fetch = git
-pkg_boss_db_repo = https://github.com/ErlyORM/boss_db
-pkg_boss_db_commit = master
-
-PACKAGES += brod
-pkg_brod_name = brod
-pkg_brod_description = Kafka client in Erlang
-pkg_brod_homepage = https://github.com/klarna/brod
-pkg_brod_fetch = git
-pkg_brod_repo = https://github.com/klarna/brod.git
-pkg_brod_commit = master
-
-PACKAGES += bson
-pkg_bson_name = bson
-pkg_bson_description = BSON documents in Erlang, see bsonspec.org
-pkg_bson_homepage = https://github.com/comtihon/bson-erlang
-pkg_bson_fetch = git
-pkg_bson_repo = https://github.com/comtihon/bson-erlang
-pkg_bson_commit = master
-
-PACKAGES += bullet
-pkg_bullet_name = bullet
-pkg_bullet_description = Simple, reliable, efficient streaming for Cowboy.
-pkg_bullet_homepage = http://ninenines.eu
-pkg_bullet_fetch = git
-pkg_bullet_repo = https://github.com/ninenines/bullet
-pkg_bullet_commit = master
-
-PACKAGES += cache
-pkg_cache_name = cache
-pkg_cache_description = Erlang in-memory cache
-pkg_cache_homepage = https://github.com/fogfish/cache
-pkg_cache_fetch = git
-pkg_cache_repo = https://github.com/fogfish/cache
-pkg_cache_commit = master
-
-PACKAGES += cake
-pkg_cake_name = cake
-pkg_cake_description = Really simple terminal colorization
-pkg_cake_homepage = https://github.com/darach/cake-erl
-pkg_cake_fetch = git
-pkg_cake_repo = https://github.com/darach/cake-erl
-pkg_cake_commit = master
-
-PACKAGES += cberl
-pkg_cberl_name = cberl
-pkg_cberl_description = NIF based Erlang bindings for Couchbase
-pkg_cberl_homepage = https://github.com/chitika/cberl
-pkg_cberl_fetch = git
-pkg_cberl_repo = https://github.com/chitika/cberl
-pkg_cberl_commit = master
-
-PACKAGES += cecho
-pkg_cecho_name = cecho
-pkg_cecho_description = An ncurses library for Erlang
-pkg_cecho_homepage = https://github.com/mazenharake/cecho
-pkg_cecho_fetch = git
-pkg_cecho_repo = https://github.com/mazenharake/cecho
-pkg_cecho_commit = master
-
-PACKAGES += cferl
-pkg_cferl_name = cferl
-pkg_cferl_description = Rackspace / Open Stack Cloud Files Erlang Client
-pkg_cferl_homepage = https://github.com/ddossot/cferl
-pkg_cferl_fetch = git
-pkg_cferl_repo = https://github.com/ddossot/cferl
-pkg_cferl_commit = master
-
-PACKAGES += chaos_monkey
-pkg_chaos_monkey_name = chaos_monkey
-pkg_chaos_monkey_description = This is The CHAOS MONKEY. It will kill your processes.
-pkg_chaos_monkey_homepage = https://github.com/dLuna/chaos_monkey
-pkg_chaos_monkey_fetch = git
-pkg_chaos_monkey_repo = https://github.com/dLuna/chaos_monkey
-pkg_chaos_monkey_commit = master
-
-PACKAGES += check_node
-pkg_check_node_name = check_node
-pkg_check_node_description = Nagios Scripts for monitoring Riak
-pkg_check_node_homepage = https://github.com/basho-labs/riak_nagios
-pkg_check_node_fetch = git
-pkg_check_node_repo = https://github.com/basho-labs/riak_nagios
-pkg_check_node_commit = master
-
-PACKAGES += chronos
-pkg_chronos_name = chronos
-pkg_chronos_description = Timer module for Erlang that makes it easy to abstract time out of the tests.
-pkg_chronos_homepage = https://github.com/lehoff/chronos
-pkg_chronos_fetch = git
-pkg_chronos_repo = https://github.com/lehoff/chronos
-pkg_chronos_commit = master
-
-PACKAGES += chumak
-pkg_chumak_name = chumak
-pkg_chumak_description = Pure Erlang implementation of ZeroMQ Message Transport Protocol.
-pkg_chumak_homepage = http://choven.ca
-pkg_chumak_fetch = git
-pkg_chumak_repo = https://github.com/chovencorp/chumak
-pkg_chumak_commit = master
-
-PACKAGES += cl
-pkg_cl_name = cl
-pkg_cl_description = OpenCL binding for Erlang
-pkg_cl_homepage = https://github.com/tonyrog/cl
-pkg_cl_fetch = git
-pkg_cl_repo = https://github.com/tonyrog/cl
-pkg_cl_commit = master
-
-PACKAGES += clique
-pkg_clique_name = clique
-pkg_clique_description = CLI Framework for Erlang
-pkg_clique_homepage = https://github.com/basho/clique
-pkg_clique_fetch = git
-pkg_clique_repo = https://github.com/basho/clique
-pkg_clique_commit = develop
-
-PACKAGES += cloudi_core
-pkg_cloudi_core_name = cloudi_core
-pkg_cloudi_core_description = CloudI internal service runtime
-pkg_cloudi_core_homepage = http://cloudi.org/
-pkg_cloudi_core_fetch = git
-pkg_cloudi_core_repo = https://github.com/CloudI/cloudi_core
-pkg_cloudi_core_commit = master
-
-PACKAGES += cloudi_service_api_requests
-pkg_cloudi_service_api_requests_name = cloudi_service_api_requests
-pkg_cloudi_service_api_requests_description = CloudI Service API requests (JSON-RPC/Erlang-term support)
-pkg_cloudi_service_api_requests_homepage = http://cloudi.org/
-pkg_cloudi_service_api_requests_fetch = git
-pkg_cloudi_service_api_requests_repo = https://github.com/CloudI/cloudi_service_api_requests
-pkg_cloudi_service_api_requests_commit = master
-
-PACKAGES += cloudi_service_db_mysql
-pkg_cloudi_service_db_mysql_name = cloudi_service_db_mysql
-pkg_cloudi_service_db_mysql_description = MySQL CloudI Service
-pkg_cloudi_service_db_mysql_homepage = http://cloudi.org/
-pkg_cloudi_service_db_mysql_fetch = git
-pkg_cloudi_service_db_mysql_repo = https://github.com/CloudI/cloudi_service_db_mysql
-pkg_cloudi_service_db_mysql_commit = master
-
-PACKAGES += cloudi_service_db_pgsql
-pkg_cloudi_service_db_pgsql_name = cloudi_service_db_pgsql
-pkg_cloudi_service_db_pgsql_description = PostgreSQL CloudI Service
-pkg_cloudi_service_db_pgsql_homepage = http://cloudi.org/
-pkg_cloudi_service_db_pgsql_fetch = git
-pkg_cloudi_service_db_pgsql_repo = https://github.com/CloudI/cloudi_service_db_pgsql
-pkg_cloudi_service_db_pgsql_commit = master
-
-PACKAGES += cloudi_service_filesystem
-pkg_cloudi_service_filesystem_name = cloudi_service_filesystem
-pkg_cloudi_service_filesystem_description = Filesystem CloudI Service
-pkg_cloudi_service_filesystem_homepage = http://cloudi.org/
-pkg_cloudi_service_filesystem_fetch = git
-pkg_cloudi_service_filesystem_repo = https://github.com/CloudI/cloudi_service_filesystem
-pkg_cloudi_service_filesystem_commit = master
-
-PACKAGES += cloudi_service_http_client
-pkg_cloudi_service_http_client_name = cloudi_service_http_client
-pkg_cloudi_service_http_client_description = HTTP client CloudI Service
-pkg_cloudi_service_http_client_homepage = http://cloudi.org/
-pkg_cloudi_service_http_client_fetch = git
-pkg_cloudi_service_http_client_repo = https://github.com/CloudI/cloudi_service_http_client
-pkg_cloudi_service_http_client_commit = master
-
-PACKAGES += cloudi_service_http_cowboy
-pkg_cloudi_service_http_cowboy_name = cloudi_service_http_cowboy
-pkg_cloudi_service_http_cowboy_description = cowboy HTTP/HTTPS CloudI Service
-pkg_cloudi_service_http_cowboy_homepage = http://cloudi.org/
-pkg_cloudi_service_http_cowboy_fetch = git
-pkg_cloudi_service_http_cowboy_repo = https://github.com/CloudI/cloudi_service_http_cowboy
-pkg_cloudi_service_http_cowboy_commit = master
-
-PACKAGES += cloudi_service_http_elli
-pkg_cloudi_service_http_elli_name = cloudi_service_http_elli
-pkg_cloudi_service_http_elli_description = elli HTTP CloudI Service
-pkg_cloudi_service_http_elli_homepage = http://cloudi.org/
-pkg_cloudi_service_http_elli_fetch = git
-pkg_cloudi_service_http_elli_repo = https://github.com/CloudI/cloudi_service_http_elli
-pkg_cloudi_service_http_elli_commit = master
-
-PACKAGES += cloudi_service_map_reduce
-pkg_cloudi_service_map_reduce_name = cloudi_service_map_reduce
-pkg_cloudi_service_map_reduce_description = Map/Reduce CloudI Service
-pkg_cloudi_service_map_reduce_homepage = http://cloudi.org/
-pkg_cloudi_service_map_reduce_fetch = git
-pkg_cloudi_service_map_reduce_repo = https://github.com/CloudI/cloudi_service_map_reduce
-pkg_cloudi_service_map_reduce_commit = master
-
-PACKAGES += cloudi_service_oauth1
-pkg_cloudi_service_oauth1_name = cloudi_service_oauth1
-pkg_cloudi_service_oauth1_description = OAuth v1.0 CloudI Service
-pkg_cloudi_service_oauth1_homepage = http://cloudi.org/
-pkg_cloudi_service_oauth1_fetch = git
-pkg_cloudi_service_oauth1_repo = https://github.com/CloudI/cloudi_service_oauth1
-pkg_cloudi_service_oauth1_commit = master
-
-PACKAGES += cloudi_service_queue
-pkg_cloudi_service_queue_name = cloudi_service_queue
-pkg_cloudi_service_queue_description = Persistent Queue Service
-pkg_cloudi_service_queue_homepage = http://cloudi.org/
-pkg_cloudi_service_queue_fetch = git
-pkg_cloudi_service_queue_repo = https://github.com/CloudI/cloudi_service_queue
-pkg_cloudi_service_queue_commit = master
-
-PACKAGES += cloudi_service_quorum
-pkg_cloudi_service_quorum_name = cloudi_service_quorum
-pkg_cloudi_service_quorum_description = CloudI Quorum Service
-pkg_cloudi_service_quorum_homepage = http://cloudi.org/
-pkg_cloudi_service_quorum_fetch = git
-pkg_cloudi_service_quorum_repo = https://github.com/CloudI/cloudi_service_quorum
-pkg_cloudi_service_quorum_commit = master
-
-PACKAGES += cloudi_service_router
-pkg_cloudi_service_router_name = cloudi_service_router
-pkg_cloudi_service_router_description = CloudI Router Service
-pkg_cloudi_service_router_homepage = http://cloudi.org/
-pkg_cloudi_service_router_fetch = git
-pkg_cloudi_service_router_repo = https://github.com/CloudI/cloudi_service_router
-pkg_cloudi_service_router_commit = master
-
-PACKAGES += cloudi_service_tcp
-pkg_cloudi_service_tcp_name = cloudi_service_tcp
-pkg_cloudi_service_tcp_description = TCP CloudI Service
-pkg_cloudi_service_tcp_homepage = http://cloudi.org/
-pkg_cloudi_service_tcp_fetch = git
-pkg_cloudi_service_tcp_repo = https://github.com/CloudI/cloudi_service_tcp
-pkg_cloudi_service_tcp_commit = master
-
-PACKAGES += cloudi_service_udp
-pkg_cloudi_service_udp_name = cloudi_service_udp
-pkg_cloudi_service_udp_description = UDP CloudI Service
-pkg_cloudi_service_udp_homepage = http://cloudi.org/
-pkg_cloudi_service_udp_fetch = git
-pkg_cloudi_service_udp_repo = https://github.com/CloudI/cloudi_service_udp
-pkg_cloudi_service_udp_commit = master
-
-PACKAGES += cloudi_service_validate
-pkg_cloudi_service_validate_name = cloudi_service_validate
-pkg_cloudi_service_validate_description = CloudI Validate Service
-pkg_cloudi_service_validate_homepage = http://cloudi.org/
-pkg_cloudi_service_validate_fetch = git
-pkg_cloudi_service_validate_repo = https://github.com/CloudI/cloudi_service_validate
-pkg_cloudi_service_validate_commit = master
-
-PACKAGES += cloudi_service_zeromq
-pkg_cloudi_service_zeromq_name = cloudi_service_zeromq
-pkg_cloudi_service_zeromq_description = ZeroMQ CloudI Service
-pkg_cloudi_service_zeromq_homepage = http://cloudi.org/
-pkg_cloudi_service_zeromq_fetch = git
-pkg_cloudi_service_zeromq_repo = https://github.com/CloudI/cloudi_service_zeromq
-pkg_cloudi_service_zeromq_commit = master
-
-PACKAGES += cluster_info
-pkg_cluster_info_name = cluster_info
-pkg_cluster_info_description = Fork of Hibari's nifty cluster_info OTP app
-pkg_cluster_info_homepage = https://github.com/basho/cluster_info
-pkg_cluster_info_fetch = git
-pkg_cluster_info_repo = https://github.com/basho/cluster_info
-pkg_cluster_info_commit = master
-
-PACKAGES += color
-pkg_color_name = color
-pkg_color_description = ANSI colors for your Erlang
-pkg_color_homepage = https://github.com/julianduque/erlang-color
-pkg_color_fetch = git
-pkg_color_repo = https://github.com/julianduque/erlang-color
-pkg_color_commit = master
-
-PACKAGES += confetti
-pkg_confetti_name = confetti
-pkg_confetti_description = Erlang configuration provider / application:get_env/2 on steroids
-pkg_confetti_homepage = https://github.com/jtendo/confetti
-pkg_confetti_fetch = git
-pkg_confetti_repo = https://github.com/jtendo/confetti
-pkg_confetti_commit = master
-
-PACKAGES += couchbeam
-pkg_couchbeam_name = couchbeam
-pkg_couchbeam_description = Apache CouchDB client in Erlang
-pkg_couchbeam_homepage = https://github.com/benoitc/couchbeam
-pkg_couchbeam_fetch = git
-pkg_couchbeam_repo = https://github.com/benoitc/couchbeam
-pkg_couchbeam_commit = master
-
-PACKAGES += covertool
-pkg_covertool_name = covertool
-pkg_covertool_description = Tool to convert Erlang cover data files into Cobertura XML reports
-pkg_covertool_homepage = https://github.com/idubrov/covertool
-pkg_covertool_fetch = git
-pkg_covertool_repo = https://github.com/idubrov/covertool
-pkg_covertool_commit = master
-
PACKAGES += cowboy
pkg_cowboy_name = cowboy
pkg_cowboy_description = Small, fast and modular HTTP server.
pkg_cowboy_homepage = http://ninenines.eu
pkg_cowboy_fetch = git
pkg_cowboy_repo = https://github.com/ninenines/cowboy
-pkg_cowboy_commit = 1.0.4
-
-PACKAGES += cowdb
-pkg_cowdb_name = cowdb
-pkg_cowdb_description = Pure Key/Value database library for Erlang Applications
-pkg_cowdb_homepage = https://github.com/refuge/cowdb
-pkg_cowdb_fetch = git
-pkg_cowdb_repo = https://github.com/refuge/cowdb
-pkg_cowdb_commit = master
+pkg_cowboy_commit = master
PACKAGES += cowlib
pkg_cowlib_name = cowlib
@@ -769,599 +313,7 @@ pkg_cowlib_description = Support library for manipulating Web protocols.
pkg_cowlib_homepage = http://ninenines.eu
pkg_cowlib_fetch = git
pkg_cowlib_repo = https://github.com/ninenines/cowlib
-pkg_cowlib_commit = 1.0.2
-
-PACKAGES += cpg
-pkg_cpg_name = cpg
-pkg_cpg_description = CloudI Process Groups
-pkg_cpg_homepage = https://github.com/okeuday/cpg
-pkg_cpg_fetch = git
-pkg_cpg_repo = https://github.com/okeuday/cpg
-pkg_cpg_commit = master
-
-PACKAGES += cqerl
-pkg_cqerl_name = cqerl
-pkg_cqerl_description = Native Erlang CQL client for Cassandra
-pkg_cqerl_homepage = https://matehat.github.io/cqerl/
-pkg_cqerl_fetch = git
-pkg_cqerl_repo = https://github.com/matehat/cqerl
-pkg_cqerl_commit = master
-
-PACKAGES += cr
-pkg_cr_name = cr
-pkg_cr_description = Chain Replication
-pkg_cr_homepage = https://synrc.com/apps/cr/doc/cr.htm
-pkg_cr_fetch = git
-pkg_cr_repo = https://github.com/spawnproc/cr
-pkg_cr_commit = master
-
-PACKAGES += cuttlefish
-pkg_cuttlefish_name = cuttlefish
-pkg_cuttlefish_description = cuttlefish configuration abstraction
-pkg_cuttlefish_homepage = https://github.com/Kyorai/cuttlefish
-pkg_cuttlefish_fetch = git
-pkg_cuttlefish_repo = https://github.com/Kyorai/cuttlefish
-pkg_cuttlefish_commit = main
-
-PACKAGES += damocles
-pkg_damocles_name = damocles
-pkg_damocles_description = Erlang library for generating adversarial network conditions for QAing distributed applications/systems on a single Linux box.
-pkg_damocles_homepage = https://github.com/lostcolony/damocles
-pkg_damocles_fetch = git
-pkg_damocles_repo = https://github.com/lostcolony/damocles
-pkg_damocles_commit = master
-
-PACKAGES += debbie
-pkg_debbie_name = debbie
-pkg_debbie_description = .DEB Built In Erlang
-pkg_debbie_homepage = https://github.com/crownedgrouse/debbie
-pkg_debbie_fetch = git
-pkg_debbie_repo = https://github.com/crownedgrouse/debbie
-pkg_debbie_commit = master
-
-PACKAGES += decimal
-pkg_decimal_name = decimal
-pkg_decimal_description = An Erlang decimal arithmetic library
-pkg_decimal_homepage = https://github.com/egobrain/decimal
-pkg_decimal_fetch = git
-pkg_decimal_repo = https://github.com/egobrain/decimal
-pkg_decimal_commit = master
-
-PACKAGES += detergent
-pkg_detergent_name = detergent
-pkg_detergent_description = An emulsifying Erlang SOAP library
-pkg_detergent_homepage = https://github.com/devinus/detergent
-pkg_detergent_fetch = git
-pkg_detergent_repo = https://github.com/devinus/detergent
-pkg_detergent_commit = master
-
-PACKAGES += dh_date
-pkg_dh_date_name = dh_date
-pkg_dh_date_description = Date formatting / parsing library for erlang
-pkg_dh_date_homepage = https://github.com/daleharvey/dh_date
-pkg_dh_date_fetch = git
-pkg_dh_date_repo = https://github.com/daleharvey/dh_date
-pkg_dh_date_commit = master
-
-PACKAGES += dirbusterl
-pkg_dirbusterl_name = dirbusterl
-pkg_dirbusterl_description = DirBuster successor in Erlang
-pkg_dirbusterl_homepage = https://github.com/silentsignal/DirBustErl
-pkg_dirbusterl_fetch = git
-pkg_dirbusterl_repo = https://github.com/silentsignal/DirBustErl
-pkg_dirbusterl_commit = master
-
-PACKAGES += dispcount
-pkg_dispcount_name = dispcount
-pkg_dispcount_description = Erlang task dispatcher based on ETS counters.
-pkg_dispcount_homepage = https://github.com/ferd/dispcount
-pkg_dispcount_fetch = git
-pkg_dispcount_repo = https://github.com/ferd/dispcount
-pkg_dispcount_commit = master
-
-PACKAGES += dlhttpc
-pkg_dlhttpc_name = dlhttpc
-pkg_dlhttpc_description = dispcount-based lhttpc fork for massive amounts of requests to limited endpoints
-pkg_dlhttpc_homepage = https://github.com/ferd/dlhttpc
-pkg_dlhttpc_fetch = git
-pkg_dlhttpc_repo = https://github.com/ferd/dlhttpc
-pkg_dlhttpc_commit = master
-
-PACKAGES += dns
-pkg_dns_name = dns
-pkg_dns_description = Erlang DNS library
-pkg_dns_homepage = https://github.com/aetrion/dns_erlang
-pkg_dns_fetch = git
-pkg_dns_repo = https://github.com/aetrion/dns_erlang
-pkg_dns_commit = main
-
-PACKAGES += dynamic_compile
-pkg_dynamic_compile_name = dynamic_compile
-pkg_dynamic_compile_description = compile and load erlang modules from string input
-pkg_dynamic_compile_homepage = https://github.com/jkvor/dynamic_compile
-pkg_dynamic_compile_fetch = git
-pkg_dynamic_compile_repo = https://github.com/jkvor/dynamic_compile
-pkg_dynamic_compile_commit = master
-
-PACKAGES += e2
-pkg_e2_name = e2
-pkg_e2_description = Library to simply writing correct OTP applications.
-pkg_e2_homepage = http://e2project.org
-pkg_e2_fetch = git
-pkg_e2_repo = https://github.com/gar1t/e2
-pkg_e2_commit = master
-
-PACKAGES += eamf
-pkg_eamf_name = eamf
-pkg_eamf_description = eAMF provides Action Message Format (AMF) support for Erlang
-pkg_eamf_homepage = https://github.com/mrinalwadhwa/eamf
-pkg_eamf_fetch = git
-pkg_eamf_repo = https://github.com/mrinalwadhwa/eamf
-pkg_eamf_commit = master
-
-PACKAGES += eavro
-pkg_eavro_name = eavro
-pkg_eavro_description = Apache Avro encoder/decoder
-pkg_eavro_homepage = https://github.com/SIfoxDevTeam/eavro
-pkg_eavro_fetch = git
-pkg_eavro_repo = https://github.com/SIfoxDevTeam/eavro
-pkg_eavro_commit = master
-
-PACKAGES += ecapnp
-pkg_ecapnp_name = ecapnp
-pkg_ecapnp_description = Cap'n Proto library for Erlang
-pkg_ecapnp_homepage = https://github.com/kaos/ecapnp
-pkg_ecapnp_fetch = git
-pkg_ecapnp_repo = https://github.com/kaos/ecapnp
-pkg_ecapnp_commit = master
-
-PACKAGES += econfig
-pkg_econfig_name = econfig
-pkg_econfig_description = simple Erlang config handler using INI files
-pkg_econfig_homepage = https://github.com/benoitc/econfig
-pkg_econfig_fetch = git
-pkg_econfig_repo = https://github.com/benoitc/econfig
-pkg_econfig_commit = master
-
-PACKAGES += edate
-pkg_edate_name = edate
-pkg_edate_description = date manipulation library for erlang
-pkg_edate_homepage = https://github.com/dweldon/edate
-pkg_edate_fetch = git
-pkg_edate_repo = https://github.com/dweldon/edate
-pkg_edate_commit = master
-
-PACKAGES += edgar
-pkg_edgar_name = edgar
-pkg_edgar_description = Erlang Does GNU AR
-pkg_edgar_homepage = https://github.com/crownedgrouse/edgar
-pkg_edgar_fetch = git
-pkg_edgar_repo = https://github.com/crownedgrouse/edgar
-pkg_edgar_commit = master
-
-PACKAGES += edns
-pkg_edns_name = edns
-pkg_edns_description = Erlang/OTP DNS server
-pkg_edns_homepage = https://github.com/hcvst/erlang-dns
-pkg_edns_fetch = git
-pkg_edns_repo = https://github.com/hcvst/erlang-dns
-pkg_edns_commit = master
-
-PACKAGES += edown
-pkg_edown_name = edown
-pkg_edown_description = EDoc extension for generating Github-flavored Markdown
-pkg_edown_homepage = https://github.com/uwiger/edown
-pkg_edown_fetch = git
-pkg_edown_repo = https://github.com/uwiger/edown
-pkg_edown_commit = master
-
-PACKAGES += eep
-pkg_eep_name = eep
-pkg_eep_description = Erlang Easy Profiling (eep) application provides a way to analyze application performance and call hierarchy
-pkg_eep_homepage = https://github.com/virtan/eep
-pkg_eep_fetch = git
-pkg_eep_repo = https://github.com/virtan/eep
-pkg_eep_commit = master
-
-PACKAGES += eep_app
-pkg_eep_app_name = eep_app
-pkg_eep_app_description = Embedded Event Processing
-pkg_eep_app_homepage = https://github.com/darach/eep-erl
-pkg_eep_app_fetch = git
-pkg_eep_app_repo = https://github.com/darach/eep-erl
-pkg_eep_app_commit = master
-
-PACKAGES += efene
-pkg_efene_name = efene
-pkg_efene_description = Alternative syntax for the Erlang Programming Language focusing on simplicity, ease of use and programmer UX
-pkg_efene_homepage = https://github.com/efene/efene
-pkg_efene_fetch = git
-pkg_efene_repo = https://github.com/efene/efene
-pkg_efene_commit = master
-
-PACKAGES += egeoip
-pkg_egeoip_name = egeoip
-pkg_egeoip_description = Erlang IP Geolocation module, currently supporting the MaxMind GeoLite City Database.
-pkg_egeoip_homepage = https://github.com/mochi/egeoip
-pkg_egeoip_fetch = git
-pkg_egeoip_repo = https://github.com/mochi/egeoip
-pkg_egeoip_commit = master
-
-PACKAGES += ehsa
-pkg_ehsa_name = ehsa
-pkg_ehsa_description = Erlang HTTP server basic and digest authentication modules
-pkg_ehsa_homepage = https://github.com/a12n/ehsa
-pkg_ehsa_fetch = git
-pkg_ehsa_repo = https://github.com/a12n/ehsa
-pkg_ehsa_commit = master
-
-PACKAGES += ej
-pkg_ej_name = ej
-pkg_ej_description = Helper module for working with Erlang terms representing JSON
-pkg_ej_homepage = https://github.com/seth/ej
-pkg_ej_fetch = git
-pkg_ej_repo = https://github.com/seth/ej
-pkg_ej_commit = master
-
-PACKAGES += ejabberd
-pkg_ejabberd_name = ejabberd
-pkg_ejabberd_description = Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform
-pkg_ejabberd_homepage = https://github.com/processone/ejabberd
-pkg_ejabberd_fetch = git
-pkg_ejabberd_repo = https://github.com/processone/ejabberd
-pkg_ejabberd_commit = master
-
-PACKAGES += ejwt
-pkg_ejwt_name = ejwt
-pkg_ejwt_description = erlang library for JSON Web Token
-pkg_ejwt_homepage = https://github.com/artefactop/ejwt
-pkg_ejwt_fetch = git
-pkg_ejwt_repo = https://github.com/artefactop/ejwt
-pkg_ejwt_commit = master
-
-PACKAGES += ekaf
-pkg_ekaf_name = ekaf
-pkg_ekaf_description = A minimal, high-performance Kafka client in Erlang.
-pkg_ekaf_homepage = https://github.com/helpshift/ekaf
-pkg_ekaf_fetch = git
-pkg_ekaf_repo = https://github.com/helpshift/ekaf
-pkg_ekaf_commit = master
-
-PACKAGES += elarm
-pkg_elarm_name = elarm
-pkg_elarm_description = Alarm Manager for Erlang.
-pkg_elarm_homepage = https://github.com/esl/elarm
-pkg_elarm_fetch = git
-pkg_elarm_repo = https://github.com/esl/elarm
-pkg_elarm_commit = master
-
-PACKAGES += eleveldb
-pkg_eleveldb_name = eleveldb
-pkg_eleveldb_description = Erlang LevelDB API
-pkg_eleveldb_homepage = https://github.com/basho/eleveldb
-pkg_eleveldb_fetch = git
-pkg_eleveldb_repo = https://github.com/basho/eleveldb
-pkg_eleveldb_commit = develop
-
-PACKAGES += elixir
-pkg_elixir_name = elixir
-pkg_elixir_description = Elixir is a dynamic, functional language designed for building scalable and maintainable applications
-pkg_elixir_homepage = https://elixir-lang.org/
-pkg_elixir_fetch = git
-pkg_elixir_repo = https://github.com/elixir-lang/elixir
-pkg_elixir_commit = main
-
-PACKAGES += elli
-pkg_elli_name = elli
-pkg_elli_description = Simple, robust and performant Erlang web server
-pkg_elli_homepage = https://github.com/elli-lib/elli
-pkg_elli_fetch = git
-pkg_elli_repo = https://github.com/elli-lib/elli
-pkg_elli_commit = main
-
-PACKAGES += elvis
-pkg_elvis_name = elvis
-pkg_elvis_description = Erlang Style Reviewer
-pkg_elvis_homepage = https://github.com/inaka/elvis
-pkg_elvis_fetch = git
-pkg_elvis_repo = https://github.com/inaka/elvis
-pkg_elvis_commit = master
-
-PACKAGES += emagick
-pkg_emagick_name = emagick
-pkg_emagick_description = Wrapper for Graphics/ImageMagick command line tool.
-pkg_emagick_homepage = https://github.com/kivra/emagick
-pkg_emagick_fetch = git
-pkg_emagick_repo = https://github.com/kivra/emagick
-pkg_emagick_commit = master
-
-PACKAGES += enm
-pkg_enm_name = enm
-pkg_enm_description = Erlang driver for nanomsg
-pkg_enm_homepage = https://github.com/basho/enm
-pkg_enm_fetch = git
-pkg_enm_repo = https://github.com/basho/enm
-pkg_enm_commit = master
-
-PACKAGES += entop
-pkg_entop_name = entop
-pkg_entop_description = A top-like tool for monitoring an Erlang node
-pkg_entop_homepage = https://github.com/mazenharake/entop
-pkg_entop_fetch = git
-pkg_entop_repo = https://github.com/mazenharake/entop
-pkg_entop_commit = master
-
-PACKAGES += epcap
-pkg_epcap_name = epcap
-pkg_epcap_description = Erlang packet capture interface using pcap
-pkg_epcap_homepage = https://github.com/msantos/epcap
-pkg_epcap_fetch = git
-pkg_epcap_repo = https://github.com/msantos/epcap
-pkg_epcap_commit = master
-
-PACKAGES += eper
-pkg_eper_name = eper
-pkg_eper_description = Erlang performance and debugging tools.
-pkg_eper_homepage = https://github.com/massemanet/eper
-pkg_eper_fetch = git
-pkg_eper_repo = https://github.com/massemanet/eper
-pkg_eper_commit = master
-
-PACKAGES += epgsql
-pkg_epgsql_name = epgsql
-pkg_epgsql_description = Erlang PostgreSQL client library.
-pkg_epgsql_homepage = https://github.com/epgsql/epgsql
-pkg_epgsql_fetch = git
-pkg_epgsql_repo = https://github.com/epgsql/epgsql
-pkg_epgsql_commit = master
-
-PACKAGES += episcina
-pkg_episcina_name = episcina
-pkg_episcina_description = A simple non intrusive resource pool for connections
-pkg_episcina_homepage = https://github.com/erlware/episcina
-pkg_episcina_fetch = git
-pkg_episcina_repo = https://github.com/erlware/episcina
-pkg_episcina_commit = master
-
-PACKAGES += eplot
-pkg_eplot_name = eplot
-pkg_eplot_description = A plot engine written in erlang.
-pkg_eplot_homepage = https://github.com/psyeugenic/eplot
-pkg_eplot_fetch = git
-pkg_eplot_repo = https://github.com/psyeugenic/eplot
-pkg_eplot_commit = master
-
-PACKAGES += epocxy
-pkg_epocxy_name = epocxy
-pkg_epocxy_description = Erlang Patterns of Concurrency
-pkg_epocxy_homepage = https://github.com/duomark/epocxy
-pkg_epocxy_fetch = git
-pkg_epocxy_repo = https://github.com/duomark/epocxy
-pkg_epocxy_commit = master
-
-PACKAGES += epubnub
-pkg_epubnub_name = epubnub
-pkg_epubnub_description = Erlang PubNub API
-pkg_epubnub_homepage = https://github.com/tsloughter/epubnub
-pkg_epubnub_fetch = git
-pkg_epubnub_repo = https://github.com/tsloughter/epubnub
-pkg_epubnub_commit = master
-
-PACKAGES += eqm
-pkg_eqm_name = eqm
-pkg_eqm_description = Erlang pub sub with supply-demand channels
-pkg_eqm_homepage = https://github.com/loucash/eqm
-pkg_eqm_fetch = git
-pkg_eqm_repo = https://github.com/loucash/eqm
-pkg_eqm_commit = master
-
-PACKAGES += eredis
-pkg_eredis_name = eredis
-pkg_eredis_description = Erlang Redis client
-pkg_eredis_homepage = https://github.com/wooga/eredis
-pkg_eredis_fetch = git
-pkg_eredis_repo = https://github.com/wooga/eredis
-pkg_eredis_commit = master
-
-PACKAGES += erl_streams
-pkg_erl_streams_name = erl_streams
-pkg_erl_streams_description = Streams in Erlang
-pkg_erl_streams_homepage = https://github.com/epappas/erl_streams
-pkg_erl_streams_fetch = git
-pkg_erl_streams_repo = https://github.com/epappas/erl_streams
-pkg_erl_streams_commit = master
-
-PACKAGES += erlang_localtime
-pkg_erlang_localtime_name = erlang_localtime
-pkg_erlang_localtime_description = Erlang library for conversion from one local time to another
-pkg_erlang_localtime_homepage = https://github.com/dmitryme/erlang_localtime
-pkg_erlang_localtime_fetch = git
-pkg_erlang_localtime_repo = https://github.com/dmitryme/erlang_localtime
-pkg_erlang_localtime_commit = master
-
-PACKAGES += erlang_smtp
-pkg_erlang_smtp_name = erlang_smtp
-pkg_erlang_smtp_description = Erlang SMTP and POP3 server code.
-pkg_erlang_smtp_homepage = https://github.com/tonyg/erlang-smtp
-pkg_erlang_smtp_fetch = git
-pkg_erlang_smtp_repo = https://github.com/tonyg/erlang-smtp
-pkg_erlang_smtp_commit = master
-
-PACKAGES += erlang_term
-pkg_erlang_term_name = erlang_term
-pkg_erlang_term_description = Erlang Term Info
-pkg_erlang_term_homepage = https://github.com/okeuday/erlang_term
-pkg_erlang_term_fetch = git
-pkg_erlang_term_repo = https://github.com/okeuday/erlang_term
-pkg_erlang_term_commit = master
-
-PACKAGES += erlastic_search
-pkg_erlastic_search_name = erlastic_search
-pkg_erlastic_search_description = An Erlang app for communicating with Elastic Search's rest interface.
-pkg_erlastic_search_homepage = https://github.com/tsloughter/erlastic_search
-pkg_erlastic_search_fetch = git
-pkg_erlastic_search_repo = https://github.com/tsloughter/erlastic_search
-pkg_erlastic_search_commit = master
-
-PACKAGES += erlbrake
-pkg_erlbrake_name = erlbrake
-pkg_erlbrake_description = Erlang Airbrake notification client
-pkg_erlbrake_homepage = https://github.com/kenpratt/erlbrake
-pkg_erlbrake_fetch = git
-pkg_erlbrake_repo = https://github.com/kenpratt/erlbrake
-pkg_erlbrake_commit = master
-
-PACKAGES += erlcloud
-pkg_erlcloud_name = erlcloud
-pkg_erlcloud_description = Cloud Computing library for erlang (Amazon EC2, S3, SQS, SimpleDB, Mechanical Turk, ELB)
-pkg_erlcloud_homepage = https://github.com/gleber/erlcloud
-pkg_erlcloud_fetch = git
-pkg_erlcloud_repo = https://github.com/gleber/erlcloud
-pkg_erlcloud_commit = master
-
-PACKAGES += erlcron
-pkg_erlcron_name = erlcron
-pkg_erlcron_description = Erlang cronish system
-pkg_erlcron_homepage = https://github.com/erlware/erlcron
-pkg_erlcron_fetch = git
-pkg_erlcron_repo = https://github.com/erlware/erlcron
-pkg_erlcron_commit = master
-
-PACKAGES += erldb
-pkg_erldb_name = erldb
-pkg_erldb_description = ORM (Object-relational mapping) application implemented in Erlang
-pkg_erldb_homepage = http://erldb.org
-pkg_erldb_fetch = git
-pkg_erldb_repo = https://github.com/erldb/erldb
-pkg_erldb_commit = master
-
-PACKAGES += erldis
-pkg_erldis_name = erldis
-pkg_erldis_description = redis erlang client library
-pkg_erldis_homepage = https://github.com/cstar/erldis
-pkg_erldis_fetch = git
-pkg_erldis_repo = https://github.com/cstar/erldis
-pkg_erldis_commit = master
-
-PACKAGES += erldns
-pkg_erldns_name = erldns
-pkg_erldns_description = DNS server, in erlang.
-pkg_erldns_homepage = https://github.com/aetrion/erl-dns
-pkg_erldns_fetch = git
-pkg_erldns_repo = https://github.com/aetrion/erl-dns
-pkg_erldns_commit = main
-
-PACKAGES += erldocker
-pkg_erldocker_name = erldocker
-pkg_erldocker_description = Docker Remote API client for Erlang
-pkg_erldocker_homepage = https://github.com/proger/erldocker
-pkg_erldocker_fetch = git
-pkg_erldocker_repo = https://github.com/proger/erldocker
-pkg_erldocker_commit = master
-
-PACKAGES += erlfsmon
-pkg_erlfsmon_name = erlfsmon
-pkg_erlfsmon_description = Erlang filesystem event watcher for Linux and OSX
-pkg_erlfsmon_homepage = https://github.com/proger/erlfsmon
-pkg_erlfsmon_fetch = git
-pkg_erlfsmon_repo = https://github.com/proger/erlfsmon
-pkg_erlfsmon_commit = master
-
-PACKAGES += erlgit
-pkg_erlgit_name = erlgit
-pkg_erlgit_description = Erlang convenience wrapper around git executable
-pkg_erlgit_homepage = https://github.com/gleber/erlgit
-pkg_erlgit_fetch = git
-pkg_erlgit_repo = https://github.com/gleber/erlgit
-pkg_erlgit_commit = master
-
-PACKAGES += erlguten
-pkg_erlguten_name = erlguten
-pkg_erlguten_description = ErlGuten is a system for high-quality typesetting, written purely in Erlang.
-pkg_erlguten_homepage = https://github.com/richcarl/erlguten
-pkg_erlguten_fetch = git
-pkg_erlguten_repo = https://github.com/richcarl/erlguten
-pkg_erlguten_commit = master
-
-PACKAGES += erlmc
-pkg_erlmc_name = erlmc
-pkg_erlmc_description = Erlang memcached binary protocol client
-pkg_erlmc_homepage = https://github.com/jkvor/erlmc
-pkg_erlmc_fetch = git
-pkg_erlmc_repo = https://github.com/jkvor/erlmc
-pkg_erlmc_commit = master
-
-PACKAGES += erlmongo
-pkg_erlmongo_name = erlmongo
-pkg_erlmongo_description = Record based Erlang driver for MongoDB with gridfs support
-pkg_erlmongo_homepage = https://github.com/SergejJurecko/erlmongo
-pkg_erlmongo_fetch = git
-pkg_erlmongo_repo = https://github.com/SergejJurecko/erlmongo
-pkg_erlmongo_commit = master
-
-PACKAGES += erlog
-pkg_erlog_name = erlog
-pkg_erlog_description = Prolog interpreter in and for Erlang
-pkg_erlog_homepage = https://github.com/rvirding/erlog
-pkg_erlog_fetch = git
-pkg_erlog_repo = https://github.com/rvirding/erlog
-pkg_erlog_commit = master
-
-PACKAGES += erlpass
-pkg_erlpass_name = erlpass
-pkg_erlpass_description = A library to handle password hashing and changing in a safe manner, independent from any kind of storage whatsoever.
-pkg_erlpass_homepage = https://github.com/ferd/erlpass
-pkg_erlpass_fetch = git
-pkg_erlpass_repo = https://github.com/ferd/erlpass
-pkg_erlpass_commit = master
-
-PACKAGES += erlsh
-pkg_erlsh_name = erlsh
-pkg_erlsh_description = Erlang shell tools
-pkg_erlsh_homepage = https://github.com/proger/erlsh
-pkg_erlsh_fetch = git
-pkg_erlsh_repo = https://github.com/proger/erlsh
-pkg_erlsh_commit = master
-
-PACKAGES += erlsha2
-pkg_erlsha2_name = erlsha2
-pkg_erlsha2_description = SHA-224, SHA-256, SHA-384, SHA-512 implemented in Erlang NIFs.
-pkg_erlsha2_homepage = https://github.com/vinoski/erlsha2
-pkg_erlsha2_fetch = git
-pkg_erlsha2_repo = https://github.com/vinoski/erlsha2
-pkg_erlsha2_commit = master
-
-PACKAGES += erlsom
-pkg_erlsom_name = erlsom
-pkg_erlsom_description = XML parser for Erlang
-pkg_erlsom_homepage = https://github.com/willemdj/erlsom
-pkg_erlsom_fetch = git
-pkg_erlsom_repo = https://github.com/willemdj/erlsom
-pkg_erlsom_commit = master
-
-PACKAGES += erlubi
-pkg_erlubi_name = erlubi
-pkg_erlubi_description = Ubigraph Erlang Client (and Process Visualizer)
-pkg_erlubi_homepage = https://github.com/krestenkrab/erlubi
-pkg_erlubi_fetch = git
-pkg_erlubi_repo = https://github.com/krestenkrab/erlubi
-pkg_erlubi_commit = master
-
-PACKAGES += erlvolt
-pkg_erlvolt_name = erlvolt
-pkg_erlvolt_description = VoltDB Erlang Client Driver
-pkg_erlvolt_homepage = https://github.com/VoltDB/voltdb-client-erlang
-pkg_erlvolt_fetch = git
-pkg_erlvolt_repo = https://github.com/VoltDB/voltdb-client-erlang
-pkg_erlvolt_commit = master
-
-PACKAGES += erlware_commons
-pkg_erlware_commons_name = erlware_commons
-pkg_erlware_commons_description = Erlware Commons is an Erlware project focused on all aspects of reusable Erlang components.
-pkg_erlware_commons_homepage = https://github.com/erlware/erlware_commons
-pkg_erlware_commons_fetch = git
-pkg_erlware_commons_repo = https://github.com/erlware/erlware_commons
-pkg_erlware_commons_commit = master
+pkg_cowlib_commit = master
PACKAGES += erlydtl
pkg_erlydtl_name = erlydtl
@@ -1371,406 +323,6 @@ pkg_erlydtl_fetch = git
pkg_erlydtl_repo = https://github.com/erlydtl/erlydtl
pkg_erlydtl_commit = master
-PACKAGES += errd
-pkg_errd_name = errd
-pkg_errd_description = Erlang RRDTool library
-pkg_errd_homepage = https://github.com/archaelus/errd
-pkg_errd_fetch = git
-pkg_errd_repo = https://github.com/archaelus/errd
-pkg_errd_commit = master
-
-PACKAGES += erserve
-pkg_erserve_name = erserve
-pkg_erserve_description = Erlang/Rserve communication interface
-pkg_erserve_homepage = https://github.com/del/erserve
-pkg_erserve_fetch = git
-pkg_erserve_repo = https://github.com/del/erserve
-pkg_erserve_commit = master
-
-PACKAGES += escalus
-pkg_escalus_name = escalus
-pkg_escalus_description = An XMPP client library in Erlang for conveniently testing XMPP servers
-pkg_escalus_homepage = https://github.com/esl/escalus
-pkg_escalus_fetch = git
-pkg_escalus_repo = https://github.com/esl/escalus
-pkg_escalus_commit = master
-
-PACKAGES += esh_mk
-pkg_esh_mk_name = esh_mk
-pkg_esh_mk_description = esh template engine plugin for erlang.mk
-pkg_esh_mk_homepage = https://github.com/crownedgrouse/esh.mk
-pkg_esh_mk_fetch = git
-pkg_esh_mk_repo = https://github.com/crownedgrouse/esh.mk.git
-pkg_esh_mk_commit = master
-
-PACKAGES += espec
-pkg_espec_name = espec
-pkg_espec_description = ESpec: Behaviour driven development framework for Erlang
-pkg_espec_homepage = https://github.com/lucaspiller/espec
-pkg_espec_fetch = git
-pkg_espec_repo = https://github.com/lucaspiller/espec
-pkg_espec_commit = master
-
-PACKAGES += estatsd
-pkg_estatsd_name = estatsd
-pkg_estatsd_description = Erlang stats aggregation app that periodically flushes data to graphite
-pkg_estatsd_homepage = https://github.com/RJ/estatsd
-pkg_estatsd_fetch = git
-pkg_estatsd_repo = https://github.com/RJ/estatsd
-pkg_estatsd_commit = master
-
-PACKAGES += etap
-pkg_etap_name = etap
-pkg_etap_description = etap is a simple erlang testing library that provides TAP compliant output.
-pkg_etap_homepage = https://github.com/ngerakines/etap
-pkg_etap_fetch = git
-pkg_etap_repo = https://github.com/ngerakines/etap
-pkg_etap_commit = master
-
-PACKAGES += etest
-pkg_etest_name = etest
-pkg_etest_description = A lightweight, convention over configuration test framework for Erlang
-pkg_etest_homepage = https://github.com/wooga/etest
-pkg_etest_fetch = git
-pkg_etest_repo = https://github.com/wooga/etest
-pkg_etest_commit = master
-
-PACKAGES += etest_http
-pkg_etest_http_name = etest_http
-pkg_etest_http_description = etest Assertions around HTTP (client-side)
-pkg_etest_http_homepage = https://github.com/wooga/etest_http
-pkg_etest_http_fetch = git
-pkg_etest_http_repo = https://github.com/wooga/etest_http
-pkg_etest_http_commit = master
-
-PACKAGES += etoml
-pkg_etoml_name = etoml
-pkg_etoml_description = TOML language erlang parser
-pkg_etoml_homepage = https://github.com/kalta/etoml
-pkg_etoml_fetch = git
-pkg_etoml_repo = https://github.com/kalta/etoml
-pkg_etoml_commit = master
-
-PACKAGES += eunit
-pkg_eunit_name = eunit
-pkg_eunit_description = The EUnit lightweight unit testing framework for Erlang - this is the canonical development repository.
-pkg_eunit_homepage = https://github.com/richcarl/eunit
-pkg_eunit_fetch = git
-pkg_eunit_repo = https://github.com/richcarl/eunit
-pkg_eunit_commit = master
-
-PACKAGES += eunit_formatters
-pkg_eunit_formatters_name = eunit_formatters
-pkg_eunit_formatters_description = Because eunit's output sucks. Let's make it better.
-pkg_eunit_formatters_homepage = https://github.com/seancribbs/eunit_formatters
-pkg_eunit_formatters_fetch = git
-pkg_eunit_formatters_repo = https://github.com/seancribbs/eunit_formatters
-pkg_eunit_formatters_commit = master
-
-PACKAGES += euthanasia
-pkg_euthanasia_name = euthanasia
-pkg_euthanasia_description = Merciful killer for your Erlang processes
-pkg_euthanasia_homepage = https://github.com/doubleyou/euthanasia
-pkg_euthanasia_fetch = git
-pkg_euthanasia_repo = https://github.com/doubleyou/euthanasia
-pkg_euthanasia_commit = master
-
-PACKAGES += evum
-pkg_evum_name = evum
-pkg_evum_description = Spawn Linux VMs as Erlang processes in the Erlang VM
-pkg_evum_homepage = https://github.com/msantos/evum
-pkg_evum_fetch = git
-pkg_evum_repo = https://github.com/msantos/evum
-pkg_evum_commit = master
-
-PACKAGES += exec
-pkg_exec_name = erlexec
-pkg_exec_description = Execute and control OS processes from Erlang/OTP.
-pkg_exec_homepage = http://saleyn.github.com/erlexec
-pkg_exec_fetch = git
-pkg_exec_repo = https://github.com/saleyn/erlexec
-pkg_exec_commit = master
-
-PACKAGES += exml
-pkg_exml_name = exml
-pkg_exml_description = XML parsing library in Erlang
-pkg_exml_homepage = https://github.com/paulgray/exml
-pkg_exml_fetch = git
-pkg_exml_repo = https://github.com/paulgray/exml
-pkg_exml_commit = master
-
-PACKAGES += exometer
-pkg_exometer_name = exometer
-pkg_exometer_description = Basic measurement objects and probe behavior
-pkg_exometer_homepage = https://github.com/Feuerlabs/exometer
-pkg_exometer_fetch = git
-pkg_exometer_repo = https://github.com/Feuerlabs/exometer
-pkg_exometer_commit = master
-
-PACKAGES += exs1024
-pkg_exs1024_name = exs1024
-pkg_exs1024_description = Xorshift1024star pseudo random number generator for Erlang.
-pkg_exs1024_homepage = https://github.com/jj1bdx/exs1024
-pkg_exs1024_fetch = git
-pkg_exs1024_repo = https://github.com/jj1bdx/exs1024
-pkg_exs1024_commit = master
-
-PACKAGES += exsplus116
-pkg_exsplus116_name = exsplus116
-pkg_exsplus116_description = Xorshift116plus for Erlang
-pkg_exsplus116_homepage = https://github.com/jj1bdx/exsplus116
-pkg_exsplus116_fetch = git
-pkg_exsplus116_repo = https://github.com/jj1bdx/exsplus116
-pkg_exsplus116_commit = master
-
-PACKAGES += ezmtp
-pkg_ezmtp_name = ezmtp
-pkg_ezmtp_description = ZMTP protocol in pure Erlang.
-pkg_ezmtp_homepage = https://github.com/a13x/ezmtp
-pkg_ezmtp_fetch = git
-pkg_ezmtp_repo = https://github.com/a13x/ezmtp
-pkg_ezmtp_commit = master
-
-PACKAGES += fast_disk_log
-pkg_fast_disk_log_name = fast_disk_log
-pkg_fast_disk_log_description = Pool-based asynchronous Erlang disk logger
-pkg_fast_disk_log_homepage = https://github.com/lpgauth/fast_disk_log
-pkg_fast_disk_log_fetch = git
-pkg_fast_disk_log_repo = https://github.com/lpgauth/fast_disk_log
-pkg_fast_disk_log_commit = master
-
-PACKAGES += feeder
-pkg_feeder_name = feeder
-pkg_feeder_description = Stream parse RSS and Atom formatted XML feeds.
-pkg_feeder_homepage = https://github.com/michaelnisi/feeder
-pkg_feeder_fetch = git
-pkg_feeder_repo = https://github.com/michaelnisi/feeder
-pkg_feeder_commit = master
-
-PACKAGES += find_crate
-pkg_find_crate_name = find_crate
-pkg_find_crate_description = Find Rust libs and exes in Erlang application priv directory
-pkg_find_crate_homepage = https://github.com/goertzenator/find_crate
-pkg_find_crate_fetch = git
-pkg_find_crate_repo = https://github.com/goertzenator/find_crate
-pkg_find_crate_commit = master
-
-PACKAGES += fix
-pkg_fix_name = fix
-pkg_fix_description = http://fixprotocol.org/ implementation.
-pkg_fix_homepage = https://github.com/maxlapshin/fix
-pkg_fix_fetch = git
-pkg_fix_repo = https://github.com/maxlapshin/fix
-pkg_fix_commit = master
-
-PACKAGES += flower
-pkg_flower_name = flower
-pkg_flower_description = FlowER - a Erlang OpenFlow development platform
-pkg_flower_homepage = https://github.com/travelping/flower
-pkg_flower_fetch = git
-pkg_flower_repo = https://github.com/travelping/flower
-pkg_flower_commit = master
-
-PACKAGES += fn
-pkg_fn_name = fn
-pkg_fn_description = Function utilities for Erlang
-pkg_fn_homepage = https://github.com/reiddraper/fn
-pkg_fn_fetch = git
-pkg_fn_repo = https://github.com/reiddraper/fn
-pkg_fn_commit = master
-
-PACKAGES += folsom
-pkg_folsom_name = folsom
-pkg_folsom_description = Expose Erlang Events and Metrics
-pkg_folsom_homepage = https://github.com/boundary/folsom
-pkg_folsom_fetch = git
-pkg_folsom_repo = https://github.com/boundary/folsom
-pkg_folsom_commit = master
-
-PACKAGES += folsom_cowboy
-pkg_folsom_cowboy_name = folsom_cowboy
-pkg_folsom_cowboy_description = A Cowboy based Folsom HTTP Wrapper.
-pkg_folsom_cowboy_homepage = https://github.com/boundary/folsom_cowboy
-pkg_folsom_cowboy_fetch = git
-pkg_folsom_cowboy_repo = https://github.com/boundary/folsom_cowboy
-pkg_folsom_cowboy_commit = master
-
-PACKAGES += fs
-pkg_fs_name = fs
-pkg_fs_description = Erlang FileSystem Listener
-pkg_fs_homepage = https://github.com/synrc/fs
-pkg_fs_fetch = git
-pkg_fs_repo = https://github.com/synrc/fs
-pkg_fs_commit = master
-
-PACKAGES += fuse
-pkg_fuse_name = fuse
-pkg_fuse_description = A Circuit Breaker for Erlang
-pkg_fuse_homepage = https://github.com/jlouis/fuse
-pkg_fuse_fetch = git
-pkg_fuse_repo = https://github.com/jlouis/fuse
-pkg_fuse_commit = master
-
-PACKAGES += gcm
-pkg_gcm_name = gcm
-pkg_gcm_description = An Erlang application for Google Cloud Messaging
-pkg_gcm_homepage = https://github.com/pdincau/gcm-erlang
-pkg_gcm_fetch = git
-pkg_gcm_repo = https://github.com/pdincau/gcm-erlang
-pkg_gcm_commit = master
-
-PACKAGES += gcprof
-pkg_gcprof_name = gcprof
-pkg_gcprof_description = Garbage Collection profiler for Erlang
-pkg_gcprof_homepage = https://github.com/knutin/gcprof
-pkg_gcprof_fetch = git
-pkg_gcprof_repo = https://github.com/knutin/gcprof
-pkg_gcprof_commit = master
-
-PACKAGES += geas
-pkg_geas_name = geas
-pkg_geas_description = Guess Erlang Application Scattering
-pkg_geas_homepage = https://github.com/crownedgrouse/geas
-pkg_geas_fetch = git
-pkg_geas_repo = https://github.com/crownedgrouse/geas
-pkg_geas_commit = master
-
-PACKAGES += geef
-pkg_geef_name = geef
-pkg_geef_description = Git NEEEEF (Erlang NIF)
-pkg_geef_homepage = https://github.com/carlosmn/geef
-pkg_geef_fetch = git
-pkg_geef_repo = https://github.com/carlosmn/geef
-pkg_geef_commit = master
-
-PACKAGES += gen_coap
-pkg_gen_coap_name = gen_coap
-pkg_gen_coap_description = Generic Erlang CoAP Client/Server
-pkg_gen_coap_homepage = https://github.com/gotthardp/gen_coap
-pkg_gen_coap_fetch = git
-pkg_gen_coap_repo = https://github.com/gotthardp/gen_coap
-pkg_gen_coap_commit = master
-
-PACKAGES += gen_cycle
-pkg_gen_cycle_name = gen_cycle
-pkg_gen_cycle_description = Simple, generic OTP behaviour for recurring tasks
-pkg_gen_cycle_homepage = https://github.com/aerosol/gen_cycle
-pkg_gen_cycle_fetch = git
-pkg_gen_cycle_repo = https://github.com/aerosol/gen_cycle
-pkg_gen_cycle_commit = develop
-
-PACKAGES += gen_icmp
-pkg_gen_icmp_name = gen_icmp
-pkg_gen_icmp_description = Erlang interface to ICMP sockets
-pkg_gen_icmp_homepage = https://github.com/msantos/gen_icmp
-pkg_gen_icmp_fetch = git
-pkg_gen_icmp_repo = https://github.com/msantos/gen_icmp
-pkg_gen_icmp_commit = master
-
-PACKAGES += gen_leader
-pkg_gen_leader_name = gen_leader
-pkg_gen_leader_description = leader election behavior
-pkg_gen_leader_homepage = https://github.com/garret-smith/gen_leader_revival
-pkg_gen_leader_fetch = git
-pkg_gen_leader_repo = https://github.com/garret-smith/gen_leader_revival
-pkg_gen_leader_commit = master
-
-PACKAGES += gen_nb_server
-pkg_gen_nb_server_name = gen_nb_server
-pkg_gen_nb_server_description = OTP behavior for writing non-blocking servers
-pkg_gen_nb_server_homepage = https://github.com/kevsmith/gen_nb_server
-pkg_gen_nb_server_fetch = git
-pkg_gen_nb_server_repo = https://github.com/kevsmith/gen_nb_server
-pkg_gen_nb_server_commit = master
-
-PACKAGES += gen_paxos
-pkg_gen_paxos_name = gen_paxos
-pkg_gen_paxos_description = An Erlang/OTP-style implementation of the PAXOS distributed consensus protocol
-pkg_gen_paxos_homepage = https://github.com/gburd/gen_paxos
-pkg_gen_paxos_fetch = git
-pkg_gen_paxos_repo = https://github.com/gburd/gen_paxos
-pkg_gen_paxos_commit = master
-
-PACKAGES += gen_rpc
-pkg_gen_rpc_name = gen_rpc
-pkg_gen_rpc_description = A scalable RPC library for Erlang-VM based languages
-pkg_gen_rpc_homepage = https://github.com/priestjim/gen_rpc.git
-pkg_gen_rpc_fetch = git
-pkg_gen_rpc_repo = https://github.com/priestjim/gen_rpc.git
-pkg_gen_rpc_commit = master
-
-PACKAGES += gen_smtp
-pkg_gen_smtp_name = gen_smtp
-pkg_gen_smtp_description = A generic Erlang SMTP server and client that can be extended via callback modules
-pkg_gen_smtp_homepage = https://github.com/Vagabond/gen_smtp
-pkg_gen_smtp_fetch = git
-pkg_gen_smtp_repo = https://github.com/Vagabond/gen_smtp
-pkg_gen_smtp_commit = master
-
-PACKAGES += gen_tracker
-pkg_gen_tracker_name = gen_tracker
-pkg_gen_tracker_description = supervisor with ets handling of children and their metadata
-pkg_gen_tracker_homepage = https://github.com/erlyvideo/gen_tracker
-pkg_gen_tracker_fetch = git
-pkg_gen_tracker_repo = https://github.com/erlyvideo/gen_tracker
-pkg_gen_tracker_commit = master
-
-PACKAGES += gen_unix
-pkg_gen_unix_name = gen_unix
-pkg_gen_unix_description = Erlang Unix socket interface
-pkg_gen_unix_homepage = https://github.com/msantos/gen_unix
-pkg_gen_unix_fetch = git
-pkg_gen_unix_repo = https://github.com/msantos/gen_unix
-pkg_gen_unix_commit = master
-
-PACKAGES += geode
-pkg_geode_name = geode
-pkg_geode_description = geohash/proximity lookup in pure, uncut erlang.
-pkg_geode_homepage = https://github.com/bradfordw/geode
-pkg_geode_fetch = git
-pkg_geode_repo = https://github.com/bradfordw/geode
-pkg_geode_commit = master
-
-PACKAGES += getopt
-pkg_getopt_name = getopt
-pkg_getopt_description = Module to parse command line arguments using the GNU getopt syntax
-pkg_getopt_homepage = https://github.com/jcomellas/getopt
-pkg_getopt_fetch = git
-pkg_getopt_repo = https://github.com/jcomellas/getopt
-pkg_getopt_commit = master
-
-PACKAGES += gettext
-pkg_gettext_name = gettext
-pkg_gettext_description = Erlang internationalization library.
-pkg_gettext_homepage = https://github.com/etnt/gettext
-pkg_gettext_fetch = git
-pkg_gettext_repo = https://github.com/etnt/gettext
-pkg_gettext_commit = master
-
-PACKAGES += giallo
-pkg_giallo_name = giallo
-pkg_giallo_description = Small and flexible web framework on top of Cowboy
-pkg_giallo_homepage = https://github.com/kivra/giallo
-pkg_giallo_fetch = git
-pkg_giallo_repo = https://github.com/kivra/giallo
-pkg_giallo_commit = master
-
-PACKAGES += gin
-pkg_gin_name = gin
-pkg_gin_description = The guards and for Erlang parse_transform
-pkg_gin_homepage = https://github.com/mad-cocktail/gin
-pkg_gin_fetch = git
-pkg_gin_repo = https://github.com/mad-cocktail/gin
-pkg_gin_commit = master
-
-PACKAGES += gitty
-pkg_gitty_name = gitty
-pkg_gitty_description = Git access in erlang
-pkg_gitty_homepage = https://github.com/maxlapshin/gitty
-pkg_gitty_fetch = git
-pkg_gitty_repo = https://github.com/maxlapshin/gitty
-pkg_gitty_commit = master
-
PACKAGES += gpb
pkg_gpb_name = gpb
pkg_gpb_description = A Google Protobuf implementation for Erlang
@@ -1779,38 +331,6 @@ pkg_gpb_fetch = git
pkg_gpb_repo = https://github.com/tomas-abrahamsson/gpb
pkg_gpb_commit = master
-PACKAGES += gproc
-pkg_gproc_name = gproc
-pkg_gproc_description = Extended process registry for Erlang
-pkg_gproc_homepage = https://github.com/uwiger/gproc
-pkg_gproc_fetch = git
-pkg_gproc_repo = https://github.com/uwiger/gproc
-pkg_gproc_commit = master
-
-PACKAGES += grapherl
-pkg_grapherl_name = grapherl
-pkg_grapherl_description = Create graphs of Erlang systems and programs
-pkg_grapherl_homepage = https://github.com/eproxus/grapherl
-pkg_grapherl_fetch = git
-pkg_grapherl_repo = https://github.com/eproxus/grapherl
-pkg_grapherl_commit = master
-
-PACKAGES += grpc
-pkg_grpc_name = grpc
-pkg_grpc_description = gRPC server in Erlang
-pkg_grpc_homepage = https://github.com/Bluehouse-Technology/grpc
-pkg_grpc_fetch = git
-pkg_grpc_repo = https://github.com/Bluehouse-Technology/grpc
-pkg_grpc_commit = master
-
-PACKAGES += grpc_client
-pkg_grpc_client_name = grpc_client
-pkg_grpc_client_description = gRPC client in Erlang
-pkg_grpc_client_homepage = https://github.com/Bluehouse-Technology/grpc_client
-pkg_grpc_client_fetch = git
-pkg_grpc_client_repo = https://github.com/Bluehouse-Technology/grpc_client
-pkg_grpc_client_commit = master
-
PACKAGES += gun
pkg_gun_name = gun
pkg_gun_description = Asynchronous SPDY, HTTP and Websocket client written in Erlang.
@@ -1819,861 +339,14 @@ pkg_gun_fetch = git
pkg_gun_repo = https://github.com/ninenines/gun
pkg_gun_commit = master
-PACKAGES += hackney
-pkg_hackney_name = hackney
-pkg_hackney_description = simple HTTP client in Erlang
-pkg_hackney_homepage = https://github.com/benoitc/hackney
-pkg_hackney_fetch = git
-pkg_hackney_repo = https://github.com/benoitc/hackney
-pkg_hackney_commit = master
-
-PACKAGES += hamcrest
-pkg_hamcrest_name = hamcrest
-pkg_hamcrest_description = Erlang port of Hamcrest
-pkg_hamcrest_homepage = https://github.com/hyperthunk/hamcrest-erlang
-pkg_hamcrest_fetch = git
-pkg_hamcrest_repo = https://github.com/hyperthunk/hamcrest-erlang
-pkg_hamcrest_commit = master
-
-PACKAGES += hottub
-pkg_hottub_name = hottub
-pkg_hottub_description = Permanent Erlang Worker Pool
-pkg_hottub_homepage = https://github.com/bfrog/hottub
-pkg_hottub_fetch = git
-pkg_hottub_repo = https://github.com/bfrog/hottub
-pkg_hottub_commit = master
-
-PACKAGES += hpack
-pkg_hpack_name = hpack
-pkg_hpack_description = HPACK Implementation for Erlang
-pkg_hpack_homepage = https://github.com/joedevivo/hpack
-pkg_hpack_fetch = git
-pkg_hpack_repo = https://github.com/joedevivo/hpack
-pkg_hpack_commit = master
-
-PACKAGES += hyper
-pkg_hyper_name = hyper
-pkg_hyper_description = Erlang implementation of HyperLogLog
-pkg_hyper_homepage = https://github.com/GameAnalytics/hyper
-pkg_hyper_fetch = git
-pkg_hyper_repo = https://github.com/GameAnalytics/hyper
-pkg_hyper_commit = master
-
-PACKAGES += i18n
-pkg_i18n_name = i18n
-pkg_i18n_description = International components for unicode from Erlang (unicode, date, string, number, format, locale, localization, transliteration, icu4e)
-pkg_i18n_homepage = https://github.com/erlang-unicode/i18n
-pkg_i18n_fetch = git
-pkg_i18n_repo = https://github.com/erlang-unicode/i18n
-pkg_i18n_commit = master
-
-PACKAGES += ibrowse
-pkg_ibrowse_name = ibrowse
-pkg_ibrowse_description = Erlang HTTP client
-pkg_ibrowse_homepage = https://github.com/cmullaparthi/ibrowse
-pkg_ibrowse_fetch = git
-pkg_ibrowse_repo = https://github.com/cmullaparthi/ibrowse
-pkg_ibrowse_commit = master
-
-PACKAGES += idna
-pkg_idna_name = idna
-pkg_idna_description = Erlang IDNA lib
-pkg_idna_homepage = https://github.com/benoitc/erlang-idna
-pkg_idna_fetch = git
-pkg_idna_repo = https://github.com/benoitc/erlang-idna
-pkg_idna_commit = master
-
-PACKAGES += irc_lib
-pkg_irc_lib_name = irc_lib
-pkg_irc_lib_description = Erlang irc client library
-pkg_irc_lib_homepage = https://github.com/OtpChatBot/irc_lib
-pkg_irc_lib_fetch = git
-pkg_irc_lib_repo = https://github.com/OtpChatBot/irc_lib
-pkg_irc_lib_commit = master
-
-PACKAGES += ircd
-pkg_ircd_name = ircd
-pkg_ircd_description = A pluggable IRC daemon application/library for Erlang.
-pkg_ircd_homepage = https://github.com/tonyg/erlang-ircd
-pkg_ircd_fetch = git
-pkg_ircd_repo = https://github.com/tonyg/erlang-ircd
-pkg_ircd_commit = master
-
-PACKAGES += iris
-pkg_iris_name = iris
-pkg_iris_description = Iris Erlang binding
-pkg_iris_homepage = https://github.com/project-iris/iris-erl
-pkg_iris_fetch = git
-pkg_iris_repo = https://github.com/project-iris/iris-erl
-pkg_iris_commit = master
-
-PACKAGES += iso8601
-pkg_iso8601_name = iso8601
-pkg_iso8601_description = Erlang ISO 8601 date formatter/parser
-pkg_iso8601_homepage = https://github.com/seansawyer/erlang_iso8601
-pkg_iso8601_fetch = git
-pkg_iso8601_repo = https://github.com/seansawyer/erlang_iso8601
-pkg_iso8601_commit = master
-
-PACKAGES += jamdb_sybase
-pkg_jamdb_sybase_name = jamdb_sybase
-pkg_jamdb_sybase_description = Erlang driver for SAP Sybase ASE
-pkg_jamdb_sybase_homepage = https://github.com/erlangbureau/jamdb_sybase
-pkg_jamdb_sybase_fetch = git
-pkg_jamdb_sybase_repo = https://github.com/erlangbureau/jamdb_sybase
-pkg_jamdb_sybase_commit = master
-
-PACKAGES += jesse
-pkg_jesse_name = jesse
-pkg_jesse_description = jesse (JSon Schema Erlang) is an implementation of a json schema validator for Erlang.
-pkg_jesse_homepage = https://github.com/for-GET/jesse
-pkg_jesse_fetch = git
-pkg_jesse_repo = https://github.com/for-GET/jesse
-pkg_jesse_commit = master
-
-PACKAGES += jiffy
-pkg_jiffy_name = jiffy
-pkg_jiffy_description = JSON NIFs for Erlang.
-pkg_jiffy_homepage = https://github.com/davisp/jiffy
-pkg_jiffy_fetch = git
-pkg_jiffy_repo = https://github.com/davisp/jiffy
-pkg_jiffy_commit = master
-
-PACKAGES += jiffy_v
-pkg_jiffy_v_name = jiffy_v
-pkg_jiffy_v_description = JSON validation utility
-pkg_jiffy_v_homepage = https://github.com/shizzard/jiffy-v
-pkg_jiffy_v_fetch = git
-pkg_jiffy_v_repo = https://github.com/shizzard/jiffy-v
-pkg_jiffy_v_commit = master
-
-PACKAGES += jobs
-pkg_jobs_name = jobs
-pkg_jobs_description = Job scheduler for load regulation
-pkg_jobs_homepage = https://github.com/uwiger/jobs
-pkg_jobs_fetch = git
-pkg_jobs_repo = https://github.com/uwiger/jobs
-pkg_jobs_commit = master
-
-PACKAGES += joxa
-pkg_joxa_name = joxa
-pkg_joxa_description = A Modern Lisp for the Erlang VM
-pkg_joxa_homepage = https://github.com/joxa/joxa
-pkg_joxa_fetch = git
-pkg_joxa_repo = https://github.com/joxa/joxa
-pkg_joxa_commit = master
-
-PACKAGES += json_rec
-pkg_json_rec_name = json_rec
-pkg_json_rec_description = JSON to erlang record
-pkg_json_rec_homepage = https://github.com/justinkirby/json_rec
-pkg_json_rec_fetch = git
-pkg_json_rec_repo = https://github.com/justinkirby/json_rec
-pkg_json_rec_commit = master
-
-PACKAGES += jsone
-pkg_jsone_name = jsone
-pkg_jsone_description = An Erlang library for encoding, decoding JSON data.
-pkg_jsone_homepage = https://github.com/sile/jsone.git
-pkg_jsone_fetch = git
-pkg_jsone_repo = https://github.com/sile/jsone.git
-pkg_jsone_commit = master
-
-PACKAGES += jsonpath
-pkg_jsonpath_name = jsonpath
-pkg_jsonpath_description = Fast Erlang JSON data retrieval and updates via javascript-like notation
-pkg_jsonpath_homepage = https://github.com/GeneStevens/jsonpath
-pkg_jsonpath_fetch = git
-pkg_jsonpath_repo = https://github.com/GeneStevens/jsonpath
-pkg_jsonpath_commit = master
-
-PACKAGES += jsonx
-pkg_jsonx_name = jsonx
-pkg_jsonx_description = JSONX is an Erlang library for efficient decode and encode JSON, written in C.
-pkg_jsonx_homepage = https://github.com/iskra/jsonx
-pkg_jsonx_fetch = git
-pkg_jsonx_repo = https://github.com/iskra/jsonx
-pkg_jsonx_commit = master
-
-PACKAGES += jsx
-pkg_jsx_name = jsx
-pkg_jsx_description = An Erlang application for consuming, producing and manipulating JSON.
-pkg_jsx_homepage = https://github.com/talentdeficit/jsx
-pkg_jsx_fetch = git
-pkg_jsx_repo = https://github.com/talentdeficit/jsx
-pkg_jsx_commit = main
-
-PACKAGES += kafka_protocol
-pkg_kafka_protocol_name = kafka_protocol
-pkg_kafka_protocol_description = Kafka protocol Erlang library
-pkg_kafka_protocol_homepage = https://github.com/kafka4beam/kafka_protocol
-pkg_kafka_protocol_fetch = git
-pkg_kafka_protocol_repo = https://github.com/kafka4beam/kafka_protocol
-pkg_kafka_protocol_commit = master
-
-PACKAGES += kai
-pkg_kai_name = kai
-pkg_kai_description = DHT storage by Takeshi Inoue
-pkg_kai_homepage = https://github.com/synrc/kai
-pkg_kai_fetch = git
-pkg_kai_repo = https://github.com/synrc/kai
-pkg_kai_commit = master
-
-PACKAGES += katja
-pkg_katja_name = katja
-pkg_katja_description = A simple Riemann client written in Erlang.
-pkg_katja_homepage = https://github.com/nifoc/katja
-pkg_katja_fetch = git
-pkg_katja_repo = https://github.com/nifoc/katja
-pkg_katja_commit = master
-
-PACKAGES += key2value
-pkg_key2value_name = key2value
-pkg_key2value_description = Erlang 2-way map
-pkg_key2value_homepage = https://github.com/okeuday/key2value
-pkg_key2value_fetch = git
-pkg_key2value_repo = https://github.com/okeuday/key2value
-pkg_key2value_commit = master
-
-PACKAGES += keys1value
-pkg_keys1value_name = keys1value
-pkg_keys1value_description = Erlang set associative map for key lists
-pkg_keys1value_homepage = https://github.com/okeuday/keys1value
-pkg_keys1value_fetch = git
-pkg_keys1value_repo = https://github.com/okeuday/keys1value
-pkg_keys1value_commit = master
-
-PACKAGES += kinetic
-pkg_kinetic_name = kinetic
-pkg_kinetic_description = Erlang Kinesis Client
-pkg_kinetic_homepage = https://github.com/AdRoll/kinetic
-pkg_kinetic_fetch = git
-pkg_kinetic_repo = https://github.com/AdRoll/kinetic
-pkg_kinetic_commit = main
-
-PACKAGES += kjell
-pkg_kjell_name = kjell
-pkg_kjell_description = Erlang Shell
-pkg_kjell_homepage = https://github.com/karlll/kjell
-pkg_kjell_fetch = git
-pkg_kjell_repo = https://github.com/karlll/kjell
-pkg_kjell_commit = master
-
-PACKAGES += kraken
-pkg_kraken_name = kraken
-pkg_kraken_description = Distributed Pubsub Server for Realtime Apps
-pkg_kraken_homepage = https://github.com/Asana/kraken
-pkg_kraken_fetch = git
-pkg_kraken_repo = https://github.com/Asana/kraken
-pkg_kraken_commit = master
-
-PACKAGES += kucumberl
-pkg_kucumberl_name = kucumberl
-pkg_kucumberl_description = A pure-erlang, open-source, implementation of Cucumber
-pkg_kucumberl_homepage = https://github.com/openshine/kucumberl
-pkg_kucumberl_fetch = git
-pkg_kucumberl_repo = https://github.com/openshine/kucumberl
-pkg_kucumberl_commit = master
-
-PACKAGES += kvc
-pkg_kvc_name = kvc
-pkg_kvc_description = KVC - Key Value Coding for Erlang data structures
-pkg_kvc_homepage = https://github.com/etrepum/kvc
-pkg_kvc_fetch = git
-pkg_kvc_repo = https://github.com/etrepum/kvc
-pkg_kvc_commit = master
-
-PACKAGES += kvlists
-pkg_kvlists_name = kvlists
-pkg_kvlists_description = Lists of key-value pairs (decoded JSON) in Erlang
-pkg_kvlists_homepage = https://github.com/jcomellas/kvlists
-pkg_kvlists_fetch = git
-pkg_kvlists_repo = https://github.com/jcomellas/kvlists
-pkg_kvlists_commit = master
-
-PACKAGES += kvs
-pkg_kvs_name = kvs
-pkg_kvs_description = Container and Iterator
-pkg_kvs_homepage = https://github.com/synrc/kvs
-pkg_kvs_fetch = git
-pkg_kvs_repo = https://github.com/synrc/kvs
-pkg_kvs_commit = master
-
-PACKAGES += lager
-pkg_lager_name = lager
-pkg_lager_description = A logging framework for Erlang/OTP.
-pkg_lager_homepage = https://github.com/erlang-lager/lager
-pkg_lager_fetch = git
-pkg_lager_repo = https://github.com/erlang-lager/lager
-pkg_lager_commit = master
-
-PACKAGES += lager_syslog
-pkg_lager_syslog_name = lager_syslog
-pkg_lager_syslog_description = Syslog backend for lager
-pkg_lager_syslog_homepage = https://github.com/erlang-lager/lager_syslog
-pkg_lager_syslog_fetch = git
-pkg_lager_syslog_repo = https://github.com/erlang-lager/lager_syslog
-pkg_lager_syslog_commit = master
-
-PACKAGES += lasse
-pkg_lasse_name = lasse
-pkg_lasse_description = SSE handler for Cowboy
-pkg_lasse_homepage = https://github.com/inaka/lasse
-pkg_lasse_fetch = git
-pkg_lasse_repo = https://github.com/inaka/lasse
-pkg_lasse_commit = master
-
-PACKAGES += ldap
-pkg_ldap_name = ldap
-pkg_ldap_description = LDAP server written in Erlang
-pkg_ldap_homepage = https://github.com/spawnproc/ldap
-pkg_ldap_fetch = git
-pkg_ldap_repo = https://github.com/spawnproc/ldap
-pkg_ldap_commit = master
-
-PACKAGES += lfe
-pkg_lfe_name = lfe
-pkg_lfe_description = Lisp Flavoured Erlang (LFE)
-pkg_lfe_homepage = https://github.com/rvirding/lfe
-pkg_lfe_fetch = git
-pkg_lfe_repo = https://github.com/rvirding/lfe
-pkg_lfe_commit = master
-
-PACKAGES += live
-pkg_live_name = live
-pkg_live_description = Automated module and configuration reloader.
-pkg_live_homepage = http://ninenines.eu
-pkg_live_fetch = git
-pkg_live_repo = https://github.com/ninenines/live
-pkg_live_commit = master
-
-PACKAGES += locker
-pkg_locker_name = locker
-pkg_locker_description = Atomic distributed 'check and set' for short-lived keys
-pkg_locker_homepage = https://github.com/wooga/locker
-pkg_locker_fetch = git
-pkg_locker_repo = https://github.com/wooga/locker
-pkg_locker_commit = master
-
-PACKAGES += locks
-pkg_locks_name = locks
-pkg_locks_description = A scalable, deadlock-resolving resource locker
-pkg_locks_homepage = https://github.com/uwiger/locks
-pkg_locks_fetch = git
-pkg_locks_repo = https://github.com/uwiger/locks
-pkg_locks_commit = master
-
-PACKAGES += log4erl
-pkg_log4erl_name = log4erl
-pkg_log4erl_description = A logger for erlang in the spirit of Log4J.
-pkg_log4erl_homepage = https://github.com/ahmednawras/log4erl
-pkg_log4erl_fetch = git
-pkg_log4erl_repo = https://github.com/ahmednawras/log4erl
-pkg_log4erl_commit = master
-
-PACKAGES += lol
-pkg_lol_name = lol
-pkg_lol_description = Lisp on erLang, and programming is fun again
-pkg_lol_homepage = https://github.com/b0oh/lol
-pkg_lol_fetch = git
-pkg_lol_repo = https://github.com/b0oh/lol
-pkg_lol_commit = master
-
-PACKAGES += lucid
-pkg_lucid_name = lucid
-pkg_lucid_description = HTTP/2 server written in Erlang
-pkg_lucid_homepage = https://github.com/tatsuhiro-t/lucid
-pkg_lucid_fetch = git
-pkg_lucid_repo = https://github.com/tatsuhiro-t/lucid
-pkg_lucid_commit = master
-
-PACKAGES += luerl
-pkg_luerl_name = luerl
-pkg_luerl_description = Lua in Erlang
-pkg_luerl_homepage = https://github.com/rvirding/luerl
-pkg_luerl_fetch = git
-pkg_luerl_repo = https://github.com/rvirding/luerl
-pkg_luerl_commit = develop
-
-PACKAGES += lux
-pkg_lux_name = lux
-pkg_lux_description = Lux (LUcid eXpect scripting) simplifies test automation and provides an Expect-style execution of commands
-pkg_lux_homepage = https://github.com/hawk/lux
-pkg_lux_fetch = git
-pkg_lux_repo = https://github.com/hawk/lux
-pkg_lux_commit = master
-
-PACKAGES += mad
-pkg_mad_name = mad
-pkg_mad_description = Small and Fast Rebar Replacement
-pkg_mad_homepage = https://github.com/synrc/mad
-pkg_mad_fetch = git
-pkg_mad_repo = https://github.com/synrc/mad
-pkg_mad_commit = master
-
-PACKAGES += marina
-pkg_marina_name = marina
-pkg_marina_description = Non-blocking Erlang Cassandra CQL3 client
-pkg_marina_homepage = https://github.com/lpgauth/marina
-pkg_marina_fetch = git
-pkg_marina_repo = https://github.com/lpgauth/marina
-pkg_marina_commit = master
-
-PACKAGES += mavg
-pkg_mavg_name = mavg
-pkg_mavg_description = Erlang :: Exponential moving average library
-pkg_mavg_homepage = https://github.com/EchoTeam/mavg
-pkg_mavg_fetch = git
-pkg_mavg_repo = https://github.com/EchoTeam/mavg
-pkg_mavg_commit = master
-
-PACKAGES += meck
-pkg_meck_name = meck
-pkg_meck_description = A mocking library for Erlang
-pkg_meck_homepage = https://github.com/eproxus/meck
-pkg_meck_fetch = git
-pkg_meck_repo = https://github.com/eproxus/meck
-pkg_meck_commit = master
-
-PACKAGES += mekao
-pkg_mekao_name = mekao
-pkg_mekao_description = SQL constructor
-pkg_mekao_homepage = https://github.com/ddosia/mekao
-pkg_mekao_fetch = git
-pkg_mekao_repo = https://github.com/ddosia/mekao
-pkg_mekao_commit = master
-
-PACKAGES += merl
-pkg_merl_name = merl
-pkg_merl_description = Metaprogramming in Erlang
-pkg_merl_homepage = https://github.com/richcarl/merl
-pkg_merl_fetch = git
-pkg_merl_repo = https://github.com/richcarl/merl
-pkg_merl_commit = master
-
-PACKAGES += mimerl
-pkg_mimerl_name = mimerl
-pkg_mimerl_description = library to handle mimetypes
-pkg_mimerl_homepage = https://github.com/benoitc/mimerl
-pkg_mimerl_fetch = git
-pkg_mimerl_repo = https://github.com/benoitc/mimerl
-pkg_mimerl_commit = master
-
-PACKAGES += mimetypes
-pkg_mimetypes_name = mimetypes
-pkg_mimetypes_description = Erlang MIME types library
-pkg_mimetypes_homepage = https://github.com/spawngrid/mimetypes
-pkg_mimetypes_fetch = git
-pkg_mimetypes_repo = https://github.com/spawngrid/mimetypes
-pkg_mimetypes_commit = master
-
-PACKAGES += mixer
-pkg_mixer_name = mixer
-pkg_mixer_description = Mix in functions from other modules
-pkg_mixer_homepage = https://github.com/chef/mixer
-pkg_mixer_fetch = git
-pkg_mixer_repo = https://github.com/chef/mixer
-pkg_mixer_commit = main
-
-PACKAGES += mochiweb
-pkg_mochiweb_name = mochiweb
-pkg_mochiweb_description = MochiWeb is an Erlang library for building lightweight HTTP servers.
-pkg_mochiweb_homepage = https://github.com/mochi/mochiweb
-pkg_mochiweb_fetch = git
-pkg_mochiweb_repo = https://github.com/mochi/mochiweb
-pkg_mochiweb_commit = main
-
-PACKAGES += mochiweb_xpath
-pkg_mochiweb_xpath_name = mochiweb_xpath
-pkg_mochiweb_xpath_description = XPath support for mochiweb's html parser
-pkg_mochiweb_xpath_homepage = https://github.com/retnuh/mochiweb_xpath
-pkg_mochiweb_xpath_fetch = git
-pkg_mochiweb_xpath_repo = https://github.com/retnuh/mochiweb_xpath
-pkg_mochiweb_xpath_commit = master
-
-PACKAGES += mockgyver
-pkg_mockgyver_name = mockgyver
-pkg_mockgyver_description = A mocking library for Erlang
-pkg_mockgyver_homepage = https://github.com/klajo/mockgyver
-pkg_mockgyver_fetch = git
-pkg_mockgyver_repo = https://github.com/klajo/mockgyver
-pkg_mockgyver_commit = master
-
-PACKAGES += modlib
-pkg_modlib_name = modlib
-pkg_modlib_description = Web framework based on Erlang's inets httpd
-pkg_modlib_homepage = https://github.com/gar1t/modlib
-pkg_modlib_fetch = git
-pkg_modlib_repo = https://github.com/gar1t/modlib
-pkg_modlib_commit = master
-
-PACKAGES += mongodb
-pkg_mongodb_name = mongodb
-pkg_mongodb_description = MongoDB driver for Erlang
-pkg_mongodb_homepage = https://github.com/comtihon/mongodb-erlang
-pkg_mongodb_fetch = git
-pkg_mongodb_repo = https://github.com/comtihon/mongodb-erlang
-pkg_mongodb_commit = master
-
-PACKAGES += mongooseim
-pkg_mongooseim_name = mongooseim
-pkg_mongooseim_description = Jabber / XMPP server with focus on performance and scalability, by Erlang Solutions
-pkg_mongooseim_homepage = https://www.erlang-solutions.com/products/mongooseim-massively-scalable-ejabberd-platform
-pkg_mongooseim_fetch = git
-pkg_mongooseim_repo = https://github.com/esl/MongooseIM
-pkg_mongooseim_commit = master
-
-PACKAGES += moyo
-pkg_moyo_name = moyo
-pkg_moyo_description = Erlang utility functions library
-pkg_moyo_homepage = https://github.com/dwango/moyo
-pkg_moyo_fetch = git
-pkg_moyo_repo = https://github.com/dwango/moyo
-pkg_moyo_commit = master
-
-PACKAGES += msgpack
-pkg_msgpack_name = msgpack
-pkg_msgpack_description = MessagePack (de)serializer implementation for Erlang
-pkg_msgpack_homepage = https://github.com/msgpack/msgpack-erlang
-pkg_msgpack_fetch = git
-pkg_msgpack_repo = https://github.com/msgpack/msgpack-erlang
-pkg_msgpack_commit = master
-
-PACKAGES += mu2
-pkg_mu2_name = mu2
-pkg_mu2_description = Erlang mutation testing tool
-pkg_mu2_homepage = https://github.com/ramsay-t/mu2
-pkg_mu2_fetch = git
-pkg_mu2_repo = https://github.com/ramsay-t/mu2
-pkg_mu2_commit = master
-
-PACKAGES += mustache
-pkg_mustache_name = mustache
-pkg_mustache_description = Mustache template engine for Erlang.
-pkg_mustache_homepage = https://github.com/mojombo/mustache.erl
-pkg_mustache_fetch = git
-pkg_mustache_repo = https://github.com/mojombo/mustache.erl
-pkg_mustache_commit = master
-
-PACKAGES += myproto
-pkg_myproto_name = myproto
-pkg_myproto_description = MySQL Server Protocol in Erlang
-pkg_myproto_homepage = https://github.com/altenwald/myproto
-pkg_myproto_fetch = git
-pkg_myproto_repo = https://github.com/altenwald/myproto
-pkg_myproto_commit = master
-
-PACKAGES += mysql
-pkg_mysql_name = mysql
-pkg_mysql_description = MySQL client library for Erlang/OTP
-pkg_mysql_homepage = https://github.com/mysql-otp/mysql-otp
-pkg_mysql_fetch = git
-pkg_mysql_repo = https://github.com/mysql-otp/mysql-otp
-pkg_mysql_commit = 1.7.0
-
-PACKAGES += n2o
-pkg_n2o_name = n2o
-pkg_n2o_description = WebSocket Application Server
-pkg_n2o_homepage = https://github.com/5HT/n2o
-pkg_n2o_fetch = git
-pkg_n2o_repo = https://github.com/5HT/n2o
-pkg_n2o_commit = master
-
-PACKAGES += nat_upnp
-pkg_nat_upnp_name = nat_upnp
-pkg_nat_upnp_description = Erlang library to map your internal port to an external using UNP IGD
-pkg_nat_upnp_homepage = https://github.com/benoitc/nat_upnp
-pkg_nat_upnp_fetch = git
-pkg_nat_upnp_repo = https://github.com/benoitc/nat_upnp
-pkg_nat_upnp_commit = master
-
-PACKAGES += neo4j
-pkg_neo4j_name = neo4j
-pkg_neo4j_description = Erlang client library for Neo4J.
-pkg_neo4j_homepage = https://github.com/dmitriid/neo4j-erlang
-pkg_neo4j_fetch = git
-pkg_neo4j_repo = https://github.com/dmitriid/neo4j-erlang
-pkg_neo4j_commit = master
-
-PACKAGES += neotoma
-pkg_neotoma_name = neotoma
-pkg_neotoma_description = Erlang library and packrat parser-generator for parsing expression grammars.
-pkg_neotoma_homepage = https://github.com/seancribbs/neotoma
-pkg_neotoma_fetch = git
-pkg_neotoma_repo = https://github.com/seancribbs/neotoma
-pkg_neotoma_commit = master
-
-PACKAGES += nifty
-pkg_nifty_name = nifty
-pkg_nifty_description = Erlang NIF wrapper generator
-pkg_nifty_homepage = https://github.com/parapluu/nifty
-pkg_nifty_fetch = git
-pkg_nifty_repo = https://github.com/parapluu/nifty
-pkg_nifty_commit = master
-
-PACKAGES += nitrogen_core
-pkg_nitrogen_core_name = nitrogen_core
-pkg_nitrogen_core_description = The core Nitrogen library.
-pkg_nitrogen_core_homepage = http://nitrogenproject.com/
-pkg_nitrogen_core_fetch = git
-pkg_nitrogen_core_repo = https://github.com/nitrogen/nitrogen_core
-pkg_nitrogen_core_commit = master
-
-PACKAGES += nkpacket
-pkg_nkpacket_name = nkpacket
-pkg_nkpacket_description = Generic Erlang transport layer
-pkg_nkpacket_homepage = https://github.com/Nekso/nkpacket
-pkg_nkpacket_fetch = git
-pkg_nkpacket_repo = https://github.com/Nekso/nkpacket
-pkg_nkpacket_commit = master
-
-PACKAGES += nksip
-pkg_nksip_name = nksip
-pkg_nksip_description = Erlang SIP application server
-pkg_nksip_homepage = https://github.com/kalta/nksip
-pkg_nksip_fetch = git
-pkg_nksip_repo = https://github.com/kalta/nksip
-pkg_nksip_commit = master
-
-PACKAGES += nodefinder
-pkg_nodefinder_name = nodefinder
-pkg_nodefinder_description = automatic node discovery via UDP multicast
-pkg_nodefinder_homepage = https://github.com/erlanger/nodefinder
-pkg_nodefinder_fetch = git
-pkg_nodefinder_repo = https://github.com/okeuday/nodefinder
-pkg_nodefinder_commit = master
-
-PACKAGES += nprocreg
-pkg_nprocreg_name = nprocreg
-pkg_nprocreg_description = Minimal Distributed Erlang Process Registry
-pkg_nprocreg_homepage = http://nitrogenproject.com/
-pkg_nprocreg_fetch = git
-pkg_nprocreg_repo = https://github.com/nitrogen/nprocreg
-pkg_nprocreg_commit = master
-
-PACKAGES += oauth
-pkg_oauth_name = oauth
-pkg_oauth_description = An Erlang OAuth 1.0 implementation
-pkg_oauth_homepage = https://github.com/tim/erlang-oauth
-pkg_oauth_fetch = git
-pkg_oauth_repo = https://github.com/tim/erlang-oauth
-pkg_oauth_commit = main
-
-PACKAGES += oauth2
-pkg_oauth2_name = oauth2
-pkg_oauth2_description = Erlang Oauth2 implementation
-pkg_oauth2_homepage = https://github.com/kivra/oauth2
-pkg_oauth2_fetch = git
-pkg_oauth2_repo = https://github.com/kivra/oauth2
-pkg_oauth2_commit = master
-
-PACKAGES += observer_cli
-pkg_observer_cli_name = observer_cli
-pkg_observer_cli_description = Visualize Erlang/Elixir Nodes On The Command Line
-pkg_observer_cli_homepage = http://zhongwencool.github.io/observer_cli
-pkg_observer_cli_fetch = git
-pkg_observer_cli_repo = https://github.com/zhongwencool/observer_cli
-pkg_observer_cli_commit = master
-
-PACKAGES += octopus
-pkg_octopus_name = octopus
-pkg_octopus_description = Small and flexible pool manager written in Erlang
-pkg_octopus_homepage = https://github.com/erlangbureau/octopus
-pkg_octopus_fetch = git
-pkg_octopus_repo = https://github.com/erlangbureau/octopus
-pkg_octopus_commit = master
-
-PACKAGES += openflow
-pkg_openflow_name = openflow
-pkg_openflow_description = An OpenFlow controller written in pure erlang
-pkg_openflow_homepage = https://github.com/renatoaguiar/erlang-openflow
-pkg_openflow_fetch = git
-pkg_openflow_repo = https://github.com/renatoaguiar/erlang-openflow
-pkg_openflow_commit = master
-
-PACKAGES += openid
-pkg_openid_name = openid
-pkg_openid_description = Erlang OpenID
-pkg_openid_homepage = https://github.com/brendonh/erl_openid
-pkg_openid_fetch = git
-pkg_openid_repo = https://github.com/brendonh/erl_openid
-pkg_openid_commit = master
-
-PACKAGES += openpoker
-pkg_openpoker_name = openpoker
-pkg_openpoker_description = Genesis Texas hold'em Game Server
-pkg_openpoker_homepage = https://github.com/hpyhacking/openpoker
-pkg_openpoker_fetch = git
-pkg_openpoker_repo = https://github.com/hpyhacking/openpoker
-pkg_openpoker_commit = master
-
-PACKAGES += otpbp
-pkg_otpbp_name = otpbp
-pkg_otpbp_description = Parse transformer for use new OTP functions in old Erlang/OTP releases (R15, R16, 17, 18, 19)
-pkg_otpbp_homepage = https://github.com/Ledest/otpbp
-pkg_otpbp_fetch = git
-pkg_otpbp_repo = https://github.com/Ledest/otpbp
-pkg_otpbp_commit = master
-
-PACKAGES += pal
-pkg_pal_name = pal
-pkg_pal_description = Pragmatic Authentication Library
-pkg_pal_homepage = https://github.com/manifest/pal
-pkg_pal_fetch = git
-pkg_pal_repo = https://github.com/manifest/pal
-pkg_pal_commit = master
-
-PACKAGES += parse_trans
-pkg_parse_trans_name = parse_trans
-pkg_parse_trans_description = Parse transform utilities for Erlang
-pkg_parse_trans_homepage = https://github.com/uwiger/parse_trans
-pkg_parse_trans_fetch = git
-pkg_parse_trans_repo = https://github.com/uwiger/parse_trans
-pkg_parse_trans_commit = master
-
-PACKAGES += parsexml
-pkg_parsexml_name = parsexml
-pkg_parsexml_description = Simple DOM XML parser with convenient and very simple API
-pkg_parsexml_homepage = https://github.com/maxlapshin/parsexml
-pkg_parsexml_fetch = git
-pkg_parsexml_repo = https://github.com/maxlapshin/parsexml
-pkg_parsexml_commit = master
-
-PACKAGES += partisan
-pkg_partisan_name = partisan
-pkg_partisan_description = High-performance, high-scalability distributed computing with Erlang and Elixir.
-pkg_partisan_homepage = http://partisan.cloud
-pkg_partisan_fetch = git
-pkg_partisan_repo = https://github.com/lasp-lang/partisan
-pkg_partisan_commit = master
-
-PACKAGES += pegjs
-pkg_pegjs_name = pegjs
-pkg_pegjs_description = An implementation of PEG.js grammar for Erlang.
-pkg_pegjs_homepage = https://github.com/dmitriid/pegjs
-pkg_pegjs_fetch = git
-pkg_pegjs_repo = https://github.com/dmitriid/pegjs
-pkg_pegjs_commit = master
-
-PACKAGES += percept2
-pkg_percept2_name = percept2
-pkg_percept2_description = Concurrent profiling tool for Erlang
-pkg_percept2_homepage = https://github.com/huiqing/percept2
-pkg_percept2_fetch = git
-pkg_percept2_repo = https://github.com/huiqing/percept2
-pkg_percept2_commit = master
-
-PACKAGES += pgo
-pkg_pgo_name = pgo
-pkg_pgo_description = Erlang Postgres client and connection pool
-pkg_pgo_homepage = https://github.com/erleans/pgo.git
-pkg_pgo_fetch = git
-pkg_pgo_repo = https://github.com/erleans/pgo.git
-pkg_pgo_commit = main
-
-PACKAGES += pgsql
-pkg_pgsql_name = pgsql
-pkg_pgsql_description = Erlang PostgreSQL driver
-pkg_pgsql_homepage = https://github.com/semiocast/pgsql
-pkg_pgsql_fetch = git
-pkg_pgsql_repo = https://github.com/semiocast/pgsql
-pkg_pgsql_commit = master
-
-PACKAGES += pkgx
-pkg_pkgx_name = pkgx
-pkg_pkgx_description = Build .deb packages from Erlang releases
-pkg_pkgx_homepage = https://github.com/arjan/pkgx
-pkg_pkgx_fetch = git
-pkg_pkgx_repo = https://github.com/arjan/pkgx
-pkg_pkgx_commit = master
-
-PACKAGES += pkt
-pkg_pkt_name = pkt
-pkg_pkt_description = Erlang network protocol library
-pkg_pkt_homepage = https://github.com/msantos/pkt
-pkg_pkt_fetch = git
-pkg_pkt_repo = https://github.com/msantos/pkt
-pkg_pkt_commit = master
-
-PACKAGES += plain_fsm
-pkg_plain_fsm_name = plain_fsm
-pkg_plain_fsm_description = A behaviour/support library for writing plain Erlang FSMs.
-pkg_plain_fsm_homepage = https://github.com/uwiger/plain_fsm
-pkg_plain_fsm_fetch = git
-pkg_plain_fsm_repo = https://github.com/uwiger/plain_fsm
-pkg_plain_fsm_commit = master
-
-PACKAGES += pmod_transform
-pkg_pmod_transform_name = pmod_transform
-pkg_pmod_transform_description = Parse transform for parameterized modules
-pkg_pmod_transform_homepage = https://github.com/erlang/pmod_transform
-pkg_pmod_transform_fetch = git
-pkg_pmod_transform_repo = https://github.com/erlang/pmod_transform
-pkg_pmod_transform_commit = master
-
-PACKAGES += pobox
-pkg_pobox_name = pobox
-pkg_pobox_description = External buffer processes to protect against mailbox overflow in Erlang
-pkg_pobox_homepage = https://github.com/ferd/pobox
-pkg_pobox_fetch = git
-pkg_pobox_repo = https://github.com/ferd/pobox
-pkg_pobox_commit = master
-
-PACKAGES += ponos
-pkg_ponos_name = ponos
-pkg_ponos_description = ponos is a simple yet powerful load generator written in erlang
-pkg_ponos_homepage = https://github.com/klarna/ponos
-pkg_ponos_fetch = git
-pkg_ponos_repo = https://github.com/klarna/ponos
-pkg_ponos_commit = master
-
-PACKAGES += poolboy
-pkg_poolboy_name = poolboy
-pkg_poolboy_description = A hunky Erlang worker pool factory
-pkg_poolboy_homepage = https://github.com/devinus/poolboy
-pkg_poolboy_fetch = git
-pkg_poolboy_repo = https://github.com/devinus/poolboy
-pkg_poolboy_commit = master
-
-PACKAGES += pooler
-pkg_pooler_name = pooler
-pkg_pooler_description = An OTP Process Pool Application
-pkg_pooler_homepage = https://github.com/seth/pooler
-pkg_pooler_fetch = git
-pkg_pooler_repo = https://github.com/seth/pooler
-pkg_pooler_commit = master
-
-PACKAGES += pqueue
-pkg_pqueue_name = pqueue
-pkg_pqueue_description = Erlang Priority Queues
-pkg_pqueue_homepage = https://github.com/okeuday/pqueue
-pkg_pqueue_fetch = git
-pkg_pqueue_repo = https://github.com/okeuday/pqueue
-pkg_pqueue_commit = master
-
-PACKAGES += procket
-pkg_procket_name = procket
-pkg_procket_description = Erlang interface to low level socket operations
-pkg_procket_homepage = http://blog.listincomprehension.com/search/label/procket
-pkg_procket_fetch = git
-pkg_procket_repo = https://github.com/msantos/procket
-pkg_procket_commit = master
-
-PACKAGES += prometheus
-pkg_prometheus_name = prometheus
-pkg_prometheus_description = Prometheus.io client in Erlang
-pkg_prometheus_homepage = https://github.com/deadtrickster/prometheus.erl
-pkg_prometheus_fetch = git
-pkg_prometheus_repo = https://github.com/deadtrickster/prometheus.erl
-pkg_prometheus_commit = master
-
-PACKAGES += prop
-pkg_prop_name = prop
-pkg_prop_description = An Erlang code scaffolding and generator system.
-pkg_prop_homepage = https://github.com/nuex/prop
-pkg_prop_fetch = git
-pkg_prop_repo = https://github.com/nuex/prop
-pkg_prop_commit = master
+PACKAGES += hex_core
+pkg_hex_core_name = hex_core
+pkg_hex_core_description = Reference implementation of Hex specifications
+pkg_hex_core_homepage = https://github.com/hexpm/hex_core
+pkg_hex_core_fetch = git
+HEX_CORE_GIT ?= https://github.com/hexpm/hex_core
+pkg_hex_core_repo = $(HEX_CORE_GIT)
+pkg_hex_core_commit = e57b4fb15cde710b3ae09b1d18f148f6999a63cc
PACKAGES += proper
pkg_proper_name = proper
@@ -2683,181 +356,13 @@ pkg_proper_fetch = git
pkg_proper_repo = https://github.com/manopapad/proper
pkg_proper_commit = master
-PACKAGES += props
-pkg_props_name = props
-pkg_props_description = Property structure library
-pkg_props_homepage = https://github.com/greyarea/props
-pkg_props_fetch = git
-pkg_props_repo = https://github.com/greyarea/props
-pkg_props_commit = master
-
-PACKAGES += protobuffs
-pkg_protobuffs_name = protobuffs
-pkg_protobuffs_description = An implementation of Google's Protocol Buffers for Erlang, based on ngerakines/erlang_protobuffs.
-pkg_protobuffs_homepage = https://github.com/basho/erlang_protobuffs
-pkg_protobuffs_fetch = git
-pkg_protobuffs_repo = https://github.com/basho/erlang_protobuffs
-pkg_protobuffs_commit = master
-
-PACKAGES += psycho
-pkg_psycho_name = psycho
-pkg_psycho_description = HTTP server that provides a WSGI-like interface for applications and middleware.
-pkg_psycho_homepage = https://github.com/gar1t/psycho
-pkg_psycho_fetch = git
-pkg_psycho_repo = https://github.com/gar1t/psycho
-pkg_psycho_commit = master
-
-PACKAGES += purity
-pkg_purity_name = purity
-pkg_purity_description = A side-effect analyzer for Erlang
-pkg_purity_homepage = https://github.com/mpitid/purity
-pkg_purity_fetch = git
-pkg_purity_repo = https://github.com/mpitid/purity
-pkg_purity_commit = master
-
-PACKAGES += qdate
-pkg_qdate_name = qdate
-pkg_qdate_description = Date, time, and timezone parsing, formatting, and conversion for Erlang.
-pkg_qdate_homepage = https://github.com/choptastic/qdate
-pkg_qdate_fetch = git
-pkg_qdate_repo = https://github.com/choptastic/qdate
-pkg_qdate_commit = master
-
-PACKAGES += qrcode
-pkg_qrcode_name = qrcode
-pkg_qrcode_description = QR Code encoder in Erlang
-pkg_qrcode_homepage = https://github.com/komone/qrcode
-pkg_qrcode_fetch = git
-pkg_qrcode_repo = https://github.com/komone/qrcode
-pkg_qrcode_commit = master
-
-PACKAGES += quest
-pkg_quest_name = quest
-pkg_quest_description = Learn Erlang through this set of challenges. An interactive system for getting to know Erlang.
-pkg_quest_homepage = https://github.com/eriksoe/ErlangQuest
-pkg_quest_fetch = git
-pkg_quest_repo = https://github.com/eriksoe/ErlangQuest
-pkg_quest_commit = master
-
-PACKAGES += quickrand
-pkg_quickrand_name = quickrand
-pkg_quickrand_description = Quick Erlang Random Number Generation
-pkg_quickrand_homepage = https://github.com/okeuday/quickrand
-pkg_quickrand_fetch = git
-pkg_quickrand_repo = https://github.com/okeuday/quickrand
-pkg_quickrand_commit = master
-
-PACKAGES += rabbit_exchange_type_riak
-pkg_rabbit_exchange_type_riak_name = rabbit_exchange_type_riak
-pkg_rabbit_exchange_type_riak_description = Custom RabbitMQ exchange type for sticking messages in Riak
-pkg_rabbit_exchange_type_riak_homepage = https://github.com/jbrisbin/riak-exchange
-pkg_rabbit_exchange_type_riak_fetch = git
-pkg_rabbit_exchange_type_riak_repo = https://github.com/jbrisbin/riak-exchange
-pkg_rabbit_exchange_type_riak_commit = master
-
-PACKAGES += rack
-pkg_rack_name = rack
-pkg_rack_description = Rack handler for erlang
-pkg_rack_homepage = https://github.com/erlyvideo/rack
-pkg_rack_fetch = git
-pkg_rack_repo = https://github.com/erlyvideo/rack
-pkg_rack_commit = master
-
-PACKAGES += radierl
-pkg_radierl_name = radierl
-pkg_radierl_description = RADIUS protocol stack implemented in Erlang.
-pkg_radierl_homepage = https://github.com/vances/radierl
-pkg_radierl_fetch = git
-pkg_radierl_repo = https://github.com/vances/radierl
-pkg_radierl_commit = master
-
PACKAGES += ranch
pkg_ranch_name = ranch
pkg_ranch_description = Socket acceptor pool for TCP protocols.
pkg_ranch_homepage = http://ninenines.eu
pkg_ranch_fetch = git
pkg_ranch_repo = https://github.com/ninenines/ranch
-pkg_ranch_commit = 1.2.1
-
-PACKAGES += rbeacon
-pkg_rbeacon_name = rbeacon
-pkg_rbeacon_description = LAN discovery and presence in Erlang.
-pkg_rbeacon_homepage = https://github.com/refuge/rbeacon
-pkg_rbeacon_fetch = git
-pkg_rbeacon_repo = https://github.com/refuge/rbeacon
-pkg_rbeacon_commit = master
-
-PACKAGES += re2
-pkg_re2_name = re2
-pkg_re2_description = Erlang NIF bindings for RE2 regex library
-pkg_re2_homepage = https://github.com/dukesoferl/re2
-pkg_re2_fetch = git
-pkg_re2_repo = https://github.com/dukesoferl/re2
-pkg_re2_commit = master
-
-PACKAGES += rebus
-pkg_rebus_name = rebus
-pkg_rebus_description = A stupid simple, internal, pub/sub event bus written in- and for Erlang.
-pkg_rebus_homepage = https://github.com/olle/rebus
-pkg_rebus_fetch = git
-pkg_rebus_repo = https://github.com/olle/rebus
-pkg_rebus_commit = master
-
-PACKAGES += rec2json
-pkg_rec2json_name = rec2json
-pkg_rec2json_description = Compile erlang record definitions into modules to convert them to/from json easily.
-pkg_rec2json_homepage = https://github.com/lordnull/rec2json
-pkg_rec2json_fetch = git
-pkg_rec2json_repo = https://github.com/lordnull/rec2json
-pkg_rec2json_commit = master
-
-PACKAGES += recon
-pkg_recon_name = recon
-pkg_recon_description = Collection of functions and scripts to debug Erlang in production.
-pkg_recon_homepage = https://github.com/ferd/recon
-pkg_recon_fetch = git
-pkg_recon_repo = https://github.com/ferd/recon
-pkg_recon_commit = master
-
-PACKAGES += record_info
-pkg_record_info_name = record_info
-pkg_record_info_description = Convert between record and proplist
-pkg_record_info_homepage = https://github.com/bipthelin/erlang-record_info
-pkg_record_info_fetch = git
-pkg_record_info_repo = https://github.com/bipthelin/erlang-record_info
-pkg_record_info_commit = master
-
-PACKAGES += redgrid
-pkg_redgrid_name = redgrid
-pkg_redgrid_description = automatic Erlang node discovery via redis
-pkg_redgrid_homepage = https://github.com/jkvor/redgrid
-pkg_redgrid_fetch = git
-pkg_redgrid_repo = https://github.com/jkvor/redgrid
-pkg_redgrid_commit = master
-
-PACKAGES += redo
-pkg_redo_name = redo
-pkg_redo_description = pipelined erlang redis client
-pkg_redo_homepage = https://github.com/jkvor/redo
-pkg_redo_fetch = git
-pkg_redo_repo = https://github.com/jkvor/redo
-pkg_redo_commit = master
-
-PACKAGES += reload_mk
-pkg_reload_mk_name = reload_mk
-pkg_reload_mk_description = Live reload plugin for erlang.mk.
-pkg_reload_mk_homepage = https://github.com/bullno1/reload.mk
-pkg_reload_mk_fetch = git
-pkg_reload_mk_repo = https://github.com/bullno1/reload.mk
-pkg_reload_mk_commit = master
-
-PACKAGES += reltool_util
-pkg_reltool_util_name = reltool_util
-pkg_reltool_util_description = Erlang reltool utility functionality application
-pkg_reltool_util_homepage = https://github.com/okeuday/reltool_util
-pkg_reltool_util_fetch = git
-pkg_reltool_util_repo = https://github.com/okeuday/reltool_util
-pkg_reltool_util_commit = master
+pkg_ranch_commit = master
PACKAGES += relx
pkg_relx_name = relx
@@ -2867,470 +372,6 @@ pkg_relx_fetch = git
pkg_relx_repo = https://github.com/erlware/relx
pkg_relx_commit = main
-PACKAGES += resource_discovery
-pkg_resource_discovery_name = resource_discovery
-pkg_resource_discovery_description = An application used to dynamically discover resources present in an Erlang node cluster.
-pkg_resource_discovery_homepage = http://erlware.org/
-pkg_resource_discovery_fetch = git
-pkg_resource_discovery_repo = https://github.com/erlware/resource_discovery
-pkg_resource_discovery_commit = master
-
-PACKAGES += restc
-pkg_restc_name = restc
-pkg_restc_description = Erlang Rest Client
-pkg_restc_homepage = https://github.com/kivra/restclient
-pkg_restc_fetch = git
-pkg_restc_repo = https://github.com/kivra/restclient
-pkg_restc_commit = master
-
-PACKAGES += rfc4627_jsonrpc
-pkg_rfc4627_jsonrpc_name = rfc4627_jsonrpc
-pkg_rfc4627_jsonrpc_description = Erlang RFC4627 (JSON) codec and JSON-RPC server implementation.
-pkg_rfc4627_jsonrpc_homepage = https://github.com/tonyg/erlang-rfc4627
-pkg_rfc4627_jsonrpc_fetch = git
-pkg_rfc4627_jsonrpc_repo = https://github.com/tonyg/erlang-rfc4627
-pkg_rfc4627_jsonrpc_commit = master
-
-PACKAGES += riak_core
-pkg_riak_core_name = riak_core
-pkg_riak_core_description = Distributed systems infrastructure used by Riak.
-pkg_riak_core_homepage = https://github.com/basho/riak_core
-pkg_riak_core_fetch = git
-pkg_riak_core_repo = https://github.com/basho/riak_core
-pkg_riak_core_commit = develop
-
-PACKAGES += riak_dt
-pkg_riak_dt_name = riak_dt
-pkg_riak_dt_description = Convergent replicated datatypes in Erlang
-pkg_riak_dt_homepage = https://github.com/basho/riak_dt
-pkg_riak_dt_fetch = git
-pkg_riak_dt_repo = https://github.com/basho/riak_dt
-pkg_riak_dt_commit = master
-
-PACKAGES += riak_ensemble
-pkg_riak_ensemble_name = riak_ensemble
-pkg_riak_ensemble_description = Multi-Paxos framework in Erlang
-pkg_riak_ensemble_homepage = https://github.com/basho/riak_ensemble
-pkg_riak_ensemble_fetch = git
-pkg_riak_ensemble_repo = https://github.com/basho/riak_ensemble
-pkg_riak_ensemble_commit = develop
-
-PACKAGES += riak_kv
-pkg_riak_kv_name = riak_kv
-pkg_riak_kv_description = Riak Key/Value Store
-pkg_riak_kv_homepage = https://github.com/basho/riak_kv
-pkg_riak_kv_fetch = git
-pkg_riak_kv_repo = https://github.com/basho/riak_kv
-pkg_riak_kv_commit = develop
-
-PACKAGES += riak_pipe
-pkg_riak_pipe_name = riak_pipe
-pkg_riak_pipe_description = Riak Pipelines
-pkg_riak_pipe_homepage = https://github.com/basho/riak_pipe
-pkg_riak_pipe_fetch = git
-pkg_riak_pipe_repo = https://github.com/basho/riak_pipe
-pkg_riak_pipe_commit = develop
-
-PACKAGES += riak_sysmon
-pkg_riak_sysmon_name = riak_sysmon
-pkg_riak_sysmon_description = Simple OTP app for managing Erlang VM system_monitor event messages
-pkg_riak_sysmon_homepage = https://github.com/basho/riak_sysmon
-pkg_riak_sysmon_fetch = git
-pkg_riak_sysmon_repo = https://github.com/basho/riak_sysmon
-pkg_riak_sysmon_commit = master
-
-PACKAGES += riakc
-pkg_riakc_name = riakc
-pkg_riakc_description = Erlang clients for Riak.
-pkg_riakc_homepage = https://github.com/basho/riak-erlang-client
-pkg_riakc_fetch = git
-pkg_riakc_repo = https://github.com/basho/riak-erlang-client
-pkg_riakc_commit = master
-
-PACKAGES += rlimit
-pkg_rlimit_name = rlimit
-pkg_rlimit_description = Magnus Klaar's rate limiter code from etorrent
-pkg_rlimit_homepage = https://github.com/jlouis/rlimit
-pkg_rlimit_fetch = git
-pkg_rlimit_repo = https://github.com/jlouis/rlimit
-pkg_rlimit_commit = master
-
-PACKAGES += rust_mk
-pkg_rust_mk_name = rust_mk
-pkg_rust_mk_description = Build Rust crates in an Erlang application
-pkg_rust_mk_homepage = https://github.com/goertzenator/rust.mk
-pkg_rust_mk_fetch = git
-pkg_rust_mk_repo = https://github.com/goertzenator/rust.mk
-pkg_rust_mk_commit = master
-
-PACKAGES += safetyvalve
-pkg_safetyvalve_name = safetyvalve
-pkg_safetyvalve_description = A safety valve for your erlang node
-pkg_safetyvalve_homepage = https://github.com/jlouis/safetyvalve
-pkg_safetyvalve_fetch = git
-pkg_safetyvalve_repo = https://github.com/jlouis/safetyvalve
-pkg_safetyvalve_commit = master
-
-PACKAGES += seestar
-pkg_seestar_name = seestar
-pkg_seestar_description = The Erlang client for Cassandra 1.2+ binary protocol
-pkg_seestar_homepage = https://github.com/iamaleksey/seestar
-pkg_seestar_fetch = git
-pkg_seestar_repo = https://github.com/iamaleksey/seestar
-pkg_seestar_commit = master
-
-PACKAGES += setup
-pkg_setup_name = setup
-pkg_setup_description = Generic setup utility for Erlang-based systems
-pkg_setup_homepage = https://github.com/uwiger/setup
-pkg_setup_fetch = git
-pkg_setup_repo = https://github.com/uwiger/setup
-pkg_setup_commit = master
-
-PACKAGES += sext
-pkg_sext_name = sext
-pkg_sext_description = Sortable Erlang Term Serialization
-pkg_sext_homepage = https://github.com/uwiger/sext
-pkg_sext_fetch = git
-pkg_sext_repo = https://github.com/uwiger/sext
-pkg_sext_commit = master
-
-PACKAGES += sfmt
-pkg_sfmt_name = sfmt
-pkg_sfmt_description = SFMT pseudo random number generator for Erlang.
-pkg_sfmt_homepage = https://github.com/jj1bdx/sfmt-erlang
-pkg_sfmt_fetch = git
-pkg_sfmt_repo = https://github.com/jj1bdx/sfmt-erlang
-pkg_sfmt_commit = master
-
-PACKAGES += sgte
-pkg_sgte_name = sgte
-pkg_sgte_description = A simple Erlang Template Engine
-pkg_sgte_homepage = https://github.com/filippo/sgte
-pkg_sgte_fetch = git
-pkg_sgte_repo = https://github.com/filippo/sgte
-pkg_sgte_commit = master
-
-PACKAGES += sheriff
-pkg_sheriff_name = sheriff
-pkg_sheriff_description = Parse transform for type based validation.
-pkg_sheriff_homepage = http://ninenines.eu
-pkg_sheriff_fetch = git
-pkg_sheriff_repo = https://github.com/extend/sheriff
-pkg_sheriff_commit = master
-
-PACKAGES += shotgun
-pkg_shotgun_name = shotgun
-pkg_shotgun_description = better than just a gun
-pkg_shotgun_homepage = https://github.com/inaka/shotgun
-pkg_shotgun_fetch = git
-pkg_shotgun_repo = https://github.com/inaka/shotgun
-pkg_shotgun_commit = master
-
-PACKAGES += sidejob
-pkg_sidejob_name = sidejob
-pkg_sidejob_description = Parallel worker and capacity limiting library for Erlang
-pkg_sidejob_homepage = https://github.com/basho/sidejob
-pkg_sidejob_fetch = git
-pkg_sidejob_repo = https://github.com/basho/sidejob
-pkg_sidejob_commit = develop
-
-PACKAGES += sieve
-pkg_sieve_name = sieve
-pkg_sieve_description = sieve is a simple TCP routing proxy (layer 7) in erlang
-pkg_sieve_homepage = https://github.com/benoitc/sieve
-pkg_sieve_fetch = git
-pkg_sieve_repo = https://github.com/benoitc/sieve
-pkg_sieve_commit = master
-
-PACKAGES += simhash
-pkg_simhash_name = simhash
-pkg_simhash_description = Simhashing for Erlang -- hashing algorithm to find near-duplicates in binary data.
-pkg_simhash_homepage = https://github.com/ferd/simhash
-pkg_simhash_fetch = git
-pkg_simhash_repo = https://github.com/ferd/simhash
-pkg_simhash_commit = master
-
-PACKAGES += simple_bridge
-pkg_simple_bridge_name = simple_bridge
-pkg_simple_bridge_description = A simple, standardized interface library to Erlang HTTP Servers.
-pkg_simple_bridge_homepage = https://github.com/nitrogen/simple_bridge
-pkg_simple_bridge_fetch = git
-pkg_simple_bridge_repo = https://github.com/nitrogen/simple_bridge
-pkg_simple_bridge_commit = master
-
-PACKAGES += simple_oauth2
-pkg_simple_oauth2_name = simple_oauth2
-pkg_simple_oauth2_description = Simple erlang OAuth2 client module for any http server framework (Google, Facebook, Yandex, Vkontakte are preconfigured)
-pkg_simple_oauth2_homepage = https://github.com/virtan/simple_oauth2
-pkg_simple_oauth2_fetch = git
-pkg_simple_oauth2_repo = https://github.com/virtan/simple_oauth2
-pkg_simple_oauth2_commit = master
-
-PACKAGES += skel
-pkg_skel_name = skel
-pkg_skel_description = A Streaming Process-based Skeleton Library for Erlang
-pkg_skel_homepage = https://github.com/ParaPhrase/skel
-pkg_skel_fetch = git
-pkg_skel_repo = https://github.com/ParaPhrase/skel
-pkg_skel_commit = master
-
-PACKAGES += slack
-pkg_slack_name = slack
-pkg_slack_description = Minimal slack notification OTP library.
-pkg_slack_homepage = https://github.com/DonBranson/slack
-pkg_slack_fetch = git
-pkg_slack_repo = https://github.com/DonBranson/slack.git
-pkg_slack_commit = master
-
-PACKAGES += snappyer
-pkg_snappyer_name = snappyer
-pkg_snappyer_description = Snappy as nif for Erlang
-pkg_snappyer_homepage = https://github.com/zmstone/snappyer
-pkg_snappyer_fetch = git
-pkg_snappyer_repo = https://github.com/zmstone/snappyer.git
-pkg_snappyer_commit = master
-
-PACKAGES += social
-pkg_social_name = social
-pkg_social_description = Cowboy handler for social login via OAuth2 providers
-pkg_social_homepage = https://github.com/dvv/social
-pkg_social_fetch = git
-pkg_social_repo = https://github.com/dvv/social
-pkg_social_commit = master
-
-PACKAGES += sqerl
-pkg_sqerl_name = sqerl
-pkg_sqerl_description = An Erlang-flavoured SQL DSL
-pkg_sqerl_homepage = https://github.com/hairyhum/sqerl
-pkg_sqerl_fetch = git
-pkg_sqerl_repo = https://github.com/hairyhum/sqerl
-pkg_sqerl_commit = master
-
-PACKAGES += srly
-pkg_srly_name = srly
-pkg_srly_description = Native Erlang Unix serial interface
-pkg_srly_homepage = https://github.com/msantos/srly
-pkg_srly_fetch = git
-pkg_srly_repo = https://github.com/msantos/srly
-pkg_srly_commit = master
-
-PACKAGES += sshrpc
-pkg_sshrpc_name = sshrpc
-pkg_sshrpc_description = Erlang SSH RPC module (experimental)
-pkg_sshrpc_homepage = https://github.com/jj1bdx/sshrpc
-pkg_sshrpc_fetch = git
-pkg_sshrpc_repo = https://github.com/jj1bdx/sshrpc
-pkg_sshrpc_commit = master
-
-PACKAGES += stable
-pkg_stable_name = stable
-pkg_stable_description = Library of assorted helpers for Cowboy web server.
-pkg_stable_homepage = https://github.com/dvv/stable
-pkg_stable_fetch = git
-pkg_stable_repo = https://github.com/dvv/stable
-pkg_stable_commit = master
-
-PACKAGES += statebox
-pkg_statebox_name = statebox
-pkg_statebox_description = Erlang state monad with merge/conflict-resolution capabilities. Useful for Riak.
-pkg_statebox_homepage = https://github.com/mochi/statebox
-pkg_statebox_fetch = git
-pkg_statebox_repo = https://github.com/mochi/statebox
-pkg_statebox_commit = master
-
-PACKAGES += statman
-pkg_statman_name = statman
-pkg_statman_description = Efficiently collect massive volumes of metrics inside the Erlang VM
-pkg_statman_homepage = https://github.com/knutin/statman
-pkg_statman_fetch = git
-pkg_statman_repo = https://github.com/knutin/statman
-pkg_statman_commit = master
-
-PACKAGES += statsderl
-pkg_statsderl_name = statsderl
-pkg_statsderl_description = StatsD client (erlang)
-pkg_statsderl_homepage = https://github.com/lpgauth/statsderl
-pkg_statsderl_fetch = git
-pkg_statsderl_repo = https://github.com/lpgauth/statsderl
-pkg_statsderl_commit = master
-
-PACKAGES += stdinout_pool
-pkg_stdinout_pool_name = stdinout_pool
-pkg_stdinout_pool_description = stdinout_pool : stuff goes in, stuff goes out. there's never any miscommunication.
-pkg_stdinout_pool_homepage = https://github.com/mattsta/erlang-stdinout-pool
-pkg_stdinout_pool_fetch = git
-pkg_stdinout_pool_repo = https://github.com/mattsta/erlang-stdinout-pool
-pkg_stdinout_pool_commit = master
-
-PACKAGES += stockdb
-pkg_stockdb_name = stockdb
-pkg_stockdb_description = Database for storing Stock Exchange quotes in erlang
-pkg_stockdb_homepage = https://github.com/maxlapshin/stockdb
-pkg_stockdb_fetch = git
-pkg_stockdb_repo = https://github.com/maxlapshin/stockdb
-pkg_stockdb_commit = master
-
-PACKAGES += subproc
-pkg_subproc_name = subproc
-pkg_subproc_description = unix subprocess manager with {active,once|false} modes
-pkg_subproc_homepage = http://dozzie.jarowit.net/trac/wiki/subproc
-pkg_subproc_fetch = git
-pkg_subproc_repo = https://github.com/dozzie/subproc
-pkg_subproc_commit = v0.1.0
-
-PACKAGES += supervisor3
-pkg_supervisor3_name = supervisor3
-pkg_supervisor3_description = OTP supervisor with additional strategies
-pkg_supervisor3_homepage = https://github.com/klarna/supervisor3
-pkg_supervisor3_fetch = git
-pkg_supervisor3_repo = https://github.com/klarna/supervisor3.git
-pkg_supervisor3_commit = master
-
-PACKAGES += swab
-pkg_swab_name = swab
-pkg_swab_description = General purpose buffer handling module
-pkg_swab_homepage = https://github.com/crownedgrouse/swab
-pkg_swab_fetch = git
-pkg_swab_repo = https://github.com/crownedgrouse/swab
-pkg_swab_commit = master
-
-PACKAGES += swarm
-pkg_swarm_name = swarm
-pkg_swarm_description = Fast and simple acceptor pool for Erlang
-pkg_swarm_homepage = https://github.com/jeremey/swarm
-pkg_swarm_fetch = git
-pkg_swarm_repo = https://github.com/jeremey/swarm
-pkg_swarm_commit = master
-
-PACKAGES += switchboard
-pkg_switchboard_name = switchboard
-pkg_switchboard_description = A framework for processing email using worker plugins.
-pkg_switchboard_homepage = https://github.com/thusfresh/switchboard
-pkg_switchboard_fetch = git
-pkg_switchboard_repo = https://github.com/thusfresh/switchboard
-pkg_switchboard_commit = master
-
-PACKAGES += syn
-pkg_syn_name = syn
-pkg_syn_description = A global Process Registry and Process Group manager for Erlang.
-pkg_syn_homepage = https://github.com/ostinelli/syn
-pkg_syn_fetch = git
-pkg_syn_repo = https://github.com/ostinelli/syn
-pkg_syn_commit = master
-
-PACKAGES += sync
-pkg_sync_name = sync
-pkg_sync_description = On-the-fly recompiling and reloading in Erlang.
-pkg_sync_homepage = https://github.com/rustyio/sync
-pkg_sync_fetch = git
-pkg_sync_repo = https://github.com/rustyio/sync
-pkg_sync_commit = master
-
-PACKAGES += syntaxerl
-pkg_syntaxerl_name = syntaxerl
-pkg_syntaxerl_description = Syntax checker for Erlang
-pkg_syntaxerl_homepage = https://github.com/ten0s/syntaxerl
-pkg_syntaxerl_fetch = git
-pkg_syntaxerl_repo = https://github.com/ten0s/syntaxerl
-pkg_syntaxerl_commit = master
-
-PACKAGES += syslog
-pkg_syslog_name = syslog
-pkg_syslog_description = Erlang port driver for interacting with syslog via syslog(3)
-pkg_syslog_homepage = https://github.com/Vagabond/erlang-syslog
-pkg_syslog_fetch = git
-pkg_syslog_repo = https://github.com/Vagabond/erlang-syslog
-pkg_syslog_commit = master
-
-PACKAGES += taskforce
-pkg_taskforce_name = taskforce
-pkg_taskforce_description = Erlang worker pools for controlled parallelisation of arbitrary tasks.
-pkg_taskforce_homepage = https://github.com/g-andrade/taskforce
-pkg_taskforce_fetch = git
-pkg_taskforce_repo = https://github.com/g-andrade/taskforce
-pkg_taskforce_commit = master
-
-PACKAGES += tddreloader
-pkg_tddreloader_name = tddreloader
-pkg_tddreloader_description = Shell utility for recompiling, reloading, and testing code as it changes
-pkg_tddreloader_homepage = https://github.com/version2beta/tddreloader
-pkg_tddreloader_fetch = git
-pkg_tddreloader_repo = https://github.com/version2beta/tddreloader
-pkg_tddreloader_commit = master
-
-PACKAGES += tempo
-pkg_tempo_name = tempo
-pkg_tempo_description = NIF-based date and time parsing and formatting for Erlang.
-pkg_tempo_homepage = https://github.com/selectel/tempo
-pkg_tempo_fetch = git
-pkg_tempo_repo = https://github.com/selectel/tempo
-pkg_tempo_commit = master
-
-PACKAGES += tinymq
-pkg_tinymq_name = tinymq
-pkg_tinymq_description = TinyMQ - a diminutive, in-memory message queue
-pkg_tinymq_homepage = https://github.com/ChicagoBoss/tinymq
-pkg_tinymq_fetch = git
-pkg_tinymq_repo = https://github.com/ChicagoBoss/tinymq
-pkg_tinymq_commit = master
-
-PACKAGES += tinymt
-pkg_tinymt_name = tinymt
-pkg_tinymt_description = TinyMT pseudo random number generator for Erlang.
-pkg_tinymt_homepage = https://github.com/jj1bdx/tinymt-erlang
-pkg_tinymt_fetch = git
-pkg_tinymt_repo = https://github.com/jj1bdx/tinymt-erlang
-pkg_tinymt_commit = master
-
-PACKAGES += tirerl
-pkg_tirerl_name = tirerl
-pkg_tirerl_description = Erlang interface to Elastic Search
-pkg_tirerl_homepage = https://github.com/inaka/tirerl
-pkg_tirerl_fetch = git
-pkg_tirerl_repo = https://github.com/inaka/tirerl
-pkg_tirerl_commit = master
-
-PACKAGES += toml
-pkg_toml_name = toml
-pkg_toml_description = TOML (0.4.0) config parser
-pkg_toml_homepage = http://dozzie.jarowit.net/trac/wiki/TOML
-pkg_toml_fetch = git
-pkg_toml_repo = https://github.com/dozzie/toml
-pkg_toml_commit = v0.2.0
-
-PACKAGES += traffic_tools
-pkg_traffic_tools_name = traffic_tools
-pkg_traffic_tools_description = Simple traffic limiting library
-pkg_traffic_tools_homepage = https://github.com/systra/traffic_tools
-pkg_traffic_tools_fetch = git
-pkg_traffic_tools_repo = https://github.com/systra/traffic_tools
-pkg_traffic_tools_commit = master
-
-PACKAGES += trails
-pkg_trails_name = trails
-pkg_trails_description = A couple of improvements over Cowboy Routes
-pkg_trails_homepage = http://inaka.github.io/cowboy-trails/
-pkg_trails_fetch = git
-pkg_trails_repo = https://github.com/inaka/cowboy-trails
-pkg_trails_commit = master
-
-PACKAGES += trane
-pkg_trane_name = trane
-pkg_trane_description = SAX style broken HTML parser in Erlang
-pkg_trane_homepage = https://github.com/massemanet/trane
-pkg_trane_fetch = git
-pkg_trane_repo = https://github.com/massemanet/trane
-pkg_trane_commit = master
-
-PACKAGES += trie
-pkg_trie_name = trie
-pkg_trie_description = Erlang Trie Implementation
-pkg_trie_homepage = https://github.com/okeuday/trie
-pkg_trie_fetch = git
-pkg_trie_repo = https://github.com/okeuday/trie
-pkg_trie_commit = master
-
PACKAGES += triq
pkg_triq_name = triq
pkg_triq_description = Trifork QuickCheck
@@ -3339,182 +380,6 @@ pkg_triq_fetch = git
pkg_triq_repo = https://gitlab.com/triq/triq.git
pkg_triq_commit = master
-PACKAGES += tunctl
-pkg_tunctl_name = tunctl
-pkg_tunctl_description = Erlang TUN/TAP interface
-pkg_tunctl_homepage = https://github.com/msantos/tunctl
-pkg_tunctl_fetch = git
-pkg_tunctl_repo = https://github.com/msantos/tunctl
-pkg_tunctl_commit = master
-
-PACKAGES += unicorn
-pkg_unicorn_name = unicorn
-pkg_unicorn_description = Generic configuration server
-pkg_unicorn_homepage = https://github.com/shizzard/unicorn
-pkg_unicorn_fetch = git
-pkg_unicorn_repo = https://github.com/shizzard/unicorn
-pkg_unicorn_commit = master
-
-PACKAGES += unsplit
-pkg_unsplit_name = unsplit
-pkg_unsplit_description = Resolves conflicts in Mnesia after network splits
-pkg_unsplit_homepage = https://github.com/uwiger/unsplit
-pkg_unsplit_fetch = git
-pkg_unsplit_repo = https://github.com/uwiger/unsplit
-pkg_unsplit_commit = master
-
-PACKAGES += uuid
-pkg_uuid_name = uuid
-pkg_uuid_description = Erlang UUID Implementation
-pkg_uuid_homepage = https://github.com/okeuday/uuid
-pkg_uuid_fetch = git
-pkg_uuid_repo = https://github.com/okeuday/uuid
-pkg_uuid_commit = master
-
-PACKAGES += ux
-pkg_ux_name = ux
-pkg_ux_description = Unicode eXtention for Erlang (Strings, Collation)
-pkg_ux_homepage = https://github.com/erlang-unicode/ux
-pkg_ux_fetch = git
-pkg_ux_repo = https://github.com/erlang-unicode/ux
-pkg_ux_commit = master
-
-PACKAGES += verx
-pkg_verx_name = verx
-pkg_verx_description = Erlang implementation of the libvirtd remote protocol
-pkg_verx_homepage = https://github.com/msantos/verx
-pkg_verx_fetch = git
-pkg_verx_repo = https://github.com/msantos/verx
-pkg_verx_commit = master
-
-PACKAGES += vmq_bridge
-pkg_vmq_bridge_name = vmq_bridge
-pkg_vmq_bridge_description = Component of VerneMQ: A distributed MQTT message broker
-pkg_vmq_bridge_homepage = https://verne.mq/
-pkg_vmq_bridge_fetch = git
-pkg_vmq_bridge_repo = https://github.com/erlio/vmq_bridge
-pkg_vmq_bridge_commit = master
-
-PACKAGES += vmstats
-pkg_vmstats_name = vmstats
-pkg_vmstats_description = tiny Erlang app that works in conjunction with statsderl in order to generate information on the Erlang VM for graphite logs.
-pkg_vmstats_homepage = https://github.com/ferd/vmstats
-pkg_vmstats_fetch = git
-pkg_vmstats_repo = https://github.com/ferd/vmstats
-pkg_vmstats_commit = master
-
-PACKAGES += walrus
-pkg_walrus_name = walrus
-pkg_walrus_description = Walrus - Mustache-like Templating
-pkg_walrus_homepage = https://github.com/devinus/walrus
-pkg_walrus_fetch = git
-pkg_walrus_repo = https://github.com/devinus/walrus
-pkg_walrus_commit = master
-
-PACKAGES += webmachine
-pkg_webmachine_name = webmachine
-pkg_webmachine_description = A REST-based system for building web applications.
-pkg_webmachine_homepage = https://github.com/basho/webmachine
-pkg_webmachine_fetch = git
-pkg_webmachine_repo = https://github.com/basho/webmachine
-pkg_webmachine_commit = master
-
-PACKAGES += websocket_client
-pkg_websocket_client_name = websocket_client
-pkg_websocket_client_description = Erlang websocket client (ws and wss supported)
-pkg_websocket_client_homepage = https://github.com/jeremyong/websocket_client
-pkg_websocket_client_fetch = git
-pkg_websocket_client_repo = https://github.com/jeremyong/websocket_client
-pkg_websocket_client_commit = master
-
-PACKAGES += worker_pool
-pkg_worker_pool_name = worker_pool
-pkg_worker_pool_description = a simple erlang worker pool
-pkg_worker_pool_homepage = https://github.com/inaka/worker_pool
-pkg_worker_pool_fetch = git
-pkg_worker_pool_repo = https://github.com/inaka/worker_pool
-pkg_worker_pool_commit = main
-
-PACKAGES += wrangler
-pkg_wrangler_name = wrangler
-pkg_wrangler_description = Import of the Wrangler svn repository.
-pkg_wrangler_homepage = http://www.cs.kent.ac.uk/projects/wrangler/Home.html
-pkg_wrangler_fetch = git
-pkg_wrangler_repo = https://github.com/RefactoringTools/wrangler
-pkg_wrangler_commit = master
-
-PACKAGES += wsock
-pkg_wsock_name = wsock
-pkg_wsock_description = Erlang library to build WebSocket clients and servers
-pkg_wsock_homepage = https://github.com/madtrick/wsock
-pkg_wsock_fetch = git
-pkg_wsock_repo = https://github.com/madtrick/wsock
-pkg_wsock_commit = master
-
-PACKAGES += xhttpc
-pkg_xhttpc_name = xhttpc
-pkg_xhttpc_description = Extensible HTTP Client for Erlang
-pkg_xhttpc_homepage = https://github.com/seriyps/xhttpc
-pkg_xhttpc_fetch = git
-pkg_xhttpc_repo = https://github.com/seriyps/xhttpc
-pkg_xhttpc_commit = master
-
-PACKAGES += xref_runner
-pkg_xref_runner_name = xref_runner
-pkg_xref_runner_description = Erlang Xref Runner (inspired in rebar xref)
-pkg_xref_runner_homepage = https://github.com/inaka/xref_runner
-pkg_xref_runner_fetch = git
-pkg_xref_runner_repo = https://github.com/inaka/xref_runner
-pkg_xref_runner_commit = master
-
-PACKAGES += yamerl
-pkg_yamerl_name = yamerl
-pkg_yamerl_description = YAML 1.2 parser in pure Erlang
-pkg_yamerl_homepage = https://github.com/yakaz/yamerl
-pkg_yamerl_fetch = git
-pkg_yamerl_repo = https://github.com/yakaz/yamerl
-pkg_yamerl_commit = master
-
-PACKAGES += yamler
-pkg_yamler_name = yamler
-pkg_yamler_description = libyaml-based yaml loader for Erlang
-pkg_yamler_homepage = https://github.com/goertzenator/yamler
-pkg_yamler_fetch = git
-pkg_yamler_repo = https://github.com/goertzenator/yamler
-pkg_yamler_commit = master
-
-PACKAGES += yaws
-pkg_yaws_name = yaws
-pkg_yaws_description = Yaws webserver
-pkg_yaws_homepage = http://yaws.hyber.org
-pkg_yaws_fetch = git
-pkg_yaws_repo = https://github.com/klacke/yaws
-pkg_yaws_commit = master
-
-PACKAGES += zippers
-pkg_zippers_name = zippers
-pkg_zippers_description = A library for functional zipper data structures in Erlang. Read more on zippers
-pkg_zippers_homepage = https://github.com/ferd/zippers
-pkg_zippers_fetch = git
-pkg_zippers_repo = https://github.com/ferd/zippers
-pkg_zippers_commit = master
-
-PACKAGES += zlists
-pkg_zlists_name = zlists
-pkg_zlists_description = Erlang lazy lists library.
-pkg_zlists_homepage = https://github.com/vjache/erlang-zlists
-pkg_zlists_fetch = git
-pkg_zlists_repo = https://github.com/vjache/erlang-zlists
-pkg_zlists_commit = master
-
-PACKAGES += zucchini
-pkg_zucchini_name = zucchini
-pkg_zucchini_description = An Erlang INI parser
-pkg_zucchini_homepage = https://github.com/devinus/zucchini
-pkg_zucchini_fetch = git
-pkg_zucchini_repo = https://github.com/devinus/zucchini
-pkg_zucchini_commit = master
-
# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -3522,7 +387,7 @@ pkg_zucchini_commit = master
define pkg_print
$(verbose) printf "%s\n" \
- $(if $(call core_eq,$(1),$(pkg_$(1)_name)),,"Pkg name: $(1)") \
+ $(if $(call core_eq,$1,$(pkg_$(1)_name)),,"Pkg name: $1") \
"App name: $(pkg_$(1)_name)" \
"Description: $(pkg_$(1)_description)" \
"Home page: $(pkg_$(1)_homepage)" \
@@ -3536,10 +401,10 @@ endef
search:
ifdef q
$(foreach p,$(PACKAGES), \
- $(if $(findstring $(call core_lc,$(q)),$(call core_lc,$(pkg_$(p)_name) $(pkg_$(p)_description))), \
- $(call pkg_print,$(p))))
+ $(if $(findstring $(call core_lc,$q),$(call core_lc,$(pkg_$(p)_name) $(pkg_$(p)_description))), \
+ $(call pkg_print,$p)))
else
- $(foreach p,$(PACKAGES),$(call pkg_print,$(p)))
+ $(foreach p,$(PACKAGES),$(call pkg_print,$p))
endif
# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
@@ -3575,16 +440,25 @@ CACHE_DEPS ?= 0
CACHE_DIR ?= $(if $(XDG_CACHE_HOME),$(XDG_CACHE_HOME),$(HOME)/.cache)/erlang.mk
export CACHE_DIR
+HEX_CONFIG ?=
+
+define hex_config.erl
+ begin
+ Config0 = hex_core:default_config(),
+ Config0$(HEX_CONFIG)
+ end
+endef
+
# External "early" plugins (see core/plugins.mk for regular plugins).
# They both use the core_dep_plugin macro.
define core_dep_plugin
-ifeq ($(2),$(PROJECT))
--include $$(patsubst $(PROJECT)/%,%,$(1))
+ifeq ($2,$(PROJECT))
+-include $$(patsubst $(PROJECT)/%,%,$1)
else
--include $(DEPS_DIR)/$(1)
+-include $(DEPS_DIR)/$1
-$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ;
+$(DEPS_DIR)/$1: $(DEPS_DIR)/$2 ;
endif
endef
@@ -3597,44 +471,42 @@ $(foreach p,$(DEP_EARLY_PLUGINS),\
# Query functions.
-query_fetch_method = $(if $(dep_$(1)),$(call _qfm_dep,$(word 1,$(dep_$(1)))),$(call _qfm_pkg,$(1)))
-_qfm_dep = $(if $(dep_fetch_$(1)),$(1),$(if $(IS_DEP),legacy,fail))
+query_fetch_method = $(if $(dep_$(1)),$(call _qfm_dep,$(word 1,$(dep_$(1)))),$(call _qfm_pkg,$1))
+_qfm_dep = $(if $(dep_fetch_$(1)),$1,fail)
_qfm_pkg = $(if $(pkg_$(1)_fetch),$(pkg_$(1)_fetch),fail)
-query_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1)))
+query_name = $(if $(dep_$(1)),$1,$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$1))
-query_repo = $(call _qr,$(1),$(call query_fetch_method,$(1)))
-_qr = $(if $(query_repo_$(2)),$(call query_repo_$(2),$(1)),$(call dep_repo,$(1)))
+query_repo = $(call _qr,$1,$(call query_fetch_method,$1))
+_qr = $(if $(query_repo_$(2)),$(call query_repo_$(2),$1),$(call query_repo_git,$1))
query_repo_default = $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))
-query_repo_git = $(patsubst git://github.com/%,https://github.com/%,$(call query_repo_default,$(1)))
-query_repo_git-subfolder = $(call query_repo_git,$(1))
+query_repo_git = $(patsubst git://github.com/%,https://github.com/%,$(call query_repo_default,$1))
+query_repo_git-subfolder = $(call query_repo_git,$1)
query_repo_git-submodule = -
-query_repo_hg = $(call query_repo_default,$(1))
-query_repo_svn = $(call query_repo_default,$(1))
-query_repo_cp = $(call query_repo_default,$(1))
-query_repo_ln = $(call query_repo_default,$(1))
-query_repo_hex = https://hex.pm/packages/$(if $(word 3,$(dep_$(1))),$(word 3,$(dep_$(1))),$(1))
+query_repo_hg = $(call query_repo_default,$1)
+query_repo_svn = $(call query_repo_default,$1)
+query_repo_cp = $(call query_repo_default,$1)
+query_repo_ln = $(call query_repo_default,$1)
+query_repo_hex = https://hex.pm/packages/$(if $(word 3,$(dep_$(1))),$(word 3,$(dep_$(1))),$1)
query_repo_fail = -
-query_repo_legacy = -
-query_version = $(call _qv,$(1),$(call query_fetch_method,$(1)))
-_qv = $(if $(query_version_$(2)),$(call query_version_$(2),$(1)),$(call dep_commit,$(1)))
+query_version = $(call _qv,$1,$(call query_fetch_method,$1))
+_qv = $(if $(query_version_$(2)),$(call query_version_$(2),$1),$(call query_version_default,$1))
query_version_default = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit)))
-query_version_git = $(call query_version_default,$(1))
-query_version_git-subfolder = $(call query_version_git,$(1))
+query_version_git = $(call query_version_default,$1)
+query_version_git-subfolder = $(call query_version_default,$1)
query_version_git-submodule = -
-query_version_hg = $(call query_version_default,$(1))
+query_version_hg = $(call query_version_default,$1)
query_version_svn = -
query_version_cp = -
query_version_ln = -
query_version_hex = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_commit)))
query_version_fail = -
-query_version_legacy = -
-query_extra = $(call _qe,$(1),$(call query_fetch_method,$(1)))
-_qe = $(if $(query_extra_$(2)),$(call query_extra_$(2),$(1)),-)
+query_extra = $(call _qe,$1,$(call query_fetch_method,$1))
+_qe = $(if $(query_extra_$(2)),$(call query_extra_$(2),$1),-)
query_extra_git = -
query_extra_git-subfolder = $(if $(dep_$(1)),subfolder=$(word 4,$(dep_$(1))),-)
@@ -3645,18 +517,17 @@ query_extra_cp = -
query_extra_ln = -
query_extra_hex = $(if $(dep_$(1)),package-name=$(word 3,$(dep_$(1))),-)
query_extra_fail = -
-query_extra_legacy = -
-query_absolute_path = $(addprefix $(DEPS_DIR)/,$(call query_name,$(1)))
+query_absolute_path = $(addprefix $(DEPS_DIR)/,$(call query_name,$1))
-# Deprecated legacy query functions.
-dep_fetch = $(call query_fetch_method,$(1))
+# Deprecated legacy query function. Used by RabbitMQ and its third party plugins.
+# Can be removed once RabbitMQ has been updated and enough time has passed.
dep_name = $(call query_name,$(1))
-dep_repo = $(call query_repo_git,$(1))
-dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(if $(filter hex,$(word 1,$(dep_$(1)))),$(word 2,$(dep_$(1))),$(word 3,$(dep_$(1)))),$(pkg_$(1)_commit)))
-LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$(a)),$(APPS_DIR)/$(a)))
-ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep))))
+# Application directories.
+
+LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$a),$(APPS_DIR)/$a))
+ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call query_name,$(dep))))
# When we are calling an app directly we don't want to include it here
# otherwise it'll be treated both as an apps and a top-level project.
@@ -3680,7 +551,7 @@ export NO_AUTOPATCH
# Verbosity.
-dep_verbose_0 = @echo " DEP $1 ($(call dep_commit,$1))";
+dep_verbose_0 = @echo " DEP $1 ($(call query_version,$1))";
dep_verbose_2 = set -x;
dep_verbose = $(dep_verbose_$(V))
@@ -3778,25 +649,25 @@ endif
# While Makefile file could be GNUmakefile or makefile,
# in practice only Makefile is needed so far.
define dep_autopatch
- if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \
+ if [ -f $(DEPS_DIR)/$1/erlang.mk ]; then \
rm -rf $(DEPS_DIR)/$1/ebin/; \
- $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \
- $(call dep_autopatch_erlang_mk,$(1)); \
- elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \
+ $(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \
+ $(call dep_autopatch_erlang_mk,$1); \
+ elif [ -f $(DEPS_DIR)/$1/Makefile ]; then \
if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \
$(call dep_autopatch2,$1); \
- elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \
- $(call dep_autopatch2,$(1)); \
- elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$(1)/Makefile` ]; then \
- $(call dep_autopatch2,$(1)); \
- elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \
- $(call dep_autopatch2,$(1)); \
+ elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$1/Makefile` ]; then \
+ $(call dep_autopatch2,$1); \
+ elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$1/Makefile` ]; then \
+ $(call dep_autopatch2,$1); \
+ elif [ -n "`find $(DEPS_DIR)/$1/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \
+ $(call dep_autopatch2,$1); \
fi \
else \
- if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \
- $(call dep_autopatch_noop,$(1)); \
+ if [ ! -d $(DEPS_DIR)/$1/src/ ]; then \
+ $(call dep_autopatch_noop,$1); \
else \
- $(call dep_autopatch2,$(1)); \
+ $(call dep_autopatch2,$1); \
fi \
fi
endef
@@ -3806,19 +677,19 @@ define dep_autopatch2
mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \
rm -f $(DEPS_DIR)/$1/ebin/$1.app; \
if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \
- $(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \
+ $(call erlang,$(call dep_autopatch_appsrc_script.erl,$1)); \
fi; \
- $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \
- if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script -o -f $(DEPS_DIR)/$1/rebar.lock ]; then \
+ $(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \
+ if [ -f $(DEPS_DIR)/$1/rebar -o -f $(DEPS_DIR)/$1/rebar.config -o -f $(DEPS_DIR)/$1/rebar.config.script -o -f $(DEPS_DIR)/$1/rebar.lock ]; then \
$(call dep_autopatch_fetch_rebar); \
- $(call dep_autopatch_rebar,$(1)); \
+ $(call dep_autopatch_rebar,$1); \
else \
- $(call dep_autopatch_gen,$(1)); \
+ $(call dep_autopatch_gen,$1); \
fi
endef
define dep_autopatch_noop
- printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile
+ printf "noop:\n" > $(DEPS_DIR)/$1/Makefile
endef
# Replace "include erlang.mk" with a line that will load the parent Erlang.mk
@@ -3840,7 +711,7 @@ endif
define dep_autopatch_gen
printf "%s\n" \
"ERLC_OPTS = +debug_info" \
- "include ../../erlang.mk" > $(DEPS_DIR)/$(1)/Makefile
+ "include ../../erlang.mk" > $(DEPS_DIR)/$1/Makefile
endef
# We use flock/lockf when available to avoid concurrency issues.
@@ -3865,11 +736,11 @@ define dep_autopatch_fetch_rebar2
endef
define dep_autopatch_rebar
- if [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \
- mv $(DEPS_DIR)/$(1)/Makefile $(DEPS_DIR)/$(1)/Makefile.orig.mk; \
+ if [ -f $(DEPS_DIR)/$1/Makefile ]; then \
+ mv $(DEPS_DIR)/$1/Makefile $(DEPS_DIR)/$1/Makefile.orig.mk; \
fi; \
- $(call erlang,$(call dep_autopatch_rebar.erl,$(1))); \
- rm -f $(DEPS_DIR)/$(1)/ebin/$(1).app
+ $(call erlang,$(call dep_autopatch_rebar.erl,$1)); \
+ rm -f $(DEPS_DIR)/$1/ebin/$1.app
endef
define dep_autopatch_rebar.erl
@@ -3988,6 +859,12 @@ define dep_autopatch_rebar.erl
GetHexVsn3Common(N, NP, S0);
(N, NP, S) -> {N, {hex, NP, S}}
end,
+ ConvertCommit = fun
+ ({branch, C}) -> C;
+ ({ref, C}) -> C;
+ ({tag, C}) -> C;
+ (C) -> C
+ end,
fun() ->
File = case lists:keyfind(deps, 1, Conf) of
false -> [];
@@ -4003,16 +880,15 @@ define dep_autopatch_rebar.erl
_ -> false
end of
false -> ok;
+ {Name, {git_subdir, Repo, Commit, SubDir}} ->
+ Write(io_lib:format("DEPS += ~s\ndep_~s = git-subfolder ~s ~s ~s~n", [Name, Name, Repo, ConvertCommit(Commit), SubDir]));
{Name, Source} ->
{Method, Repo, Commit} = case Source of
{hex, NPV, V} -> {hex, V, NPV};
{git, R} -> {git, R, master};
- {M, R, {branch, C}} -> {M, R, C};
- {M, R, {ref, C}} -> {M, R, C};
- {M, R, {tag, C}} -> {M, R, C};
{M, R, C} -> {M, R, C}
end,
- Write(io_lib:format("DEPS += ~s\ndep_~s = ~s ~s ~s~n", [Name, Name, Method, Repo, Commit]))
+ Write(io_lib:format("DEPS += ~s\ndep_~s = ~s ~s ~s~n", [Name, Name, Method, Repo, ConvertCommit(Commit)]))
end end || Dep <- Deps]
end
end(),
@@ -4242,7 +1118,7 @@ define dep_autopatch_appsrc.erl
case filelib:is_regular(AppSrcIn) of
false -> ok;
true ->
- {ok, [{application, $(1), L0}]} = file:consult(AppSrcIn),
+ {ok, [{application, $1, L0}]} = file:consult(AppSrcIn),
L1 = lists:keystore(modules, 1, L0, {modules, []}),
L2 = case lists:keyfind(vsn, 1, L1) of
{_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, lists:droplast(os:cmd("git -C $(DEPS_DIR)/$1 describe --dirty --tags --always"))});
@@ -4250,7 +1126,7 @@ define dep_autopatch_appsrc.erl
_ -> L1
end,
L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end,
- ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])),
+ ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $1, L3}])),
case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end
end,
halt()
@@ -4260,45 +1136,46 @@ ifeq ($(CACHE_DEPS),1)
define dep_cache_fetch_git
mkdir -p $(CACHE_DIR)/git; \
- if test -d "$(join $(CACHE_DIR)/git/,$(call dep_name,$1))"; then \
- cd $(join $(CACHE_DIR)/git/,$(call dep_name,$1)); \
- if ! git checkout -q $(call dep_commit,$1); then \
- git remote set-url origin $(call dep_repo,$1) && \
+ if test -d "$(join $(CACHE_DIR)/git/,$(call query_name,$1))"; then \
+ cd $(join $(CACHE_DIR)/git/,$(call query_name,$1)); \
+ if ! git checkout -q $(call query_version,$1); then \
+ git remote set-url origin $(call query_repo_git,$1) && \
git pull --all && \
- git cat-file -e $(call dep_commit,$1) 2>/dev/null; \
+ git cat-file -e $(call query_version_git,$1) 2>/dev/null; \
fi; \
else \
- git clone -q -n -- $(call dep_repo,$1) $(join $(CACHE_DIR)/git/,$(call dep_name,$1)); \
+ git clone -q -n -- $(call query_repo_git,$1) $(join $(CACHE_DIR)/git/,$(call query_name,$1)); \
fi; \
- git clone -q --branch $(call dep_commit,$1) --single-branch -- $(join $(CACHE_DIR)/git/,$(call dep_name,$1)) $2
+ git clone -q --single-branch -- $(join $(CACHE_DIR)/git/,$(call query_name,$1)) $2; \
+ cd $2 && git checkout -q $(call query_version_git,$1)
endef
define dep_fetch_git
- $(call dep_cache_fetch_git,$1,$(DEPS_DIR)/$(call dep_name,$1));
+ $(call dep_cache_fetch_git,$1,$(DEPS_DIR)/$(call query_name,$1));
endef
define dep_fetch_git-subfolder
mkdir -p $(ERLANG_MK_TMP)/git-subfolder; \
- $(call dep_cache_fetch_git,$1,$(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1)); \
- ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1)/$(word 4,$(dep_$1)) \
- $(DEPS_DIR)/$(call dep_name,$1);
+ $(call dep_cache_fetch_git,$1,$(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1)); \
+ ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1)/$(word 4,$(dep_$1)) \
+ $(DEPS_DIR)/$(call query_name,$1);
endef
else
define dep_fetch_git
- git clone -q -n -- $(call dep_repo,$1) $(DEPS_DIR)/$(call dep_name,$1); \
- cd $(DEPS_DIR)/$(call dep_name,$1) && git checkout -q $(call dep_commit,$1);
+ git clone -q -n -- $(call query_repo_git,$1) $(DEPS_DIR)/$(call query_name,$1); \
+ cd $(DEPS_DIR)/$(call query_name,$1) && git checkout -q $(call query_version_git,$1);
endef
define dep_fetch_git-subfolder
mkdir -p $(ERLANG_MK_TMP)/git-subfolder; \
- git clone -q -n -- $(call dep_repo,$1) \
- $(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1); \
- cd $(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1) \
- && git checkout -q $(call dep_commit,$1); \
- ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call dep_name,$1)/$(word 4,$(dep_$1)) \
- $(DEPS_DIR)/$(call dep_name,$1);
+ git clone -q -n -- $(call query_repo_git-subfolder,$1) \
+ $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1); \
+ cd $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1) \
+ && git checkout -q $(call query_version_git-subfolder,$1); \
+ ln -s $(ERLANG_MK_TMP)/git-subfolder/$(call query_name,$1)/$(word 4,$(dep_$1)) \
+ $(DEPS_DIR)/$(call query_name,$1);
endef
endif
@@ -4308,20 +1185,34 @@ define dep_fetch_git-submodule
endef
define dep_fetch_hg
- hg clone -q -U $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \
- cd $(DEPS_DIR)/$(call dep_name,$(1)) && hg update -q $(call dep_commit,$(1));
+ hg clone -q -U $(call query_repo_hg,$1) $(DEPS_DIR)/$(call query_name,$1); \
+ cd $(DEPS_DIR)/$(call query_name,$1) && hg update -q $(call query_version_hg,$1);
endef
define dep_fetch_svn
- svn checkout -q $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1));
+ svn checkout -q $(call query_repo_svn,$1) $(DEPS_DIR)/$(call query_name,$1);
endef
define dep_fetch_cp
- cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1));
+ cp -R $(call query_repo_cp,$1) $(DEPS_DIR)/$(call query_name,$1);
endef
define dep_fetch_ln
- ln -s $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1));
+ ln -s $(call query_repo_ln,$1) $(DEPS_DIR)/$(call query_name,$1);
+endef
+
+define hex_get_tarball.erl
+ {ok, _} = application:ensure_all_started(ssl),
+ {ok, _} = application:ensure_all_started(inets),
+ Config = $(hex_config.erl),
+ case hex_repo:get_tarball(Config, <<"$1">>, <<"$(strip $2)">>) of
+ {ok, {200, _, Tarball}} ->
+ ok = file:write_file("$3", Tarball),
+ halt(0);
+ {ok, {Status, _, Errors}} ->
+ io:format("Error ~b: ~0p~n", [Status, Errors]),
+ halt(79)
+ end
endef
ifeq ($(CACHE_DEPS),1)
@@ -4329,9 +1220,10 @@ ifeq ($(CACHE_DEPS),1)
# Hex only has a package version. No need to look in the Erlang.mk packages.
define dep_fetch_hex
mkdir -p $(CACHE_DIR)/hex $(DEPS_DIR)/$1; \
- $(eval hex_tar_name=$(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1)-$(strip $(word 2,$(dep_$1))).tar) \
- $(if $(wildcard $(CACHE_DIR)/hex/$(hex_tar_name)),,$(call core_http_get,$(CACHE_DIR)/hex/$(hex_tar_name),\
- https://repo.hex.pm/tarballs/$(hex_tar_name);)) \
+ $(eval hex_pkg_name := $(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1)) \
+ $(eval hex_tar_name := $(hex_pkg_name)-$(strip $(word 2,$(dep_$1))).tar) \
+ $(if $(wildcard $(CACHE_DIR)/hex/$(hex_tar_name)),,\
+ $(call erlang,$(call hex_get_tarball.erl,$(hex_pkg_name),$(word 2,$(dep_$1)),$(CACHE_DIR)/hex/$(hex_tar_name)));) \
tar -xOf $(CACHE_DIR)/hex/$(hex_tar_name) contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -;
endef
@@ -4340,58 +1232,62 @@ else
# Hex only has a package version. No need to look in the Erlang.mk packages.
define dep_fetch_hex
mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \
- $(call core_http_get,$(ERLANG_MK_TMP)/hex/$1.tar,\
- https://repo.hex.pm/tarballs/$(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1)-$(strip $(word 2,$(dep_$1))).tar); \
+ $(call erlang,$(call hex_get_tarball.erl,$(if $(word 3,$(dep_$1)),$(word 3,$(dep_$1)),$1),$(word 2,$(dep_$1)),$(ERLANG_MK_TMP)/hex/$1.tar)); \
tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -;
endef
endif
define dep_fetch_fail
- echo "Error: Unknown or invalid dependency: $(1)." >&2; \
+ echo "Error: Unknown or invalid dependency: $1." >&2; \
exit 78;
endef
-# Kept for compatibility purposes with older Erlang.mk configuration.
-define dep_fetch_legacy
- $(warning WARNING: '$(1)' dependency configuration uses deprecated format.) \
- git clone -q -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1); \
- cd $(DEPS_DIR)/$(1) && git checkout -q $(if $(word 2,$(dep_$(1))),$(word 2,$(dep_$(1))),master);
-endef
-
define dep_target
-$(DEPS_DIR)/$(call dep_name,$1): | $(ERLANG_MK_TMP)
- $(eval DEP_NAME := $(call dep_name,$1))
+$(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method,$1)),hex-core) $(ERLANG_MK_TMP)
+ $(eval DEP_NAME := $(call query_name,$1))
$(eval DEP_STR := $(if $(filter $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))"))
$(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \
echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)." >&2; \
exit 17; \
fi
$(verbose) mkdir -p $(DEPS_DIR)
- $(dep_verbose) $(call dep_fetch_$(strip $(call dep_fetch,$(1))),$(1))
- $(verbose) if [ -f $(DEPS_DIR)/$(1)/configure.ac -o -f $(DEPS_DIR)/$(1)/configure.in ] \
- && [ ! -f $(DEPS_DIR)/$(1)/configure ]; then \
+ $(dep_verbose) $(call dep_fetch_$(strip $(call query_fetch_method,$1)),$1)
+ $(verbose) if [ -f $(DEPS_DIR)/$1/configure.ac -o -f $(DEPS_DIR)/$1/configure.in ] \
+ && [ ! -f $(DEPS_DIR)/$1/configure ]; then \
echo " AUTO " $(DEP_STR); \
- cd $(DEPS_DIR)/$(1) && autoreconf -Wall -vif -I m4; \
+ cd $(DEPS_DIR)/$1 && autoreconf -Wall -vif -I m4; \
fi
- $(verbose) if [ -f $(DEPS_DIR)/$(DEP_NAME)/configure ]; then \
echo " CONF " $(DEP_STR); \
cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \
fi
-ifeq ($(filter $(1),$(NO_AUTOPATCH)),)
+ifeq ($(filter $1,$(NO_AUTOPATCH)),)
$(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME)
endif
-.PHONY: autopatch-$(call dep_name,$1)
+.PHONY: autopatch-$(call query_name,$1)
-autopatch-$(call dep_name,$1)::
+autopatch-$(call query_name,$1)::
$(verbose) if [ "$1" = "elixir" -a "$(ELIXIR_PATCH)" ]; then \
ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/; \
else \
- $$(call dep_autopatch,$(call dep_name,$1)) \
+ $$(call dep_autopatch,$(call query_name,$1)) \
fi
endef
+# We automatically depend on hex_core when the project isn't already.
+$(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\
+ $(eval $(call dep_target,hex_core)))
+
+.PHONY: hex-core
+
+hex-core: $(DEPS_DIR)/hex_core
+ $(verbose) if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \
+ $(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \
+ touch $(DEPS_DIR)/hex_core/ebin/dep_built; \
+ fi
+
$(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep))))
ifndef IS_APP
@@ -4549,10 +1445,10 @@ define app_file
{application, '$(PROJECT)', [
{description, "$(PROJECT_DESCRIPTION)"},
{vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP),
- {id$(comma)$(space)"$(1)"}$(comma))
- {modules, [$(call comma_list,$(2))]},
+ {id$(comma)$(space)"$1"}$(comma))
+ {modules, [$(call comma_list,$2)]},
{registered, []},
- {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]},
+ {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]},
{optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},
{env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),)
]}.
@@ -4562,10 +1458,10 @@ define app_file
{application, '$(PROJECT)', [
{description, "$(PROJECT_DESCRIPTION)"},
{vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP),
- {id$(comma)$(space)"$(1)"}$(comma))
- {modules, [$(call comma_list,$(2))]},
+ {id$(comma)$(space)"$1"}$(comma))
+ {modules, [$(call comma_list,$2)]},
{registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]},
- {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]},
+ {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]},
{optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},
{mod, {$(PROJECT_MOD), []}},
{env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),)
@@ -4591,7 +1487,7 @@ ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES))))
define compile_asn1
$(verbose) mkdir -p include/
- $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $(1)
+ $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $1
$(verbose) mv asn1/*.erl src/
-$(verbose) mv asn1/*.hrl include/
$(verbose) mv asn1/*.asn1db include/
@@ -4753,7 +1649,7 @@ define makedep.erl
[233] -> unicode:characters_to_binary(Output0);
_ -> Output0
end,
- ok = file:write_file("$(1)", Output),
+ ok = file:write_file("$1", Output),
halt()
endef
@@ -4789,7 +1685,7 @@ ebin/:
define compile_erl
$(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \
- -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $(1))
+ -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $1)
endef
define validate_app_file
@@ -4923,7 +1819,7 @@ test_erlc_verbose = $(test_erlc_verbose_$(V))
define compile_test_erl
$(test_erlc_verbose) erlc -v $(TEST_ERLC_OPTS) -o $(TEST_DIR) \
- -pa ebin/ -I include/ $(1)
+ -pa ebin/ -I include/ $1
endef
ERL_TEST_FILES = $(call core_find,$(TEST_DIR)/,*.erl)
@@ -4977,6 +1873,8 @@ endif
.PHONY: rebar.config
+compat_ref = {$(shell (git -C $(DEPS_DIR)/$1 show-ref -q --verify "refs/heads/$2" && echo branch) || (git -C $(DEPS_DIR)/$1 show-ref -q --verify "refs/tags/$2" && echo tag) || echo ref),"$2"}
+
# We strip out -Werror because we don't want to fail due to
# warnings when used as a dependency.
@@ -4995,122 +1893,18 @@ endef
define compat_rebar_config
{deps, [
$(call comma_list,$(foreach d,$(DEPS),\
- $(if $(filter hex,$(call dep_fetch,$d)),\
- {$(call dep_name,$d)$(comma)"$(call dep_repo,$d)"},\
- {$(call dep_name,$d)$(comma)".*"$(comma){git,"$(call dep_repo,$d)"$(comma)"$(call dep_commit,$d)"}})))
+ $(if $(filter hex,$(call query_fetch_method,$d)),\
+ {$(call query_name,$d)$(comma)"$(call query_version_hex,$d)"},\
+ {$(call query_name,$d)$(comma)".*"$(comma){git,"$(call query_repo,$d)"$(comma)$(call compat_ref,$(call query_name,$d),$(call query_version,$d))}})))
]}.
{erl_opts, $(call compat_erlc_opts_to_list,$(ERLC_OPTS))}.
endef
-rebar.config:
+rebar.config: deps
$(gen_verbose) $(call core_render,compat_rebar_config,rebar.config)
-# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
-# This file is part of erlang.mk and subject to the terms of the ISC License.
-
-ifeq ($(filter asciideck,$(DEPS) $(DOC_DEPS)),asciideck)
-
-.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc-guide distclean-asciidoc-manual
-
-# Core targets.
-
-docs:: asciidoc
-
-distclean:: distclean-asciidoc-guide distclean-asciidoc-manual
-
-# Plugin-specific targets.
-
-asciidoc: asciidoc-guide asciidoc-manual
-
-# User guide.
-
-ifeq ($(wildcard doc/src/guide/book.asciidoc),)
-asciidoc-guide:
-else
-asciidoc-guide: distclean-asciidoc-guide doc-deps
- a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf
- a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/
-
-distclean-asciidoc-guide:
- $(gen_verbose) rm -rf doc/html/ doc/guide.pdf
-endif
-
-# Man pages.
-
-ASCIIDOC_MANUAL_FILES := $(wildcard doc/src/manual/*.asciidoc)
-
-ifeq ($(ASCIIDOC_MANUAL_FILES),)
-asciidoc-manual:
-else
-
-# Configuration.
-
-MAN_INSTALL_PATH ?= /usr/local/share/man
-MAN_SECTIONS ?= 3 7
-MAN_PROJECT ?= $(shell echo $(PROJECT) | sed 's/^./\U&\E/')
-MAN_VERSION ?= $(PROJECT_VERSION)
-
-# Plugin-specific targets.
-
-define asciidoc2man.erl
-try
- [begin
- io:format(" ADOC ~s~n", [F]),
- ok = asciideck:to_manpage(asciideck:parse_file(F), #{
- compress => gzip,
- outdir => filename:dirname(F),
- extra2 => "$(MAN_PROJECT) $(MAN_VERSION)",
- extra3 => "$(MAN_PROJECT) Function Reference"
- })
- end || F <- [$(shell echo $(addprefix $(comma)\",$(addsuffix \",$1)) | sed 's/^.//')]],
- halt(0)
-catch C:E$(if $V,:S) ->
- io:format("Exception: ~p:~p~n$(if $V,Stacktrace: ~p~n)", [C, E$(if $V,$(comma) S)]),
- halt(1)
-end.
-endef
-
-asciidoc-manual:: doc-deps
-
-asciidoc-manual:: $(ASCIIDOC_MANUAL_FILES)
- $(gen_verbose) $(call erlang,$(call asciidoc2man.erl,$?))
- $(verbose) $(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;)
-
-install-docs:: install-asciidoc
-
-install-asciidoc: asciidoc-manual
- $(foreach s,$(MAN_SECTIONS),\
- mkdir -p $(MAN_INSTALL_PATH)/man$s/ && \
- install -g `id -g` -o `id -u` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;)
-
-distclean-asciidoc-manual:
- $(gen_verbose) rm -rf $(addprefix doc/man,$(MAN_SECTIONS))
-endif
-endif
-
-# Copyright (c) 2014-2016, Loïc Hoguin <[email protected]>
-# This file is part of erlang.mk and subject to the terms of the ISC License.
-
-.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates
-
-# Core targets.
-
-help::
- $(verbose) printf "%s\n" "" \
- "Bootstrap targets:" \
- " bootstrap Generate a skeleton of an OTP application" \
- " bootstrap-lib Generate a skeleton of an OTP library" \
- " bootstrap-rel Generate the files needed to build a release" \
- " new-app in=NAME Create a new local OTP application NAME" \
- " new-lib in=NAME Create a new local OTP library NAME" \
- " new t=TPL n=NAME Generate a module NAME based on the template TPL" \
- " new t=T n=N in=APP Generate a module NAME based on the template TPL in APP" \
- " list-templates List available templates"
-
-# Bootstrap templates.
-
-define bs_appsrc
-{application, $p, [
+define tpl_application.app.src
+{application, project_name, [
{description, ""},
{vsn, "0.1.0"},
{id, "git"},
@@ -5120,178 +1914,134 @@ define bs_appsrc
kernel,
stdlib
]},
- {mod, {$p_app, []}},
+ {mod, {project_name_app, []}},
{env, []}
]}.
endef
-define bs_appsrc_lib
-{application, $p, [
- {description, ""},
- {vsn, "0.1.0"},
- {id, "git"},
- {modules, []},
- {registered, []},
- {applications, [
- kernel,
- stdlib
- ]}
-]}.
-endef
-
-# To prevent autocompletion issues with ZSH, we add "include erlang.mk"
-# separately during the actual bootstrap.
-define bs_Makefile
-PROJECT = $p
-PROJECT_DESCRIPTION = New project
-PROJECT_VERSION = 0.1.0
-$(if $(SP),
-# Whitespace to be used when creating files from templates.
-SP = $(SP)
-)
-endef
-
-define bs_apps_Makefile
-PROJECT = $p
-PROJECT_DESCRIPTION = New project
-PROJECT_VERSION = 0.1.0
-$(if $(SP),
-# Whitespace to be used when creating files from templates.
-SP = $(SP)
-)
-# Make sure we know where the applications are located.
-ROOT_DIR ?= $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)
-APPS_DIR ?= ..
-DEPS_DIR ?= $(call core_relpath,$(DEPS_DIR),$(APPS_DIR)/app)
-
-include $$(ROOT_DIR)/erlang.mk
-endef
-
-define bs_app
--module($p_app).
+define tpl_application
+-module(project_name_app).
-behaviour(application).
-export([start/2]).
-export([stop/1]).
start(_Type, _Args) ->
- $p_sup:start_link().
+ project_name_sup:start_link().
stop(_State) ->
ok.
endef
-define bs_relx_config
-{release, {$p_release, "1"}, [$p, sasl, runtime_tools]}.
-{dev_mode, false}.
-{include_erts, true}.
-{extended_start_script, true}.
-{sys_config, "config/sys.config"}.
-{vm_args, "config/vm.args"}.
-endef
+define tpl_apps_Makefile
+PROJECT = project_name
+PROJECT_DESCRIPTION = New project
+PROJECT_VERSION = 0.1.0
+template_sp
+# Make sure we know where the applications are located.
+ROOT_DIR ?= rel_root_dir
+APPS_DIR ?= ..
+DEPS_DIR ?= rel_deps_dir
-define bs_sys_config
-[
-].
+include rel_root_dir/erlang.mk
endef
-define bs_vm_args
--setcookie $p
--heart
-endef
+define tpl_cowboy_http_h
+-module(template_name).
+-behaviour(cowboy_http_handler).
-# Normal templates.
+-export([init/3]).
+-export([handle/2]).
+-export([terminate/3]).
-define tpl_supervisor
--module($(n)).
--behaviour(supervisor).
+-record(state, {
+}).
--export([start_link/0]).
--export([init/1]).
+init(_, Req, _Opts) ->
+ {ok, Req, #state{}}.
-start_link() ->
- supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+handle(Req, State=#state{}) ->
+ {ok, Req2} = cowboy_req:reply(200, Req),
+ {ok, Req2, State}.
-init([]) ->
- Procs = [],
- {ok, {{one_for_one, 1, 5}, Procs}}.
+terminate(_Reason, _Req, _State) ->
+ ok.
endef
-define tpl_gen_server
--module($(n)).
--behaviour(gen_server).
-
-%% API.
--export([start_link/0]).
+define tpl_cowboy_loop_h
+-module(template_name).
+-behaviour(cowboy_loop_handler).
-%% gen_server.
--export([init/1]).
--export([handle_call/3]).
--export([handle_cast/2]).
--export([handle_info/2]).
--export([terminate/2]).
--export([code_change/3]).
+-export([init/3]).
+-export([info/3]).
+-export([terminate/3]).
-record(state, {
}).
-%% API.
-
--spec start_link() -> {ok, pid()}.
-start_link() ->
- gen_server:start_link(?MODULE, [], []).
-
-%% gen_server.
+init(_, Req, _Opts) ->
+ {loop, Req, #state{}, 5000, hibernate}.
-init([]) ->
- {ok, #state{}}.
+info(_Info, Req, State) ->
+ {loop, Req, State, hibernate}.
-handle_call(_Request, _From, State) ->
- {reply, ignored, State}.
+terminate(_Reason, _Req, _State) ->
+ ok.
+endef
-handle_cast(_Msg, State) ->
- {noreply, State}.
+define tpl_cowboy_rest_h
+-module(template_name).
-handle_info(_Info, State) ->
- {noreply, State}.
+-export([init/3]).
+-export([content_types_provided/2]).
+-export([get_html/2]).
-terminate(_Reason, _State) ->
- ok.
+init(_, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_rest}.
-code_change(_OldVsn, State, _Extra) ->
- {ok, State}.
-endef
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"html">>, '*'}, get_html}], Req, State}.
-define tpl_module
--module($(n)).
--export([]).
+get_html(Req, State) ->
+ {<<"<html><body>This is REST!</body></html>">>, Req, State}.
endef
-define tpl_cowboy_http
--module($(n)).
--behaviour(cowboy_http_handler).
+define tpl_cowboy_websocket_h
+-module(template_name).
+-behaviour(cowboy_websocket_handler).
-export([init/3]).
--export([handle/2]).
--export([terminate/3]).
+-export([websocket_init/3]).
+-export([websocket_handle/3]).
+-export([websocket_info/3]).
+-export([websocket_terminate/3]).
-record(state, {
}).
-init(_, Req, _Opts) ->
- {ok, Req, #state{}}.
+init(_, _, _) ->
+ {upgrade, protocol, cowboy_websocket}.
-handle(Req, State=#state{}) ->
- {ok, Req2} = cowboy_req:reply(200, Req),
- {ok, Req2, State}.
+websocket_init(_, Req, _Opts) ->
+ Req2 = cowboy_req:compact(Req),
+ {ok, Req2, #state{}}.
-terminate(_Reason, _Req, _State) ->
+websocket_handle({text, Data}, Req, State) ->
+ {reply, {text, Data}, Req, State};
+websocket_handle({binary, Data}, Req, State) ->
+ {reply, {binary, Data}, Req, State};
+websocket_handle(_Frame, Req, State) ->
+ {ok, Req, State}.
+
+websocket_info(_Info, Req, State) ->
+ {ok, Req, State}.
+
+websocket_terminate(_Reason, _Req, _State) ->
ok.
endef
define tpl_gen_fsm
--module($(n)).
+-module(template_name).
-behaviour(gen_fsm).
%% API.
@@ -5343,8 +2093,53 @@ code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
endef
+define tpl_gen_server
+-module(template_name).
+-behaviour(gen_server).
+
+%% API.
+-export([start_link/0]).
+
+%% gen_server.
+-export([init/1]).
+-export([handle_call/3]).
+-export([handle_cast/2]).
+-export([handle_info/2]).
+-export([terminate/2]).
+-export([code_change/3]).
+
+-record(state, {
+}).
+
+%% API.
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+ gen_server:start_link(?MODULE, [], []).
+
+%% gen_server.
+
+init([]) ->
+ {ok, #state{}}.
+
+handle_call(_Request, _From, State) ->
+ {reply, ignored, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+endef
+
define tpl_gen_statem
--module($(n)).
+-module(template_name).
-behaviour(gen_statem).
%% API.
@@ -5388,80 +2183,27 @@ code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
endef
-define tpl_cowboy_loop
--module($(n)).
--behaviour(cowboy_loop_handler).
-
--export([init/3]).
--export([info/3]).
--export([terminate/3]).
-
--record(state, {
-}).
-
-init(_, Req, _Opts) ->
- {loop, Req, #state{}, 5000, hibernate}.
-
-info(_Info, Req, State) ->
- {loop, Req, State, hibernate}.
-
-terminate(_Reason, _Req, _State) ->
- ok.
-endef
-
-define tpl_cowboy_rest
--module($(n)).
-
--export([init/3]).
--export([content_types_provided/2]).
--export([get_html/2]).
-
-init(_, _Req, _Opts) ->
- {upgrade, protocol, cowboy_rest}.
-
-content_types_provided(Req, State) ->
- {[{{<<"text">>, <<"html">>, '*'}, get_html}], Req, State}.
-
-get_html(Req, State) ->
- {<<"<html><body>This is REST!</body></html>">>, Req, State}.
+define tpl_library.app.src
+{application, project_name, [
+ {description, ""},
+ {vsn, "0.1.0"},
+ {id, "git"},
+ {modules, []},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]}
+]}.
endef
-define tpl_cowboy_ws
--module($(n)).
--behaviour(cowboy_websocket_handler).
-
--export([init/3]).
--export([websocket_init/3]).
--export([websocket_handle/3]).
--export([websocket_info/3]).
--export([websocket_terminate/3]).
-
--record(state, {
-}).
-
-init(_, _, _) ->
- {upgrade, protocol, cowboy_websocket}.
-
-websocket_init(_, Req, _Opts) ->
- Req2 = cowboy_req:compact(Req),
- {ok, Req2, #state{}}.
-
-websocket_handle({text, Data}, Req, State) ->
- {reply, {text, Data}, Req, State};
-websocket_handle({binary, Data}, Req, State) ->
- {reply, {binary, Data}, Req, State};
-websocket_handle(_Frame, Req, State) ->
- {ok, Req, State}.
-
-websocket_info(_Info, Req, State) ->
- {ok, Req, State}.
-
-websocket_terminate(_Reason, _Req, _State) ->
- ok.
+define tpl_module
+-module(template_name).
+-export([]).
endef
define tpl_ranch_protocol
--module($(n)).
+-module(template_name).
-behaviour(ranch_protocol).
-export([start_link/4]).
@@ -5488,6 +2230,152 @@ loop(State) ->
loop(State).
endef
+define tpl_relx.config
+{release, {project_name_release, "1"}, [project_name, sasl, runtime_tools]}.
+{dev_mode, false}.
+{include_erts, true}.
+{extended_start_script, true}.
+{sys_config, "config/sys.config"}.
+{vm_args, "config/vm.args"}.
+endef
+
+define tpl_supervisor
+-module(template_name).
+-behaviour(supervisor).
+
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ Procs = [],
+ {ok, {{one_for_one, 1, 5}, Procs}}.
+endef
+
+define tpl_sys.config
+[
+].
+endef
+
+define tpl_top_Makefile
+PROJECT = project_name
+PROJECT_DESCRIPTION = New project
+PROJECT_VERSION = 0.1.0
+template_sp
+include erlang.mk
+endef
+
+define tpl_vm.args
+-setcookie project_name
+-heart
+endef
+
+
+# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ifeq ($(filter asciideck,$(DEPS) $(DOC_DEPS)),asciideck)
+
+.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc-guide distclean-asciidoc-manual
+
+# Core targets.
+
+docs:: asciidoc
+
+distclean:: distclean-asciidoc-guide distclean-asciidoc-manual
+
+# Plugin-specific targets.
+
+asciidoc: asciidoc-guide asciidoc-manual
+
+# User guide.
+
+ifeq ($(wildcard doc/src/guide/book.asciidoc),)
+asciidoc-guide:
+else
+asciidoc-guide: distclean-asciidoc-guide doc-deps
+ a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf
+ a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/
+
+distclean-asciidoc-guide:
+ $(gen_verbose) rm -rf doc/html/ doc/guide.pdf
+endif
+
+# Man pages.
+
+ASCIIDOC_MANUAL_FILES := $(wildcard doc/src/manual/*.asciidoc)
+
+ifeq ($(ASCIIDOC_MANUAL_FILES),)
+asciidoc-manual:
+else
+
+# Configuration.
+
+MAN_INSTALL_PATH ?= /usr/local/share/man
+MAN_SECTIONS ?= 3 7
+MAN_PROJECT ?= $(shell echo $(PROJECT) | sed 's/^./\U&\E/')
+MAN_VERSION ?= $(PROJECT_VERSION)
+
+# Plugin-specific targets.
+
+define asciidoc2man.erl
+try
+ [begin
+ io:format(" ADOC ~s~n", [F]),
+ ok = asciideck:to_manpage(asciideck:parse_file(F), #{
+ compress => gzip,
+ outdir => filename:dirname(F),
+ extra2 => "$(MAN_PROJECT) $(MAN_VERSION)",
+ extra3 => "$(MAN_PROJECT) Function Reference"
+ })
+ end || F <- [$(shell echo $(addprefix $(comma)\",$(addsuffix \",$1)) | sed 's/^.//')]],
+ halt(0)
+catch C:E$(if $V,:S) ->
+ io:format("Exception: ~p:~p~n$(if $V,Stacktrace: ~p~n)", [C, E$(if $V,$(comma) S)]),
+ halt(1)
+end.
+endef
+
+asciidoc-manual:: doc-deps
+
+asciidoc-manual:: $(ASCIIDOC_MANUAL_FILES)
+ $(gen_verbose) $(call erlang,$(call asciidoc2man.erl,$?))
+ $(verbose) $(foreach s,$(MAN_SECTIONS),mkdir -p doc/man$s/ && mv doc/src/manual/*.$s.gz doc/man$s/;)
+
+install-docs:: install-asciidoc
+
+install-asciidoc: asciidoc-manual
+ $(foreach s,$(MAN_SECTIONS),\
+ mkdir -p $(MAN_INSTALL_PATH)/man$s/ && \
+ install -g `id -g` -o `id -u` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;)
+
+distclean-asciidoc-manual:
+ $(gen_verbose) rm -rf $(addprefix doc/man,$(MAN_SECTIONS))
+endif
+endif
+
+# Copyright (c) 2014-2016, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates
+
+# Core targets.
+
+help::
+ $(verbose) printf "%s\n" "" \
+ "Bootstrap targets:" \
+ " bootstrap Generate a skeleton of an OTP application" \
+ " bootstrap-lib Generate a skeleton of an OTP library" \
+ " bootstrap-rel Generate the files needed to build a release" \
+ " new-app in=NAME Create a new local OTP application NAME" \
+ " new-lib in=NAME Create a new local OTP library NAME" \
+ " new t=TPL n=NAME Generate a module NAME based on the template TPL" \
+ " new t=T n=N in=APP Generate a module NAME based on the template TPL in APP" \
+ " list-templates List available templates"
+
# Plugin-specific targets.
ifndef WS
@@ -5498,6 +2386,26 @@ WS = $(tab)
endif
endif
+ifdef SP
+define template_sp
+
+# By default templates indent with a single tab per indentation
+# level. Set this variable to the number of spaces you prefer:
+SP = $(SP)
+
+endef
+else
+template_sp =
+endif
+
+# @todo Additional template placeholders could be added.
+subst_template = $(subst rel_root_dir,$(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app),$(subst rel_deps_dir,$(call core_relpath,$(DEPS_DIR),$(APPS_DIR)/app),$(subst template_sp,$(template_sp),$(subst project_name,$p,$(subst template_name,$n,$1)))))
+
+define core_render_template
+ $(eval define _tpl_$(1)$(newline)$(call subst_template,$(tpl_$(1)))$(newline)endef)
+ $(verbose) $(call core_render,_tpl_$(1),$2)
+endef
+
bootstrap:
ifneq ($(wildcard src/),)
$(error Error: src/ directory already exists)
@@ -5506,14 +2414,13 @@ endif
$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
$(error Error: Invalid characters in the application name))
$(eval n := $(PROJECT)_sup)
- $(verbose) $(call core_render,bs_Makefile,Makefile)
- $(verbose) echo "include erlang.mk" >> Makefile
+ $(verbose) $(call core_render_template,top_Makefile,Makefile)
$(verbose) mkdir src/
ifdef LEGACY
- $(verbose) $(call core_render,bs_appsrc,src/$(PROJECT).app.src)
+ $(verbose) $(call core_render_template,application.app.src,src/$(PROJECT).app.src)
endif
- $(verbose) $(call core_render,bs_app,src/$(PROJECT)_app.erl)
- $(verbose) $(call core_render,tpl_supervisor,src/$(PROJECT)_sup.erl)
+ $(verbose) $(call core_render_template,application,src/$(PROJECT)_app.erl)
+ $(verbose) $(call core_render_template,supervisor,src/$(PROJECT)_sup.erl)
bootstrap-lib:
ifneq ($(wildcard src/),)
@@ -5522,11 +2429,10 @@ endif
$(eval p := $(PROJECT))
$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
$(error Error: Invalid characters in the application name))
- $(verbose) $(call core_render,bs_Makefile,Makefile)
- $(verbose) echo "include erlang.mk" >> Makefile
+ $(verbose) $(call core_render_template,top_Makefile,Makefile)
$(verbose) mkdir src/
ifdef LEGACY
- $(verbose) $(call core_render,bs_appsrc_lib,src/$(PROJECT).app.src)
+ $(verbose) $(call core_render_template,library.app.src,src/$(PROJECT).app.src)
endif
bootstrap-rel:
@@ -5537,10 +2443,10 @@ ifneq ($(wildcard config/),)
$(error Error: config/ directory already exists)
endif
$(eval p := $(PROJECT))
- $(verbose) $(call core_render,bs_relx_config,relx.config)
+ $(verbose) $(call core_render_template,relx.config,relx.config)
$(verbose) mkdir config/
- $(verbose) $(call core_render,bs_sys_config,config/sys.config)
- $(verbose) $(call core_render,bs_vm_args,config/vm.args)
+ $(verbose) $(call core_render_template,sys.config,config/sys.config)
+ $(verbose) $(call core_render_template,vm.args,config/vm.args)
$(verbose) awk '/^include erlang.mk/ && !ins {print "REL_DEPS += relx";ins=1};{print}' Makefile > Makefile.bak
$(verbose) mv Makefile.bak Makefile
@@ -5556,12 +2462,12 @@ endif
$(error Error: Invalid characters in the application name))
$(eval n := $(in)_sup)
$(verbose) mkdir -p $(APPS_DIR)/$p/src/
- $(verbose) $(call core_render,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile)
+ $(verbose) $(call core_render_template,apps_Makefile,$(APPS_DIR)/$p/Makefile)
ifdef LEGACY
- $(verbose) $(call core_render,bs_appsrc,$(APPS_DIR)/$p/src/$p.app.src)
+ $(verbose) $(call core_render_template,application.app.src,$(APPS_DIR)/$p/src/$p.app.src)
endif
- $(verbose) $(call core_render,bs_app,$(APPS_DIR)/$p/src/$p_app.erl)
- $(verbose) $(call core_render,tpl_supervisor,$(APPS_DIR)/$p/src/$p_sup.erl)
+ $(verbose) $(call core_render_template,application,$(APPS_DIR)/$p/src/$p_app.erl)
+ $(verbose) $(call core_render_template,supervisor,$(APPS_DIR)/$p/src/$p_sup.erl)
new-lib:
ifndef in
@@ -5574,30 +2480,40 @@ endif
$(if $(shell echo $p | LC_ALL=C grep -x "[a-z0-9_]*"),,\
$(error Error: Invalid characters in the application name))
$(verbose) mkdir -p $(APPS_DIR)/$p/src/
- $(verbose) $(call core_render,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile)
+ $(verbose) $(call core_render_template,apps_Makefile,$(APPS_DIR)/$p/Makefile)
ifdef LEGACY
- $(verbose) $(call core_render,bs_appsrc_lib,$(APPS_DIR)/$p/src/$p.app.src)
+ $(verbose) $(call core_render_template,library.app.src,$(APPS_DIR)/$p/src/$p.app.src)
endif
+# These are not necessary because we don't expose those as "normal" templates.
+BOOTSTRAP_TEMPLATES = apps_Makefile top_Makefile \
+ application.app.src library.app.src application \
+ relx.config sys.config vm.args
+
+# Templates may override the path they will be written to when using 'new'.
+# Only special template paths must be listed. Default is src/template_name.erl
+# Substitution is also applied to the paths. Examples:
+#
+#tplp_top_Makefile = Makefile
+#tplp_application.app.src = src/project_name.app.src
+#tplp_application = src/project_name_app.erl
+#tplp_relx.config = relx.config
+
+# Erlang.mk bundles its own templates at build time into the erlang.mk file.
+
new:
-ifeq ($(wildcard src/)$(in),)
- $(error Error: src/ directory does not exist)
-endif
-ifndef t
- $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])
-endif
-ifndef n
- $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])
-endif
-ifdef in
- $(verbose) $(call core_render,tpl_$(t),$(APPS_DIR)/$(in)/src/$(n).erl)
-else
- $(verbose) $(call core_render,tpl_$(t),src/$(n).erl)
-endif
+ $(if $(t),,$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]))
+ $(if $(n),,$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]))
+ $(if $(tpl_$(t)),,$(error Error: $t template does not exist; try $(Make) list-templates))
+ $(eval dest := $(if $(in),$(APPS_DIR)/$(in)/)$(call subst_template,$(if $(tplp_$(t)),$(tplp_$(t)),src/template_name.erl)))
+ $(if $(wildcard $(dir $(dest))),,$(error Error: $(dir $(dest)) directory does not exist))
+ $(if $(wildcard $(dest)),$(error Error: The file $(dest) already exists))
+ $(eval p := $(PROJECT))
+ $(call core_render_template,$(t),$(dest))
list-templates:
$(verbose) @echo Available templates:
- $(verbose) printf " %s\n" $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES))))
+ $(verbose) printf " %s\n" $(sort $(filter-out $(BOOTSTRAP_TEMPLATES),$(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))))
# Copyright (c) 2014-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -5894,7 +2810,7 @@ ci-setup::
ci-extra::
$(verbose) :
-ci_verbose_0 = @echo " CI " $(1);
+ci_verbose_0 = @echo " CI " $1;
ci_verbose = $(ci_verbose_$(V))
define ci_target
@@ -6380,37 +3296,6 @@ endif
# Copyright (c) 2020, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
-HEX_CORE_GIT ?= https://github.com/hexpm/hex_core
-HEX_CORE_COMMIT ?= v0.7.0
-
-PACKAGES += hex_core
-pkg_hex_core_name = hex_core
-pkg_hex_core_description = Reference implementation of Hex specifications
-pkg_hex_core_homepage = $(HEX_CORE_GIT)
-pkg_hex_core_fetch = git
-pkg_hex_core_repo = $(HEX_CORE_GIT)
-pkg_hex_core_commit = $(HEX_CORE_COMMIT)
-
-# We automatically depend on hex_core when the project isn't already.
-$(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\
- $(eval $(call dep_target,hex_core)))
-
-hex-core: $(DEPS_DIR)/hex_core
- $(verbose) if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \
- $(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \
- touch $(DEPS_DIR)/hex_core/ebin/dep_built; \
- fi
-
-# @todo This must also apply to fetching.
-HEX_CONFIG ?=
-
-define hex_config.erl
- begin
- Config0 = hex_core:default_config(),
- Config0$(HEX_CONFIG)
- end
-endef
-
define hex_user_create.erl
{ok, _} = application:ensure_all_started(ssl),
{ok, _} = application:ensure_all_started(inets),
@@ -6481,7 +3366,7 @@ HEX_TARBALL_FILES ?= \
$(sort $(call core_find,priv/,*)) \
$(wildcard README*) \
$(wildcard rebar.config) \
- $(sort $(call core_find,src/,*))
+ $(sort $(if $(LEGACY),$(filter-out src/$(PROJECT).app.src,$(call core_find,src/,*)),$(call core_find,src/,*)))
HEX_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT).tar
@@ -6501,7 +3386,7 @@ define hex_tarball_create.erl
<<"$(if $(subst hex,,$(call query_fetch_method,$d)),$d,$(if $(word 3,$(dep_$d)),$(word 3,$(dep_$d)),$d))">> => #{
<<"app">> => <<"$d">>,
<<"optional">> => false,
- <<"requirement">> => <<"$(call query_version,$d)">>
+ <<"requirement">> => <<"$(if $(hex_req_$d),$(strip $(hex_req_$d)),$(call query_version,$d))">>
},)
$(if $(DEPS),dummy => dummy)
},
@@ -6940,17 +3825,13 @@ endef
relx-rel: rel-deps app
$(call erlang,$(call relx_release.erl),-pa ebin/)
$(verbose) $(MAKE) relx-post-rel
-ifeq ($(RELX_TAR),1)
- $(call erlang,$(call relx_tar.erl),-pa ebin/)
-endif
+ $(if $(filter-out 0,$(RELX_TAR)),$(call erlang,$(call relx_tar.erl),-pa ebin/))
relx-relup: rel-deps app
$(call erlang,$(call relx_release.erl),-pa ebin/)
$(MAKE) relx-post-rel
$(call erlang,$(call relx_relup.erl),-pa ebin/)
-ifeq ($(RELX_TAR),1)
- $(call erlang,$(call relx_tar.erl),-pa ebin/)
-endif
+ $(if $(filter-out 0,$(RELX_TAR)),$(call erlang,$(call relx_tar.erl),-pa ebin/))
distclean-relx-rel:
$(gen_verbose) rm -rf $(RELX_OUTPUT_DIR)
@@ -6993,6 +3874,7 @@ ifeq ($(PLATFORM),msys2)
RELX_REL_EXT := .cmd
endif
+run:: RELX_TAR := 0
run:: all
$(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) $(RELX_REL_CMD)
@@ -7750,14 +4632,14 @@ list-deps list-doc-deps list-rel-deps list-test-deps list-shell-deps:
QUERY ?= name fetch_method repo version
define query_target
-$(1): $(2) clean-tmp-query.log
+$1: $2 clean-tmp-query.log
ifeq ($(IS_APP)$(IS_DEP),)
- $(verbose) rm -f $(4)
+ $(verbose) rm -f $4
endif
- $(verbose) $(foreach dep,$(3),\
- echo $(PROJECT): $(foreach q,$(QUERY),$(call query_$(q),$(dep))) >> $(4) ;)
- $(if $(filter-out query-deps,$(1)),,\
- $(verbose) set -e; for dep in $(3) ; do \
+ $(verbose) $(foreach dep,$3,\
+ echo $(PROJECT): $(foreach q,$(QUERY),$(call query_$(q),$(dep))) >> $4 ;)
+ $(if $(filter-out query-deps,$1),,\
+ $(verbose) set -e; for dep in $3 ; do \
if grep -qs ^$$$$dep$$$$ $(ERLANG_MK_TMP)/query.log; then \
:; \
else \
@@ -7766,8 +4648,8 @@ endif
fi \
done)
ifeq ($(IS_APP)$(IS_DEP),)
- $(verbose) touch $(4)
- $(verbose) cat $(4)
+ $(verbose) touch $4
+ $(verbose) cat $4
endif
endef
diff --git a/examples/file_server/Makefile b/examples/file_server/Makefile
index df8f311..12a4e15 100644
--- a/examples/file_server/Makefile
+++ b/examples/file_server/Makefile
@@ -2,7 +2,7 @@ PROJECT = file_server
PROJECT_DESCRIPTION = Cowboy file server example with directory listing
PROJECT_VERSION = 1
-DEPS = cowboy jsx
+DEPS = cowboy
dep_cowboy_commit = master
REL_DEPS = relx
diff --git a/examples/file_server/src/directory_h.erl b/examples/file_server/src/directory_h.erl
index 7d7bd9a..b52fc74 100644
--- a/examples/file_server/src/directory_h.erl
+++ b/examples/file_server/src/directory_h.erl
@@ -37,7 +37,7 @@ charsets_provided(Req, State) ->
list_json(Req, {Path, Fs}) ->
Files = [unicode:characters_to_binary(F) || F <- Fs],
- {jsx:encode(Files), Req, Path}.
+ {json:encode(Files), Req, Path}.
list_html(Req, {Path, Fs}) ->
Body = [[links(Path, unicode:characters_to_binary(F)) || F <- [".."|Fs]]],
diff --git a/rebar.config b/rebar.config
index c22692c..146f88f 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,4 +1,4 @@
{deps, [
-{cowlib,".*",{git,"https://github.com/ninenines/cowlib","master"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.8.0"}}
+{cowlib,".*",{git,"https://github.com/ninenines/cowlib",{branch,"master"}}},{ranch,".*",{git,"https://github.com/ninenines/ranch",{tag,"1.8.1"}}}
]}.
{erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}.
diff --git a/src/cowboy.erl b/src/cowboy.erl
index e5ed831..6a5634e 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -51,8 +51,12 @@
start_clear(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
- {TransOpts, ConnectionType} = ensure_connection_type(TransOpts1),
- ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
+ {TransOpts2, DynamicBuffer} = ensure_dynamic_buffer(TransOpts1, ProtoOpts0),
+ {TransOpts, ConnectionType} = ensure_connection_type(TransOpts2),
+ ProtoOpts = ProtoOpts0#{
+ connection_type => ConnectionType,
+ dynamic_buffer => DynamicBuffer
+ },
ranch:start_listener(Ref, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts).
-spec start_tls(ranch:ref(), ranch:opts(), opts())
@@ -60,12 +64,13 @@ start_clear(Ref, TransOpts0, ProtoOpts0) ->
start_tls(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
- SocketOpts = maps:get(socket_opts, TransOpts1, []),
- TransOpts2 = TransOpts1#{socket_opts => [
- {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
- |SocketOpts]},
- {TransOpts, ConnectionType} = ensure_connection_type(TransOpts2),
- ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
+ {TransOpts2, DynamicBuffer} = ensure_dynamic_buffer(TransOpts1, ProtoOpts0),
+ TransOpts3 = ensure_alpn(TransOpts2),
+ {TransOpts, ConnectionType} = ensure_connection_type(TransOpts3),
+ ProtoOpts = ProtoOpts0#{
+ connection_type => ConnectionType,
+ dynamic_buffer => DynamicBuffer
+ },
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).
%% @todo Experimental function to start a barebone QUIC listener.
@@ -77,6 +82,7 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
-spec start_quic(ranch:ref(), #{socket_opts => [{atom(), _}]}, cowboy_http3:opts())
-> {ok, pid()}.
+%% @todo Implement dynamic_buffer for HTTP/3 if/when it applies.
start_quic(Ref, TransOpts, ProtoOpts) ->
{ok, _} = application:ensure_all_started(quicer),
Parent = self(),
@@ -89,8 +95,14 @@ start_quic(Ref, TransOpts, ProtoOpts) ->
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}
+ %% We only need 3 for control and QPACK enc/dec,
+ %% but we need more for WebTransport. %% @todo Use 3 if WT is disabled.
+ {peer_unidi_stream_count, 100},
+ {peer_bidi_stream_count, 100},
+ %% For WebTransport.
+ %% @todo We probably don't want it enabled if WT isn't used.
+ {datagram_send_enabled, 1},
+ {datagram_receive_enabled, 1}
|SocketOpts2],
_ListenerPid = spawn(fun() ->
{ok, Listener} = quicer:listen(Port, SocketOpts),
@@ -139,11 +151,32 @@ port_0() ->
end,
Port.
+ensure_alpn(TransOpts) ->
+ SocketOpts = maps:get(socket_opts, TransOpts, []),
+ TransOpts#{socket_opts => [
+ {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
+ |SocketOpts]}.
+
ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) ->
{TransOpts, ConnectionType};
ensure_connection_type(TransOpts) ->
{TransOpts#{connection_type => supervisor}, supervisor}.
+%% Dynamic buffer was set; accept transport options as-is.
+%% Note that initial 'buffer' size may be lower than dynamic buffer allows.
+ensure_dynamic_buffer(TransOpts, #{dynamic_buffer := DynamicBuffer}) ->
+ {TransOpts, DynamicBuffer};
+%% Dynamic buffer was not set; define default dynamic buffer
+%% only if 'buffer' size was not configured. In that case we
+%% set the 'buffer' size to the lowest value.
+ensure_dynamic_buffer(TransOpts=#{socket_opts := SocketOpts}, _) ->
+ case proplists:get_value(buffer, SocketOpts, undefined) of
+ undefined ->
+ {TransOpts#{socket_opts => [{buffer, 1024}|SocketOpts]}, {1024, 131072}};
+ _ ->
+ {TransOpts, false}
+ end.
+
-spec stop_listener(ranch:ref()) -> ok | {error, not_found}.
stop_listener(Ref) ->
diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl
index 95ae564..e58e1f6 100644
--- a/src/cowboy_app.erl
+++ b/src/cowboy_app.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl
index f23167d..d0e7301 100644
--- a/src/cowboy_bstr.erl
+++ b/src/cowboy_bstr.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_children.erl b/src/cowboy_children.erl
index 305c989..2e00c37 100644
--- a/src/cowboy_children.erl
+++ b/src/cowboy_children.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_clear.erl b/src/cowboy_clear.erl
index eaeab74..845fdc1 100644
--- a/src/cowboy_clear.erl
+++ b/src/cowboy_clear.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,10 +36,6 @@ connection_process(Parent, Ref, Transport, Opts) ->
ProxyInfo = get_proxy_info(Ref, Opts),
{ok, Socket} = ranch:handshake(Ref),
%% Use cowboy_http2 directly only when 'http' is missing.
- %% Otherwise switch to cowboy_http2 from cowboy_http.
- %%
- %% @todo Extend this option to cowboy_tls and allow disabling
- %% the switch to cowboy_http2 in cowboy_http. Also document it.
Protocol = case maps:get(protocols, Opts, [http2, http]) of
[http2] -> cowboy_http2;
[_|_] -> cowboy_http
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index e2cdf62..b6e39f4 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -93,7 +93,7 @@ handle_cast(_Msg, State) ->
-spec handle_info(any(), State) -> {noreply, State} when State::#state{}.
handle_info(update, #state{universaltime=Prev, rfc1123=B1, tref=TRef0}) ->
%% Cancel the timer in case an external process sent an update message.
- _ = erlang:cancel_timer(TRef0),
+ _ = erlang:cancel_timer(TRef0, [{async, true}, {info, false}]),
T = erlang:universaltime(),
B2 = update_rfc1123(B1, Prev, T),
ets:insert(?MODULE, {rfc1123, B2}),
diff --git a/src/cowboy_compress_h.erl b/src/cowboy_compress_h.erl
index 338ea9f..785eb0d 100644
--- a/src/cowboy_compress_h.erl
+++ b/src/cowboy_compress_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_constraints.erl b/src/cowboy_constraints.erl
index 33f0111..84ff249 100644
--- a/src/cowboy_constraints.erl
+++ b/src/cowboy_constraints.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2014-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_decompress_h.erl b/src/cowboy_decompress_h.erl
index 4e86e23..84283e5 100644
--- a/src/cowboy_decompress_h.erl
+++ b/src/cowboy_decompress_h.erl
@@ -1,5 +1,5 @@
-%% Copyright (c) 2024, jdamanalo <[email protected]>
-%% Copyright (c) 2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) jdamanalo <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -66,9 +66,11 @@ data(StreamID, IsFin, Data, State=#state{next=Next0, enabled=false, read_body_bu
{Commands, Next} = cowboy_stream:data(StreamID, IsFin,
buffer_to_binary([Data|Buffer]), Next0),
fold(Commands, State#state{next=Next, read_body_is_fin=IsFin});
-data(StreamID, IsFin, Data, State0=#state{next=Next0, ratio_limit=RatioLimit,
+data(StreamID, IsFin, Data0, State0=#state{next=Next0, ratio_limit=RatioLimit,
inflate=Z, is_reading=true, read_body_buffer=Buffer}) ->
- case inflate(Z, RatioLimit, buffer_to_iovec([Data|Buffer])) of
+ Data = buffer_to_iovec([Data0|Buffer]),
+ Limit = iolist_size(Data) * RatioLimit,
+ case cow_deflate:inflate(Z, Data, Limit) of
{error, ErrorType} ->
zlib:close(Z),
Status = case ErrorType of
@@ -236,22 +238,3 @@ do_build_accept_encoding([{ContentCoding, Q}|Tail], Acc0) ->
do_build_accept_encoding(Tail, Acc);
do_build_accept_encoding([], Acc) ->
Acc.
-
-inflate(Z, RatioLimit, Data) ->
- try
- {Status, Output} = zlib:safeInflate(Z, Data),
- Size = iolist_size(Output),
- do_inflate(Z, Size, iolist_size(Data) * RatioLimit, Status, [Output])
- catch
- error:data_error ->
- {error, data_error}
- end.
-
-do_inflate(_, Size, Limit, _, _) when Size > Limit ->
- {error, size_error};
-do_inflate(Z, Size0, Limit, continue, Acc) ->
- {Status, Output} = zlib:safeInflate(Z, []),
- Size = Size0 + iolist_size(Output),
- do_inflate(Z, Size, Limit, Status, [Output | Acc]);
-do_inflate(_, _, _, finished, Acc) ->
- {ok, iolist_to_binary(lists:reverse(Acc))}.
diff --git a/src/cowboy_dynamic_buffer.hrl b/src/cowboy_dynamic_buffer.hrl
new file mode 100644
index 0000000..4d05e50
--- /dev/null
+++ b/src/cowboy_dynamic_buffer.hrl
@@ -0,0 +1,80 @@
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%%
+%% 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.
+
+%% These functions are common to cowboy_http, cowboy_http2 and
+%% cowboy_websocket. It requires the options and the state
+%% to use the same field names.
+
+%% Experiments have shown that the size of the 'buffer' can greatly
+%% impact performance: a buffer too small leads to more messages
+%% being handled and typically more binary appends; and a buffer
+%% too large results in inefficient use of memory which in turn
+%% reduces the throughput, presumably because large binary appends
+%% are not as efficient as smaller ones, and because while the
+%% buffer gets allocated only when there is data, the allocated
+%% size remains until the binary is GC and so under-use hurts.
+%%
+%% The performance of a given 'buffer' size will also depend on
+%% how the client is sending data, and on the protocol. For example,
+%% HTTP/1.1 doesn't need a very large 'buffer' size for reading
+%% request headers, but it does need one for reading large request
+%% bodies. At the same time, HTTP/2 performs best reading large
+%% request bodies when the 'buffer' size is about half that of
+%% HTTP/1.1.
+%%
+%% It therefore becomes important to resize the buffer dynamically
+%% depending on what is currently going on. We do this based on
+%% the size of data packets we received from the transport. We
+%% maintain a moving average and when that moving average is
+%% 90% of the current 'buffer' size, we double the 'buffer' size.
+%% When things slow down and the moving average falls below
+%% 40% of the current 'buffer' size, we halve the 'buffer' size.
+%%
+%% To calculate the moving average we do (MovAvg + DataLen) div 2.
+%% This means that the moving average will change very quickly when
+%% DataLen increases or decreases rapidly. That's OK, we want to
+%% be reactive, but also setting the buffer size is a pretty fast
+%% operation. The formula could be changed to the following if it
+%% became a problem: (MovAvg * N + DataLen) div (N + 1).
+%%
+%% Note that this works best when active,N uses low values of N.
+%% We don't want to accumulate too much data because we resize
+%% the buffer.
+
+init_dynamic_buffer_size(#{dynamic_buffer_initial_size := DynamicBuffer}) ->
+ DynamicBuffer;
+init_dynamic_buffer_size(#{dynamic_buffer := {LowDynamicBuffer, _}}) ->
+ LowDynamicBuffer;
+init_dynamic_buffer_size(_) ->
+ false.
+
+maybe_resize_buffer(State=#state{dynamic_buffer_size=false}, _) ->
+ State;
+maybe_resize_buffer(State=#state{transport=Transport, socket=Socket,
+ opts=#{dynamic_buffer := {LowDynamicBuffer, HighDynamicBuffer}},
+ dynamic_buffer_size=BufferSize0, dynamic_buffer_moving_average=MovingAvg0}, Data) ->
+ DataLen = byte_size(Data),
+ MovingAvg = (MovingAvg0 + DataLen) div 2,
+ if
+ BufferSize0 < HighDynamicBuffer andalso MovingAvg > BufferSize0 * 0.9 ->
+ BufferSize = min(BufferSize0 * 2, HighDynamicBuffer),
+ ok = maybe_socket_error(State, Transport:setopts(Socket, [{buffer, BufferSize}])),
+ State#state{dynamic_buffer_moving_average=MovingAvg, dynamic_buffer_size=BufferSize};
+ BufferSize0 > LowDynamicBuffer andalso MovingAvg < BufferSize0 * 0.4 ->
+ BufferSize = max(BufferSize0 div 2, LowDynamicBuffer),
+ ok = maybe_socket_error(State, Transport:setopts(Socket, [{buffer, BufferSize}])),
+ State#state{dynamic_buffer_moving_average=MovingAvg, dynamic_buffer_size=BufferSize};
+ true ->
+ State#state{dynamic_buffer_moving_average=MovingAvg}
+ end.
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl
index 5048168..1989512 100644
--- a/src/cowboy_handler.erl
+++ b/src/cowboy_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 9c92ec5..10eb519 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -17,6 +17,7 @@
-module(cowboy_http).
-export([init/6]).
+-export([loop/1]).
-export([system_continue/3]).
-export([system_terminate/4]).
@@ -24,11 +25,16 @@
-type opts() :: #{
active_n => pos_integer(),
+ alpn_default_protocol => http | http2,
chunked => boolean(),
compress_buffering => boolean(),
compress_threshold => non_neg_integer(),
connection_type => worker | supervisor,
+ dynamic_buffer => false | {pos_integer(), pos_integer()},
+ dynamic_buffer_initial_average => non_neg_integer(),
+ dynamic_buffer_initial_size => pos_integer(),
env => cowboy_middleware:env(),
+ hibernate => boolean(),
http10_keepalive => boolean(),
idle_timeout => timeout(),
inactivity_timeout => timeout(),
@@ -47,6 +53,7 @@
metrics_req_filter => fun((cowboy_req:req()) -> map()),
metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()),
middlewares => [module()],
+ protocols => [http | http2],
proxy_header => boolean(),
request_timeout => timeout(),
reset_idle_timeout_on_send => boolean(),
@@ -137,6 +144,10 @@
%% Flow requested for the current stream.
flow = infinity :: non_neg_integer() | infinity,
+ %% Dynamic buffer moving average and current buffer size.
+ dynamic_buffer_size :: pos_integer() | false,
+ dynamic_buffer_moving_average :: non_neg_integer(),
+
%% Identifier for the stream currently being written.
%% Note that out_streamid =< in_streamid.
out_streamid = 1 :: pos_integer(),
@@ -181,12 +192,16 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
parent=Parent, ref=Ref, socket=Socket,
transport=Transport, proxy_header=ProxyHeader, opts=Opts,
peer=Peer, sock=Sock, cert=Cert,
+ dynamic_buffer_size=init_dynamic_buffer_size(Opts),
+ dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0),
last_streamid=maps:get(max_keepalive, Opts, 1000)},
safe_setopts_active(State),
- loop(set_timeout(State, request_timeout)).
+ before_loop(set_timeout(State, request_timeout)).
+
+-include("cowboy_dynamic_buffer.hrl").
setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) ->
- N = maps:get(active_n, Opts, 100),
+ N = maps:get(active_n, Opts, 1),
Transport:setopts(Socket, [{active, N}]).
safe_setopts_active(State) ->
@@ -212,6 +227,13 @@ flush_passive(Socket, Messages) ->
ok
end.
+before_loop(State=#state{opts=#{hibernate := true}}) ->
+ proc_lib:hibernate(?MODULE, loop, [State]);
+before_loop(State) ->
+ loop(State).
+
+-spec loop(#state{}) -> ok.
+
loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
buffer=Buffer, timer=TimerRef, children=Children, in_streamid=InStreamID,
last_streamid=LastStreamID}) ->
@@ -220,11 +242,13 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
receive
%% Discard data coming in after the last request
%% we want to process was received fully.
- {OK, Socket, _} when OK =:= element(1, Messages), InStreamID > LastStreamID ->
- loop(State);
+ {OK, Socket, Data} when OK =:= element(1, Messages), InStreamID > LastStreamID ->
+ State1 = maybe_resize_buffer(State, Data),
+ before_loop(State1);
%% Socket messages.
{OK, Socket, Data} when OK =:= element(1, Messages) ->
- parse(<< Buffer/binary, Data/binary >>, State);
+ State1 = maybe_resize_buffer(State, Data),
+ parse(<< Buffer/binary, Data/binary >>, State1);
{Closed, Socket} when Closed =:= element(2, Messages) ->
terminate(State, {socket_error, closed, 'The socket has been closed.'});
{Error, Socket, Reason} when Error =:= element(3, Messages) ->
@@ -233,37 +257,37 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts,
%% Hardcoded for compatibility with Ranch 1.x.
Passive =:= tcp_passive; Passive =:= ssl_passive ->
safe_setopts_active(State),
- loop(State);
+ before_loop(State);
%% Timeouts.
{timeout, Ref, {shutdown, Pid}} ->
cowboy_children:shutdown_timeout(Children, Ref, Pid),
- loop(State);
+ before_loop(State);
{timeout, TimerRef, Reason} ->
timeout(State, Reason);
{timeout, _, _} ->
- loop(State);
+ before_loop(State);
%% System messages.
{'EXIT', Parent, shutdown} ->
Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'},
- loop(initiate_closing(State, Reason));
+ before_loop(initiate_closing(State, Reason));
{'EXIT', Parent, Reason} ->
terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'});
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State);
%% Messages pertaining to a stream.
{{Pid, StreamID}, Msg} when Pid =:= self() ->
- loop(info(State, StreamID, Msg));
+ before_loop(info(State, StreamID, Msg));
%% Exit signal from children.
Msg = {'EXIT', Pid, _} ->
- loop(down(State, Pid, Msg));
+ before_loop(down(State, Pid, Msg));
%% Calls from supervisor module.
{'$gen_call', From, Call} ->
cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE),
- loop(State);
+ before_loop(State);
%% Unknown messages.
Msg ->
cowboy:log(warning, "Received stray message ~p.~n", [Msg], Opts),
- loop(State)
+ before_loop(State)
after InactivityTimeout ->
terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
end.
@@ -295,6 +319,7 @@ set_timeout(State=#state{streams=[], in_state=InState}, idle_timeout)
when element(1, InState) =/= ps_body ->
State;
%% Otherwise we can set the timeout.
+%% @todo Don't do this so often, use a strategy similar to Websocket/H2 if possible.
set_timeout(State0=#state{opts=Opts, overriden_opts=Override}, Name) ->
State = cancel_timeout(State0),
Default = case Name of
@@ -327,7 +352,7 @@ cancel_timeout(State=#state{timer=TimerRef}) ->
_ ->
%% Do a synchronous cancel and remove the message if any
%% to avoid receiving stray messages.
- _ = erlang:cancel_timer(TimerRef),
+ _ = erlang:cancel_timer(TimerRef, [{async, false}, {info, false}]),
receive
{timeout, TimerRef, _} -> ok
after 0 ->
@@ -348,12 +373,12 @@ timeout(State, idle_timeout) ->
'Connection idle longer than configuration allows.'}).
parse(<<>>, State) ->
- loop(State#state{buffer= <<>>});
+ before_loop(State#state{buffer= <<>>});
%% Do not process requests that come in after the last request
%% and discard the buffer if any to save memory.
parse(_, State=#state{in_streamid=InStreamID, in_state=#ps_request_line{},
last_streamid=LastStreamID}) when InStreamID > LastStreamID ->
- loop(State#state{buffer= <<>>});
+ before_loop(State#state{buffer= <<>>});
parse(Buffer, State=#state{in_state=#ps_request_line{empty_lines=EmptyLines}}) ->
after_parse(parse_request(Buffer, State, EmptyLines));
parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=undefined}}) ->
@@ -422,13 +447,13 @@ after_parse({data, StreamID, IsFin, Data, State0=#state{opts=Opts, buffer=Buffer
end;
%% No corresponding stream. We must skip the body of the previous request
%% in order to process the next one.
-after_parse({data, _, IsFin, _, State}) ->
- loop(set_timeout(State, case IsFin of
+after_parse({data, _, IsFin, _, State=#state{buffer=Buffer}}) ->
+ parse(Buffer, set_timeout(State, case IsFin of
fin -> request_timeout;
nofin -> idle_timeout
end));
after_parse({more, State}) ->
- loop(set_timeout(State, idle_timeout)).
+ before_loop(set_timeout(State, idle_timeout)).
update_flow(fin, _, State) ->
%% This function is only called after parsing, therefore we
@@ -488,8 +513,13 @@ parse_request(Buffer, State=#state{opts=Opts, in_streamid=InStreamID}, EmptyLine
'The TRACE method is currently not implemented. (RFC7231 4.3.8)'});
%% Accept direct HTTP/2 only at the beginning of the connection.
<< "PRI * HTTP/2.0\r\n", _/bits >> when InStreamID =:= 1 ->
- %% @todo Might be worth throwing to get a clean stacktrace.
- http2_upgrade(State, Buffer);
+ case lists:member(http2, maps:get(protocols, Opts, [http2, http])) of
+ true ->
+ http2_upgrade(State, Buffer);
+ false ->
+ error_terminate(501, State, {connection_error, no_error,
+ 'Prior knowledge upgrade to HTTP/2 is disabled by configuration.'})
+ end;
_ ->
parse_method(Buffer, State, <<>>,
maps:get(max_method_length, Opts, 32))
@@ -777,7 +807,7 @@ default_port(_) -> 80.
%% End of request parsing.
request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock, cert=Cert,
- proxy_header=ProxyHeader, in_streamid=StreamID, in_state=
+ opts=Opts, proxy_header=ProxyHeader, in_streamid=StreamID, in_state=
PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
Headers, Host, Port) ->
Scheme = case Transport:secure() of
@@ -841,7 +871,7 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock
undefined -> Req0;
_ -> Req0#{proxy_header => ProxyHeader}
end,
- case is_http2_upgrade(Headers, Version) of
+ case is_http2_upgrade(Headers, Version, Opts) of
false ->
State = case HasBody of
true ->
@@ -863,12 +893,13 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock
%% HTTP/2 upgrade.
-%% @todo We must not upgrade to h2c over a TLS connection.
is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
- <<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') ->
+ <<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1', Opts) ->
Conns = cow_http_hd:parse_connection(Conn),
- case {lists:member(<<"upgrade">>, Conns), lists:member(<<"http2-settings">>, Conns)} of
- {true, true} ->
+ case lists:member(<<"upgrade">>, Conns)
+ andalso lists:member(<<"http2-settings">>, Conns)
+ andalso lists:member(http2, maps:get(protocols, Opts, [http2, http])) of
+ true ->
Protocols = cow_http_hd:parse_upgrade(Upgrade),
case lists:member(<<"h2c">>, Protocols) of
true ->
@@ -879,17 +910,17 @@ is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
_ ->
false
end;
-is_http2_upgrade(_, _) ->
+is_http2_upgrade(_, _, _) ->
false.
%% Prior knowledge upgrade, without an HTTP/1.1 request.
http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
- proxy_header=ProxyHeader, opts=Opts, peer=Peer, sock=Sock, cert=Cert}, Buffer) ->
+ proxy_header=ProxyHeader, peer=Peer, sock=Sock, cert=Cert}, Buffer) ->
case Transport:secure() of
false ->
_ = cancel_timeout(State),
- cowboy_http2:init(Parent, Ref, Socket, Transport,
- ProxyHeader, Opts, Peer, Sock, Cert, Buffer);
+ cowboy_http2:init(Parent, Ref, Socket, Transport, ProxyHeader,
+ opts_for_upgrade(State), Peer, Sock, Cert, Buffer);
true ->
error_terminate(400, State, {connection_error, protocol_error,
'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'})
@@ -897,22 +928,37 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
%% Upgrade via an HTTP/1.1 request.
http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
- proxy_header=ProxyHeader, opts=Opts, peer=Peer, sock=Sock, cert=Cert},
+ proxy_header=ProxyHeader, peer=Peer, sock=Sock, cert=Cert},
Buffer, HTTP2Settings, Req) ->
- %% @todo
- %% However if the client sent a body, we need to read the body in full
- %% and if we can't do that, return a 413 response. Some options are in order.
- %% Always half-closed stream coming from this side.
- try cow_http_hd:parse_http2_settings(HTTP2Settings) of
- Settings ->
- _ = cancel_timeout(State),
- cowboy_http2:init(Parent, Ref, Socket, Transport,
- ProxyHeader, Opts, Peer, Sock, Cert, Buffer, Settings, Req)
- catch _:_ ->
- error_terminate(400, State, {connection_error, protocol_error,
- 'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'})
+ case Transport:secure() of
+ false ->
+ %% @todo
+ %% However if the client sent a body, we need to read the body in full
+ %% and if we can't do that, return a 413 response. Some options are in order.
+ %% Always half-closed stream coming from this side.
+ try cow_http_hd:parse_http2_settings(HTTP2Settings) of
+ Settings ->
+ _ = cancel_timeout(State),
+ cowboy_http2:init(Parent, Ref, Socket, Transport, ProxyHeader,
+ opts_for_upgrade(State), Peer, Sock, Cert, Buffer, Settings, Req)
+ catch _:_ ->
+ error_terminate(400, State, {connection_error, protocol_error,
+ 'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'})
+ end;
+ true ->
+ error_terminate(400, State, {connection_error, protocol_error,
+ 'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'})
end.
+opts_for_upgrade(#state{opts=Opts, dynamic_buffer_size=false}) ->
+ Opts;
+opts_for_upgrade(#state{opts=Opts, dynamic_buffer_size=Size,
+ dynamic_buffer_moving_average=MovingAvg}) ->
+ Opts#{
+ dynamic_buffer_initial_average => MovingAvg,
+ dynamic_buffer_initial_size => Size
+ }.
+
%% Request body parsing.
parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
@@ -1209,7 +1255,7 @@ commands(State0=#state{socket=Socket, transport=Transport, streams=Streams, out_
commands(State, StreamID, Tail);
%% Protocol takeover.
commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transport,
- out_state=OutState, opts=Opts, buffer=Buffer, children=Children}, StreamID,
+ out_state=OutState, buffer=Buffer, children=Children}, StreamID,
[{switch_protocol, Headers, Protocol, InitialState}|_Tail]) ->
%% @todo If there's streams opened after this one, fail instead of 101.
State1 = cancel_timeout(State0),
@@ -1233,23 +1279,19 @@ commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transpor
%% Turn off the trap_exit process flag
%% since this process will no longer be a supervisor.
process_flag(trap_exit, false),
- Protocol:takeover(Parent, Ref, Socket, Transport, Opts, Buffer, InitialState);
+ Protocol:takeover(Parent, Ref, Socket, Transport,
+ opts_for_upgrade(State), Buffer, InitialState);
%% Set options dynamically.
-commands(State0=#state{overriden_opts=Opts},
- StreamID, [{set_options, SetOpts}|Tail]) ->
- State1 = case SetOpts of
- #{idle_timeout := IdleTimeout} ->
- set_timeout(State0#state{overriden_opts=Opts#{idle_timeout => IdleTimeout}},
+commands(State0, StreamID, [{set_options, SetOpts}|Tail]) ->
+ State = maps:fold(fun
+ (chunked, Chunked, StateF=#state{overriden_opts=Opts}) ->
+ StateF#state{overriden_opts=Opts#{chunked => Chunked}};
+ (idle_timeout, IdleTimeout, StateF=#state{overriden_opts=Opts}) ->
+ set_timeout(StateF#state{overriden_opts=Opts#{idle_timeout => IdleTimeout}},
idle_timeout);
- _ ->
- State0
- end,
- State = case SetOpts of
- #{chunked := Chunked} ->
- State1#state{overriden_opts=Opts#{chunked => Chunked}};
- _ ->
- State1
- end,
+ (_, _, StateF) ->
+ StateF
+ end, State0, SetOpts),
commands(State, StreamID, Tail);
%% Stream shutdown.
commands(State, StreamID, [stop|Tail]) ->
@@ -1368,23 +1410,24 @@ stream_terminate(State0=#state{opts=Opts, in_streamid=InStreamID, in_state=InSta
end.
stream_next(State0=#state{opts=Opts, active=Active, out_streamid=OutStreamID, streams=Streams}) ->
+ %% Enable active mode again if it was disabled.
+ State1 = case Active of
+ true -> State0;
+ false -> active(State0)
+ end,
NextOutStreamID = OutStreamID + 1,
case lists:keyfind(NextOutStreamID, #stream.id, Streams) of
false ->
- State = State0#state{out_streamid=NextOutStreamID, out_state=wait},
+ State = State1#state{out_streamid=NextOutStreamID, out_state=wait},
%% There are no streams remaining. We therefore can
%% and want to switch back to the request_timeout.
set_timeout(State, request_timeout);
#stream{queue=Commands} ->
- State = case Active of
- true -> State0;
- false -> active(State0)
- end,
%% @todo Remove queue from the stream.
%% We set the flow to the initial flow size even though
%% we might have sent some data through already due to pipelining.
Flow = maps:get(initial_stream_flow_size, Opts, 65535),
- commands(State#state{flow=Flow, out_streamid=NextOutStreamID, out_state=wait},
+ commands(State1#state{flow=Flow, out_streamid=NextOutStreamID, out_state=wait},
NextOutStreamID, Commands)
end.
@@ -1597,12 +1640,12 @@ terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) ->
-spec system_continue(_, _, #state{}) -> ok.
system_continue(_, _, State) ->
- loop(State).
+ before_loop(State).
-spec system_terminate(any(), _, _, #state{}) -> no_return().
system_terminate(Reason0, _, _, State) ->
Reason = {stop, {exit, Reason0}, 'sys:terminate/2,3 was called.'},
- loop(initiate_closing(State, Reason)).
+ before_loop(initiate_closing(State, Reason)).
-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}.
system_code_change(Misc, _, _, _) ->
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index 0e110cd..0d22fa1 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -17,6 +17,7 @@
-export([init/6]).
-export([init/10]).
-export([init/12]).
+-export([loop/2]).
-export([system_continue/3]).
-export([system_terminate/4]).
@@ -24,15 +25,20 @@
-type opts() :: #{
active_n => pos_integer(),
+ alpn_default_protocol => http | http2,
compress_buffering => boolean(),
compress_threshold => non_neg_integer(),
connection_type => worker | supervisor,
connection_window_margin_size => 0..16#7fffffff,
connection_window_update_threshold => 0..16#7fffffff,
+ dynamic_buffer => false | {pos_integer(), pos_integer()},
+ dynamic_buffer_initial_average => non_neg_integer(),
+ dynamic_buffer_initial_size => pos_integer(),
enable_connect_protocol => boolean(),
env => cowboy_middleware:env(),
goaway_initial_timeout => timeout(),
goaway_complete_timeout => timeout(),
+ hibernate => boolean(),
idle_timeout => timeout(),
inactivity_timeout => timeout(),
initial_connection_window_size => 65535..16#7fffffff,
@@ -57,6 +63,7 @@
metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()),
middlewares => [module()],
preface_timeout => timeout(),
+ protocols => [http | http2],
proxy_header => boolean(),
reset_idle_timeout_on_send => boolean(),
sendfile => boolean(),
@@ -133,6 +140,10 @@
%% Flow requested for all streams.
flow = 0 :: non_neg_integer(),
+ %% Dynamic buffer moving average and current buffer size.
+ dynamic_buffer_size :: pos_integer() | false,
+ dynamic_buffer_moving_average :: non_neg_integer(),
+
%% Currently active HTTP/2 streams. Streams may be initiated either
%% by the client or by the server through PUSH_PROMISE frames.
streams = #{} :: #{cow_http2:streamid() => #stream{}},
@@ -143,7 +154,8 @@
}).
-spec init(pid(), ranch:ref(), inet:socket(), module(),
- ranch_proxy_header:proxy_info() | undefined, cowboy:opts()) -> ok.
+ ranch_proxy_header:proxy_info() | undefined, cowboy:opts()) -> no_return().
+
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
{ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket),
'A socket error occurred when retrieving the peer name.'),
@@ -167,18 +179,22 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
-spec init(pid(), ranch:ref(), inet:socket(), module(),
ranch_proxy_header:proxy_info() | undefined, cowboy:opts(),
{inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()},
- binary() | undefined, binary()) -> ok.
+ binary() | undefined, binary()) -> no_return().
+
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) ->
+ DynamicBuffer = init_dynamic_buffer_size(Opts),
{ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts),
%% Send the preface before doing all the init in case we get a socket error.
ok = maybe_socket_error(undefined, Transport:send(Socket, Preface)),
State = set_idle_timeout(init_rate_limiting(#state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, proxy_header=ProxyHeader,
opts=Opts, peer=Peer, sock=Sock, cert=Cert,
+ dynamic_buffer_size=DynamicBuffer,
+ dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0),
http2_status=sequence, http2_machine=HTTP2Machine}), 0),
safe_setopts_active(State),
case Buffer of
- <<>> -> loop(State, Buffer);
+ <<>> -> before_loop(State, Buffer);
_ -> parse(State, Buffer)
end.
@@ -213,15 +229,19 @@ add_period(Time, Period) -> Time + Period.
-spec init(pid(), ranch:ref(), inet:socket(), module(),
ranch_proxy_header:proxy_info() | undefined, cowboy:opts(),
{inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()},
- binary() | undefined, binary(), map() | undefined, cowboy_req:req()) -> ok.
+ binary() | undefined, binary(), map() | undefined, cowboy_req:req()) -> no_return().
+
init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer,
_Settings, Req=#{method := Method}) ->
+ DynamicBuffer = init_dynamic_buffer_size(Opts),
{ok, Preface, HTTP2Machine0} = cow_http2_machine:init(server, Opts),
{ok, StreamID, HTTP2Machine}
= cow_http2_machine:init_upgrade_stream(Method, HTTP2Machine0),
State0 = #state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, proxy_header=ProxyHeader,
opts=Opts, peer=Peer, sock=Sock, cert=Cert,
+ dynamic_buffer_size=DynamicBuffer,
+ dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0),
http2_status=upgrade, http2_machine=HTTP2Machine},
State1 = headers_frame(State0#state{
http2_machine=HTTP2Machine}, StreamID, Req),
@@ -237,20 +257,30 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
ok = maybe_socket_error(State, Transport:send(Socket, Preface)),
safe_setopts_active(State),
case Buffer of
- <<>> -> loop(State, Buffer);
+ <<>> -> before_loop(State, Buffer);
_ -> parse(State, Buffer)
end.
+-include("cowboy_dynamic_buffer.hrl").
+
%% Because HTTP/2 has flow control and Cowboy has other rate limiting
%% mechanisms implemented, a very large active_n value should be fine,
%% as long as the stream handlers do their work in a timely manner.
+%% However large active_n values reduce the impact of dynamic_buffer.
setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) ->
- N = maps:get(active_n, Opts, 100),
+ N = maps:get(active_n, Opts, 1),
Transport:setopts(Socket, [{active, N}]).
safe_setopts_active(State) ->
ok = maybe_socket_error(State, setopts_active(State)).
+before_loop(State=#state{opts=#{hibernate := true}}, Buffer) ->
+ proc_lib:hibernate(?MODULE, loop, [State, Buffer]);
+before_loop(State, Buffer) ->
+ loop(State, Buffer).
+
+-spec loop(#state{}, binary()) -> no_return().
+
loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
opts=Opts, timer=TimerRef, children=Children}, Buffer) ->
Messages = Transport:messages(),
@@ -258,7 +288,8 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
receive
%% Socket messages.
{OK, Socket, Data} when OK =:= element(1, Messages) ->
- parse(State#state{idle_timeout_num=0}, << Buffer/binary, Data/binary >>);
+ State1 = maybe_resize_buffer(State, Data),
+ parse(State1#state{idle_timeout_num=0}, << Buffer/binary, Data/binary >>);
{Closed, Socket} when Closed =:= element(2, Messages) ->
Reason = case State#state.http2_status of
closing -> {stop, closed, 'The client is going away.'};
@@ -271,11 +302,11 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
%% Hardcoded for compatibility with Ranch 1.x.
Passive =:= tcp_passive; Passive =:= ssl_passive ->
safe_setopts_active(State),
- loop(State, Buffer);
+ before_loop(State, Buffer);
%% System messages.
{'EXIT', Parent, shutdown} ->
Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'},
- loop(initiate_closing(State, Reason), Buffer);
+ before_loop(initiate_closing(State, Reason), Buffer);
{'EXIT', Parent, Reason} ->
terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'});
{system, From, Request} ->
@@ -285,27 +316,27 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
tick_idle_timeout(State, Buffer);
{timeout, Ref, {shutdown, Pid}} ->
cowboy_children:shutdown_timeout(Children, Ref, Pid),
- loop(State, Buffer);
+ before_loop(State, Buffer);
{timeout, TRef, {cow_http2_machine, Name}} ->
- loop(timeout(State, Name, TRef), Buffer);
+ before_loop(timeout(State, Name, TRef), Buffer);
{timeout, TimerRef, {goaway_initial_timeout, Reason}} ->
- loop(closing(State, Reason), Buffer);
+ before_loop(closing(State, Reason), Buffer);
{timeout, TimerRef, {goaway_complete_timeout, Reason}} ->
terminate(State, {stop, stop_reason(Reason),
'Graceful shutdown timed out.'});
%% Messages pertaining to a stream.
{{Pid, StreamID}, Msg} when Pid =:= self() ->
- loop(info(State, StreamID, Msg), Buffer);
+ before_loop(info(State, StreamID, Msg), Buffer);
%% Exit signal from children.
Msg = {'EXIT', Pid, _} ->
- loop(down(State, Pid, Msg), Buffer);
+ before_loop(down(State, Pid, Msg), Buffer);
%% Calls from supervisor module.
{'$gen_call', From, Call} ->
cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE),
- loop(State, Buffer);
+ before_loop(State, Buffer);
Msg ->
cowboy:log(warning, "Received stray message ~p.", [Msg], Opts),
- loop(State, Buffer)
+ before_loop(State, Buffer)
after InactivityTimeout ->
terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
end.
@@ -314,7 +345,7 @@ tick_idle_timeout(State=#state{idle_timeout_num=?IDLE_TIMEOUT_TICKS}, _) ->
terminate(State, {stop, timeout,
'Connection idle longer than configuration allows.'});
tick_idle_timeout(State=#state{idle_timeout_num=TimeoutNum}, Buffer) ->
- loop(set_idle_timeout(State, TimeoutNum + 1), Buffer).
+ before_loop(set_idle_timeout(State, TimeoutNum + 1), Buffer).
set_idle_timeout(State=#state{http2_status=Status, timer=TimerRef}, _)
when Status =:= closing_initiated orelse Status =:= closing,
@@ -355,7 +386,7 @@ parse(State=#state{http2_status=sequence}, Data) ->
{ok, Rest} ->
parse(State#state{http2_status=settings}, Rest);
more ->
- loop(State, Data);
+ before_loop(State, Data);
Error = {connection_error, _, _} ->
terminate(State, Error)
end;
@@ -374,7 +405,7 @@ parse(State=#state{http2_status=Status, http2_machine=HTTP2Machine, streams=Stre
more when Status =:= closing, Streams =:= #{} ->
terminate(State, {stop, normal, 'The connection is going away.'});
more ->
- loop(State, Data)
+ before_loop(State, Data)
end.
%% Frame rate flood protection.
@@ -1106,7 +1137,9 @@ goaway_streams(State, [Stream|Tail], LastStreamID, Reason, Acc) ->
%% in-flight stream creation (at least one round-trip time), the server can send
%% another GOAWAY frame with an updated last stream identifier. This ensures
%% that a connection can be cleanly shut down without losing requests.
+
-spec initiate_closing(#state{}, _) -> #state{}.
+
initiate_closing(State=#state{http2_status=connected, socket=Socket,
transport=Transport, opts=Opts}, Reason) ->
ok = maybe_socket_error(State, Transport:send(Socket,
@@ -1123,7 +1156,9 @@ initiate_closing(State, Reason) ->
terminate(State, {stop, stop_reason(Reason), 'The connection is going away.'}).
%% Switch to 'closing' state and stop accepting new streams.
+
-spec closing(#state{}, Reason :: term()) -> #state{}.
+
closing(State=#state{streams=Streams}, Reason) when Streams =:= #{} ->
terminate(State, Reason);
closing(State0=#state{http2_status=closing_initiated,
@@ -1160,6 +1195,7 @@ maybe_socket_error(State, {error, Reason}, Human) ->
terminate(State, {socket_error, Reason, Human}).
-spec terminate(#state{} | undefined, _) -> no_return().
+
terminate(undefined, Reason) ->
exit({shutdown, Reason});
terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status,
@@ -1360,15 +1396,18 @@ terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) ->
%% System callbacks.
--spec system_continue(_, _, {#state{}, binary()}) -> ok.
+-spec system_continue(_, _, {#state{}, binary()}) -> no_return().
+
system_continue(_, _, {State, Buffer}) ->
- loop(State, Buffer).
+ before_loop(State, Buffer).
-spec system_terminate(any(), _, _, {#state{}, binary()}) -> no_return().
+
system_terminate(Reason0, _, _, {State, Buffer}) ->
Reason = {stop, {exit, Reason0}, 'sys:terminate/2,3 was called.'},
- loop(initiate_closing(State, Reason), Buffer).
+ before_loop(initiate_closing(State, Reason), Buffer).
-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}.
+
system_code_change(Misc, _, _, _) ->
{ok, Misc}.
diff --git a/src/cowboy_http3.erl b/src/cowboy_http3.erl
index ef3e3f6..9aa6be5 100644
--- a/src/cowboy_http3.erl
+++ b/src/cowboy_http3.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2023-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,10 +32,10 @@
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_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()),
@@ -51,18 +51,30 @@
}.
-export_type([opts/0]).
+%% HTTP/3 or WebTransport stream.
+%%
+%% WebTransport sessions involve one bidirectional CONNECT stream
+%% that must stay open (and can be used for signaling using the
+%% Capsule Protocol) and an application-defined number of
+%% unidirectional and bidirectional streams, as well as datagrams.
+%%
+%% WebTransport sessions run in the CONNECT request process and
+%% all events related to the session is sent there as a message.
+%% The pid of the process is kept in the state.
-record(stream, {
id :: cow_http3:stream_id(),
%% Whether the stream is currently in a special state.
status :: header | {unidi, control | encoder | decoder}
- | normal | {data | ignore, non_neg_integer()} | stopping,
+ | normal | {data | ignore, non_neg_integer()} | stopping
+ | {webtransport_session, normal | {ignore, non_neg_integer()}}
+ | {webtransport_stream, cow_http3:stream_id()},
%% Stream buffer.
buffer = <<>> :: binary(),
%% Stream state.
- state = undefined :: undefined | {module, any()}
+ state = undefined :: undefined | {module(), any()}
}).
-record(state, {
@@ -152,6 +164,9 @@ loop(State0=#state{opts=Opts, children=Children}) ->
%% Messages pertaining to a stream.
{{Pid, StreamID}, Msg} when Pid =:= self() ->
loop(info(State0, StreamID, Msg));
+ %% WebTransport commands.
+ {'$webtransport_commands', SessionID, Commands} ->
+ loop(webtransport_commands(State0, SessionID, Commands));
%% Exit signal from children.
Msg = {'EXIT', Pid, _} ->
loop(down(State0, Pid, Msg));
@@ -164,12 +179,17 @@ handle_quic_msg(State0=#state{opts=Opts}, Msg) ->
case cowboy_quicer:handle(Msg) of
{data, StreamID, IsFin, Data} ->
parse(State0, StreamID, Data, IsFin);
+ {datagram, Data} ->
+ parse_datagram(State0, Data);
{stream_started, StreamID, StreamType} ->
State = stream_new_remote(State0, StreamID, StreamType),
loop(State);
{stream_closed, StreamID, ErrorCode} ->
State = stream_closed(State0, StreamID, ErrorCode),
loop(State);
+ {peer_send_shutdown, StreamID} ->
+ State = stream_peer_send_shutdown(State0, StreamID),
+ loop(State);
closed ->
%% @todo Different error reason if graceful?
Reason = {socket_error, closed, 'The socket has been closed.'},
@@ -216,6 +236,56 @@ parse1(State=#state{http3_machine=HTTP3Machine0},
{error, Error={connection_error, _, _}, HTTP3Machine} ->
terminate(State#state{http3_machine=HTTP3Machine}, Error)
end;
+%% @todo Handle when IsFin = fin which must terminate the WT session.
+parse1(State=#state{conn=Conn}, Stream=#stream{id=SessionID, status=
+ {webtransport_session, normal}}, Data, IsFin) ->
+ case cow_capsule:parse(Data) of
+ {ok, wt_drain_session, Rest} ->
+ webtransport_event(State, SessionID, close_initiated),
+ parse1(State, Stream, Rest, IsFin);
+ {ok, {wt_close_session, AppCode, AppMsg}, Rest} ->
+ %% This event will be handled specially and lead
+ %% to the termination of the session process.
+ webtransport_event(State, SessionID, {closed, AppCode, AppMsg}),
+ %% Shutdown the CONNECT stream immediately.
+ cowboy_quicer:shutdown_stream(Conn, SessionID),
+ %% @todo Will we receive a {stream_closed,...} after that?
+ %% If any data is received past that point this is an error.
+ %% @todo Don't crash, error out properly.
+ <<>> = Rest,
+ loop(webtransport_terminate_session(State, Stream));
+ more ->
+ loop(stream_store(State, Stream#stream{buffer=Data}));
+ %% Ignore unhandled/unknown capsules.
+ %% @todo Do this when cow_capsule includes some.
+% {ok, _, Rest} ->
+% parse1(State, Stream, Rest, IsFin);
+% {ok, Rest} ->
+% parse1(State, Stream, Rest, IsFin);
+ %% @todo Make the max length configurable?
+ {skip, Len} when Len =< 8192 ->
+ loop(stream_store(State, Stream#stream{
+ status={webtransport_session, {ignore, Len}}}));
+ {skip, Len} ->
+ %% @todo What should be done on capsule error?
+ error({todo, capsule_too_long, Len});
+ error ->
+ %% @todo What should be done on capsule error?
+ error({todo, capsule_error, Data})
+ end;
+parse1(State, Stream=#stream{status=
+ {webtransport_session, {ignore, Len}}}, Data, IsFin) ->
+ case Data of
+ <<_:Len/unit:8, Rest/bits>> ->
+ parse1(State, Stream#stream{status={webtransport_session, normal}}, Rest, IsFin);
+ _ ->
+ loop(stream_store(State, Stream#stream{
+ status={webtransport_session, {ignore, Len - byte_size(Data)}}}))
+ end;
+parse1(State, #stream{id=StreamID, status={webtransport_stream, SessionID}}, Data, IsFin) ->
+ webtransport_event(State, SessionID, {stream_data, StreamID, IsFin, Data}),
+ %% No need to store the stream again, WT streams don't get changed here.
+ loop(State);
parse1(State, Stream=#stream{status={data, Len}, id=StreamID}, Data, IsFin) ->
DataLen = byte_size(Data),
if
@@ -246,6 +316,9 @@ parse1(State=#state{opts=Opts}, Stream=#stream{id=StreamID}, Data, IsFin) ->
{ok, Frame, Rest} ->
FrameIsFin = is_fin(IsFin, Rest),
parse(frame(State, Stream, Frame, FrameIsFin), StreamID, Rest, IsFin);
+ %% The WebTransport stream header is not a real frame.
+ {webtransport_stream_header, SessionID, Rest} ->
+ become_webtransport_stream(State, Stream, bidi, SessionID, Rest, IsFin);
{more, Frame = {data, _}, Len} ->
%% We're at the end of the data so FrameIsFin is equivalent to IsFin.
case IsFin of
@@ -317,13 +390,24 @@ parse_unidirectional_stream_header(State0=#state{http3_machine=HTTP3Machine0},
{error, Error={connection_error, _, _}, HTTP3Machine} ->
terminate(State0#state{http3_machine=HTTP3Machine}, Error)
end;
+ %% @todo Perhaps do this in cow_http3_machine directly.
{ok, push, _} ->
terminate(State0, {connection_error, h3_stream_creation_error,
'Only servers can push. (RFC9114 6.2.2)'});
+ {ok, {webtransport, SessionID}, Rest} ->
+ become_webtransport_stream(State0, Stream0, unidi, SessionID, Rest, IsFin);
%% Unknown stream types must be ignored. We choose to abort the
%% stream instead of reading and discarding the incoming data.
{undefined, _} ->
- loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error))
+ loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error));
+ %% Very unlikely to happen but WebTransport headers may be fragmented
+ %% as they are more than one byte. The fin flag in this case is an error,
+ %% but because it happens in WebTransport application data (the Session ID)
+ %% we only reset the impacted stream and not the entire connection.
+ more when IsFin =:= fin ->
+ loop(stream_abort_receive(State0, Stream0, h3_stream_creation_error));
+ more ->
+ loop(stream_store(State0, Stream0#stream{buffer=Data}))
end.
frame(State=#state{http3_machine=HTTP3Machine0},
@@ -449,6 +533,8 @@ headers_to_map([{Name, Value}|Tail], Acc0) ->
end,
headers_to_map(Tail, Acc).
+%% @todo WebTransport CONNECT requests must have extra checks on settings.
+%% @todo We may also need to defer them if we didn't get settings.
headers_frame(State=#state{opts=Opts}, Stream=#stream{id=StreamID}, Req) ->
try cowboy_stream:init(StreamID, Req, Opts) of
{Commands, StreamState} ->
@@ -488,6 +574,18 @@ early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer},
send_headers(State0, Stream, fin, StatusCode0, RespHeaders0)
end.
+%% Datagrams.
+
+parse_datagram(State, Data0) ->
+ {SessionID, Data} = cow_http3:parse_datagram(Data0),
+ case stream_get(State, SessionID) of
+ #stream{status={webtransport_session, _}} ->
+ webtransport_event(State, SessionID, {datagram, Data}),
+ loop(State);
+ _ ->
+ error(todo) %% @todo Might be a future WT session or an error.
+ end.
+
%% Erlang messages.
down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) ->
@@ -654,6 +752,22 @@ commands(State, Stream, [Error = {internal_error, _, _}|_Tail]) ->
%% Use a different protocol within the stream (CONNECT :protocol).
%% @todo Make sure we error out when the feature is disabled.
commands(State0, Stream0=#stream{id=StreamID},
+ [{switch_protocol, Headers, cowboy_webtransport, WTState=#{}}|Tail]) ->
+ State = info(stream_store(State0, Stream0), StreamID, {headers, 200, Headers}),
+ #state{http3_machine=HTTP3Machine0} = State,
+ Stream1 = #stream{state=StreamState} = stream_get(State, StreamID),
+ %% The stream becomes a WT session at that point. It is the
+ %% parent stream of all streams in this WT session. The
+ %% cowboy_stream state is kept because it will be needed
+ %% to terminate the stream properly.
+ HTTP3Machine = cow_http3_machine:become_webtransport_session(StreamID, HTTP3Machine0),
+ Stream = Stream1#stream{
+ status={webtransport_session, normal},
+ state={cowboy_webtransport, WTState#{stream_state => StreamState}}
+ },
+ %% @todo We must propagate the buffer to capsule handling if any.
+ commands(State#state{http3_machine=HTTP3Machine}, Stream, Tail);
+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),
@@ -758,6 +872,157 @@ send_instructions(State=#state{conn=Conn, local_encoder_id=EncoderID},
cowboy_quicer:send(Conn, EncoderID, EncData)),
State.
+%% We mark the stream as being a WebTransport stream
+%% and then continue parsing the data as a WebTransport
+%% stream. This function is common for incoming unidi
+%% and bidi streams.
+become_webtransport_stream(State0=#state{http3_machine=HTTP3Machine0},
+ Stream0=#stream{id=StreamID}, StreamType, SessionID, Rest, IsFin) ->
+ case cow_http3_machine:become_webtransport_stream(StreamID, SessionID, HTTP3Machine0) of
+ {ok, HTTP3Machine} ->
+ State = State0#state{http3_machine=HTTP3Machine},
+ Stream = Stream0#stream{status={webtransport_stream, SessionID}},
+ webtransport_event(State, SessionID, {stream_open, StreamID, StreamType}),
+ %% We don't need to parse the remaining data if there isn't any.
+ case {Rest, IsFin} of
+ {<<>>, nofin} -> loop(stream_store(State, Stream));
+ _ -> parse(stream_store(State, Stream), StreamID, Rest, IsFin)
+ end
+ %% @todo Error conditions.
+ end.
+
+webtransport_event(State, SessionID, Event) ->
+ #stream{
+ status={webtransport_session, _},
+ state={cowboy_webtransport, #{session_pid := SessionPid}}
+ } = stream_get(State, SessionID),
+ SessionPid ! {'$webtransport_event', SessionID, Event},
+ ok.
+
+webtransport_commands(State, SessionID, Commands) ->
+ case stream_get(State, SessionID) of
+ Session = #stream{status={webtransport_session, _}} ->
+ wt_commands(State, Session, Commands);
+ %% The stream has been terminated, ignore pending commands.
+ error ->
+ State
+ end.
+
+wt_commands(State, _, []) ->
+ State;
+wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID},
+ [{open_stream, OpenStreamRef, StreamType, InitialData}|Tail]) ->
+ %% Because opening the stream involves sending a short header
+ %% we necessarily write data. The InitialData variable allows
+ %% providing additional data to be sent in the same packet.
+ StartF = case StreamType of
+ bidi -> start_bidi_stream;
+ unidi -> start_unidi_stream
+ end,
+ Header = cow_http3:webtransport_stream_header(SessionID, StreamType),
+ case cowboy_quicer:StartF(Conn, [Header, InitialData]) of
+ {ok, StreamID} ->
+ %% @todo Pass Session directly?
+ webtransport_event(State0, SessionID,
+ {opened_stream_id, OpenStreamRef, StreamID}),
+ State = stream_new_local(State0, StreamID, StreamType,
+ {webtransport_stream, SessionID}),
+ wt_commands(State, Session, Tail)
+ %% @todo Handle errors.
+ end;
+wt_commands(State, Session, [{close_stream, StreamID, Code}|Tail]) ->
+ %% @todo Check that StreamID belongs to Session.
+ error({todo, State, Session, [{close_stream, StreamID, Code}|Tail]});
+wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID},
+ [{send, datagram, Data}|Tail]) ->
+ case cowboy_quicer:send_datagram(Conn, cow_http3:datagram(SessionID, Data)) of
+ ok ->
+ wt_commands(State, Session, Tail)
+ %% @todo Handle errors.
+ end;
+wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, Data}|Tail]) ->
+ %% @todo Check that StreamID belongs to Session.
+ case cowboy_quicer:send(Conn, StreamID, Data, nofin) of
+ ok ->
+ wt_commands(State, Session, Tail)
+ %% @todo Handle errors.
+ end;
+wt_commands(State=#state{conn=Conn}, Session, [{send, StreamID, IsFin, Data}|Tail]) ->
+ %% @todo Check that StreamID belongs to Session.
+ case cowboy_quicer:send(Conn, StreamID, Data, IsFin) of
+ ok ->
+ wt_commands(State, Session, Tail)
+ %% @todo Handle errors.
+ end;
+wt_commands(State=#state{conn=Conn}, Session=#stream{id=SessionID}, [initiate_close|Tail]) ->
+ %% We must send a WT_DRAIN_SESSION capsule on the CONNECT stream.
+ Capsule = cow_capsule:wt_drain_session(),
+ case cowboy_quicer:send(Conn, SessionID, Capsule, nofin) of
+ ok ->
+ wt_commands(State, Session, Tail)
+ %% @todo Handle errors.
+ end;
+wt_commands(State0=#state{conn=Conn}, Session=#stream{id=SessionID}, [Cmd|Tail])
+ when Cmd =:= close; element(1, Cmd) =:= close ->
+ %% We must send a WT_CLOSE_SESSION capsule on the CONNECT stream.
+ {AppCode, AppMsg} = case Cmd of
+ close -> {0, <<>>};
+ {close, AppCode0} -> {AppCode0, <<>>};
+ {close, AppCode0, AppMsg0} -> {AppCode0, AppMsg0}
+ end,
+ Capsule = cow_capsule:wt_close_session(AppCode, AppMsg),
+ case cowboy_quicer:send(Conn, SessionID, Capsule, fin) of
+ ok ->
+ State = webtransport_terminate_session(State0, Session),
+ %% @todo Because the handler is in a separate process
+ %% we must wait for it to stop and eventually
+ %% kill the process if it takes too long.
+ %% @todo We may need to fully close the CONNECT stream (if remote doesn't reset it).
+ wt_commands(State, Session, Tail)
+ %% @todo Handle errors.
+ end.
+
+webtransport_terminate_session(State=#state{conn=Conn, http3_machine=HTTP3Machine0,
+ streams=Streams0, lingering_streams=Lingering0}, #stream{id=SessionID}) ->
+ %% Reset/abort the WT streams.
+ Streams = maps:filtermap(fun
+ (_, #stream{id=StreamID, status={webtransport_session, _}})
+ when StreamID =:= SessionID ->
+ %% We remove the session stream but do the shutdown outside this function.
+ false;
+ (StreamID, #stream{status={webtransport_stream, StreamSessionID}})
+ when StreamSessionID =:= SessionID ->
+ cowboy_quicer:shutdown_stream(Conn, StreamID,
+ both, cow_http3:error_to_code(wt_session_gone)),
+ false;
+ (_, _) ->
+ true
+ end, Streams0),
+ %% Keep the streams in lingering state.
+ %% We only keep up to 100 streams in this state. @todo Make it configurable?
+ Terminated = maps:keys(Streams0) -- maps:keys(Streams),
+ Lingering = lists:sublist(Terminated ++ Lingering0, 100),
+ %% Update the HTTP3 state machine.
+ HTTP3Machine = cow_http3_machine:close_webtransport_session(SessionID, HTTP3Machine0),
+ State#state{
+ http3_machine=HTTP3Machine,
+ streams=Streams,
+ lingering_streams=Lingering
+ }.
+
+stream_peer_send_shutdown(State=#state{conn=Conn}, StreamID) ->
+ case stream_get(State, StreamID) of
+ %% Cleanly terminating the CONNECT stream is equivalent
+ %% to an application error code of 0 and empty message.
+ Stream = #stream{status={webtransport_session, _}} ->
+ webtransport_event(State, StreamID, {closed, 0, <<>>}),
+ %% Shutdown the CONNECT stream fully.
+ cowboy_quicer:shutdown_stream(Conn, StreamID),
+ webtransport_terminate_session(State, Stream);
+ _ ->
+ State
+ end.
+
reset_stream(State0=#state{conn=Conn, http3_machine=HTTP3Machine0},
Stream=#stream{id=StreamID}, Error) ->
Reason = case Error of
@@ -903,15 +1168,25 @@ terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reas
stream_get(#state{streams=Streams}, StreamID) ->
maps:get(StreamID, Streams, error).
-stream_new_remote(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
- StreamID, StreamType) ->
+stream_new_local(State, StreamID, StreamType, Status) ->
+ stream_new(State, StreamID, StreamType, unidi_local, Status).
+
+stream_new_remote(State, StreamID, StreamType) ->
+ Status = case StreamType of
+ unidi -> header;
+ bidi -> normal
+ end,
+ stream_new(State, StreamID, StreamType, unidi_remote, Status).
+
+stream_new(State=#state{http3_machine=HTTP3Machine0, streams=Streams},
+ StreamID, StreamType, UnidiType, Status) ->
{HTTP3Machine, Status} = case StreamType of
unidi ->
- {cow_http3_machine:init_unidi_stream(StreamID, unidi_remote, HTTP3Machine0),
- header};
+ {cow_http3_machine:init_unidi_stream(StreamID, UnidiType, HTTP3Machine0),
+ Status};
bidi ->
{cow_http3_machine:init_bidi_stream(StreamID, HTTP3Machine0),
- normal}
+ Status}
end,
Stream = #stream{id=StreamID, status=Status},
State#state{http3_machine=HTTP3Machine, streams=Streams#{StreamID => Stream}}.
@@ -926,6 +1201,11 @@ stream_closed(State=#state{local_decoder_id=StreamID}, StreamID, _) ->
stream_closed(State=#state{opts=Opts,
streams=Streams0, children=Children0}, StreamID, ErrorCode) ->
case maps:take(StreamID, Streams0) of
+ %% In the WT session's case, streams will be
+ %% removed in webtransport_terminate_session.
+ {Stream=#stream{status={webtransport_session, _}}, _} ->
+ webtransport_event(State, StreamID, closed_abruptly),
+ webtransport_terminate_session(State, Stream);
{#stream{state=undefined}, Streams} ->
%% Unidi stream has no handler/children.
stream_closed1(State#state{streams=Streams}, StreamID);
diff --git a/src/cowboy_loop.erl b/src/cowboy_loop.erl
index 6859c82..629d06e 100644
--- a/src/cowboy_loop.erl
+++ b/src/cowboy_loop.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_metrics_h.erl b/src/cowboy_metrics_h.erl
index 27f14d4..67bf1a6 100644
--- a/src/cowboy_metrics_h.erl
+++ b/src/cowboy_metrics_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl
index efeef4f..97c1498 100644
--- a/src/cowboy_middleware.erl
+++ b/src/cowboy_middleware.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2013-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_quicer.erl b/src/cowboy_quicer.erl
index d9bbe1f..aa52fae 100644
--- a/src/cowboy_quicer.erl
+++ b/src/cowboy_quicer.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2023, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,9 +23,12 @@
-export([shutdown/2]).
%% Streams.
+-export([start_bidi_stream/2]).
-export([start_unidi_stream/2]).
-export([send/3]).
-export([send/4]).
+-export([send_datagram/2]).
+-export([shutdown_stream/2]).
-export([shutdown_stream/4]).
%% Messages.
@@ -45,6 +48,9 @@ peercert(_) -> no_quicer().
-spec shutdown(_, _) -> no_return().
shutdown(_, _) -> no_quicer().
+-spec start_bidi_stream(_, _) -> no_return().
+start_bidi_stream(_, _) -> no_quicer().
+
-spec start_unidi_stream(_, _) -> no_return().
start_unidi_stream(_, _) -> no_quicer().
@@ -54,6 +60,12 @@ send(_, _, _) -> no_quicer().
-spec send(_, _, _, _) -> no_return().
send(_, _, _, _) -> no_quicer().
+-spec send_datagram(_, _) -> no_return().
+send_datagram(_, _) -> no_quicer().
+
+-spec shutdown_stream(_, _) -> no_return().
+shutdown_stream(_, _) -> no_quicer().
+
-spec shutdown_stream(_, _, _, _) -> no_return().
shutdown_stream(_, _, _, _) -> no_quicer().
@@ -109,16 +121,26 @@ shutdown(Conn, ErrorCode) ->
%% Streams.
+-spec start_bidi_stream(quicer_connection_handle(), iodata())
+ -> {ok, cow_http3:stream_id()}
+ | {error, any()}.
+
+start_bidi_stream(Conn, InitialData) ->
+ start_stream(Conn, InitialData, ?QUIC_STREAM_OPEN_FLAG_NONE).
+
-spec start_unidi_stream(quicer_connection_handle(), iodata())
-> {ok, cow_http3:stream_id()}
| {error, any()}.
-start_unidi_stream(Conn, HeaderData) ->
+start_unidi_stream(Conn, InitialData) ->
+ start_stream(Conn, InitialData, ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL).
+
+start_stream(Conn, InitialData, OpenFlag) ->
case quicer:start_stream(Conn, #{
active => true,
- open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}) of
+ open_flag => OpenFlag}) of
{ok, StreamRef} ->
- case quicer:send(StreamRef, HeaderData) of
+ case quicer:send(StreamRef, InitialData) of
{ok, _} ->
{ok, StreamID} = quicer:get_stream_id(StreamRef),
put({quicer_stream, StreamID}, StreamRef),
@@ -156,6 +178,29 @@ send(_Conn, StreamID, Data, IsFin) ->
send_flag(nofin) -> ?QUIC_SEND_FLAG_NONE;
send_flag(fin) -> ?QUIC_SEND_FLAG_FIN.
+-spec send_datagram(quicer_connection_handle(), iodata())
+ -> ok | {error, any()}.
+
+send_datagram(Conn, Data) ->
+ %% @todo Fix/ignore the Dialyzer error instead of doing this.
+ DataBin = iolist_to_binary(Data),
+ Size = byte_size(DataBin),
+ case quicer:send_dgram(Conn, DataBin) of
+ {ok, Size} ->
+ ok;
+ %% @todo Handle error cases.
+ Error ->
+ Error
+ end.
+
+-spec shutdown_stream(quicer_connection_handle(), cow_http3:stream_id())
+ -> ok.
+
+shutdown_stream(_Conn, StreamID) ->
+ StreamRef = get({quicer_stream, StreamID}),
+ _ = quicer:shutdown_stream(StreamRef),
+ ok.
+
-spec shutdown_stream(quicer_connection_handle(),
cow_http3:stream_id(), both | receiving, quicer_app_errno())
-> ok.
@@ -165,6 +210,7 @@ shutdown_stream(_Conn, StreamID, Dir, ErrorCode) ->
_ = quicer:shutdown_stream(StreamRef, shutdown_flag(Dir), ErrorCode, infinity),
ok.
+%% @todo Are these flags correct for what we want?
shutdown_flag(both) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT;
shutdown_flag(receiving) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE.
@@ -173,9 +219,11 @@ shutdown_flag(receiving) -> ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE.
%% @todo Probably should have the Conn given as argument too?
-spec handle({quic, _, _, _})
-> {data, cow_http3:stream_id(), cow_http:fin(), binary()}
+ | {datagram, binary()}
| {stream_started, cow_http3:stream_id(), unidi | bidi}
| {stream_closed, cow_http3:stream_id(), quicer_app_errno()}
| closed
+ | {peer_send_shutdown, cow_http3:stream_id()}
| ok
| unknown
| {socket_error, any()}.
@@ -187,6 +235,9 @@ handle({quic, Data, StreamRef, #{flags := Flags}}) when is_binary(Data) ->
_ -> nofin
end,
{data, StreamID, IsFin, Data};
+%% @todo Match on Conn.
+handle({quic, Data, _Conn, Flags}) when is_binary(Data), is_integer(Flags) ->
+ {datagram, Data};
%% QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED.
handle({quic, new_stream, StreamRef, #{flags := Flags}}) ->
case quicer:setopt(StreamRef, active, true) of
@@ -219,8 +270,9 @@ handle({quic, dgram_state_changed, _Conn, _Props}) ->
%% QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT
handle({quic, transport_shutdown, _Conn, _Flags}) ->
ok;
-handle({quic, peer_send_shutdown, _StreamRef, undefined}) ->
- ok;
+handle({quic, peer_send_shutdown, StreamRef, undefined}) ->
+ {ok, StreamID} = quicer:get_stream_id(StreamRef),
+ {peer_send_shutdown, StreamID};
handle({quic, send_shutdown_complete, _StreamRef, _IsGraceful}) ->
ok;
handle({quic, shutdown, _Conn, success}) ->
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 3f87677..550054e 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -1,5 +1,5 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
-%% Copyright (c) 2011, Anthony Ramine <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%% Copyright (c) Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -445,6 +445,7 @@ parse_header_fun(<<"sec-websocket-protocol">>) -> fun cow_http_hd:parse_sec_webs
parse_header_fun(<<"sec-websocket-version">>) -> fun cow_http_hd:parse_sec_websocket_version_req/1;
parse_header_fun(<<"trailer">>) -> fun cow_http_hd:parse_trailer/1;
parse_header_fun(<<"upgrade">>) -> fun cow_http_hd:parse_upgrade/1;
+parse_header_fun(<<"wt-available-protocols">>) -> fun cow_http_hd:parse_wt_available_protocols/1;
parse_header_fun(<<"x-forwarded-for">>) -> fun cow_http_hd:parse_x_forwarded_for/1.
parse_header(Name, Req, Default, ParseFun) ->
@@ -462,7 +463,7 @@ filter_cookies(Names0, Req=#{headers := Headers}) ->
case header(<<"cookie">>, Req) of
undefined -> Req;
Value0 ->
- Cookies0 = binary:split(Value0, <<$;>>),
+ Cookies0 = binary:split(Value0, <<$;>>, [global]),
Cookies = lists:filter(fun(Cookie) ->
lists:member(cookie_name(Cookie), Names)
end, Cookies0),
@@ -726,8 +727,10 @@ set_resp_header(Name, Value, Req=#{resp_headers := RespHeaders}) ->
set_resp_header(Name,Value, Req) ->
Req#{resp_headers => #{Name => Value}}.
--spec set_resp_headers(cowboy:http_headers(), Req)
+-spec set_resp_headers(cowboy:http_headers() | [{binary(), iodata()}], Req)
-> Req when Req::req().
+set_resp_headers(Headers, Req) when is_list(Headers) ->
+ set_resp_headers_list(Headers, Req, #{});
set_resp_headers(#{<<"set-cookie">> := _}, _) ->
exit({response_error, invalid_header,
'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
@@ -736,6 +739,19 @@ set_resp_headers(Headers, Req=#{resp_headers := RespHeaders}) ->
set_resp_headers(Headers, Req) ->
Req#{resp_headers => Headers}.
+set_resp_headers_list([], Req, Acc) ->
+ set_resp_headers(Acc, Req);
+set_resp_headers_list([{<<"set-cookie">>, _}|_], _, _) ->
+ exit({response_error, invalid_header,
+ 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'});
+set_resp_headers_list([{Name, Value}|Tail], Req, Acc) ->
+ case Acc of
+ #{Name := ValueAcc} ->
+ set_resp_headers_list(Tail, Req, Acc#{Name => [ValueAcc, <<", ">>, Value]});
+ _ ->
+ set_resp_headers_list(Tail, Req, Acc#{Name => Value})
+ end.
+
-spec resp_header(binary(), req()) -> binary() | undefined.
resp_header(Name, Req) ->
resp_header(Name, Req, undefined).
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index fcea71c..9f30fcf 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -246,9 +246,6 @@
handler :: atom(),
handler_state :: any(),
- %% Allowed methods. Only used for OPTIONS requests.
- allowed_methods :: [binary()] | undefined,
-
%% Media type.
content_types_p = [] ::
[{binary() | {binary(), binary(), [{binary(), binary()}] | '*'},
@@ -307,17 +304,17 @@ known_methods(Req, State=#state{method=Method}) ->
Method =:= <<"POST">>; Method =:= <<"PUT">>;
Method =:= <<"PATCH">>; Method =:= <<"DELETE">>;
Method =:= <<"OPTIONS">> ->
- next(Req, State, fun uri_too_long/2);
+ uri_too_long(Req, State);
no_call ->
- next(Req, State, 501);
+ respond(Req, State, 501);
{stop, Req2, State2} ->
terminate(Req2, State2);
{Switch, Req2, State2} when element(1, Switch) =:= switch_handler ->
switch_handler(Switch, Req2, State2);
{List, Req2, State2} ->
case lists:member(Method, List) of
- true -> next(Req2, State2, fun uri_too_long/2);
- false -> next(Req2, State2, 501)
+ true -> uri_too_long(Req2, State2);
+ false -> respond(Req2, State2, 501)
end
end.
@@ -327,39 +324,26 @@ uri_too_long(Req, State) ->
%% allowed_methods/2 should return a list of binary methods.
allowed_methods(Req, State=#state{method=Method}) ->
case call(Req, State, allowed_methods) of
- no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">> ->
- next(Req, State, fun malformed_request/2);
- no_call when Method =:= <<"OPTIONS">> ->
- next(Req, State#state{allowed_methods=
- [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]},
- fun malformed_request/2);
+ no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>; Method =:= <<"OPTIONS">> ->
+ Req2 = cowboy_req:set_resp_header(<<"allow">>, <<"HEAD, GET, OPTIONS">>, Req),
+ malformed_request(Req2, State);
no_call ->
- method_not_allowed(Req, State,
- [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]);
+ Req2 = cowboy_req:set_resp_header(<<"allow">>, <<"HEAD, GET, OPTIONS">>, Req),
+ respond(Req2, State, 405);
{stop, Req2, State2} ->
terminate(Req2, State2);
{Switch, Req2, State2} when element(1, Switch) =:= switch_handler ->
switch_handler(Switch, Req2, State2);
{List, Req2, State2} ->
+ Req3 = cowboy_req:set_resp_header(<<"allow">>, cow_http_hd:allow(List), Req2),
case lists:member(Method, List) of
- true when Method =:= <<"OPTIONS">> ->
- next(Req2, State2#state{allowed_methods=List},
- fun malformed_request/2);
true ->
- next(Req2, State2, fun malformed_request/2);
+ malformed_request(Req3, State2);
false ->
- method_not_allowed(Req2, State2, List)
+ respond(Req3, State2, 405)
end
end.
-method_not_allowed(Req, State, []) ->
- Req2 = cowboy_req:set_resp_header(<<"allow">>, <<>>, Req),
- respond(Req2, State, 405);
-method_not_allowed(Req, State, Methods) ->
- << ", ", Allow/binary >> = << << ", ", M/binary >> || M <- Methods >>,
- Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req),
- respond(Req2, State, 405).
-
malformed_request(Req, State) ->
expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
@@ -413,16 +397,10 @@ valid_entity_length(Req, State) ->
%% If you need to add additional headers to the response at this point,
%% you should do it directly in the options/2 call using set_resp_headers.
-options(Req, State=#state{allowed_methods=Methods, method= <<"OPTIONS">>}) ->
+options(Req, State=#state{method= <<"OPTIONS">>}) ->
case call(Req, State, options) of
- no_call when Methods =:= [] ->
- Req2 = cowboy_req:set_resp_header(<<"allow">>, <<>>, Req),
- respond(Req2, State, 200);
no_call ->
- << ", ", Allow/binary >>
- = << << ", ", M/binary >> || M <- Methods >>,
- Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req),
- respond(Req2, State, 200);
+ respond(Req, State, 200);
{stop, Req2, State2} ->
terminate(Req2, State2);
{Switch, Req2, State2} when element(1, Switch) =:= switch_handler ->
@@ -471,7 +449,7 @@ content_types_provided(Req, State) ->
{[], Req2, State2} ->
not_acceptable(Req2, State2);
{CTP, Req2, State2} ->
- CTP2 = [normalize_content_types(P) || P <- CTP],
+ CTP2 = [normalize_content_types(P, provide) || P <- CTP],
State3 = State2#state{content_types_p=CTP2},
try cowboy_req:parse_header(<<"accept">>, Req2) of
undefined ->
@@ -491,10 +469,14 @@ content_types_provided(Req, State) ->
end
end.
-normalize_content_types({ContentType, Callback})
+normalize_content_types({ContentType, Callback}, _)
when is_binary(ContentType) ->
{cow_http_hd:parse_content_type(ContentType), Callback};
-normalize_content_types(Normalized) ->
+normalize_content_types(Normalized = {{Type, SubType, _}, _}, _)
+ when is_binary(Type), is_binary(SubType) ->
+ Normalized;
+%% Wildcard for content_types_accepted.
+normalize_content_types(Normalized = {'*', _}, accept) ->
Normalized.
prioritize_accept(Accept) ->
@@ -1059,7 +1041,7 @@ accept_resource(Req, State) ->
{Switch, Req2, State2} when element(1, Switch) =:= switch_handler ->
switch_handler(Switch, Req2, State2);
{CTA, Req2, State2} ->
- CTA2 = [normalize_content_types(P) || P <- CTA],
+ CTA2 = [normalize_content_types(P, accept) || P <- CTA],
try cowboy_req:parse_header(<<"content-type">>, Req2) of
%% We do not match against the boundary parameter for multipart.
{Type = <<"multipart">>, SubType, Params} ->
@@ -1099,9 +1081,9 @@ process_content_type(Req, State=#state{method=Method, exists=Exists}, Fun) ->
{Switch, Req2, State2} when element(1, Switch) =:= switch_handler ->
switch_handler(Switch, Req2, State2);
{true, Req2, State2} when Exists ->
- next(Req2, State2, fun has_resp_body/2);
+ has_resp_body(Req2, State2);
{true, Req2, State2} ->
- next(Req2, State2, fun maybe_created/2);
+ maybe_created(Req2, State2);
{false, Req2, State2} ->
respond(Req2, State2, 400);
{{created, ResURL}, Req2, State2} when Method =:= <<"POST">> ->
@@ -1640,5 +1622,6 @@ error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class,
erlang:raise(Class, Reason, Stacktrace).
terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
+ %% @todo I don't think the result is used anywhere?
Result = cowboy_handler:terminate(normal, Req, HandlerState, Handler),
{ok, Req, Result}.
diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl
index 61c9012..393d82d 100644
--- a/src/cowboy_router.erl
+++ b/src/cowboy_router.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index a185ef1..ce34b01 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -1,5 +1,5 @@
-%% Copyright (c) 2013-2024, Loïc Hoguin <[email protected]>
-%% Copyright (c) 2011, Magnus Klaar <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%% Copyright (c) Magnus Klaar <[email protected]>
%%
%% 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,7 @@
-type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
-type extra_etag() :: {etag, module(), function()} | {etag, false}.
-type extra_mimetypes() :: {mimetypes, module(), function()}
- | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
+ | {mimetypes, binary() | {binary(), binary(), '*' | [{binary(), binary()}]}}.
-type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()].
-type opts() :: {file | dir, string() | binary()}
| {file | dir, string() | binary(), extra()}
@@ -332,7 +332,7 @@ forbidden(Req, State) ->
%% Detect the mimetype of the file.
-spec content_types_provided(Req, State)
- -> {[{binary(), get_file}], Req, State}
+ -> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, get_file}], Req, State}
when State::state().
content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) ->
case lists:keyfind(mimetypes, 1, Extra) of
@@ -347,7 +347,7 @@ content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) ->
%% Detect the charset of the file.
-spec charsets_provided(Req, State)
- -> {[binary()], Req, State}
+ -> {[binary()], Req, State} | no_call
when State::state().
charsets_provided(Req, State={Path, _, Extra}) ->
case lists:keyfind(charset, 1, Extra) of
@@ -381,7 +381,7 @@ resource_exists(Req, State) ->
%% Generate an etag for the file.
-spec generate_etag(Req, State)
- -> {{strong | weak, binary()}, Req, State}
+ -> {{strong | weak, binary() | undefined}, Req, State}
when State::state().
generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}},
Extra}) ->
@@ -408,7 +408,7 @@ last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) ->
%% Stream the file.
-spec get_file(Req, State)
- -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
+ -> {{sendfile, 0, non_neg_integer(), binary()} | binary(), Req, State}
when State::state().
get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) ->
{{sendfile, 0, Size, Path}, Req, State};
diff --git a/src/cowboy_stream.erl b/src/cowboy_stream.erl
index 6ceb5ba..6680bdc 100644
--- a/src/cowboy_stream.erl
+++ b/src/cowboy_stream.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -49,6 +49,7 @@
-type reason() :: normal | switch_protocol
| {internal_error, timeout | {error | exit | throw, any()}, human_reason()}
| {socket_error, closed | atom(), human_reason()}
+ %% @todo Or cow_http3:error().
| {stream_error, cow_http2:error(), human_reason()}
| {connection_error, cow_http2:error(), human_reason()}
| {stop, cow_http2:frame() | {exit, any()}, human_reason()}.
diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl
index b373344..3c3c084 100644
--- a/src/cowboy_stream_h.erl
+++ b/src/cowboy_stream_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -138,7 +138,7 @@ info(StreamID, Info={'EXIT', Pid, {{request_error, Reason, _HumanReadable}, _}},
{error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>},
stop
], State);
-info(StreamID, Exit={'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref, pid=Pid}) ->
+info(StreamID, Exit={'EXIT', Pid, Reason}, State=#state{ref=Ref, pid=Pid}) ->
Commands0 = [{internal_error, Exit, 'Stream process crashed.'}],
Commands = case Reason of
normal -> Commands0;
@@ -146,9 +146,8 @@ info(StreamID, Exit={'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref, p
{shutdown, _} -> Commands0;
_ -> [{log, error,
"Ranch listener ~p, connection process ~p, stream ~p "
- "had its request process ~p exit with reason "
- "~999999p and stacktrace ~999999p~n",
- [Ref, self(), StreamID, Pid, Reason, Stacktrace]}
+ "had its request process ~p exit with reason ~0p~n",
+ [Ref, self(), StreamID, Pid, Reason]}
|Commands0]
end,
%% @todo We are trying to send a 500 response before resetting
diff --git a/src/cowboy_sub_protocol.erl b/src/cowboy_sub_protocol.erl
index 062fd38..1f24d00 100644
--- a/src/cowboy_sub_protocol.erl
+++ b/src/cowboy_sub_protocol.erl
@@ -1,5 +1,5 @@
-%% Copyright (c) 2013-2024, Loïc Hoguin <[email protected]>
-%% Copyright (c) 2013, James Fish <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%% Copyright (c) James Fish <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl
index e37f4cf..224ef7d 100644
--- a/src/cowboy_sup.erl
+++ b/src/cowboy_sup.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_tls.erl b/src/cowboy_tls.erl
index 60ab2ed..6d0dcd3 100644
--- a/src/cowboy_tls.erl
+++ b/src/cowboy_tls.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,11 @@ connection_process(Parent, Ref, Transport, Opts) ->
{ok, <<"h2">>} ->
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http2);
_ -> %% http/1.1 or no protocol negotiated.
- init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http)
+ Protocol = case maps:get(alpn_default_protocol, Opts, http) of
+ http -> cowboy_http;
+ http2 -> cowboy_http2
+ end,
+ init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol)
end.
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) ->
diff --git a/src/cowboy_tracer_h.erl b/src/cowboy_tracer_h.erl
index b1196fe..91a431b 100644
--- a/src/cowboy_tracer_h.erl
+++ b/src/cowboy_tracer_h.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index 577de47..cb30c3f 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -69,6 +69,9 @@
active_n => pos_integer(),
compress => boolean(),
deflate_opts => cow_ws:deflate_opts(),
+ dynamic_buffer => false | {pos_integer(), pos_integer()},
+ dynamic_buffer_initial_average => non_neg_integer(),
+ dynamic_buffer_initial_size => pos_integer(),
idle_timeout => timeout(),
max_frame_size => non_neg_integer() | infinity,
req_filter => fun((cowboy_req:req()) -> map()),
@@ -97,6 +100,11 @@
timeout_num = 0 :: 0..?IDLE_TIMEOUT_TICKS,
messages = undefined :: undefined | {atom(), atom(), atom()}
| {atom(), atom(), atom(), atom()},
+
+ %% Dynamic buffer moving average and current buffer size.
+ dynamic_buffer_size = false :: pos_integer() | false,
+ dynamic_buffer_moving_average = 0 :: non_neg_integer(),
+
hibernate = false :: boolean(),
frag_state = undefined :: cow_ws:frag_state(),
frag_buffer = <<>> :: binary(),
@@ -270,7 +278,7 @@ websocket_handshake(State, Req=#{ref := Ref, pid := Pid, streamid := StreamID},
%% @todo We don't want date and server headers.
Headers = cowboy_req:response_headers(#{}, Req),
Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, {State, HandlerState}}},
- takeover(Pid, Ref, {Pid, StreamID}, undefined, undefined, <<>>,
+ takeover(Pid, Ref, {Pid, StreamID}, undefined, #{}, <<>>,
{State, HandlerState}).
%% Connection process.
@@ -295,8 +303,8 @@ websocket_handshake(State, Req=#{ref := Ref, pid := Pid, streamid := StreamID},
-spec takeover(pid(), ranch:ref(), inet:socket() | {pid(), cowboy_stream:streamid()},
module() | undefined, any(), binary(),
{#state{}, any()}) -> no_return().
-takeover(Parent, Ref, Socket, Transport, _Opts, Buffer,
- {State0=#state{handler=Handler, req=Req}, HandlerState}) ->
+takeover(Parent, Ref, Socket, Transport, Opts, Buffer,
+ {State0=#state{opts=WsOpts, handler=Handler, req=Req}, HandlerState}) ->
case Req of
#{version := 'HTTP/3'} -> ok;
%% @todo We should have an option to disable this behavior.
@@ -308,7 +316,11 @@ takeover(Parent, Ref, Socket, Transport, _Opts, Buffer,
end,
State = set_idle_timeout(State0#state{parent=Parent,
ref=Ref, socket=Socket, transport=Transport,
- key=undefined, messages=Messages}, 0),
+ opts=WsOpts#{dynamic_buffer => maps:get(dynamic_buffer, Opts, false)},
+ key=undefined, messages=Messages,
+ %% Dynamic buffer only applies to HTTP/1.1 Websocket.
+ dynamic_buffer_size=init_dynamic_buffer_size(Opts),
+ dynamic_buffer_moving_average=maps:get(dynamic_buffer_initial_average, Opts, 0)}, 0),
%% We call parse_header/3 immediately because there might be
%% some data in the buffer that was sent along with the handshake.
%% While it is not allowed by the protocol to send frames immediately,
@@ -319,6 +331,12 @@ takeover(Parent, Ref, Socket, Transport, _Opts, Buffer,
false -> after_init(State, HandlerState, #ps_header{buffer=Buffer})
end.
+-include("cowboy_dynamic_buffer.hrl").
+
+%% @todo Implement early socket error detection.
+maybe_socket_error(_, _) ->
+ ok.
+
after_init(State=#state{active=true}, HandlerState, ParseState) ->
%% Enable active,N for HTTP/1.1, and auto read_body for HTTP/2.
%% We must do this only after calling websocket_init/1 (if any)
@@ -340,7 +358,7 @@ after_init(State, HandlerState, ParseState) ->
setopts_active(#state{transport=undefined}) ->
ok;
setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) ->
- N = maps:get(active_n, Opts, 100),
+ N = maps:get(active_n, Opts, 1),
Transport:setopts(Socket, [{active, N}]).
maybe_read_body(#state{socket=Stream={Pid, _}, transport=undefined, active=true}) ->
@@ -384,6 +402,7 @@ before_loop(State, HandlerState, ParseState) ->
-spec set_idle_timeout(#state{}, 0..?IDLE_TIMEOUT_TICKS) -> #state{}.
+%% @todo Do we really need this for HTTP/2?
set_idle_timeout(State=#state{opts=Opts, timeout_ref=PrevRef}, TimeoutNum) ->
%% Most of the time we don't need to cancel the timer since it
%% will have triggered already. But this call is harmless so
@@ -391,7 +410,7 @@ set_idle_timeout(State=#state{opts=Opts, timeout_ref=PrevRef}, TimeoutNum) ->
%% options are changed dynamically.
_ = case PrevRef of
undefined -> ignore;
- PrevRef -> erlang:cancel_timer(PrevRef)
+ PrevRef -> erlang:cancel_timer(PrevRef, [{async, true}, {info, false}])
end,
case maps:get(idle_timeout, Opts, 60000) of
infinity ->
@@ -414,7 +433,8 @@ loop(State=#state{parent=Parent, socket=Socket, messages=Messages,
receive
%% Socket messages. (HTTP/1.1)
{OK, Socket, Data} when OK =:= element(1, Messages) ->
- parse(?reset_idle_timeout(State), HandlerState, ParseState, Data);
+ State1 = maybe_resize_buffer(State, Data),
+ parse(?reset_idle_timeout(State1), HandlerState, ParseState, Data);
{Closed, Socket} when Closed =:= element(2, Messages) ->
terminate(State, HandlerState, {error, closed});
{Error, Socket, Reason} when Error =:= element(3, Messages) ->
@@ -480,12 +500,16 @@ parse_header(State=#state{opts=Opts, frag_state=FragState, extensions=Extensions
websocket_close(State, HandlerState, {error, badframe})
end.
-parse_payload(State=#state{frag_state=FragState, utf8_state=Incomplete, extensions=Extensions},
+parse_payload(State=#state{opts=Opts, frag_state=FragState, utf8_state=Incomplete, extensions=Extensions},
HandlerState, ParseState=#ps_payload{
type=Type, len=Len, mask_key=MaskKey, rsv=Rsv,
unmasked=Unmasked, unmasked_len=UnmaskedLen}, Data) ->
+ MaxFrameSize = case maps:get(max_frame_size, Opts, infinity) of
+ infinity -> infinity;
+ MaxFrameSize0 -> MaxFrameSize0 - UnmaskedLen
+ end,
case cow_ws:parse_payload(Data, MaskKey, Incomplete, UnmaskedLen,
- Type, Len, FragState, Extensions, Rsv) of
+ Type, Len, FragState, Extensions#{max_inflate_size => MaxFrameSize}, Rsv) of
{ok, CloseCode, Payload, Utf8State, Rest} ->
dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState,
ParseState#ps_payload{unmasked= <<Unmasked/binary, Payload/binary>>,
@@ -615,14 +639,16 @@ commands([{active, Active}|Tail], State0=#state{active=Active0}, Data) when is_b
commands(Tail, State#state{active=Active}, Data);
commands([{deflate, Deflate}|Tail], State, Data) when is_boolean(Deflate) ->
commands(Tail, State#state{deflate=Deflate}, Data);
-commands([{set_options, SetOpts}|Tail], State0=#state{opts=Opts}, Data) ->
- State = case SetOpts of
- #{idle_timeout := IdleTimeout} ->
+commands([{set_options, SetOpts}|Tail], State0, Data) ->
+ State = maps:fold(fun
+ (idle_timeout, IdleTimeout, StateF=#state{opts=Opts}) ->
%% We reset the number of ticks when changing the idle_timeout option.
- set_idle_timeout(State0#state{opts=Opts#{idle_timeout => IdleTimeout}}, 0);
- _ ->
- State0
- end,
+ set_idle_timeout(StateF#state{opts=Opts#{idle_timeout => IdleTimeout}}, 0);
+ (max_frame_size, MaxFrameSize, StateF=#state{opts=Opts}) ->
+ StateF#state{opts=Opts#{max_frame_size => MaxFrameSize}};
+ (_, _, StateF) ->
+ StateF
+ end, State0, SetOpts),
commands(Tail, State, Data);
commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) ->
commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data);
diff --git a/src/cowboy_webtransport.erl b/src/cowboy_webtransport.erl
new file mode 100644
index 0000000..8c8ca39
--- /dev/null
+++ b/src/cowboy_webtransport.erl
@@ -0,0 +1,292 @@
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @todo To enable WebTransport the following options need to be set:
+%%
+%% QUIC:
+%% - max_datagram_frame_size > 0
+%%
+%% HTTP/3:
+%% - SETTINGS_H3_DATAGRAM = 1
+%% - SETTINGS_ENABLE_CONNECT_PROTOCOL = 1
+%% - SETTINGS_WT_MAX_SESSIONS >= 1
+
+%% Cowboy supports versions 07 through 13 of the WebTransport drafts.
+%% Cowboy also has some compatibility with version 02.
+%%
+%% WebTransport CONNECT requests go through cowboy_stream as normal
+%% and then an upgrade/switch_protocol is issued (just like Websocket).
+%% After that point none of the events go through cowboy_stream except
+%% the final terminate event. The request process becomes the process
+%% handling all events in the WebTransport session.
+%%
+%% WebTransport sessions can be ended via a command, via a crash or
+%% exit, via the closing of the connection (client or server inititated),
+%% via the client ending the session (mirroring the command) or via
+%% the client terminating the CONNECT stream.
+-module(cowboy_webtransport).
+
+-export([upgrade/4]).
+-export([upgrade/5]).
+
+%% cowboy_stream.
+-export([info/3]).
+-export([terminate/3]).
+
+-type stream_type() :: unidi | bidi.
+-type open_stream_ref() :: any().
+
+-type event() ::
+ {stream_open, cow_http3:stream_id(), stream_type()} |
+ {opened_stream_id, open_stream_ref(), cow_http3:stream_id()} |
+ {stream_data, cow_http3:stream_id(), cow_http:fin(), binary()} |
+ {datagram, binary()} |
+ close_initiated.
+
+-type commands() :: [
+ {open_stream, open_stream_ref(), stream_type(), iodata()} |
+ {close_stream, cow_http3:stream_id(), cow_http3:wt_app_error_code()} |
+ {send, cow_http3:stream_id() | datagram, iodata()} |
+ initiate_close |
+ close |
+ {close, cow_http3:wt_app_error_code()} |
+ {close, cow_http3:wt_app_error_code(), iodata()}
+].
+-export_type([commands/0]).
+
+-type call_result(State) :: {commands(), State} | {commands(), State, hibernate}.
+
+-callback init(Req, any())
+ -> {ok | module(), Req, any()}
+ | {module(), Req, any(), any()}
+ when Req::cowboy_req:req().
+
+-callback webtransport_init(State)
+ -> call_result(State) when State::any().
+-optional_callbacks([webtransport_init/1]).
+
+-callback webtransport_handle(event(), State)
+ -> call_result(State) when State::any().
+-optional_callbacks([webtransport_handle/2]).
+
+-callback webtransport_info(any(), State)
+ -> call_result(State) when State::any().
+-optional_callbacks([webtransport_info/2]).
+
+-callback terminate(any(), cowboy_req:req(), any()) -> ok.
+-optional_callbacks([terminate/3]).
+
+-type opts() :: #{
+ req_filter => fun((cowboy_req:req()) -> map())
+}.
+-export_type([opts/0]).
+
+-record(state, {
+ id :: cow_http3:stream_id(),
+ parent :: pid(),
+ opts = #{} :: opts(),
+ handler :: module(),
+ hibernate = false :: boolean(),
+ req = #{} :: map()
+}).
+
+%% This function mirrors a similar function for Websocket.
+
+-spec is_upgrade_request(cowboy_req:req()) -> boolean().
+
+is_upgrade_request(#{version := Version, method := <<"CONNECT">>, protocol := Protocol})
+ when Version =:= 'HTTP/3' ->
+ %% @todo scheme MUST BE "https"
+ <<"webtransport">> =:= cowboy_bstr:to_lower(Protocol);
+
+is_upgrade_request(_) ->
+ false.
+
+%% Stream process.
+
+-spec upgrade(Req, Env, module(), any())
+ -> {ok, Req, Env}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+
+upgrade(Req, Env, Handler, HandlerState) ->
+ upgrade(Req, Env, Handler, HandlerState, #{}).
+
+-spec upgrade(Req, Env, module(), any(), opts())
+ -> {ok, Req, Env}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+
+%% @todo Immediately crash if a response has already been sent.
+upgrade(Req=#{version := 'HTTP/3', pid := Pid, streamid := StreamID}, Env, Handler, HandlerState, Opts) ->
+ FilteredReq = case maps:get(req_filter, Opts, undefined) of
+ undefined -> maps:with([method, version, scheme, host, port, path, qs, peer], Req);
+ FilterFun -> FilterFun(Req)
+ end,
+ State = #state{id=StreamID, parent=Pid, opts=Opts, handler=Handler, req=FilteredReq},
+ %% @todo Must ensure the relevant settings are enabled (QUIC and H3).
+ %% Either we check them BEFORE, or we check them when the handler
+ %% is OK to initiate a webtransport session. Probably need to
+ %% check them BEFORE as we need to become (takeover) the webtransport process
+ %% after we are done with the upgrade. Maybe in cow_http3_machine but
+ %% it doesn't have QUIC settings currently (max_datagram_size).
+ case is_upgrade_request(Req) of
+ true ->
+ Headers = cowboy_req:response_headers(#{}, Req),
+ Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE,
+ #{session_pid => self()}}},
+ webtransport_init(State, HandlerState);
+ %% Use 501 Not Implemented to mirror the recommendation in
+ %% by RFC9220 3 (WebSockets Upgrade over HTTP/3).
+ false ->
+ %% @todo I don't think terminate will be called.
+ {ok, cowboy_req:reply(501, Req), Env}
+ end.
+
+webtransport_init(State=#state{handler=Handler}, HandlerState) ->
+ case erlang:function_exported(Handler, webtransport_init, 1) of
+ true -> handler_call(State, HandlerState, webtransport_init, undefined);
+ false -> before_loop(State, HandlerState)
+ end.
+
+before_loop(State=#state{hibernate=true}, HandlerState) ->
+ proc_lib:hibernate(?MODULE, loop, [State#state{hibernate=false}, HandlerState]);
+before_loop(State, HandlerState) ->
+ loop(State, HandlerState).
+
+-spec loop(#state{}, any()) -> no_return().
+
+loop(State=#state{id=SessionID, parent=Parent}, HandlerState) ->
+ receive
+ {'$webtransport_event', SessionID, Event={closed, _, _}} ->
+ terminate_proc(State, HandlerState, Event);
+ {'$webtransport_event', SessionID, Event=closed_abruptly} ->
+ terminate_proc(State, HandlerState, Event);
+ {'$webtransport_event', SessionID, Event} ->
+ handler_call(State, HandlerState, webtransport_handle, Event);
+ %% Timeouts.
+%% @todo idle_timeout
+% {timeout, TRef, ?MODULE} ->
+% tick_idle_timeout(State, HandlerState, ParseState);
+% {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
+% before_loop(State, HandlerState, ParseState);
+ %% System messages.
+ {'EXIT', Parent, Reason} ->
+ %% @todo We should exit gracefully.
+ exit(Reason);
+ {system, From, Request} ->
+ sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
+ {State, HandlerState});
+ %% Calls from supervisor module.
+ {'$gen_call', From, Call} ->
+ cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE),
+ before_loop(State, HandlerState);
+ Message ->
+ handler_call(State, HandlerState, webtransport_info, Message)
+ end.
+
+handler_call(State=#state{handler=Handler}, HandlerState, Callback, Message) ->
+ try case Callback of
+ webtransport_init -> Handler:webtransport_init(HandlerState);
+ _ -> Handler:Callback(Message, HandlerState)
+ end of
+ {Commands, HandlerState2} when is_list(Commands) ->
+ handler_call_result(State, HandlerState2, Commands);
+ {Commands, HandlerState2, hibernate} when is_list(Commands) ->
+ handler_call_result(State#state{hibernate=true}, HandlerState2, Commands)
+ catch Class:Reason:Stacktrace ->
+ %% @todo Do we need to send a close? Let cowboy_http3 detect and handle it?
+ handler_terminate(State, HandlerState, {crash, Class, Reason}),
+ erlang:raise(Class, Reason, Stacktrace)
+ end.
+
+handler_call_result(State0, HandlerState, Commands) ->
+ case commands(Commands, State0, ok, []) of
+ {ok, State} ->
+ before_loop(State, HandlerState);
+ {stop, State} ->
+ terminate_proc(State, HandlerState, stop)
+ end.
+
+%% We accumulate the commands that must be sent to the connection process
+%% because we want to send everything into one message. Other commands are
+%% processed immediately.
+
+commands([], State, Res, []) ->
+ {Res, State};
+commands([], State=#state{id=SessionID, parent=Pid}, Res, Commands) ->
+ Pid ! {'$webtransport_commands', SessionID, lists:reverse(Commands)},
+ {Res, State};
+%% {open_stream, OpenStreamRef, StreamType, InitialData}.
+commands([Command={open_stream, _, _, _}|Tail], State, Res, Acc) ->
+ commands(Tail, State, Res, [Command|Acc]);
+%% {close_stream, StreamID, Code}.
+commands([Command={close_stream, _, _}|Tail], State, Res, Acc) ->
+ commands(Tail, State, Res, [Command|Acc]);
+%% @todo We must reject send to a remote unidi stream.
+%% {send, StreamID | datagram, Data}.
+commands([Command={send, _, _}|Tail], State, Res, Acc) ->
+ commands(Tail, State, Res, [Command|Acc]);
+%% {send, StreamID, IsFin, Data}.
+commands([Command={send, _, _, _}|Tail], State, Res, Acc) ->
+ commands(Tail, State, Res, [Command|Acc]);
+%% initiate_close - DRAIN_WT_SESSION
+commands([Command=initiate_close|Tail], State, Res, Acc) ->
+ commands(Tail, State, Res, [Command|Acc]);
+%% close | {close, Code} | {close, Code, Msg} - CLOSE_WT_SESSION
+%% @todo At this point the handler must not issue stream or send commands.
+commands([Command=close|Tail], State, _, Acc) ->
+ commands(Tail, State, stop, [Command|Acc]);
+commands([Command={close, _}|Tail], State, _, Acc) ->
+ commands(Tail, State, stop, [Command|Acc]);
+commands([Command={close, _, _}|Tail], State, _, Acc) ->
+ commands(Tail, State, stop, [Command|Acc]).
+%% @todo A set_options command could be useful to increase the number of allowed streams
+%% or other forms of flow control. Alternatively a flow command. Or both.
+%% @todo A shutdown_reason command could be useful for the same reasons as Websocekt.
+
+-spec terminate_proc(_, _, _) -> no_return().
+
+terminate_proc(State, HandlerState, Reason) ->
+ handler_terminate(State, HandlerState, Reason),
+ %% @todo This is what should be done if shutdown_reason gets implemented.
+% case Shutdown of
+% normal -> exit(normal);
+% _ -> exit({shutdown, Shutdown})
+% end.
+ exit(normal).
+
+handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) ->
+ cowboy_handler:terminate(Reason, Req, HandlerState, Handler).
+
+%% cowboy_stream callbacks.
+%%
+%% We shortcut stream handlers but still need to process some events
+%% such as process exiting or termination. We implement the relevant
+%% callbacks here. Note that as far as WebTransport is concerned,
+%% receiving stream data here would be an error therefore the data
+%% callback is not implemented.
+%%
+%% @todo Better type than map() for the cowboy_stream state.
+
+-spec info(cowboy_stream:streamid(), any(), State)
+ -> {cowboy_stream:commands(), State} when State::map().
+
+info(StreamID, Msg, WTState=#{stream_state := StreamState0}) ->
+ {Commands, StreamState} = cowboy_stream:info(StreamID, Msg, StreamState0),
+ {Commands, WTState#{stream_state => StreamState}}.
+
+-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), map())
+ -> any().
+
+terminate(StreamID, Reason, #{stream_state := StreamState}) ->
+ cowboy_stream:terminate(StreamID, Reason, StreamState).
diff --git a/test/compress_SUITE.erl b/test/compress_SUITE.erl
index a6a100c..9da9769 100644
--- a/test/compress_SUITE.erl
+++ b/test/compress_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/cowboy_ct_hook.erl b/test/cowboy_ct_hook.erl
index e76ec21..46e56a2 100644
--- a/test/cowboy_ct_hook.erl
+++ b/test/cowboy_ct_hook.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2014-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/cowboy_test.erl b/test/cowboy_test.erl
index 670da18..541e8f9 100644
--- a/test/cowboy_test.erl
+++ b/test/cowboy_test.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2014-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -120,50 +120,53 @@ common_groups(Tests, Parallel) ->
Groups
end.
-init_common_groups(Name = http, Config, Mod) ->
- init_http(Name, #{
+init_common_groups(Name, Config, Mod) ->
+ init_common_groups(Name, Config, Mod, #{}).
+
+init_common_groups(Name = http, Config, Mod, ProtoOpts) ->
+ init_http(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]);
-init_common_groups(Name = https, Config, Mod) ->
- init_https(Name, #{
+init_common_groups(Name = https, Config, Mod, ProtoOpts) ->
+ init_https(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]);
-init_common_groups(Name = h2, Config, Mod) ->
- init_http2(Name, #{
+init_common_groups(Name = h2, Config, Mod, ProtoOpts) ->
+ init_http2(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]);
-init_common_groups(Name = h2c, Config, Mod) ->
- Config1 = init_http(Name, #{
+init_common_groups(Name = h2c, Config, Mod, ProtoOpts) ->
+ Config1 = init_http(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]),
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
-init_common_groups(Name = h3, Config, Mod) ->
- init_http3(Name, #{
+init_common_groups(Name = h3, Config, Mod, ProtoOpts) ->
+ init_http3(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)}
}, [{flavor, vanilla}|Config]);
-init_common_groups(Name = http_compress, Config, Mod) ->
- init_http(Name, #{
+init_common_groups(Name = http_compress, Config, Mod, ProtoOpts) ->
+ init_http(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]);
-init_common_groups(Name = https_compress, Config, Mod) ->
- init_https(Name, #{
+init_common_groups(Name = https_compress, Config, Mod, ProtoOpts) ->
+ init_https(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]);
-init_common_groups(Name = h2_compress, Config, Mod) ->
- init_http2(Name, #{
+init_common_groups(Name = h2_compress, Config, Mod, ProtoOpts) ->
+ init_http2(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]);
-init_common_groups(Name = h2c_compress, Config, Mod) ->
- Config1 = init_http(Name, #{
+init_common_groups(Name = h2c_compress, Config, Mod, ProtoOpts) ->
+ Config1 = init_http(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]),
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
-init_common_groups(Name = h3_compress, Config, Mod) ->
- init_http3(Name, #{
+init_common_groups(Name = h3_compress, Config, Mod, ProtoOpts) ->
+ init_http3(Name, ProtoOpts#{
env => #{dispatch => Mod:init_dispatch(Config)},
stream_handlers => [cowboy_compress_h, cowboy_stream_h]
}, [{flavor, compress}|Config]).
diff --git a/test/decompress_SUITE.erl b/test/decompress_SUITE.erl
index f61bb5d..f1eb13a 100644
--- a/test/decompress_SUITE.erl
+++ b/test/decompress_SUITE.erl
@@ -1,5 +1,5 @@
-%% Copyright (c) 2024, jdamanalo <[email protected]>
-%% Copyright (c) 2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) jdamanalo <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/draft_h3_webtransport_SUITE.erl b/test/draft_h3_webtransport_SUITE.erl
new file mode 100644
index 0000000..05a6c17
--- /dev/null
+++ b/test/draft_h3_webtransport_SUITE.erl
@@ -0,0 +1,814 @@
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(draft_h3_webtransport_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-import(ct_helper, [config/2]).
+-import(ct_helper, [doc/1]).
+-import(rfc9114_SUITE, [do_wait_stream_aborted/1]).
+
+-ifdef(COWBOY_QUICER).
+
+-include_lib("quicer/include/quicer.hrl").
+
+all() ->
+ [{group, enabled}].
+
+groups() ->
+ Tests = ct_helper:all(?MODULE),
+ [{enabled, [], Tests}]. %% @todo Enable parallel when all is better.
+
+init_per_group(Name = enabled, Config) ->
+ cowboy_test:init_http3(Name, #{
+ enable_connect_protocol => true,
+ h3_datagram => true,
+ enable_webtransport => true, %% For compatibility with draft-02.
+ wt_max_sessions => 10,
+ env => #{dispatch => cowboy_router:compile(init_routes(Config))}
+ }, Config).
+
+end_per_group(Name, _) ->
+ cowboy_test:stop_group(Name).
+
+init_routes(_) -> [
+ {"localhost", [
+ {"/wt", wt_echo_h, []}
+ ]}
+].
+
+%% Temporary.
+
+%% To start Chromium the command line is roughly:
+%% chromium --ignore-certificate-errors-spki-list=LeLykt63i2FRAm+XO91yBoSjKfrXnAFygqe5xt0zgDA= --ignore-certificate-errors --user-data-dir=/tmp/chromium-wt --allow-insecure-localhost --webtransport-developer-mode --enable-quic https://googlechrome.github.io/samples/webtransport/client.html
+%%
+%% To find the SPKI the command is roughly:
+%% openssl x509 -in ~/ninenines/cowboy/test/rfc9114_SUITE_data/server.pem -pubkey -noout | \
+%% openssl pkey -pubin -outform der | \
+%% openssl dgst -sha256 -binary | \
+%% openssl enc -base64
+
+%run(Config) ->
+% ct:pal("port ~p", [config(port, Config)]),
+% timer:sleep(infinity).
+
+%% 3. Session Establishment
+
+%% 3.1. Establishing a WebTransport-Capable HTTP/3 Connection
+
+%% In order to indicate support for WebTransport, the server MUST send a SETTINGS_WT_MAX_SESSIONS value greater than "0" in its SETTINGS frame. (3.1)
+%% @todo reject_session_disabled
+%% @todo accept_session_below
+%% @todo accept_session_equal
+%% @todo reject_session_above
+
+%% The client MUST NOT send a WebTransport request until it has received the setting indicating WebTransport support from the server. (3.1)
+
+%% For draft verisons of WebTransport only, the server MUST NOT process any incoming WebTransport requests until the client settings have been received, as the client may be using a version of the WebTransport extension that is different from the one used by the server. (3.1)
+
+%% Because WebTransport over HTTP/3 requires support for HTTP/3 datagrams and the Capsule Protocol, both the client and the server MUST indicate support for HTTP/3 datagrams by sending a SETTINGS_H3_DATAGRAM value set to 1 in their SETTINGS frame (see Section 2.1.1 of [HTTP-DATAGRAM]). (3.1)
+%% @todo settings_h3_datagram_enabled
+
+%% WebTransport over HTTP/3 also requires support for QUIC datagrams. To indicate support, both the client and the server MUST send a max_datagram_frame_size transport parameter with a value greater than 0 (see Section 3 of [QUIC-DATAGRAM]). (3.1)
+%% @todo quic_datagram_enabled (if size is too low the CONNECT stream can be used for capsules)
+
+%% Any WebTransport requests sent by the client without enabling QUIC and HTTP datagrams MUST be treated as malformed by the server, as described in Section 4.1.2 of [HTTP3]. (3.1)
+%% @todo reject_h3_datagram_disabled
+%% @todo reject_quic_datagram_disabled
+
+%% WebTransport over HTTP/3 relies on the RESET_STREAM_AT frame defined in [RESET-STREAM-AT]. To indicate support, both the client and the server MUST enable the extension as described in Section 3 of [RESET-STREAM-AT]. (3.1)
+%% @todo reset_stream_at_enabled
+
+%% 3.2. Extended CONNECT in HTTP/3
+
+%% [RFC8441] defines an extended CONNECT method in Section 4, enabled by the SETTINGS_ENABLE_CONNECT_PROTOCOL setting. That setting is defined for HTTP/3 by [RFC9220]. A server supporting WebTransport over HTTP/3 MUST send both the SETTINGS_WT_MAX_SESSIONS setting with a value greater than "0" and the SETTINGS_ENABLE_CONNECT_PROTOCOL setting with a value of "1". (3.2)
+%% @todo settings_enable_connect_protocol_enabled
+%% @todo reject_settings_enable_connect_protocol_disabled
+
+%% 3.3. Creating a New Session
+
+%% As WebTransport sessions are established over HTTP/3, they are identified using the https URI scheme ([HTTP], Section 4.2.2). (3.3)
+
+%% In order to create a new WebTransport session, a client can send an HTTP CONNECT request. The :protocol pseudo-header field ([RFC8441]) MUST be set to webtransport. The :scheme field MUST be https. Both the :authority and the :path value MUST be set; those fields indicate the desired WebTransport server. If the WebTransport session is coming from a browser client, an Origin header [RFC6454] MUST be provided within the request; otherwise, the header is OPTIONAL. (3.3)
+
+%% If it does not (have a WT server), it SHOULD reply with status code 404 (Section 15.5.5 of [HTTP]). (3.3)
+
+%% When the request contains the Origin header, the WebTransport server MUST verify the Origin header to ensure that the specified origin is allowed to access the server in question. If the verification fails, the WebTransport server SHOULD reply with status code 403 (Section 15.5.4 of [HTTP]). (3.3)
+
+accept_session_when_enabled(Config) ->
+ doc("Confirm that a WebTransport session can be established over HTTP/3. "
+ "(draft_webtrans_http3 3.3, RFC9220)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send Hello, get Hello back.
+ {ok, BidiStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(BidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>),
+ {nofin, <<"Hello">>} = do_receive_data(BidiStreamRef),
+ ok.
+
+%% If the server accepts 0-RTT, the server MUST NOT reduce the limit of maximum open WebTransport sessions from the one negotiated during the previous session; such change would be deemed incompatible, and MUST result in a H3_SETTINGS_ERROR connection error. (3.3)
+
+%% The capsule-protocol header field Section 3.4 of [HTTP-DATAGRAM] is not required by WebTransport and can safely be ignored by WebTransport endpoints. (3.3)
+
+%% 3.4. Application Protocol Negotiation
+
+application_protocol_negotiation(Config) ->
+ doc("Applications can negotiate a protocol to use via WebTransport. "
+ "(draft_webtrans_http3 3.4)"),
+ %% Connect to the WebTransport server.
+ WTAvailableProtocols = cow_http_hd:wt_available_protocols([<<"foo">>, <<"bar">>]),
+ #{
+ resp_headers := RespHeaders
+ } = do_webtransport_connect(Config, [{<<"wt-available-protocols">>, WTAvailableProtocols}]),
+ {<<"wt-protocol">>, WTProtocol} = lists:keyfind(<<"wt-protocol">>, 1, RespHeaders),
+ <<"foo">> = iolist_to_binary(cow_http_hd:parse_wt_protocol(WTProtocol)),
+ ok.
+
+%% Both WT-Available-Protocols and WT-Protocol are Structured Fields [RFC8941]. WT-Available-Protocols is a List of Tokens, and WT-Protocol is a Token. The token in the WT-Protocol response header field MUST be one of the tokens listed in WT-Available-Protocols of the request. (3.4)
+
+%% @todo 3.5 Prioritization
+
+%% 4. WebTransport Features
+
+%% The client MAY optimistically open unidirectional and bidirectional streams, as well as send datagrams, for a session that it has sent the CONNECT request for, even if it has not yet received the server's response to the request. (4)
+
+%% If at any point a session ID is received that cannot be a valid ID for a client-initiated bidirectional stream, the recipient MUST close the connection with an H3_ID_ERROR error code. (4)
+%% @todo Open bidi with Session ID 0, then do the CONNECT request.
+
+%% 4.1. Unidirectional streams
+
+unidirectional_streams(Config) ->
+ doc("Both endpoints can open and use unidirectional streams. "
+ "(draft_webtrans_http3 4.1)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a unidi stream, send Hello with a Fin flag.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn,
+ #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+ {ok, _} = quicer:send(LocalStreamRef,
+ <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>,
+ ?QUIC_SEND_FLAG_FIN),
+ %% Accept an identical unidi stream.
+ {unidi, RemoteStreamRef} = do_receive_new_stream(),
+ {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef),
+ {fin, <<"Hello">>} = do_receive_data(RemoteStreamRef),
+ ok.
+
+%% 4.2. Bidirectional Streams
+
+bidirectional_streams_client(Config) ->
+ doc("The WT client can open and use bidirectional streams. "
+ "(draft_webtrans_http3 4.2)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send Hello, get Hello back.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>),
+ {nofin, <<"Hello">>} = do_receive_data(LocalStreamRef),
+ ok.
+
+bidirectional_streams_server(Config) ->
+ doc("The WT server can open and use bidirectional streams. "
+ "(draft_webtrans_http3 4.2)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction
+ %% to make the server create another bidi stream.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>),
+ %% Accept the bidi stream and receive the data.
+ {bidi, RemoteStreamRef} = do_receive_new_stream(),
+ {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteStreamRef),
+ {ok, _} = quicer:send(RemoteStreamRef, <<"Hello">>,
+ ?QUIC_SEND_FLAG_FIN),
+ {fin, <<"Hello">>} = do_receive_data(RemoteStreamRef),
+ ok.
+
+%% Endpoints MUST NOT send WT_STREAM as a frame type on HTTP/3 streams other than the very first bytes of a request stream. Receiving this frame type in any other circumstances MUST be treated as a connection error of type H3_FRAME_ERROR. (4.2)
+
+%% 4.3. Resetting Data Streams
+
+%% A WebTransport endpoint may send a RESET_STREAM or a STOP_SENDING frame for a WebTransport data stream. Those signals are propagated by the WebTransport implementation to the application. (4.3)
+
+%% A WebTransport application SHALL provide an error code for those operations. (4.3)
+
+%% WebTransport implementations MUST use the RESET_STREAM_AT frame [RESET-STREAM-AT] with a Reliable Size set to at least the size of the WebTransport header when resetting a WebTransport data stream. This ensures that the ID field associating the data stream with a WebTransport session is always delivered. (4.3)
+
+%% WebTransport implementations SHALL forward the error code for a stream associated with a known session to the application that owns that session (4.3)
+
+%% 4.4. Datagrams
+
+datagrams(Config) ->
+ doc("Both endpoints can send and receive datagrams. (draft_webtrans_http3 4.4)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ QuarterID = SessionID div 4,
+ %% Send a Hello datagram.
+ {ok, _} = quicer:send_dgram(Conn, <<0:2, QuarterID:6, "Hello">>),
+ %% Receive a Hello datagram back.
+ {datagram, SessionID, <<"Hello">>} = do_receive_datagram(Conn),
+ ok.
+
+%% @todo datagrams_via_capsule?
+
+%% 4.5. Buffering Incoming Streams and Datagrams
+
+%% To handle this case (out of order stream_open/CONNECT), WebTransport endpoints SHOULD buffer streams and datagrams until those can be associated with an established session. (4.5)
+
+%% To avoid resource exhaustion, the endpoints MUST limit the number of buffered streams and datagrams. When the number of buffered streams is exceeded, a stream SHALL be closed by sending a RESET_STREAM and/or STOP_SENDING with the WT_BUFFERED_STREAM_REJECTED error code. When the number of buffered datagrams is exceeded, a datagram SHALL be dropped. It is up to an implementation to choose what stream or datagram to discard. (4.5)
+
+%% 4.6. Interaction with HTTP/3 GOAWAY frame
+
+%% A client receiving GOAWAY cannot initiate CONNECT requests for new WebTransport sessions on that HTTP/3 connection; it must open a new HTTP/3 connection to initiate new WebTransport sessions with the same peer. (4.6)
+
+%% An HTTP/3 GOAWAY frame is also a signal to applications to initiate shutdown for all WebTransport sessions. (4.6)
+
+%% @todo Currently receipt of a GOAWAY frame immediately ends the connection.
+%% We want to allow WT sessions to gracefully shut down before that.
+%goaway_client(Config) ->
+% doc("The HTTP/3 client can initiate the close of all WT sessions "
+% "by sending a GOAWAY frame. (draft_webtrans_http3 4.6)"),
+% %% Connect to the WebTransport server.
+% #{
+% conn := Conn,
+% connect_stream_ref := ConnectStreamRef,
+% session_id := SessionID
+% } = do_webtransport_connect(Config),
+% %% Open a control stream and send a GOAWAY frame.
+% {ok, ControlRef} = quicer:start_stream(Conn,
+% #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+% {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}),
+% {ok, _} = quicer:send(ControlRef, [
+% <<0>>, %% CONTROL stream.
+% SettingsBin,
+% <<7>>, %% GOAWAY frame.
+% cow_http3:encode_int(1),
+% cow_http3:encode_int(0)
+% ]),
+% %% Receive a datagram indicating processing by the WT handler.
+% {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn),
+% ok.
+
+wt_drain_session_client(Config) ->
+ doc("The WT client can initiate the close of a single session. "
+ "(draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Send the WT_DRAIN_SESSION capsule on the CONNECT stream.
+ {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_drain_session()),
+ %% Receive a datagram indicating processing by the WT handler.
+ {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn),
+ ok.
+
+wt_drain_session_server(Config) ->
+ doc("The WT server can initiate the close of a single session. "
+ "(draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it initiate the close.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:initiate_close">>),
+ %% Receive the WT_DRAIN_SESSION capsule on the CONNECT stream.
+ DrainWTSessionCapsule = cow_capsule:wt_drain_session(),
+ {nofin, DrainWTSessionCapsule} = do_receive_data(ConnectStreamRef),
+ ok.
+
+wt_drain_session_continue_client(Config) ->
+ doc("After the WT client has initiated the close of the session, "
+ "both client and server can continue using the session and "
+ "open new streams. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Send the WT_DRAIN_SESSION capsule on the CONNECT stream.
+ {ok, _} = quicer:send(ConnectStreamRef, cow_capsule:wt_drain_session()),
+ %% Receive a datagram indicating processing by the WT handler.
+ {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn),
+ %% Create a new bidi stream, send Hello, get Hello back.
+ {ok, ContinueStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(ContinueStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>),
+ {nofin, <<"Hello">>} = do_receive_data(ContinueStreamRef),
+ ok.
+
+wt_drain_session_continue_server(Config) ->
+ doc("After the WT server has initiated the close of the session, "
+ "both client and server can continue using the session and "
+ "open new streams. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it initiate the close.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:initiate_close">>),
+ %% Receive the WT_DRAIN_SESSION capsule on the CONNECT stream.
+ DrainWTSessionCapsule = cow_capsule:wt_drain_session(),
+ {nofin, DrainWTSessionCapsule} = do_receive_data(ConnectStreamRef),
+ %% Create a new bidi stream, send Hello, get Hello back.
+ {ok, ContinueStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(ContinueStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "Hello">>),
+ {nofin, <<"Hello">>} = do_receive_data(ContinueStreamRef),
+ ok.
+
+%% @todo 4.7. Use of Keying Material Exporters
+
+%% 5. Flow Control
+
+%% 5.1. Limiting the Number of Simultaneous Sessions
+
+%% This document defines a SETTINGS_WT_MAX_SESSIONS parameter that allows the server to limit the maximum number of concurrent WebTransport sessions on a single HTTP/3 connection. The client MUST NOT open more simultaneous sessions than indicated in the server SETTINGS parameter. The server MUST NOT close the connection if the client opens sessions exceeding this limit, as the client and the server do not have a consistent view of how many sessions are open due to the asynchronous nature of the protocol; instead, it MUST reset all of the CONNECT streams it is not willing to process with the H3_REQUEST_REJECTED status defined in [HTTP3]. (5.1)
+
+%% 5.2. Limiting the Number of Streams Within a Session
+
+%% The WT_MAX_STREAMS capsule (Section 5.6.1) establishes a limit on the number of streams within a WebTransport session. (5.2)
+
+%% Note that the CONNECT stream for the session is not included in either the bidirectional or the unidirectional stream limits (5.2)
+
+%% The session-level stream limit applies in addition to the QUIC MAX_STREAMS frame, which provides a connection-level stream limit. New streams can only be created within the session if both the stream- and the connection-level limit permit (5.2)
+
+%% The WT_STREAMS_BLOCKED capsule (Section 5.7) can be sent to indicate that an endpoint was unable to create a stream due to the session-level stream limit. (5.2)
+
+%% Note that enforcing this limit requires reliable resets for stream headers so that both endpoints can agree on the number of streams that are open. (5.2)
+
+%% 5.3. Data Limits
+
+%% The WT_MAX_DATA capsule (Section 5.8) establishes a limit on the amount of data that can be sent within a WebTransport session. This limit counts all data that is sent on streams of the corresponding type, excluding the stream header (see Section 4.1 and Section 4.2). (5.3)
+
+%% Implementing WT_MAX_DATA requires that the QUIC stack provide the WebTransport implementation with information about the final size of streams; see { {Section 4.5 of !RFC9000}}. This allows both endpoints to agree on how much data was consumed by that stream, although the stream header exclusion above applies. (5.3)
+
+%% The WT_DATA_BLOCKED capsule (Section 5.9) can be sent to indicate that an endpoint was unable to send data due to a limit set by the WT_MAX_DATA capsule. (5.3)
+
+%% The WT_MAX_STREAM_DATA and WT_STREAM_DATA_BLOCKED capsules (Part XX of [I-D.ietf-webtrans-http2]) are not used and so are prohibited. Endpoints MUST treat receipt of a WT_MAX_STREAM_DATA or a WT_STREAM_DATA_BLOCKED capsule as a session error. (5.3)
+
+%% 5.4. Flow Control and Intermediaries
+
+%% In practice, an intermediary that translates flow control signals between similar WebTransport protocols, such as between two HTTP/3 connections, can often simply reexpress the same limits received on one connection directly on the other connection. (5.4)
+
+%% 5.5. Flow Control SETTINGS
+
+%% WT_MAX_STREAMS via SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI (5.5)
+
+%% WT_MAX_DATA via SETTINGS_WT_INITIAL_MAX_DATA (5.5)
+
+%% 5.6. Flow Control Capsules
+
+%% 5.6.1. WT_MAX_STREAMS Capsule
+
+%% An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_STREAMS is introduced to inform the peer of the cumulative number of streams of a given type it is permitted to open. A WT_MAX_STREAMS capsule with a type of 0x190B4D3F applies to bidirectional streams, and a WT_MAX_STREAMS capsule with a type of 0x190B4D40 applies to unidirectional streams. (5.6.1)
+
+%% Note that, because Maximum Streams is a cumulative value representing the total allowed number of streams, including previously closed streams, endpoints repeatedly send new WT_MAX_STREAMS capsules with increasing Maximum Streams values as streams are opened. (5.6.1)
+
+%% Maximum Streams: A count of the cumulative number of streams of the corresponding type that can be opened over the lifetime of the session. This value cannot exceed 260, as it is not possible to encode stream IDs larger than 262-1. (5.6.1)
+
+%% An endpoint MUST NOT open more streams than permitted by the current stream limit set by its peer. (5.6.1)
+
+%% Note that this limit includes streams that have been closed as well as those that are open. (5.6.1)
+
+%% Initial values for these limits MAY be communicated by sending non-zero values for SETTINGS_WT_INITIAL_MAX_STREAMS_UNI and SETTINGS_WT_INITIAL_MAX_STREAMS_BIDI. (5.6.1)
+
+%% 5.7. WT_STREAMS_BLOCKED Capsule
+
+%% A sender SHOULD send a WT_STREAMS_BLOCKED capsule (type=0x190B4D43 for bidi or 0x190B4D44 for unidi) when it wishes to open a stream but is unable to do so due to the maximum stream limit set by its peer. (5.7)
+
+%% 5.8. WT_MAX_DATA Capsule
+
+%% An HTTP capsule [HTTP-DATAGRAM] called WT_MAX_DATA (type=0x190B4D3D) is introduced to inform the peer of the maximum amount of data that can be sent on the WebTransport session as a whole. (5.8)
+
+%% This limit counts all data that is sent on streams of the corresponding type, excluding the stream header (see Section 4.1 and Section 4.2). Implementing WT_MAX_DATA requires that the QUIC stack provide the WebTransport implementation with information about the final size of streams; see Section 4.5 of [RFC9000]. (5.8)
+
+%% All data sent in WT_STREAM capsules counts toward this limit. The sum of the lengths of Stream Data fields in WT_STREAM capsules MUST NOT exceed the value advertised by a receiver. (5.8)
+
+%% The initial value for this limit MAY be communicated by sending a non-zero value for SETTINGS_WT_INITIAL_MAX_DATA. (5.8)
+
+%% 5.9. WT_DATA_BLOCKED Capsule
+
+%% A sender SHOULD send a WT_DATA_BLOCKED capsule (type=0x190B4D41) when it wishes to send data but is unable to do so due to WebTransport session-level flow control. (5.9)
+
+%% WT_DATA_BLOCKED capsules can be used as input to tuning of flow control algorithms. (5.9)
+
+%% 6. Session Termination
+
+%% A WebTransport session over HTTP/3 is considered terminated when either of the following conditions is met:
+%% * the CONNECT stream is closed, either cleanly or abruptly, on either side; or
+%% * a WT_CLOSE_SESSION capsule is either sent or received.
+%% (6)
+
+wt_close_session_client(Config) ->
+ doc("The WT client can close a single session. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ connect_stream_ref := ConnectStreamRef
+ } = do_webtransport_connect(Config),
+ %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ {ok, _} = quicer:send(ConnectStreamRef,
+ cow_capsule:wt_close_session(0, <<>>),
+ ?QUIC_SEND_FLAG_FIN),
+ %% Normally we should also stop reading but in order to detect
+ %% that the server stops the stream we must not otherwise the
+ %% stream will be de facto closed on our end.
+ %%
+ %% The recipient must close or reset the stream in response.
+ receive
+ {quic, stream_closed, ConnectStreamRef, _} ->
+ ok
+ after 1000 ->
+ error({timeout, waiting_for_stream_closed})
+ end.
+
+wt_close_session_server(Config) ->
+ doc("The WT server can close a single session. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it initiate the close.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:close">>),
+ %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ CloseWTSessionCapsule = cow_capsule:wt_close_session(0, <<>>),
+ {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef),
+ ok.
+
+wt_session_gone_client(Config) ->
+ doc("Upon learning that the session has been terminated, "
+ "the WT server must reset associated streams with the "
+ "WEBTRANSPORT_SESSION_GONE error code. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a unidi stream.
+ {ok, LocalUnidiStreamRef} = quicer:start_stream(Conn,
+ #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+ {ok, _} = quicer:send(LocalUnidiStreamRef,
+ <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>),
+ %% Accept an identical unidi stream.
+ {unidi, RemoteUnidiStreamRef} = do_receive_new_stream(),
+ {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteUnidiStreamRef),
+ {nofin, <<"Hello">>} = do_receive_data(RemoteUnidiStreamRef),
+ %% Create a bidi stream, send a special instruction
+ %% to make the server create another bidi stream.
+ {ok, LocalBidiStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalBidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>),
+ %% Accept the bidi stream and receive the data.
+ {bidi, RemoteBidiStreamRef} = do_receive_new_stream(),
+ {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteBidiStreamRef),
+ {ok, _} = quicer:send(RemoteBidiStreamRef, <<"Hello">>),
+ {nofin, <<"Hello">>} = do_receive_data(RemoteBidiStreamRef),
+ %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ {ok, _} = quicer:send(ConnectStreamRef,
+ cow_capsule:wt_close_session(0, <<>>),
+ ?QUIC_SEND_FLAG_FIN),
+ %% All streams from that WT session have been aborted.
+ #{reason := wt_session_gone} = do_wait_stream_aborted(LocalUnidiStreamRef),
+ #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteUnidiStreamRef),
+ #{reason := wt_session_gone} = do_wait_stream_aborted(LocalBidiStreamRef),
+ #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteBidiStreamRef),
+ ok.
+
+wt_session_gone_server(Config) ->
+ doc("After the session has been terminated by the WT server, "
+ "the WT server must reset associated streams with the "
+ "WT_SESSION_GONE error code. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a unidi stream.
+ {ok, LocalUnidiStreamRef} = quicer:start_stream(Conn,
+ #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}),
+ {ok, _} = quicer:send(LocalUnidiStreamRef,
+ <<1:2, 16#54:14, 0:2, SessionID:6, "Hello">>),
+ %% Accept an identical unidi stream.
+ {unidi, RemoteUnidiStreamRef} = do_receive_new_stream(),
+ {nofin, <<1:2, 16#54:14, 0:2, SessionID:6>>} = do_receive_data(RemoteUnidiStreamRef),
+ {nofin, <<"Hello">>} = do_receive_data(RemoteUnidiStreamRef),
+ %% Create a bidi stream, send a special instruction
+ %% to make the server create another bidi stream.
+ {ok, LocalBidiStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalBidiStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:open_bidi">>),
+ %% Accept the bidi stream and receive the data.
+ {bidi, RemoteBidiStreamRef} = do_receive_new_stream(),
+ {nofin, <<1:2, 16#41:14, 0:2, SessionID:6>>} = do_receive_data(RemoteBidiStreamRef),
+ {ok, _} = quicer:send(RemoteBidiStreamRef, <<"Hello">>),
+ {nofin, <<"Hello">>} = do_receive_data(RemoteBidiStreamRef),
+
+ %% Send a special instruction to make the server initiate the close.
+ {ok, _} = quicer:send(LocalBidiStreamRef, <<"TEST:close">>),
+ %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ CloseWTSessionCapsule = cow_capsule:wt_close_session(0, <<>>),
+ {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef),
+ %% All streams from that WT session have been aborted.
+ #{reason := wt_session_gone} = do_wait_stream_aborted(LocalUnidiStreamRef),
+ #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteUnidiStreamRef),
+ #{reason := wt_session_gone} = do_wait_stream_aborted(LocalBidiStreamRef),
+ #{reason := wt_session_gone} = do_wait_stream_aborted(RemoteBidiStreamRef),
+ ok.
+
+%% Application Error Message: A UTF-8 encoded error message string provided by the application closing the session. The message takes up the remainder of the capsule, and its length MUST NOT exceed 1024 bytes. (6)
+%% @todo What if it's larger?
+
+wt_close_session_app_code_msg_client(Config) ->
+ doc("The WT client can close a single session with an application error code "
+ "and an application error message. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it propagate events.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ EventPidBin = term_to_binary(self()),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6,
+ "TEST:event_pid:", EventPidBin/binary>>),
+ %% Send the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ {ok, _} = quicer:send(ConnectStreamRef,
+ cow_capsule:wt_close_session(17, <<"seventeen">>),
+ ?QUIC_SEND_FLAG_FIN),
+ %% @todo Stop reading from the CONNECt stream too. (STOP_SENDING)
+ %% Receive the terminate event from the WT handler.
+ receive
+ {'$wt_echo_h', terminate, {closed, 17, <<"seventeen">>}, _, _} ->
+ ok
+ after 1000 ->
+ error({timeout, waiting_for_terminate_event})
+ end.
+
+wt_close_session_app_code_server(Config) ->
+ doc("The WT server can close a single session with an application error code. "
+ "(draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it initiate the close.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6,
+ "TEST:close_app_code">>),
+ %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ CloseWTSessionCapsule = cow_capsule:wt_close_session(1234567890, <<>>),
+ {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef),
+ ok.
+
+wt_close_session_app_code_msg_server(Config) ->
+ doc("The WT server can close a single session with an application error code "
+ "and an application error message. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it initiate the close.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6,
+ "TEST:close_app_code_msg">>),
+ %% Receive the WT_CLOSE_SESSION capsule on the CONNECT stream.
+ CloseWTSessionCapsule = iolist_to_binary(cow_capsule:wt_close_session(1234567890,
+ <<"onetwothreefourfivesixseveneightnineten">>)),
+ {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef),
+ ok.
+
+%% An endpoint that sends a WT_CLOSE_SESSION capsule MUST immediately send a FIN. The endpoint MAY send a STOP_SENDING to indicate it is no longer reading from the CONNECT stream. The recipient MUST either close or reset the stream in response. (6)
+%% @todo wt_close_session_server_fin
+%% @todo The part about close/reset should be tested in wt_close_session_client.
+
+%% If any additional stream data is received on the CONNECT stream after receiving a WT_CLOSE_SESSION capsule, the stream MUST be reset with code H3_MESSAGE_ERROR. (6)
+%% @todo wt_close_session_followed_by_data
+
+connect_stream_closed_cleanly_fin(Config) ->
+ doc("The WT client closing the CONNECT stream cleanly "
+ "is equivalent to a capsule with an application error code of 0 "
+ "and an empty error string. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it propagate events.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ EventPidBin = term_to_binary(self()),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6,
+ "TEST:event_pid:", EventPidBin/binary>>),
+ {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef),
+ %% Cleanly terminate the CONNECT stream.
+ {ok, _} = quicer:send(ConnectStreamRef, <<>>, ?QUIC_SEND_FLAG_FIN),
+ %% Receive the terminate event from the WT handler.
+ receive
+ {'$wt_echo_h', terminate, {closed, 0, <<>>}, _, _} ->
+ ok
+ after 1000 ->
+ error({timeout, waiting_for_terminate_event})
+ end.
+
+connect_stream_closed_cleanly_shutdown(Config) ->
+ doc("The WT client closing the CONNECT stream cleanly "
+ "is equivalent to a capsule with an application error code of 0 "
+ "and an empty error string. (draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it propagate events.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ EventPidBin = term_to_binary(self()),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6,
+ "TEST:event_pid:", EventPidBin/binary>>),
+ {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef),
+ %% Cleanly terminate the CONNECT stream.
+ _ = quicer:shutdown_stream(ConnectStreamRef),
+ %% Receive the terminate event from the WT handler.
+ receive
+ {'$wt_echo_h', terminate, {closed, 0, <<>>}, _, _} ->
+ ok
+ after 1000 ->
+ error({timeout, waiting_for_terminate_event})
+ end.
+
+connect_stream_closed_abruptly(Config) ->
+ doc("The WT client may close the CONNECT stream abruptly. "
+ "(draft_webtrans_http3 4.6)"),
+ %% Connect to the WebTransport server.
+ #{
+ conn := Conn,
+ connect_stream_ref := ConnectStreamRef,
+ session_id := SessionID
+ } = do_webtransport_connect(Config),
+ %% Create a bidi stream, send a special instruction to make it propagate events.
+ {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}),
+ EventPidBin = term_to_binary(self()),
+ {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6,
+ "TEST:event_pid:", EventPidBin/binary>>),
+ {nofin, <<"event_pid_received">>} = do_receive_data(LocalStreamRef),
+ %% Abruptly terminate the CONNECT stream.
+ _ = quicer:shutdown_stream(ConnectStreamRef, ?QUIC_STREAM_SHUTDOWN_FLAG_ABORT,
+ 0, infinity),
+ %% Receive the terminate event from the WT handler.
+ receive
+ %% @todo It would be good to forward a stream error as well
+ %% so that a WT error can be sent, but I have been unsuccessful.
+ {'$wt_echo_h', terminate, closed_abruptly, _, _} ->
+ ok
+ after 1000 ->
+ error({timeout, waiting_for_terminate_event})
+ end.
+
+%% @todo This one is about gracefully closing HTTP/3 connection with WT sessions.
+%% the endpoint SHOULD wait until all CONNECT streams have been closed by the peer before sending the CONNECTION_CLOSE (6)
+
+%% Helpers.
+
+do_webtransport_connect(Config) ->
+ do_webtransport_connect(Config, []).
+
+do_webtransport_connect(Config, ExtraHeaders) ->
+ %% Connect to server.
+ #{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config, #{
+ peer_unidi_stream_count => 100,
+ datagram_send_enabled => 1,
+ datagram_receive_enabled => 1
+ }),
+ %% Confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
+ #{enable_connect_protocol := true} = Settings,
+ %% Confirm that SETTINGS_WT_MAX_SESSIONS >= 1.
+ #{wt_max_sessions := WTMaxSessions} = Settings,
+ true = WTMaxSessions >= 1,
+ %% Confirm that SETTINGS_H3_DATAGRAM = 1.
+ #{h3_datagram := true} = Settings,
+ %% Confirm that QUIC's max_datagram_size > 0.
+ receive {quic, dgram_state_changed, Conn, DatagramState} ->
+ #{
+ dgram_max_len := DatagramMaxLen,
+ dgram_send_enabled := DatagramSendEnabled
+ } = DatagramState,
+ true = DatagramMaxLen > 0,
+ true = DatagramSendEnabled,
+ ok
+ after 5000 ->
+ error({timeout, waiting_for_datagram_state_change})
+ end,
+ %% Send a CONNECT :protocol request to upgrade the stream to Websocket.
+ {ok, ConnectStreamRef} = quicer:start_stream(Conn, #{}),
+ {ok, EncodedRequest, _EncData, _EncSt} = cow_qpack:encode_field_section([
+ {<<":method">>, <<"CONNECT">>},
+ {<<":protocol">>, <<"webtransport">>},
+ {<<":scheme">>, <<"https">>},
+ {<<":path">>, <<"/wt">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<"origin">>, <<"https://localhost">>}
+ |ExtraHeaders], 0, cow_qpack:init(encoder)),
+ {ok, _} = quicer:send(ConnectStreamRef, [
+ <<1>>, %% HEADERS frame.
+ cow_http3:encode_int(iolist_size(EncodedRequest)),
+ EncodedRequest
+ ]),
+ %% Receive a 200 response.
+ {nofin, Data} = do_receive_data(ConnectStreamRef),
+ {HLenEnc, HLenBits} = rfc9114_SUITE:do_guess_int_encoding(Data),
+ <<
+ 1, %% HEADERS frame.
+ HLenEnc:2, HLen:HLenBits,
+ EncodedResponse:HLen/bytes
+ >> = Data,
+ {ok, DecodedResponse, _DecData, _DecSt}
+ = cow_qpack:decode_field_section(EncodedResponse, 0, cow_qpack:init(decoder)),
+ #{<<":status">> := <<"200">>} = maps:from_list(DecodedResponse),
+ %% Retrieve the Session ID.
+ {ok, SessionID} = quicer:get_stream_id(ConnectStreamRef),
+ %% Accept QPACK streams to avoid conflicts with unidi streams from tests.
+ Unidi1 = rfc9114_SUITE:do_accept_qpack_stream(Conn),
+ Unidi2 = rfc9114_SUITE:do_accept_qpack_stream(Conn),
+ %% Done.
+ #{
+ conn => Conn,
+ connect_stream_ref => ConnectStreamRef,
+ session_id => SessionID,
+ resp_headers => DecodedResponse,
+ enc_or_dec1 => Unidi1,
+ enc_or_dec2 => Unidi2
+ }.
+
+do_receive_new_stream() ->
+ receive
+ {quic, new_stream, StreamRef, #{flags := Flags}} ->
+ ok = quicer:setopt(StreamRef, active, true),
+ case quicer:is_unidirectional(Flags) of
+ true -> {unidi, StreamRef};
+ false -> {bidi, StreamRef}
+ end
+ after 5000 ->
+ error({timeout, waiting_for_stream})
+ end.
+
+do_receive_data(StreamRef) ->
+ receive {quic, Data, StreamRef, #{flags := Flags}} ->
+ IsFin = case Flags band ?QUIC_RECEIVE_FLAG_FIN of
+ ?QUIC_RECEIVE_FLAG_FIN -> fin;
+ _ -> nofin
+ end,
+ {IsFin, Data}
+ after 5000 ->
+ error({timeout, waiting_for_data})
+ end.
+
+do_receive_datagram(Conn) ->
+ receive {quic, <<0:2, QuarterID:6, Data/bits>>, Conn, Flags} when is_integer(Flags) ->
+ {datagram, QuarterID * 4, Data}
+ after 5000 ->
+ ct:pal("~p", [process_info(self(), messages)]),
+ error({timeout, waiting_for_datagram})
+ end.
+
+-endif.
diff --git a/test/examples_SUITE.erl b/test/examples_SUITE.erl
index e2327bc..3d7c48b 100644
--- a/test/examples_SUITE.erl
+++ b/test/examples_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,10 +35,10 @@ init_per_suite(Config) ->
%% reuse the same build across all tests.
Make = do_find_make_cmd(),
CommonDir = config(priv_dir, Config),
- ct:log("~s~n", [os:cmd("git clone --depth 1 https://github.com/ninenines/cowboy "
+ ct:log("~ts~n", [os:cmd("git clone --depth 1 https://github.com/ninenines/cowboy "
++ CommonDir ++ "cowboy")]),
- ct:log("~s~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy distclean")]),
- ct:log("~s~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy DEPS_DIR=" ++ CommonDir)]),
+ ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy distclean")]),
+ ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy DEPS_DIR=" ++ CommonDir)]),
Config.
end_per_suite(_) ->
@@ -70,24 +70,24 @@ do_get_paths(Example0) ->
do_compile_and_start(Example, Config) ->
Make = do_find_make_cmd(),
{Dir, Rel, _} = do_get_paths(Example),
- ct:log("~s~n", [os:cmd(Make ++ " -C " ++ Dir ++ " distclean")]),
+ ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ Dir ++ " distclean")]),
%% We use a common build for Cowboy, Cowlib and Ranch to speed things up.
CommonDir = config(priv_dir, Config),
- ct:log("~s~n", [os:cmd("mkdir " ++ Dir ++ "/deps")]),
- ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "cowboy " ++ Dir ++ "/deps/cowboy")]),
- ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "cowlib " ++ Dir ++ "/deps/cowlib")]),
- ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "ranch " ++ Dir ++ "/deps/ranch")]),
+ ct:log("~ts~n", [os:cmd("mkdir " ++ Dir ++ "/deps")]),
+ ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "cowboy " ++ Dir ++ "/deps/cowboy")]),
+ ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "cowlib " ++ Dir ++ "/deps/cowlib")]),
+ ct:log("~ts~n", [os:cmd("ln -s " ++ CommonDir ++ "ranch " ++ Dir ++ "/deps/ranch")]),
%% TERM=dumb disables relx coloring.
- ct:log("~s~n", [os:cmd(Make ++ " -C " ++ Dir ++ " TERM=dumb")]),
- ct:log("~s~n", [os:cmd(Rel ++ " stop")]),
- ct:log("~s~n", [os:cmd(Rel ++ " daemon")]),
+ ct:log("~ts~n", [os:cmd(Make ++ " -C " ++ Dir ++ " TERM=dumb")]),
+ ct:log("~ts~n", [os:cmd(Rel ++ " stop")]),
+ ct:log("~ts~n", [os:cmd(Rel ++ " daemon")]),
timer:sleep(2000),
ok.
do_stop(Example) ->
{_, Rel, Log} = do_get_paths(Example),
- ct:log("~s~n", [os:cmd(Rel ++ " stop")]),
- ct:log("~s~n", [element(2, file:read_file(Log))]),
+ ct:log("~ts~n", [os:cmd(Rel ++ " stop")]),
+ ct:log("~ts~n", [element(2, file:read_file(Log))]),
ok.
%% Fetch a response.
diff --git a/test/h2spec_SUITE.erl b/test/h2spec_SUITE.erl
index 67ccf03..71a8a41 100644
--- a/test/h2spec_SUITE.erl
+++ b/test/h2spec_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/handlers/content_types_provided_h.erl b/test/handlers/content_types_provided_h.erl
index 5220c19..397026b 100644
--- a/test/handlers/content_types_provided_h.erl
+++ b/test/handlers/content_types_provided_h.erl
@@ -11,9 +11,14 @@
init(Req, Opts) ->
{cowboy_rest, Req, Opts}.
+content_types_provided(Req=#{qs := <<"invalid-type">>}, State) ->
+ ct_helper:ignore(cowboy_rest, normalize_content_types, 2),
+ {[{{'*', '*', '*'}, get_text_plain}], Req, State};
content_types_provided(Req=#{qs := <<"wildcard-param">>}, State) ->
{[{{<<"text">>, <<"plain">>, '*'}, get_text_plain}], Req, State}.
+get_text_plain(Req=#{qs := <<"invalid-type">>}, State) ->
+ {<<"invalid-type">>, Req, State};
get_text_plain(Req=#{qs := <<"wildcard-param">>}, State) ->
{_, _, Param} = maps:get(media_type, Req),
Body = if
diff --git a/test/handlers/crash_h.erl b/test/handlers/crash_h.erl
index b687aba..57d4d85 100644
--- a/test/handlers/crash_h.erl
+++ b/test/handlers/crash_h.erl
@@ -7,6 +7,9 @@
-export([init/2]).
-spec init(_, _) -> no_return().
+init(_, external_exit) ->
+ ct_helper:ignore(?MODULE, init, 2),
+ exit(self(), ct_helper_ignore);
init(_, no_reply) ->
ct_helper:ignore(?MODULE, init, 2),
error(crash);
diff --git a/test/handlers/read_body_h.erl b/test/handlers/read_body_h.erl
new file mode 100644
index 0000000..a0de3b3
--- /dev/null
+++ b/test/handlers/read_body_h.erl
@@ -0,0 +1,15 @@
+%% This module reads the request body fully and send a 204 response.
+
+-module(read_body_h).
+
+-export([init/2]).
+
+init(Req0, Opts) ->
+ {ok, Req} = read_body(Req0),
+ {ok, cowboy_req:reply(200, #{}, Req), Opts}.
+
+read_body(Req0) ->
+ case cowboy_req:read_body(Req0) of
+ {ok, _, Req} -> {ok, Req};
+ {more, _, Req} -> read_body(Req)
+ end.
diff --git a/test/handlers/resp_h.erl b/test/handlers/resp_h.erl
index 6e9b5f7..d1c46e0 100644
--- a/test/handlers/resp_h.erl
+++ b/test/handlers/resp_h.erl
@@ -43,12 +43,27 @@ do(<<"set_resp_headers">>, Req0, Opts) ->
<<"content-encoding">> => <<"compress">>
}, Req0),
{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
+do(<<"set_resp_headers_list">>, Req0, Opts) ->
+ Req = cowboy_req:set_resp_headers([
+ {<<"content-type">>, <<"text/plain">>},
+ {<<"test-header">>, <<"one">>},
+ {<<"content-encoding">>, <<"compress">>},
+ {<<"test-header">>, <<"two">>}
+ ], Req0),
+ {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
do(<<"set_resp_headers_cookie">>, Req0, Opts) ->
ct_helper:ignore(cowboy_req, set_resp_headers, 2),
Req = cowboy_req:set_resp_headers(#{
<<"set-cookie">> => <<"name=value">>
}, Req0),
{ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
+do(<<"set_resp_headers_list_cookie">>, Req0, Opts) ->
+ ct_helper:ignore(cowboy_req, set_resp_headers_list, 3),
+ Req = cowboy_req:set_resp_headers([
+ {<<"set-cookie">>, <<"name=value">>},
+ {<<"set-cookie">>, <<"name2=value2">>}
+ ], Req0),
+ {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts};
do(<<"set_resp_headers_http11">>, Req0, Opts) ->
Req = cowboy_req:set_resp_headers(#{
<<"connection">> => <<"custom-header, close">>,
diff --git a/test/handlers/stream_hello_h.erl b/test/handlers/stream_hello_h.erl
new file mode 100644
index 0000000..e67e220
--- /dev/null
+++ b/test/handlers/stream_hello_h.erl
@@ -0,0 +1,15 @@
+%% This module is the fastest way of producing a Hello world!
+
+-module(stream_hello_h).
+
+-export([init/3]).
+-export([terminate/3]).
+
+init(_, _, State) ->
+ {[
+ {response, 200, #{<<"content-length">> => <<"12">>}, <<"Hello world!">>},
+ stop
+ ], State}.
+
+terminate(_, _, _) ->
+ ok.
diff --git a/test/handlers/ws_ignore.erl b/test/handlers/ws_ignore.erl
new file mode 100644
index 0000000..9fe3322
--- /dev/null
+++ b/test/handlers/ws_ignore.erl
@@ -0,0 +1,20 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(ws_ignore).
+
+-export([init/2]).
+-export([websocket_handle/2]).
+-export([websocket_info/2]).
+
+init(Req, _) ->
+ {cowboy_websocket, Req, undefined, #{
+ compress => true
+ }}.
+
+websocket_handle({text, <<"CHECK">>}, State) ->
+ {[{text, <<"CHECK">>}], State};
+websocket_handle(_Frame, State) ->
+ {[], State}.
+
+websocket_info(_Info, State) ->
+ {[], State}.
diff --git a/test/handlers/ws_set_options_commands_h.erl b/test/handlers/ws_set_options_commands_h.erl
index 88d4e72..1ab0af4 100644
--- a/test/handlers/ws_set_options_commands_h.erl
+++ b/test/handlers/ws_set_options_commands_h.erl
@@ -11,10 +11,21 @@ init(Req, RunOrHibernate) ->
{cowboy_websocket, Req, RunOrHibernate,
#{idle_timeout => infinity}}.
-websocket_handle(Frame={text, <<"idle_timeout_short">>}, State=run) ->
- {[{set_options, #{idle_timeout => 500}}, Frame], State};
-websocket_handle(Frame={text, <<"idle_timeout_short">>}, State=hibernate) ->
- {[{set_options, #{idle_timeout => 500}}, Frame], State, hibernate}.
+%% Set the idle_timeout option dynamically.
+websocket_handle({text, <<"idle_timeout_short">>}, State=run) ->
+ {[{set_options, #{idle_timeout => 500}}], State};
+websocket_handle({text, <<"idle_timeout_short">>}, State=hibernate) ->
+ {[{set_options, #{idle_timeout => 500}}], State, hibernate};
+%% Set the max_frame_size option dynamically.
+websocket_handle({text, <<"max_frame_size_small">>}, State=run) ->
+ {[{set_options, #{max_frame_size => 1000}}], State};
+websocket_handle({text, <<"max_frame_size_small">>}, State=hibernate) ->
+ {[{set_options, #{max_frame_size => 1000}}], State, hibernate};
+%% We just echo binary frames.
+websocket_handle(Frame={binary, _}, State=run) ->
+ {[Frame], State};
+websocket_handle(Frame={binary, _}, State=hibernate) ->
+ {[Frame], State, hibernate}.
websocket_info(_Info, State) ->
{[], State}.
diff --git a/test/handlers/wt_echo_h.erl b/test/handlers/wt_echo_h.erl
new file mode 100644
index 0000000..5198565
--- /dev/null
+++ b/test/handlers/wt_echo_h.erl
@@ -0,0 +1,103 @@
+%% This module echoes client events back,
+%% including creating new streams.
+
+-module(wt_echo_h).
+-behavior(cowboy_webtransport).
+
+-export([init/2]).
+-export([webtransport_handle/2]).
+-export([webtransport_info/2]).
+-export([terminate/3]).
+
+%% -define(DEBUG, 1).
+-ifdef(DEBUG).
+-define(LOG(Fmt, Args), ct:pal(Fmt, Args)).
+-else.
+-define(LOG(Fmt, Args), _ = Fmt, _ = Args, ok).
+-endif.
+
+init(Req0, _) ->
+ ?LOG("WT init ~p~n", [Req0]),
+ Req = case cowboy_req:parse_header(<<"wt-available-protocols">>, Req0) of
+ undefined ->
+ Req0;
+ [Protocol|_] ->
+ cowboy_req:set_resp_header(<<"wt-protocol">>, cow_http_hd:wt_protocol(Protocol), Req0)
+ end,
+ {cowboy_webtransport, Req, #{}}.
+
+webtransport_handle(Event = {stream_open, StreamID, bidi}, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ {[], Streams#{StreamID => bidi}};
+webtransport_handle(Event = {stream_open, StreamID, unidi}, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ OpenStreamRef = make_ref(),
+ {[{open_stream, OpenStreamRef, unidi, <<>>}], Streams#{
+ StreamID => {unidi_remote, OpenStreamRef},
+ OpenStreamRef => {unidi_local, StreamID}}};
+webtransport_handle(Event = {opened_stream_id, OpenStreamRef, OpenStreamID}, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ case Streams of
+ #{OpenStreamRef := bidi} ->
+ {[], maps:remove(OpenStreamRef, Streams#{
+ OpenStreamID => bidi
+ })};
+ #{OpenStreamRef := {unidi_local, RemoteStreamID}} ->
+ #{RemoteStreamID := {unidi_remote, OpenStreamRef}} = Streams,
+ {[], maps:remove(OpenStreamRef, Streams#{
+ RemoteStreamID => {unidi_remote, OpenStreamID},
+ OpenStreamID => {unidi_local, RemoteStreamID}
+ })}
+ end;
+webtransport_handle(Event = {stream_data, StreamID, _IsFin, <<"TEST:", Test/bits>>}, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ case Test of
+ <<"open_bidi">> ->
+ OpenStreamRef = make_ref(),
+ {[{open_stream, OpenStreamRef, bidi, <<>>}],
+ Streams#{OpenStreamRef => bidi}};
+ <<"initiate_close">> ->
+ {[initiate_close], Streams};
+ <<"close">> ->
+ {[close], Streams};
+ <<"close_app_code">> ->
+ {[{close, 1234567890}], Streams};
+ <<"close_app_code_msg">> ->
+ {[{close, 1234567890, <<"onetwothreefourfivesixseveneightnineten">>}], Streams};
+ <<"event_pid:", EventPidBin/bits>> ->
+ {[{send, StreamID, nofin, <<"event_pid_received">>}],
+ Streams#{event_pid => binary_to_term(EventPidBin)}}
+ end;
+webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ case Streams of
+ #{StreamID := bidi} ->
+ {[{send, StreamID, IsFin, Data}], Streams};
+ #{StreamID := {unidi_remote, Ref}} when is_reference(Ref) ->
+ %% The stream isn't ready. We try again later.
+ erlang:send_after(100, self(), {try_again, Event}),
+ {[], Streams};
+ #{StreamID := {unidi_remote, LocalStreamID}} ->
+ {[{send, LocalStreamID, IsFin, Data}], Streams}
+ end;
+webtransport_handle(Event = {datagram, Data}, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ {[{send, datagram, Data}], Streams};
+webtransport_handle(Event = close_initiated, Streams) ->
+ ?LOG("WT handle ~p~n", [Event]),
+ {[{send, datagram, <<"TEST:close_initiated">>}], Streams};
+webtransport_handle(Event, Streams) ->
+ ?LOG("WT handle ignore ~p~n", [Event]),
+ {[], Streams}.
+
+webtransport_info({try_again, Event}, Streams) ->
+ ?LOG("WT try_again ~p", [Event]),
+ webtransport_handle(Event, Streams).
+
+terminate(Reason, Req, State=#{event_pid := EventPid}) ->
+ ?LOG("WT terminate ~0p~n~0p~n~0p", [Reason, Req, State]),
+ EventPid ! {'$wt_echo_h', terminate, Reason, Req, State},
+ ok;
+terminate(Reason, Req, State) ->
+ ?LOG("WT terminate ~0p~n~0p~n~0p", [Reason, Req, State]),
+ ok.
diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl
index d17508a..6f2d020 100644
--- a/test/http2_SUITE.erl
+++ b/test/http2_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -51,6 +51,27 @@ do_handshake(Settings, Config) ->
{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
{ok, Socket}.
+hibernate(Config) ->
+ doc("Ensure that we can enable hibernation for HTTP/1.1 connections."),
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
+ env => #{dispatch => init_dispatch(Config)},
+ hibernate => true
+ }),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]),
+ {ok, http2} = gun:await_up(ConnPid),
+ StreamRef1 = gun:get(ConnPid, "/"),
+ StreamRef2 = gun:get(ConnPid, "/"),
+ StreamRef3 = gun:get(ConnPid, "/"),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3),
+ gun:close(ConnPid)
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
idle_timeout(Config) ->
doc("Terminate when the idle timeout is reached."),
ProtoOpts = #{
@@ -449,8 +470,8 @@ graceful_shutdown_listener_timeout(Config) ->
send_timeout_close(Config) ->
doc("Check that connections are closed on send timeout."),
TransOpts = #{
- port => 0,
socket_opts => [
+ {port, 0},
{send_timeout, 100},
{send_timeout_close, true},
{sndbuf, 10}
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 0325279..9928136 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -28,9 +28,16 @@
-import(cowboy_test, [raw_recv/3]).
-import(cowboy_test, [raw_expect_recv/2]).
-all() -> [{group, clear}].
+all() ->
+ [{group, clear_no_parallel}, {group, clear}].
-groups() -> [{clear, [parallel], ct_helper:all(?MODULE)}].
+groups() ->
+ [
+ %% cowboy:stop_listener can be slow when called many times
+ %% in parallel so we must run this test separately from the others.
+ {clear_no_parallel, [], [graceful_shutdown_listener]},
+ {clear, [parallel], ct_helper:all(?MODULE) -- [graceful_shutdown_listener]}
+ ].
init_per_group(Name, Config) ->
cowboy_test:init_http(Name, #{
@@ -43,6 +50,7 @@ end_per_group(Name, _) ->
init_dispatch(_) ->
cowboy_router:compile([{"localhost", [
{"/", hello_h, []},
+ {"/delay_hello", delay_hello_h, #{delay => 1000, notify_received => self()}},
{"/echo/:key", echo_h, []},
{"/resp/:key[/:arg]", resp_h, []},
{"/set_options/:key", set_options_h, []},
@@ -198,6 +206,94 @@ do_chunked_body(ChunkSize0, Data, Acc) ->
do_chunked_body(ChunkSize, Rest,
[iolist_to_binary(cow_http_te:chunk(Chunk))|Acc]).
+disable_http1_tls(Config) ->
+ doc("Ensure that we can disable HTTP/1.1 over TLS (force HTTP/2)."),
+ TlsOpts = ct_helper:get_certs_from_ets(),
+ {ok, _} = cowboy:start_tls(?FUNCTION_NAME, TlsOpts ++ [{port, 0}], #{
+ env => #{dispatch => init_dispatch(Config)},
+ alpn_default_protocol => http2
+ }),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ {ok, Socket} = ssl:connect("localhost", Port,
+ [binary, {active, false}|TlsOpts]),
+ %% ALPN was not negotiated but we're still over HTTP/2.
+ {error, protocol_not_negotiated} = ssl:negotiated_protocol(Socket),
+ %% Send a valid preface.
+ ok = ssl:send(Socket, [
+ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n",
+ cow_http2:settings(#{})]),
+ %% Receive the server preface.
+ {ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000),
+ {ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000),
+ ok
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
+disable_http2_prior_knowledge(Config) ->
+ doc("Ensure that we can disable prior knowledge HTTP/2 upgrade."),
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
+ env => #{dispatch => init_dispatch(Config)},
+ protocols => [http]
+ }),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]),
+ %% Send a valid preface.
+ ok = gen_tcp:send(Socket, [
+ "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n",
+ cow_http2:settings(#{})]),
+ {ok, <<"HTTP/1.1 501">>} = gen_tcp:recv(Socket, 12, 1000),
+ ok
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
+disable_http2_upgrade(Config) ->
+ doc("Ensure that we can disable HTTP/1.1 upgrade to HTTP/2."),
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
+ env => #{dispatch => init_dispatch(Config)},
+ protocols => [http]
+ }),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]),
+ %% Send a valid preface.
+ ok = gen_tcp:send(Socket, [
+ "GET / HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade, HTTP2-Settings\r\n"
+ "Upgrade: h2c\r\n"
+ "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n",
+ "\r\n"]),
+ {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000),
+ ok
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
+hibernate(Config) ->
+ doc("Ensure that we can enable hibernation for HTTP/1.1 connections."),
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
+ env => #{dispatch => init_dispatch(Config)},
+ hibernate => true
+ }),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
+ {ok, http} = gun:await_up(ConnPid),
+ StreamRef1 = gun:get(ConnPid, "/"),
+ StreamRef2 = gun:get(ConnPid, "/"),
+ StreamRef3 = gun:get(ConnPid, "/"),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3),
+ gun:close(ConnPid)
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
http10_keepalive_false(Config) ->
doc("Confirm the option http10_keepalive => false disables keep-alive "
"completely for HTTP/1.0 connections."),
@@ -454,6 +550,26 @@ request_timeout_pipeline(Config) ->
cowboy:stop_listener(?FUNCTION_NAME)
end.
+request_timeout_pipeline_delay(Config) ->
+ doc("Ensure the request_timeout does not trigger on requests "
+ "coming in after a large request body."),
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
+ env => #{dispatch => init_dispatch(Config)},
+ request_timeout => 500
+ }),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
+ {ok, http} = gun:await_up(ConnPid),
+ StreamRef1 = gun:post(ConnPid, "/", #{}, <<0:8000000>>),
+ StreamRef2 = gun:get(ConnPid, "/delay_hello"),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
+ {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2),
+ {error, {down, {shutdown, closed}}} = gun:await(ConnPid, undefined, 1000)
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
request_timeout_skip_body(Config) ->
doc("Ensure the request_timeout drops connections when requests "
"fail to come in fast enough after skipping a request body."),
@@ -779,8 +895,8 @@ graceful_shutdown_listener(Config) ->
send_timeout_close(_Config) ->
doc("Check that connections are closed on send timeout."),
TransOpts = #{
- port => 0,
socket_opts => [
+ {port, 0},
{send_timeout, 100},
{send_timeout_close, true},
{sndbuf, 10}
diff --git a/test/http_perf_SUITE.erl b/test/http_perf_SUITE.erl
new file mode 100644
index 0000000..1484c03
--- /dev/null
+++ b/test/http_perf_SUITE.erl
@@ -0,0 +1,220 @@
+%% Copyright (c) Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(http_perf_SUITE).
+-compile(export_all).
+-compile(nowarn_export_all).
+
+-import(ct_helper, [config/2]).
+-import(ct_helper, [doc/1]).
+-import(cowboy_test, [gun_open/1]).
+
+%% ct.
+
+all() ->
+ %% @todo Enable HTTP/3 for this test suite.
+ cowboy_test:common_all() -- [{group, h3}, {group, h3_compress}].
+
+groups() ->
+ cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel).
+
+init_per_suite(Config) ->
+ do_log("", []),
+ %% Optionally enable `perf` for the current node.
+% spawn(fun() -> ct:pal(os:cmd("perf record -g -F 9999 -o /tmp/http_perf.data -p " ++ os:getpid() ++ " -- sleep 60")) end),
+ Config.
+
+end_per_suite(_) ->
+ ok.
+
+init_per_group(Name, Config) ->
+ [{group, Name}|cowboy_test:init_common_groups(Name, Config, ?MODULE, #{
+ %% HTTP/1.1
+ max_keepalive => infinity,
+ %% HTTP/2
+ %% @todo Must configure Gun for performance too.
+ connection_window_margin_size => 64*1024,
+ enable_connect_protocol => true,
+ env => #{dispatch => init_dispatch(Config)},
+ max_frame_size_sent => 64*1024,
+ max_frame_size_received => 16384 * 1024 - 1,
+ max_received_frame_rate => {10_000_000, 1},
+ stream_window_data_threshold => 1024,
+ stream_window_margin_size => 64*1024
+ })].
+
+end_per_group(Name, _) ->
+ do_log("", []),
+ cowboy_test:stop_group(Name).
+
+%% Routes.
+
+init_dispatch(_) ->
+ cowboy_router:compile([{'_', [
+ {"/", hello_h, []},
+ {"/read_body", read_body_h, []}
+ ]}]).
+
+%% Tests: Hello world.
+
+plain_h_hello_1(Config) ->
+ doc("Plain HTTP handler Hello World; 10K requests per 1 client."),
+ do_bench_get(?FUNCTION_NAME, "/", #{}, 1, 10000, Config).
+
+plain_h_hello_10(Config) ->
+ doc("Plain HTTP handler Hello World; 10K requests per 10 clients."),
+ do_bench_get(?FUNCTION_NAME, "/", #{}, 10, 10000, Config).
+
+stream_h_hello_1(Config) ->
+ doc("Stream handler Hello World; 10K requests per 1 client."),
+ do_stream_h_hello(Config, 1).
+
+stream_h_hello_10(Config) ->
+ doc("Stream handler Hello World; 10K requests per 10 clients."),
+ do_stream_h_hello(Config, 10).
+
+do_stream_h_hello(Config, NumClients) ->
+ Ref = config(ref, Config),
+ ProtoOpts = ranch:get_protocol_options(Ref),
+ StreamHandlers = case ProtoOpts of
+ #{stream_handlers := _} -> [cowboy_compress_h, stream_hello_h];
+ _ -> [stream_hello_h]
+ end,
+ ranch:set_protocol_options(Ref, ProtoOpts#{
+ env => #{},
+ stream_handlers => StreamHandlers
+ }),
+ do_bench_get(?FUNCTION_NAME, "/", #{}, NumClients, 10000, Config),
+ ranch:set_protocol_options(Ref, ProtoOpts).
+
+%% Tests: Large body upload.
+
+plain_h_1M_post_1(Config) ->
+ doc("Plain HTTP handler body reading; 10K requests per 1 client."),
+ do_bench_post(?FUNCTION_NAME, "/read_body", #{}, <<0:8_000_000>>, 1, 10000, Config).
+
+plain_h_1M_post_10(Config) ->
+ doc("Plain HTTP handler body reading; 10K requests per 10 clients."),
+ do_bench_post(?FUNCTION_NAME, "/read_body", #{}, <<0:8_000_000>>, 10, 10000, Config).
+
+plain_h_10G_post(Config) ->
+ doc("Plain HTTP handler body reading; 1 request with a 10GB body."),
+ do_bench_post_one_large(?FUNCTION_NAME, "/read_body", #{}, 10_000, <<0:8_000_000>>, Config).
+
+%% Internal.
+
+do_bench_get(What, Path, Headers, NumClients, NumRuns, Config) ->
+ Clients = [spawn_link(?MODULE, do_bench_get_proc,
+ [self(), What, Path, Headers, NumRuns, Config])
+ || _ <- lists:seq(1, NumClients)],
+ {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, Clients]),
+ do_log("~32s: ~8bµs ~8.1freqs/s", [
+ [atom_to_list(config(group, Config)), $., atom_to_list(What)],
+ Time,
+ (NumClients * NumRuns) / Time * 1_000_000]),
+ ok.
+
+do_bench_get_proc(Parent, What, Path, Headers0, NumRuns, Config) ->
+ ConnPid = gun_open(Config),
+ Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>},
+ Parent ! {What, ready},
+ receive {What, go} -> ok end,
+ do_bench_get_run(ConnPid, Path, Headers, NumRuns),
+ Parent ! {What, done},
+ gun:close(ConnPid).
+
+do_bench_get_run(_, _, _, 0) ->
+ ok;
+do_bench_get_run(ConnPid, Path, Headers, Num) ->
+ Ref = gun:request(ConnPid, <<"GET">>, Path, Headers, <<>>),
+ {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, Ref, infinity),
+ {ok, _} = case IsFin of
+ nofin -> gun:await_body(ConnPid, Ref, infinity);
+ fin -> {ok, <<>>}
+ end,
+ do_bench_get_run(ConnPid, Path, Headers, Num - 1).
+
+do_bench_post(What, Path, Headers, Body, NumClients, NumRuns, Config) ->
+ Clients = [spawn_link(?MODULE, do_bench_post_proc,
+ [self(), What, Path, Headers, Body, NumRuns, Config])
+ || _ <- lists:seq(1, NumClients)],
+ {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, Clients]),
+ do_log("~32s: ~8bµs ~8.1freqs/s", [
+ [atom_to_list(config(group, Config)), $., atom_to_list(What)],
+ Time,
+ (NumClients * NumRuns) / Time * 1_000_000]),
+ ok.
+
+do_bench_post_proc(Parent, What, Path, Headers0, Body, NumRuns, Config) ->
+ ConnPid = gun_open(Config),
+ Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>},
+ Parent ! {What, ready},
+ receive {What, go} -> ok end,
+ do_bench_post_run(ConnPid, Path, Headers, Body, NumRuns),
+ Parent ! {What, done},
+ gun:close(ConnPid).
+
+do_bench_post_run(_, _, _, _, 0) ->
+ ok;
+do_bench_post_run(ConnPid, Path, Headers, Body, Num) ->
+ Ref = gun:request(ConnPid, <<"POST">>, Path, Headers, Body),
+ {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, Ref, infinity),
+ {ok, _} = case IsFin of
+ nofin -> gun:await_body(ConnPid, Ref, infinity);
+ fin -> {ok, <<>>}
+ end,
+ do_bench_post_run(ConnPid, Path, Headers, Body, Num - 1).
+
+do_bench_post_one_large(What, Path, Headers, NumChunks, BodyChunk, Config) ->
+ Client = spawn_link(?MODULE, do_bench_post_one_large_proc,
+ [self(), What, Path, Headers, NumChunks, BodyChunk, Config]),
+ {Time, _} = timer:tc(?MODULE, do_bench_wait, [What, [Client]]),
+ do_log("~32s: ~8bµs ~8.1freqs/s", [
+ [atom_to_list(config(group, Config)), $., atom_to_list(What)],
+ Time,
+ 1 / Time * 1_000_000]),
+ ok.
+
+do_bench_post_one_large_proc(Parent, What, Path, Headers0, NumChunks, BodyChunk, Config) ->
+ ConnPid = gun_open(Config),
+ Headers = Headers0#{<<"accept-encoding">> => <<"gzip">>},
+ Parent ! {What, ready},
+ receive {What, go} -> ok end,
+ StreamRef = gun:headers(ConnPid, <<"POST">>, Path, Headers#{
+ <<"content-length">> => integer_to_binary(NumChunks * byte_size(BodyChunk))
+ }),
+ do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks - 1, BodyChunk),
+ {response, IsFin, 200, _RespHeaders} = gun:await(ConnPid, StreamRef, infinity),
+ {ok, _} = case IsFin of
+ nofin -> gun:await_body(ConnPid, StreamRef, infinity);
+ fin -> {ok, <<>>}
+ end,
+ Parent ! {What, done},
+ gun:close(ConnPid).
+
+do_bench_post_one_large_run(ConnPid, StreamRef, 0, BodyChunk) ->
+ gun:data(ConnPid, StreamRef, fin, BodyChunk);
+do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks, BodyChunk) ->
+ gun:data(ConnPid, StreamRef, nofin, BodyChunk),
+ do_bench_post_one_large_run(ConnPid, StreamRef, NumChunks - 1, BodyChunk).
+
+do_bench_wait(What, Clients) ->
+ _ = [receive {What, ready} -> ok end || _ <- Clients],
+ _ = [ClientPid ! {What, go} || ClientPid <- Clients],
+ _ = [receive {What, done} -> ok end || _ <- Clients],
+ ok.
+
+do_log(Str, Args) ->
+ ct:log(Str, Args),
+ io:format(ct_default_gl, Str ++ "~n", Args).
diff --git a/test/loop_handler_SUITE.erl b/test/loop_handler_SUITE.erl
index c5daaf8..71aa801 100644
--- a/test/loop_handler_SUITE.erl
+++ b/test/loop_handler_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/metrics_SUITE.erl b/test/metrics_SUITE.erl
index 6a272f2..784bec1 100644
--- a/test/metrics_SUITE.erl
+++ b/test/metrics_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/misc_SUITE.erl b/test/misc_SUITE.erl
index c918321..e834156 100644
--- a/test/misc_SUITE.erl
+++ b/test/misc_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/plain_handler_SUITE.erl b/test/plain_handler_SUITE.erl
index 756c0a6..7684e6b 100644
--- a/test/plain_handler_SUITE.erl
+++ b/test/plain_handler_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -45,6 +45,7 @@ end_per_group(Name, _) ->
init_dispatch(_) ->
cowboy_router:compile([{"localhost", [
+ {"/crash/external_exit", crash_h, external_exit},
{"/crash/no_reply", crash_h, no_reply},
{"/crash/reply", crash_h, reply}
]}]).
@@ -78,3 +79,13 @@ crash_before_reply(Config) ->
]),
{response, fin, 500, _} = gun:await(ConnPid, Ref),
gun:close(ConnPid).
+
+external_exit_before_reply(Config) ->
+ doc("A plain handler exits externally before a response was sent "
+ "results in a 500 response."),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/crash/external_exit", [
+ {<<"accept-encoding">>, <<"gzip">>}
+ ]),
+ {response, fin, 500, _} = gun:await(ConnPid, Ref),
+ gun:close(ConnPid).
diff --git a/test/proxy_header_SUITE.erl b/test/proxy_header_SUITE.erl
index cb6ab47..c8f63a3 100644
--- a/test/proxy_header_SUITE.erl
+++ b/test/proxy_header_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl
index 9036cac..9adc6e4 100644
--- a/test/req_SUITE.erl
+++ b/test/req_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -324,7 +324,7 @@ filter_then_parse_cookies(Config) ->
[{<<"cookie">>, "bad name=strawberry"}], Config),
<<"[{<<\"cake\">>,<<\"strawberry\">>}]">>
= do_get_body("/filter_then_parse_cookies",
- [{<<"cookie">>, "bad name=strawberry; cake=strawberry"}], Config),
+ [{<<"cookie">>, "bad name=strawberry; another bad name=strawberry; cake=strawberry"}], Config),
<<"[]">>
= do_get_body("/filter_then_parse_cookies",
[{<<"cookie">>, "Blocked by http://www.example.com/upgrade-to-remove"}], Config),
@@ -858,11 +858,16 @@ set_resp_header(Config) ->
set_resp_headers(Config) ->
doc("Response using set_resp_headers."),
- {200, Headers, <<"OK">>} = do_get("/resp/set_resp_headers", Config),
- true = lists:keymember(<<"content-type">>, 1, Headers),
- true = lists:keymember(<<"content-encoding">>, 1, Headers),
+ {200, Headers1, <<"OK">>} = do_get("/resp/set_resp_headers", Config),
+ true = lists:keymember(<<"content-type">>, 1, Headers1),
+ true = lists:keymember(<<"content-encoding">>, 1, Headers1),
+ {200, Headers2, <<"OK">>} = do_get("/resp/set_resp_headers_list", Config),
+ true = lists:keymember(<<"content-type">>, 1, Headers2),
+ true = lists:keymember(<<"content-encoding">>, 1, Headers2),
+ {_, <<"one, two">>} = lists:keyfind(<<"test-header">>, 1, Headers2),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)),
+ {500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_list_cookie", Config)),
ok.
resp_header(Config) ->
diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl
index 6c1f1c1..a3d9533 100644
--- a/test/rest_handler_SUITE.erl
+++ b/test/rest_handler_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -404,6 +404,18 @@ content_types_accepted_wildcard_param_content_type_with_param(Config) ->
{response, fin, 204, _} = gun:await(ConnPid, Ref),
ok.
+content_types_provided_invalid_type(Config) ->
+ doc("When an invalid type is returned from the "
+ "content_types_provided callback, the "
+ "resource is incorrect and a 500 response is expected."),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/content_types_provided?invalid-type", [
+ {<<"accept">>, <<"*/*">>},
+ {<<"accept-encoding">>, <<"gzip">>}
+ ]),
+ {response, _, 500, _} = do_maybe_h3_error(gun:await(ConnPid, Ref)),
+ ok.
+
content_types_provided_wildcard_param_no_accept_param(Config) ->
doc("When a wildcard is returned for parameters from the "
"content_types_provided callback, an accept header "
@@ -805,6 +817,7 @@ provide_callback(Config) ->
]),
{response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
{_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+ {_, <<"HEAD, GET, OPTIONS">>} = lists:keyfind(<<"allow">>, 1, Headers),
{ok, <<"This is REST!">>} = gun:await_body(ConnPid, Ref),
ok.
diff --git a/test/rfc6585_SUITE.erl b/test/rfc6585_SUITE.erl
index 17cbb07..4a627e5 100644
--- a/test/rfc6585_SUITE.erl
+++ b/test/rfc6585_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc7230_SUITE.erl b/test/rfc7230_SUITE.erl
index 17d1905..d0da0df 100644
--- a/test/rfc7230_SUITE.erl
+++ b/test/rfc7230_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc7231_SUITE.erl b/test/rfc7231_SUITE.erl
index 4475899..183fa0f 100644
--- a/test/rfc7231_SUITE.erl
+++ b/test/rfc7231_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc7538_SUITE.erl b/test/rfc7538_SUITE.erl
index c46d388..ea51209 100644
--- a/test/rfc7538_SUITE.erl
+++ b/test/rfc7538_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index f040601..76aa95f 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,8 +35,9 @@ all() -> [{group, clear}, {group, tls}].
groups() ->
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],
+ RejectTLS = [http_upgrade_reject_tls, prior_knowledge_reject_tls],
+ Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- RejectTLS,
+ TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ RejectTLS,
[{clear, [parallel], Clear}, {tls, [parallel], TLS}].
init_per_group(Name = clear, Config) ->
@@ -68,6 +69,24 @@ init_routes(_) -> [
%% Starting HTTP/2 for "http" URIs.
+http_upgrade_reject_tls(Config) ->
+ doc("Implementations that support HTTP/2 over TLS must use ALPN. (RFC7540 3.4)"),
+ TlsOpts = ct_helper:get_certs_from_ets(),
+ {ok, Socket} = ssl:connect("localhost", config(port, Config),
+ [binary, {active, false}|TlsOpts]),
+ %% Send a valid preface.
+ ok = ssl:send(Socket, [
+ "GET / HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade, HTTP2-Settings\r\n"
+ "Upgrade: h2c\r\n"
+ "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n",
+ "\r\n"]),
+ %% We expect the server to send an HTTP 400 error
+ %% when trying to use HTTP/2 without going through ALPN negotiation.
+ {ok, <<"HTTP/1.1 400">>} = ssl:recv(Socket, 12, 1000),
+ ok.
+
http_upgrade_ignore_h2(Config) ->
doc("An h2 token in an Upgrade field must be ignored. (RFC7540 3.2)"),
{ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]),
diff --git a/test/rfc8297_SUITE.erl b/test/rfc8297_SUITE.erl
index c6c1c9d..42ae92e 100644
--- a/test/rfc8297_SUITE.erl
+++ b/test/rfc8297_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc8441_SUITE.erl b/test/rfc8441_SUITE.erl
index 3e71667..b788f9f 100644
--- a/test/rfc8441_SUITE.erl
+++ b/test/rfc8441_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc9114_SUITE.erl b/test/rfc9114_SUITE.erl
index 4a36ee1..a03b493 100644
--- a/test/rfc9114_SUITE.erl
+++ b/test/rfc9114_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2023-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc9204_SUITE.erl b/test/rfc9204_SUITE.erl
index e8defd2..942c41b 100644
--- a/test/rfc9204_SUITE.erl
+++ b/test/rfc9204_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/rfc9220_SUITE.erl b/test/rfc9220_SUITE.erl
index 7f447ed..38a59b2 100644
--- a/test/rfc9220_SUITE.erl
+++ b/test/rfc9220_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -426,7 +426,7 @@ reject_upgrade_header(Config) ->
% Examples.
accept_handshake_when_enabled(Config) ->
- doc("Confirm the example for Websocket over HTTP/2 works. (RFC9220, RFC8441 5.1)"),
+ doc("Confirm the example for Websocket over HTTP/3 works. (RFC9220, RFC8441 5.1)"),
%% Connect to server and confirm that SETTINGS_ENABLE_CONNECT_PROTOCOL = 1.
#{conn := Conn, settings := Settings} = rfc9114_SUITE:do_connect(Config),
#{enable_connect_protocol := true} = Settings,
diff --git a/test/security_SUITE.erl b/test/security_SUITE.erl
index 944c491..25d5280 100644
--- a/test/security_SUITE.erl
+++ b/test/security_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/static_handler_SUITE.erl b/test/static_handler_SUITE.erl
index 9620f66..6721b48 100644
--- a/test/static_handler_SUITE.erl
+++ b/test/static_handler_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -230,7 +230,7 @@ execute(Req=#{path := Path}, Env) ->
<<"/bad/dir/route">> -> ct_helper:ignore(cowboy_static, escape_reserved, 1);
<<"/bad">> -> ct_helper:ignore(cowboy_static, init_opts, 2);
<<"/bad/options">> -> ct_helper:ignore(cowboy_static, content_types_provided, 2);
- <<"/bad/options/mime">> -> ct_helper:ignore(cowboy_rest, set_content_type, 2);
+ <<"/bad/options/mime">> -> ct_helper:ignore(cowboy_rest, normalize_content_types, 2);
<<"/bad/options/etag">> -> ct_helper:ignore(cowboy_static, generate_etag, 2);
<<"/bad/options/charset">> -> ct_helper:ignore(cowboy_static, charsets_provided, 2);
_ -> ok
diff --git a/test/stream_handler_SUITE.erl b/test/stream_handler_SUITE.erl
index f8e2200..90229c0 100644
--- a/test/stream_handler_SUITE.erl
+++ b/test/stream_handler_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/sys_SUITE.erl b/test/sys_SUITE.erl
index 2feb716..3591490 100644
--- a/test/sys_SUITE.erl
+++ b/test/sys_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/tracer_SUITE.erl b/test/tracer_SUITE.erl
index af1f8f3..4298b44 100644
--- a/test/tracer_SUITE.erl
+++ b/test/tracer_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2017-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl
index 3b74339..6fa4e61 100644
--- a/test/ws_SUITE.erl
+++ b/test/ws_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -203,6 +203,25 @@ do_ws_version(Socket) ->
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
+ws_deflate_max_frame_size_close(Config) ->
+ doc("Server closes connection when decompressed frame size exceeds max_frame_size option"),
+ %% max_frame_size is set to 8 bytes in ws_max_frame_size.
+ {ok, Socket, Headers} = do_handshake("/ws_max_frame_size",
+ "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config),
+ {_, "permessage-deflate"} = lists:keyfind("sec-websocket-extensions", 1, Headers),
+ Mask = 16#11223344,
+ Z = zlib:open(),
+ zlib:deflateInit(Z, best_compression, deflated, -15, 8, default),
+ CompressedData0 = iolist_to_binary(zlib:deflate(Z, <<0:800>>, sync)),
+ CompressedData = binary:part(CompressedData0, 0, byte_size(CompressedData0) - 4),
+ MaskedData = do_mask(CompressedData, Mask, <<>>),
+ Len = byte_size(MaskedData),
+ true = Len < 8,
+ ok = gen_tcp:send(Socket, << 1:1, 1:1, 0:2, 1:4, 1:1, Len:7, Mask:32, MaskedData/binary >>),
+ {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1009:16 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
ws_deflate_opts_client_context_takeover(Config) ->
doc("Handler is configured with client context takeover enabled."),
{ok, _, Headers1} = do_handshake("/ws_deflate_opts?client_context_takeover",
@@ -248,6 +267,21 @@ ws_deflate_opts_client_max_window_bits_override(Config) ->
= lists:keyfind("sec-websocket-extensions", 1, Headers2),
ok.
+%% @todo This might be better in an rfc7692_SUITE.
+%%
+%% 7.1.2.2
+%% If a received extension negotiation offer doesn't have the
+%% "client_max_window_bits" extension parameter, the corresponding
+%% extension negotiation response to the offer MUST NOT include the
+%% "client_max_window_bits" extension parameter.
+ws_deflate_opts_client_max_window_bits_only_in_server(Config) ->
+ doc("Handler is configured with non-default client max window bits but "
+ "client doesn't send the parameter; compression is disabled."),
+ {ok, _, Headers} = do_handshake("/ws_deflate_opts?client_max_window_bits",
+ "Sec-WebSocket-Extensions: permessage-deflate\r\n", Config),
+ false = lists:keyfind("sec-websocket-extensions", 1, Headers),
+ ok.
+
ws_deflate_opts_server_context_takeover(Config) ->
doc("Handler is configured with server context takeover enabled."),
{ok, _, Headers1} = do_handshake("/ws_deflate_opts?server_context_takeover",
diff --git a/test/ws_SUITE_data/ws_max_frame_size.erl b/test/ws_SUITE_data/ws_max_frame_size.erl
index 3d81497..76df0b0 100644
--- a/test/ws_SUITE_data/ws_max_frame_size.erl
+++ b/test/ws_SUITE_data/ws_max_frame_size.erl
@@ -5,7 +5,7 @@
-export([websocket_info/2]).
init(Req, State) ->
- {cowboy_websocket, Req, State, #{max_frame_size => 8}}.
+ {cowboy_websocket, Req, State, #{max_frame_size => 8, compress => true}}.
websocket_handle({text, Data}, State) ->
{[{text, Data}], State};
diff --git a/test/ws_autobahn_SUITE.erl b/test/ws_autobahn_SUITE.erl
index 0e12300..58d15fa 100644
--- a/test/ws_autobahn_SUITE.erl
+++ b/test/ws_autobahn_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/test/ws_handler_SUITE.erl b/test/ws_handler_SUITE.erl
index ab1ffc8..ab9dbe2 100644
--- a/test/ws_handler_SUITE.erl
+++ b/test/ws_handler_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2018-2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -296,6 +296,41 @@ websocket_set_options_idle_timeout(Config) ->
error(timeout)
end.
+websocket_set_options_max_frame_size(Config) ->
+ doc("The max_frame_size option can be modified using the "
+ "command {set_options, Opts} at runtime."),
+ ConnPid = gun_open(Config),
+ StreamRef = gun:ws_upgrade(ConnPid, "/set_options"),
+ receive
+ {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
+ ok;
+ {gun_response, ConnPid, _, _, Status, Headers} ->
+ exit({ws_upgrade_failed, Status, Headers});
+ {gun_error, ConnPid, StreamRef, Reason} ->
+ exit({ws_upgrade_failed, Reason})
+ after 1000 ->
+ error(timeout)
+ end,
+ %% We first send a 1MB frame to confirm that yes, we can
+ %% send a frame that large. The default max_frame_size is infinity.
+ gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000000>>}),
+ {ws, {binary, <<0:8000000>>}} = gun:await(ConnPid, StreamRef),
+ %% Trigger the change in max_frame_size. From now on we will
+ %% only allow frames of up to 1000 bytes.
+ gun:ws_send(ConnPid, StreamRef, {text, <<"max_frame_size_small">>}),
+ %% Confirm that we can send frames of up to 1000 bytes.
+ gun:ws_send(ConnPid, StreamRef, {binary, <<0:8000>>}),
+ {ws, {binary, <<0:8000>>}} = gun:await(ConnPid, StreamRef),
+ %% Confirm that sending frames larger than 1000 bytes
+ %% results in the closing of the connection.
+ gun:ws_send(ConnPid, StreamRef, {binary, <<0:8008>>}),
+ receive
+ {gun_down, ConnPid, _, _, _} ->
+ ok
+ after 2000 ->
+ error(timeout)
+ end.
+
websocket_shutdown_reason(Config) ->
doc("The command {shutdown_reason, any()} can be used to "
"change the shutdown reason of a Websocket connection."),
diff --git a/test/ws_perf_SUITE.erl b/test/ws_perf_SUITE.erl
index 19d1c31..ff88554 100644
--- a/test/ws_perf_SUITE.erl
+++ b/test/ws_perf_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2024, Loïc Hoguin <[email protected]>
+%% Copyright (c) Loïc Hoguin <[email protected]>
%%
%% 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,7 @@ all() ->
groups() ->
CommonGroups = cowboy_test:common_groups(ct_helper:all(?MODULE), no_parallel),
SubGroups = [G || G = {GN, _, _} <- CommonGroups,
- GN =:= http orelse GN =:= h2c],
+ GN =:= http orelse GN =:= h2c orelse GN =:= http_compress orelse GN =:= h2c_compress],
[
{binary, [], SubGroups},
{ascii, [], SubGroups},
@@ -45,24 +45,25 @@ init_per_suite(Config) ->
end_per_suite(_Config) ->
ok.
-init_per_group(Name=http, Config) ->
- ct:pal("Websocket over cleartext HTTP/1.1 (~s)",
- [init_data_info(Config)]),
- cowboy_test:init_http(Name, #{
- env => #{dispatch => init_dispatch(Config)}
- }, [{flavor, vanilla}|Config]);
-init_per_group(Name=h2c, Config) ->
- ct:pal("Websocket over cleartext HTTP/2 (~s)",
- [init_data_info(Config)]),
- Config1 = cowboy_test:init_http(Name, #{
+init_per_group(Name, Config) when Name =:= http; Name =:= http_compress ->
+ init_info(Name, Config),
+ cowboy_test:init_common_groups(Name, Config, ?MODULE);
+init_per_group(Name, Config) when Name =:= h2c; Name =:= h2c_compress ->
+ init_info(Name, Config),
+ {Flavor, Opts} = case Name of
+ h2c -> {vanilla, #{}};
+ h2c_compress -> {compress, #{stream_handlers => [cowboy_compress_h, cowboy_stream_h]}}
+ end,
+ Config1 = cowboy_test:init_http(Name, Opts#{
connection_window_margin_size => 64*1024,
enable_connect_protocol => true,
env => #{dispatch => init_dispatch(Config)},
max_frame_size_sent => 64*1024,
max_frame_size_received => 16384 * 1024 - 1,
+ max_received_frame_rate => {10_000_000, 1},
stream_window_data_threshold => 1024,
stream_window_margin_size => 64*1024
- }, [{flavor, vanilla}|Config]),
+ }, [{flavor, Flavor}|Config]),
lists:keyreplace(protocol, 1, Config1, {protocol, http2});
init_per_group(ascii, Config) ->
init_text_data("ascii.txt", Config);
@@ -73,11 +74,18 @@ init_per_group(japanese, Config) ->
init_per_group(binary, Config) ->
[{frame_type, binary}|Config].
-init_data_info(Config) ->
- case config(frame_type, Config) of
+init_info(Name, Config) ->
+ DataInfo = case config(frame_type, Config) of
text -> config(text_data_filename, Config);
binary -> binary
- end.
+ end,
+ ConnInfo = case Name of
+ http -> "cleartext HTTP/1.1";
+ http_compress -> "cleartext HTTP/1.1 with compression";
+ h2c -> "cleartext HTTP/2";
+ h2c_compress -> "cleartext HTTP/2 with compression"
+ end,
+ ct:pal("Websocket over ~s (~s)", [ConnInfo, DataInfo]).
init_text_data(Filename, Config) ->
{ok, Text} = file:read_file(filename:join(config(data_dir, Config), Filename)),
@@ -95,15 +103,15 @@ end_per_group(Name, _Config) ->
init_dispatch(_Config) ->
cowboy_router:compile([
{"localhost", [
- {"/ws_echo", ws_echo, []}
+ {"/ws_echo", ws_echo, []},
+ {"/ws_ignore", ws_ignore, []}
]}
]).
%% Support functions for testing using Gun.
-do_gun_open_ws(Config) ->
+do_gun_open_ws(Path, Config) ->
ConnPid = gun_open(Config, #{
- tcp_opts => [{nodelay, true}],
http2_opts => #{
connection_window_margin_size => 64*1024,
max_frame_size_sent => 64*1024,
@@ -111,7 +119,9 @@ do_gun_open_ws(Config) ->
notify_settings_changed => true,
stream_window_data_threshold => 1024,
stream_window_margin_size => 64*1024
- }
+ },
+ tcp_opts => [{nodelay, true}],
+ ws_opts => #{compress => config(flavor, Config) =:= compress}
}),
case config(protocol, Config) of
http -> ok;
@@ -119,7 +129,7 @@ do_gun_open_ws(Config) ->
{notify, settings_changed, #{enable_connect_protocol := true}}
= gun:await(ConnPid, undefined) %% @todo Maybe have a gun:await/1?
end,
- StreamRef = gun:ws_upgrade(ConnPid, "/ws_echo"),
+ StreamRef = gun:ws_upgrade(ConnPid, Path),
receive
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
{ok, ConnPid, StreamRef};
@@ -141,72 +151,140 @@ receive_ws(ConnPid, StreamRef) ->
%% Tests.
-one_00064KiB(Config) ->
+echo_1_00064KiB(Config) ->
doc("Send and receive a 64KiB frame."),
- do_full(Config, one, 1, 64 * 1024).
+ do_echo(Config, echo_1, 1, 64 * 1024).
-one_00256KiB(Config) ->
+echo_1_00256KiB(Config) ->
doc("Send and receive a 256KiB frame."),
- do_full(Config, one, 1, 256 * 1024).
+ do_echo(Config, echo_1, 1, 256 * 1024).
-one_01024KiB(Config) ->
+echo_1_01024KiB(Config) ->
doc("Send and receive a 1024KiB frame."),
- do_full(Config, one, 1, 1024 * 1024).
+ do_echo(Config, echo_1, 1, 1024 * 1024).
-one_04096KiB(Config) ->
+echo_1_04096KiB(Config) ->
doc("Send and receive a 4096KiB frame."),
- do_full(Config, one, 1, 4096 * 1024).
+ do_echo(Config, echo_1, 1, 4096 * 1024).
%% Minus one because frames can only get so big.
-one_16384KiB(Config) ->
+echo_1_16384KiB(Config) ->
doc("Send and receive a 16384KiB - 1 frame."),
- do_full(Config, one, 1, 16384 * 1024 - 1).
+ do_echo(Config, echo_1, 1, 16384 * 1024 - 1).
-repeat_00000B(Config) ->
+echo_N_00000B(Config) ->
doc("Send and receive a 0B frame 1000 times."),
- do_full(Config, repeat, 1000, 0).
+ do_echo(Config, echo_N, 1000, 0).
-repeat_00256B(Config) ->
+echo_N_00256B(Config) ->
doc("Send and receive a 256B frame 1000 times."),
- do_full(Config, repeat, 1000, 256).
+ do_echo(Config, echo_N, 1000, 256).
-repeat_01024B(Config) ->
+echo_N_01024B(Config) ->
doc("Send and receive a 1024B frame 1000 times."),
- do_full(Config, repeat, 1000, 1024).
+ do_echo(Config, echo_N, 1000, 1024).
-repeat_04096B(Config) ->
+echo_N_04096B(Config) ->
doc("Send and receive a 4096B frame 1000 times."),
- do_full(Config, repeat, 1000, 4096).
+ do_echo(Config, echo_N, 1000, 4096).
-repeat_16384B(Config) ->
+echo_N_16384B(Config) ->
doc("Send and receive a 16384B frame 1000 times."),
- do_full(Config, repeat, 1000, 16384).
+ do_echo(Config, echo_N, 1000, 16384).
-%repeat_16384B_10K(Config) ->
+%echo_N_16384B_10K(Config) ->
% doc("Send and receive a 16384B frame 10000 times."),
-% do_full(Config, repeat, 10000, 16384).
+% do_echo(Config, echo_N, 10000, 16384).
-do_full(Config, What, Num, FrameSize) ->
- {ok, ConnPid, StreamRef} = do_gun_open_ws(Config),
+do_echo(Config, What, Num, FrameSize) ->
+ {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_echo", Config),
FrameType = config(frame_type, Config),
FrameData = case FrameType of
text -> do_text_data(Config, FrameSize);
binary -> rand:bytes(FrameSize)
end,
%% Heat up the processes before doing the real run.
-% do_full1(ConnPid, StreamRef, Num, FrameType, FrameData),
- {Time, _} = timer:tc(?MODULE, do_full1, [ConnPid, StreamRef, Num, FrameType, FrameData]),
+% do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData),
+ {Time, _} = timer:tc(?MODULE, do_echo_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]),
do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]),
gun:ws_send(ConnPid, StreamRef, close),
{ok, close} = receive_ws(ConnPid, StreamRef),
gun_down(ConnPid).
-do_full1(_, _, 0, _, _) ->
+do_echo_loop(_, _, 0, _, _) ->
ok;
-do_full1(ConnPid, StreamRef, Num, FrameType, FrameData) ->
+do_echo_loop(ConnPid, StreamRef, Num, FrameType, FrameData) ->
gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}),
{ok, {FrameType, FrameData}} = receive_ws(ConnPid, StreamRef),
- do_full1(ConnPid, StreamRef, Num - 1, FrameType, FrameData).
+ do_echo_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData).
+
+send_1_00064KiB(Config) ->
+ doc("Send a 64KiB frame."),
+ do_send(Config, send_1, 1, 64 * 1024).
+
+send_1_00256KiB(Config) ->
+ doc("Send a 256KiB frame."),
+ do_send(Config, send_1, 1, 256 * 1024).
+
+send_1_01024KiB(Config) ->
+ doc("Send a 1024KiB frame."),
+ do_send(Config, send_1, 1, 1024 * 1024).
+
+send_1_04096KiB(Config) ->
+ doc("Send a 4096KiB frame."),
+ do_send(Config, send_1, 1, 4096 * 1024).
+
+%% Minus one because frames can only get so big.
+send_1_16384KiB(Config) ->
+ doc("Send a 16384KiB - 1 frame."),
+ do_send(Config, send_1, 1, 16384 * 1024 - 1).
+
+send_N_00000B(Config) ->
+ doc("Send a 0B frame 10000 times."),
+ do_send(Config, send_N, 10000, 0).
+
+send_N_00256B(Config) ->
+ doc("Send a 256B frame 10000 times."),
+ do_send(Config, send_N, 10000, 256).
+
+send_N_01024B(Config) ->
+ doc("Send a 1024B frame 10000 times."),
+ do_send(Config, send_N, 10000, 1024).
+
+send_N_04096B(Config) ->
+ doc("Send a 4096B frame 10000 times."),
+ do_send(Config, send_N, 10000, 4096).
+
+send_N_16384B(Config) ->
+ doc("Send a 16384B frame 10000 times."),
+ do_send(Config, send_N, 10000, 16384).
+
+%send_N_16384B_10K(Config) ->
+% doc("Send and receive a 16384B frame 10000 times."),
+% do_send(Config, send_N, 10000, 16384).
+
+do_send(Config, What, Num, FrameSize) ->
+ {ok, ConnPid, StreamRef} = do_gun_open_ws("/ws_ignore", Config),
+ FrameType = config(frame_type, Config),
+ FrameData = case FrameType of
+ text -> do_text_data(Config, FrameSize);
+ binary -> rand:bytes(FrameSize)
+ end,
+ %% Heat up the processes before doing the real run.
+% do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData),
+ {Time, _} = timer:tc(?MODULE, do_send_loop, [ConnPid, StreamRef, Num, FrameType, FrameData]),
+ do_log("~-6s ~-6s ~6s: ~8bµs", [What, FrameType, do_format_size(FrameSize), Time]),
+ gun:ws_send(ConnPid, StreamRef, close),
+ {ok, close} = receive_ws(ConnPid, StreamRef),
+ gun_down(ConnPid).
+
+do_send_loop(ConnPid, StreamRef, 0, _, _) ->
+ gun:ws_send(ConnPid, StreamRef, {text, <<"CHECK">>}),
+ {ok, {text, <<"CHECK">>}} = receive_ws(ConnPid, StreamRef),
+ ok;
+do_send_loop(ConnPid, StreamRef, Num, FrameType, FrameData) ->
+ gun:ws_send(ConnPid, StreamRef, {FrameType, FrameData}),
+ do_send_loop(ConnPid, StreamRef, Num - 1, FrameType, FrameData).
%% Internal.