diff --git a/cowboy/CONTRIBUTING.asciidoc b/cowboy/CONTRIBUTING.asciidoc new file mode 100644 index 0000000..7a3bc84 --- /dev/null +++ b/cowboy/CONTRIBUTING.asciidoc @@ -0,0 +1,334 @@ += Contributing + +This document is a guide on how to best contribute to this project. + +== Definitions + +*SHOULD* describes optional steps. *MUST* describes mandatory steps. + +*SHOULD NOT* and *MUST NOT* describes pitfalls to avoid. + +_Your local copy_ refers to the copy of the repository that you have +on your computer. _origin_ refers to your fork of the project. _upstream_ +refers to the official repository for this project. + +== Discussions + +For general discussion about this project, please open a ticket. +Feedback is always welcome and may transform in tasks to improve +the project, so having the discussion start there is a plus. + +Alternatively you may try the #ninenines IRC channel on Freenode, +or, if you need the discussion to stay private, you can send an +email at contact@ninenines.eu. + +== Support + +Free support is generally not available. The rule is that free +support is only given if doing so benefits most users. In practice +this means that free support will only be given if the issues are +due to a fault in the project itself or its documentation. + +Paid support is available for all price ranges. Please send an +email to contact@ninenines.eu for more information. + +== Bug reports + +You *SHOULD* open a ticket for every bug you encounter, regardless +of the version you use. A ticket not only helps the project ensure +that bugs are squashed, it also helps other users who later run +into this issue. You *SHOULD* give as much information as possible +including what commit/branch, what OS/version and so on. + +You *SHOULD NOT* open a ticket if another already exists for the +same issue. You *SHOULD* instead either add more information by +commenting on it, or simply comment to inform the maintainer that +you are also affected. The maintainer *SHOULD* reply to every +new ticket when they are opened. If the maintainer didn't say +anything after a few days, you *SHOULD* write a new comment asking +for more information. + +You *SHOULD* provide a reproducible test case, either in the +ticket or by sending a pull request and updating the test suite. + +When you have a fix ready, you *SHOULD* open a pull request, +even if the code does not fit the requirements discussed below. +Providing a fix, even a dirty one, can help other users and/or +at least get the maintainer on the right tracks. + +You *SHOULD* try to relax and be patient. Some tickets are merged +or fixed quickly, others aren't. There's no real rules around that. +You can become a paying customer if you need something fast. + +== Security reports + +You *SHOULD* open a ticket when you identify a DoS vulnerability +in this project. You *SHOULD* include the resources needed to +DoS the project; every project can be brought down if you have +the necessary resources. + +You *SHOULD* send an email to contact@ninenines.eu when you +identify a security vulnerability. If the vulnerability originates +from code inside Erlang/OTP itself, you *SHOULD* also consult +with OTP Team directly to get the problem fixed upstream. + +== Feature requests + +Feature requests are always welcome. To be accepted, however, they +must be well defined, make sense in the context of the project and +benefit most users. + +Feature requests not benefiting most users may only be accepted +when accompanied with a proper pull request. + +You *MUST* open a ticket to explain what the new feature is, even +if you are going to submit a pull request for it. + +All these conditions are meant to ensure that the project stays +lightweight and maintainable. + +== Documentation submissions + +You *SHOULD* follow the code submission guidelines to submit +documentation. + +The documentation is available in the 'doc/src/' directory. There +are three kinds of documentation: manual, guide and tutorials. The +format for the documentation is Asciidoc. + +You *SHOULD* follow the same style as the surrounding documentation +when editing existing files. + +You *MUST* include the source when providing media. + +== Examples submissions + +You *SHOULD* follow the code submission guidelines to submit examples. + +The examples are available in the 'examples/' directory. + +You *SHOULD* focus on exactly one thing per example. + +== Code submissions + +You *SHOULD* open a pull request to submit code. + +You *SHOULD* open a ticket to discuss backward incompatible changes +before you submit code. This step ensures that you do not work on +a large change that will then be rejected. + +You *SHOULD* send your code submission using a pull request on GitHub. +If you can't, please send an email to contact@ninenines.eu with your +patch. + +The following sections explain the normal GitHub workflow. + +=== Cloning + +You *MUST* fork the project's repository on GitHub by clicking on the +_Fork_ button. + +On the right page of your fork's page is a field named _SSH clone URL_. +Its contents will be identified as `$ORIGIN_URL` in the following snippet. + +On the right side of the project's repository page is a similar field. +Its contents will be identified as `$UPSTREAM_URL`. + +Finally, `$PROJECT` is the name of this project. + +To setup your clone and be able to rebase when requested, run the +following commands: + +[source,bash] +$ git clone $ORIGIN_URL +$ cd $PROJECT +$ git remote add upstream $UPSTREAM_URL + +=== Branching + +You *SHOULD* base your branch on _master_, unless your patch applies +to a stable release, in which case you need to base your branch on +the stable branch, for example _1.0.x_. + +The first step is therefore to checkout the branch in question: + +[source,bash] +$ git checkout 1.0.x + +The next step is to update the branch to the current version from +_upstream_. In the following snippet, replace _1.0.x_ by _master_ +if you are patching _master_. + +[source,bash] +$ git fetch upstream +$ git rebase upstream/1.0.x + +This last command may fail and ask you to stash your changes. When +that happens, run the following sequence of commands: + +[source,bash] +$ git stash +$ git rebase upstream/1.0.x +$ git stash pop + +The final step is to create a new branch you can work in. The name +of the new branch is up to you, there is no particular requirement. +Replace `$BRANCH` with the branch name you came up with: + +[source,bash] +$ git checkout -b $BRANCH + +_Your local copy_ is now ready. + +=== Source editing + +There are very few rules with regard to source code editing. + +You *MUST* use horizontal tabs for indentation. Use one tab +per indentation level. + +You *MUST NOT* align code. You can only add or remove one +indentation level compared to the previous line. + +You *SHOULD NOT* write lines more than about a hundred +characters. There is no hard limit, just try to keep it +as readable as possible. + +You *SHOULD* write small functions when possible. + +You *SHOULD* avoid a too big hierarchy of case clauses inside +a single function. + +You *SHOULD* add tests to make sure your code works. + +=== Committing + +You *SHOULD* run Dialyzer and the test suite while working on +your patch, and you *SHOULD* ensure that no additional tests +fail when you finish. + +You can use the following command to run Dialyzer: + +[source,bash] +$ make dialyze + +You have two options to run tests. You can either run tests +across all supported Erlang versions, or just on the version +you are currently using. + +To test across all supported Erlang versions: + +[source,bash] +$ make -k ci + +To test using the current version: + +[source,bash] +$ make tests + +You can then open Common Test logs in 'logs/all_runs.html'. + +By default Cowboy excludes a few test suites that take too +long to complete. For example all the examples are built and +tested, and one Websocket test suite is very extensive. In +order to run everything, do: + +[source,bash] +$ make tests FULL=1 + +Once all tests pass (or at least, no new tests are failing), +you can commit your changes. + +First you need to add your changes: + +[source,bash] +$ git add src/file_you_edited.erl + +If you want an interactive session, allowing you to filter +out changes that have nothing to do with this commit: + +[source,bash] +$ git add -p + +You *MUST* put all related changes inside a single commit. The +general rule is that all commits must pass tests. Fix one bug +per commit. Add one feature per commit. Separate features in +multiple commits only if smaller parts of the feature make +sense on their own. + +Finally once all changes are added you can commit. This +command will open the editor of your choice where you can +put a proper commit title and message. + +[source,bash] +$ git commit + +Do not use the `-m` option as it makes it easy to break the +following rules: + +You *MUST* write a proper commit title and message. The commit +title is the first line and *MUST* be at most 72 characters. +The second line *MUST* be left blank. Everything after that is +the commit message. You *SHOULD* write a detailed commit +message. The lines of the message *MUST* be at most 80 characters. +You *SHOULD* explain what the commit does, what references you +used and any other information that helps understanding why +this commit exists. You *MUST NOT* include commands to close +GitHub tickets automatically. + +=== Cleaning the commit history + +If you create a new commit every time you make a change, however +insignificant, you *MUST* consolidate those commits before +sending the pull request. + +This is done through _rebasing_. The easiest way to do so is +to use interactive rebasing, which allows you to choose which +commits to keep, squash, edit and so on. To rebase, you need +to give the original commit before you made your changes. If +you only did two changes, you can use the shortcut form `HEAD^^`: + +[source,bash] +$ git rebase -i HEAD^^ + +=== Submitting the pull request + +You *MUST* push your branch to your fork on GitHub. Replace +`$BRANCH` with your branch name: + +[source,bash] +$ git push origin $BRANCH + +You can then submit the pull request using the GitHub interface. +You *SHOULD* provide an explanatory message and refer to any +previous ticket related to this patch. You *MUST NOT* include +commands to close other tickets automatically. + +=== Updating the pull request + +Sometimes the maintainer will ask you to change a few things. +Other times you will notice problems with your submission and +want to fix them on your own. + +In either case you do not need to close the pull request. You +can just push your changes again and, if needed, force them. +This will update the pull request automatically. + +[source,bash] +$ git push -f origin $BRANCH + +=== Merging + +This is an open source project maintained by independent developers. +Please be patient when your changes aren't merged immediately. + +All pull requests run through a Continuous Integration service +to ensure nothing gets broken by the changes submitted. + +Bug fixes will be merged immediately when all tests pass. +The maintainer may do style changes in the merge commit if +the submitter is not available. The maintainer *MUST* open +a new ticket if the solution could still be improved. + +New features and backward incompatible changes will be merged +when all tests pass and all other requirements are fulfilled. diff --git a/cowboy/LICENSE b/cowboy/LICENSE new file mode 100644 index 0000000..efeaf45 --- /dev/null +++ b/cowboy/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2011-2024, Loïc Hoguin + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/cowboy/Makefile b/cowboy/Makefile new file mode 100644 index 0000000..160022d --- /dev/null +++ b/cowboy/Makefile @@ -0,0 +1,7 @@ +.PHONY: all clean + +all: + ${MAKE} -C src + +clean: + ${MAKE} -C src clean diff --git a/cowboy/README.asciidoc b/cowboy/README.asciidoc new file mode 100644 index 0000000..02acaa6 --- /dev/null +++ b/cowboy/README.asciidoc @@ -0,0 +1,37 @@ += Cowboy + +Cowboy is a small, fast and modern HTTP server for Erlang/OTP. + +== Goals + +Cowboy aims to provide a *complete* HTTP stack in a *small* code base. +It is optimized for *low latency* and *low memory usage*, in part +because it uses *binary strings*. + +Cowboy provides *routing* capabilities, selectively dispatching requests +to handlers written in Erlang. + +Because it uses Ranch for managing connections, Cowboy can easily be +*embedded* in any other application. + +Cowboy is *clean* and *well tested* Erlang code. + +== Online documentation + +* https://ninenines.eu/docs/en/cowboy/2.12/guide[User guide] +* https://ninenines.eu/docs/en/cowboy/2.12/manual[Function reference] + +== Offline documentation + +* While still online, run `make docs` +* User guide available in `doc/` in PDF and HTML formats +* Function reference man pages available in `doc/man3/` and `doc/man7/` +* Run `make install-docs` to install man pages on your system +* Full documentation in Asciidoc available in `doc/src/` +* Examples available in `examples/` + +== Getting help + +* https://github.com/ninenines/cowboy/issues[Issues tracker] +* https://ninenines.eu/services[Commercial Support] +* https://github.com/sponsors/essen[Sponsor me!] diff --git a/cowboy/ebin/cowboy.app b/cowboy/ebin/cowboy.app new file mode 100644 index 0000000..4050893 --- /dev/null +++ b/cowboy/ebin/cowboy.app @@ -0,0 +1,10 @@ +{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_loop','cowboy_metrics_h','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_tracer_h','cowboy_websocket']}, + {registered, [cowboy_sup,cowboy_clock]}, + {applications, [kernel,stdlib,crypto,cowlib,ranch]}, + {optional_applications, []}, + {mod, {cowboy_app, []}}, + {env, []} +]}. \ No newline at end of file diff --git a/cowboy/src/Makefile b/cowboy/src/Makefile new file mode 100644 index 0000000..0a49aab --- /dev/null +++ b/cowboy/src/Makefile @@ -0,0 +1,26 @@ +.PHONY: all +.SUFFIXES: .erl .beam + +ERLC?= erlc -server +ERLFLAGS+= -I ../include -pa ../../cowlib -pa ../../ranch/src -pa ${PWD} +ERLOPTS+= +warn_missing_spec +warn_untyped_record + +OBJS= cowboy_stream.beam cowboy_middleware.beam cowboy_sub_protocol.beam +OBJS+= cowboy_children.beam cowboy_clear.beam cowboy_clock.beam +OBJS+= cowboy_compress_h.beam cowboy_constraints.beam +OBJS+= cowboy_decompress_h.beam cowboy_handler.beam +OBJS+= cowboy_http.beam cowboy_http2.beam cowboy_loop.beam +OBJS+= cowboy_metrics_h.beam cowboy_bstr.beam +OBJS+= cowboy_req.beam cowboy_rest.beam cowboy_router.beam +OBJS+= cowboy_static.beam cowboy_stream_h.beam +OBJS+= cowboy_sup.beam cowboy_tls.beam cowboy_app.beam +OBJS+= cowboy_tracer_h.beam cowboy_websocket.beam cowboy.beam + +all: ${OBJS} + +.erl.beam: + ${ERLC} ${ERLOPTS} ${ERLFLAGS} $< + +clean: + rm -f *.beam + diff --git a/cowboy/src/cowboy.erl b/cowboy/src/cowboy.erl new file mode 100644 index 0000000..ad45919 --- /dev/null +++ b/cowboy/src/cowboy.erl @@ -0,0 +1,118 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy). + +-export([start_clear/3]). +-export([start_tls/3]). +-export([stop_listener/1]). +-export([get_env/2]). +-export([get_env/3]). +-export([set_env/3]). + +%% Internal. +-export([log/2]). +-export([log/4]). + +-type opts() :: cowboy_http:opts() | cowboy_http2:opts(). +-export_type([opts/0]). + +-type fields() :: [atom() + | {atom(), cowboy_constraints:constraint() | [cowboy_constraints:constraint()]} + | {atom(), cowboy_constraints:constraint() | [cowboy_constraints:constraint()], any()}]. +-export_type([fields/0]). + +-type http_headers() :: #{binary() => iodata()}. +-export_type([http_headers/0]). + +-type http_status() :: non_neg_integer() | binary(). +-export_type([http_status/0]). + +-type http_version() :: 'HTTP/2' | 'HTTP/1.1' | 'HTTP/1.0'. +-export_type([http_version/0]). + +-spec start_clear(ranch:ref(), ranch:opts(), opts()) + -> {ok, pid()} | {error, any()}. +start_clear(Ref, TransOpts0, ProtoOpts0) -> + TransOpts1 = ranch:normalize_opts(TransOpts0), + {TransOpts, ConnectionType} = ensure_connection_type(TransOpts1), + ProtoOpts = ProtoOpts0#{connection_type => ConnectionType}, + ranch:start_listener(Ref, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts). + +-spec start_tls(ranch:ref(), ranch:opts(), opts()) + -> {ok, pid()} | {error, any()}. +start_tls(Ref, TransOpts0, ProtoOpts0) -> + TransOpts1 = ranch:normalize_opts(TransOpts0), + 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}, + ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts). + +ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) -> + {TransOpts, ConnectionType}; +ensure_connection_type(TransOpts) -> + {TransOpts#{connection_type => supervisor}, supervisor}. + +-spec stop_listener(ranch:ref()) -> ok | {error, not_found}. +stop_listener(Ref) -> + ranch:stop_listener(Ref). + +-spec get_env(ranch:ref(), atom()) -> ok. +get_env(Ref, Name) -> + Opts = ranch:get_protocol_options(Ref), + Env = maps:get(env, Opts, #{}), + maps:get(Name, Env). + +-spec get_env(ranch:ref(), atom(), any()) -> ok. +get_env(Ref, Name, Default) -> + Opts = ranch:get_protocol_options(Ref), + Env = maps:get(env, Opts, #{}), + maps:get(Name, Env, Default). + +-spec set_env(ranch:ref(), atom(), any()) -> ok. +set_env(Ref, Name, Value) -> + Opts = ranch:get_protocol_options(Ref), + Env = maps:get(env, Opts, #{}), + Opts2 = maps:put(env, maps:put(Name, Value, Env), Opts), + ok = ranch:set_protocol_options(Ref, Opts2). + +%% Internal. + +-spec log({log, logger:level(), io:format(), list()}, opts()) -> ok. +log({log, Level, Format, Args}, Opts) -> + log(Level, Format, Args, Opts). + +-spec log(logger:level(), io:format(), list(), opts()) -> ok. +log(Level, Format, Args, #{logger := Logger}) + when Logger =/= error_logger -> + _ = Logger:Level(Format, Args), + ok; +%% We use error_logger by default. Because error_logger does +%% not have all the levels we accept we have to do some +%% mapping to error_logger functions. +log(Level, Format, Args, _) -> + Function = case Level of + emergency -> error_msg; + alert -> error_msg; + critical -> error_msg; + error -> error_msg; + warning -> warning_msg; + notice -> warning_msg; + info -> info_msg; + debug -> info_msg + end, + error_logger:Function(Format, Args). diff --git a/cowboy/src/cowboy_app.erl b/cowboy/src/cowboy_app.erl new file mode 100644 index 0000000..95ae564 --- /dev/null +++ b/cowboy/src/cowboy_app.erl @@ -0,0 +1,27 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_app). +-behaviour(application). + +-export([start/2]). +-export([stop/1]). + +-spec start(_, _) -> {ok, pid()}. +start(_, _) -> + cowboy_sup:start_link(). + +-spec stop(_) -> ok. +stop(_) -> + ok. diff --git a/cowboy/src/cowboy_bstr.erl b/cowboy/src/cowboy_bstr.erl new file mode 100644 index 0000000..f23167d --- /dev/null +++ b/cowboy/src/cowboy_bstr.erl @@ -0,0 +1,123 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_bstr). + +%% Binary strings. +-export([capitalize_token/1]). +-export([to_lower/1]). +-export([to_upper/1]). + +%% Characters. +-export([char_to_lower/1]). +-export([char_to_upper/1]). + +%% The first letter and all letters after a dash are capitalized. +%% This is the form seen for header names in the HTTP/1.1 RFC and +%% others. Note that using this form isn't required, as header names +%% are case insensitive, and it is only provided for use with eventual +%% badly implemented clients. +-spec capitalize_token(B) -> B when B::binary(). +capitalize_token(B) -> + capitalize_token(B, true, <<>>). +capitalize_token(<<>>, _, Acc) -> + Acc; +capitalize_token(<< $-, Rest/bits >>, _, Acc) -> + capitalize_token(Rest, true, << Acc/binary, $- >>); +capitalize_token(<< C, Rest/bits >>, true, Acc) -> + capitalize_token(Rest, false, << Acc/binary, (char_to_upper(C)) >>); +capitalize_token(<< C, Rest/bits >>, false, Acc) -> + capitalize_token(Rest, false, << Acc/binary, (char_to_lower(C)) >>). + +-spec to_lower(B) -> B when B::binary(). +to_lower(B) -> + << << (char_to_lower(C)) >> || << C >> <= B >>. + +-spec to_upper(B) -> B when B::binary(). +to_upper(B) -> + << << (char_to_upper(C)) >> || << C >> <= B >>. + +-spec char_to_lower(char()) -> char(). +char_to_lower($A) -> $a; +char_to_lower($B) -> $b; +char_to_lower($C) -> $c; +char_to_lower($D) -> $d; +char_to_lower($E) -> $e; +char_to_lower($F) -> $f; +char_to_lower($G) -> $g; +char_to_lower($H) -> $h; +char_to_lower($I) -> $i; +char_to_lower($J) -> $j; +char_to_lower($K) -> $k; +char_to_lower($L) -> $l; +char_to_lower($M) -> $m; +char_to_lower($N) -> $n; +char_to_lower($O) -> $o; +char_to_lower($P) -> $p; +char_to_lower($Q) -> $q; +char_to_lower($R) -> $r; +char_to_lower($S) -> $s; +char_to_lower($T) -> $t; +char_to_lower($U) -> $u; +char_to_lower($V) -> $v; +char_to_lower($W) -> $w; +char_to_lower($X) -> $x; +char_to_lower($Y) -> $y; +char_to_lower($Z) -> $z; +char_to_lower(Ch) -> Ch. + +-spec char_to_upper(char()) -> char(). +char_to_upper($a) -> $A; +char_to_upper($b) -> $B; +char_to_upper($c) -> $C; +char_to_upper($d) -> $D; +char_to_upper($e) -> $E; +char_to_upper($f) -> $F; +char_to_upper($g) -> $G; +char_to_upper($h) -> $H; +char_to_upper($i) -> $I; +char_to_upper($j) -> $J; +char_to_upper($k) -> $K; +char_to_upper($l) -> $L; +char_to_upper($m) -> $M; +char_to_upper($n) -> $N; +char_to_upper($o) -> $O; +char_to_upper($p) -> $P; +char_to_upper($q) -> $Q; +char_to_upper($r) -> $R; +char_to_upper($s) -> $S; +char_to_upper($t) -> $T; +char_to_upper($u) -> $U; +char_to_upper($v) -> $V; +char_to_upper($w) -> $W; +char_to_upper($x) -> $X; +char_to_upper($y) -> $Y; +char_to_upper($z) -> $Z; +char_to_upper(Ch) -> Ch. + +%% Tests. + +-ifdef(TEST). +capitalize_token_test_() -> + Tests = [ + {<<"heLLo-woRld">>, <<"Hello-World">>}, + {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>}, + {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>}, + {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>}, + {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>}, + {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>}, + {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>} + ], + [{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests]. +-endif. diff --git a/cowboy/src/cowboy_children.erl b/cowboy/src/cowboy_children.erl new file mode 100644 index 0000000..305c989 --- /dev/null +++ b/cowboy/src/cowboy_children.erl @@ -0,0 +1,192 @@ +%% Copyright (c) 2017-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_children). + +-export([init/0]). +-export([up/4]). +-export([down/2]). +-export([shutdown/2]). +-export([shutdown_timeout/3]). +-export([terminate/1]). +-export([handle_supervisor_call/4]). + +-record(child, { + pid :: pid(), + streamid :: cowboy_stream:streamid() | undefined, + shutdown :: timeout(), + timer = undefined :: undefined | reference() +}). + +-type children() :: [#child{}]. +-export_type([children/0]). + +-spec init() -> []. +init() -> + []. + +-spec up(Children, pid(), cowboy_stream:streamid(), timeout()) + -> Children when Children::children(). +up(Children, Pid, StreamID, Shutdown) -> + [#child{ + pid=Pid, + streamid=StreamID, + shutdown=Shutdown + }|Children]. + +-spec down(Children, pid()) + -> {ok, cowboy_stream:streamid() | undefined, Children} | error + when Children::children(). +down(Children0, Pid) -> + case lists:keytake(Pid, #child.pid, Children0) of + {value, #child{streamid=StreamID, timer=Ref}, Children} -> + _ = case Ref of + undefined -> ok; + _ -> erlang:cancel_timer(Ref, [{async, true}, {info, false}]) + end, + {ok, StreamID, Children}; + false -> + error + end. + +%% We ask the processes to shutdown first. This gives +%% a chance to processes that are trapping exits to +%% shut down gracefully. Others will exit immediately. +%% +%% @todo We currently fire one timer per process being +%% shut down. This is probably not the most efficient. +%% A more efficient solution could be to maintain a +%% single timer and decrease the shutdown time of all +%% processes when it fires. This is however much more +%% complex, and there aren't that many processes that +%% will need to be shutdown through this function, so +%% this is left for later. +-spec shutdown(Children, cowboy_stream:streamid()) + -> Children when Children::children(). +shutdown(Children0, StreamID) -> + [ + case Child of + #child{pid=Pid, streamid=StreamID, shutdown=Shutdown} -> + exit(Pid, shutdown), + Ref = erlang:start_timer(Shutdown, self(), {shutdown, Pid}), + Child#child{streamid=undefined, timer=Ref}; + _ -> + Child + end + || Child <- Children0]. + +-spec shutdown_timeout(children(), reference(), pid()) -> ok. +shutdown_timeout(Children, Ref, Pid) -> + case lists:keyfind(Pid, #child.pid, Children) of + #child{timer=Ref} -> + exit(Pid, kill), + ok; + _ -> + ok + end. + +-spec terminate(children()) -> ok. +terminate(Children) -> + %% For each child, either ask for it to shut down, + %% or cancel its shutdown timer if it already is. + %% + %% We do not need to flush stray timeout messages out because + %% we are either terminating or switching protocols, + %% and in the latter case we flush all messages. + _ = [case TRef of + undefined -> exit(Pid, shutdown); + _ -> erlang:cancel_timer(TRef, [{async, true}, {info, false}]) + end || #child{pid=Pid, timer=TRef} <- Children], + before_terminate_loop(Children). + +before_terminate_loop([]) -> + ok; +before_terminate_loop(Children) -> + %% Find the longest shutdown time. + Time = longest_shutdown_time(Children, 0), + %% We delay the creation of the timer if one of the + %% processes has an infinity shutdown value. + TRef = case Time of + infinity -> undefined; + _ -> erlang:start_timer(Time, self(), terminate) + end, + %% Loop until that time or until all children are dead. + terminate_loop(Children, TRef). + +terminate_loop([], TRef) -> + %% Don't forget to cancel the timer, if any! + case TRef of + undefined -> + ok; + _ -> + _ = erlang:cancel_timer(TRef, [{async, true}, {info, false}]), + ok + end; +terminate_loop(Children, TRef) -> + receive + {'EXIT', Pid, _} when TRef =:= undefined -> + {value, #child{shutdown=Shutdown}, Children1} + = lists:keytake(Pid, #child.pid, Children), + %% We delayed the creation of the timer. If a process with + %% infinity shutdown just ended, we might have to start that timer. + case Shutdown of + infinity -> before_terminate_loop(Children1); + _ -> terminate_loop(Children1, TRef) + end; + {'EXIT', Pid, _} -> + terminate_loop(lists:keydelete(Pid, #child.pid, Children), TRef); + {timeout, TRef, terminate} -> + %% Brutally kill any remaining children. + _ = [exit(Pid, kill) || #child{pid=Pid} <- Children], + ok + end. + +longest_shutdown_time([], Time) -> + Time; +longest_shutdown_time([#child{shutdown=ChildTime}|Tail], Time) when ChildTime > Time -> + longest_shutdown_time(Tail, ChildTime); +longest_shutdown_time([_|Tail], Time) -> + longest_shutdown_time(Tail, Time). + +-spec handle_supervisor_call(any(), {pid(), any()}, children(), module()) -> ok. +handle_supervisor_call(which_children, {From, Tag}, Children, Module) -> + From ! {Tag, which_children(Children, Module)}, + ok; +handle_supervisor_call(count_children, {From, Tag}, Children, _) -> + From ! {Tag, count_children(Children)}, + ok; +%% We disable start_child since only incoming requests +%% end up creating a new process. +handle_supervisor_call({start_child, _}, {From, Tag}, _, _) -> + From ! {Tag, {error, start_child_disabled}}, + ok; +%% All other calls refer to children. We act in a similar way +%% to a simple_one_for_one so we never find those. +handle_supervisor_call(_, {From, Tag}, _, _) -> + From ! {Tag, {error, not_found}}, + ok. + +-spec which_children(children(), module()) -> [{module(), pid(), worker, [module()]}]. +which_children(Children, Module) -> + [{Module, Pid, worker, [Module]} || #child{pid=Pid} <- Children]. + +-spec count_children(children()) -> [{atom(), non_neg_integer()}]. +count_children(Children) -> + Count = length(Children), + [ + {specs, 1}, + {active, Count}, + {supervisors, 0}, + {workers, Count} + ]. diff --git a/cowboy/src/cowboy_clear.erl b/cowboy/src/cowboy_clear.erl new file mode 100644 index 0000000..eaeab74 --- /dev/null +++ b/cowboy/src/cowboy_clear.erl @@ -0,0 +1,62 @@ +%% Copyright (c) 2016-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_clear). +-behavior(ranch_protocol). + +-export([start_link/3]). +-export([start_link/4]). +-export([connection_process/4]). + +%% Ranch 1. +-spec start_link(ranch:ref(), inet:socket(), module(), cowboy:opts()) -> {ok, pid()}. +start_link(Ref, _Socket, Transport, Opts) -> + start_link(Ref, Transport, Opts). + +%% Ranch 2. +-spec start_link(ranch:ref(), module(), cowboy:opts()) -> {ok, pid()}. +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, connection_process, + [self(), Ref, Transport, Opts]), + {ok, Pid}. + +-spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok. +connection_process(Parent, Ref, Transport, Opts) -> + ProxyInfo = get_proxy_info(Ref, Opts), + {ok, Socket} = ranch:handshake(Ref), + %% Use cowboy_http2 directly only when 'http' is missing. + %% 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 + end, + init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol). + +init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) -> + _ = case maps:get(connection_type, Opts, supervisor) of + worker -> ok; + supervisor -> process_flag(trap_exit, true) + end, + Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts). + +get_proxy_info(Ref, #{proxy_header := true}) -> + case ranch:recv_proxy_header(Ref, 1000) of + {ok, ProxyInfo} -> ProxyInfo; + {error, closed} -> exit({shutdown, closed}) + end; +get_proxy_info(_, _) -> + undefined. diff --git a/cowboy/src/cowboy_clock.erl b/cowboy/src/cowboy_clock.erl new file mode 100644 index 0000000..e2cdf62 --- /dev/null +++ b/cowboy/src/cowboy_clock.erl @@ -0,0 +1,221 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% While a gen_server process runs in the background to update +%% the cache of formatted dates every second, all API calls are +%% local and directly read from the ETS cache table, providing +%% fast time and date computations. +-module(cowboy_clock). +-behaviour(gen_server). + +%% API. +-export([start_link/0]). +-export([stop/0]). +-export([rfc1123/0]). +-export([rfc1123/1]). + +%% gen_server. +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([code_change/3]). + +-record(state, { + universaltime = undefined :: undefined | calendar:datetime(), + rfc1123 = <<>> :: binary(), + tref = undefined :: undefined | reference() +}). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec stop() -> stopped. +stop() -> + gen_server:call(?MODULE, stop). + +%% When the ets table doesn't exist, either because of a bug +%% or because Cowboy is being restarted, we perform in a +%% slightly degraded state and build a new timestamp for +%% every request. +-spec rfc1123() -> binary(). +rfc1123() -> + try + ets:lookup_element(?MODULE, rfc1123, 2) + catch error:badarg -> + rfc1123(erlang:universaltime()) + end. + +-spec rfc1123(calendar:datetime()) -> binary(). +rfc1123(DateTime) -> + update_rfc1123(<<>>, undefined, DateTime). + +%% gen_server. + +-spec init([]) -> {ok, #state{}}. +init([]) -> + ?MODULE = ets:new(?MODULE, [set, protected, + named_table, {read_concurrency, true}]), + T = erlang:universaltime(), + B = update_rfc1123(<<>>, undefined, T), + TRef = erlang:send_after(1000, self(), update), + ets:insert(?MODULE, {rfc1123, B}), + {ok, #state{universaltime=T, rfc1123=B, tref=TRef}}. + +-type from() :: {pid(), term()}. +-spec handle_call + (stop, from(), State) -> {stop, normal, stopped, State} + when State::#state{}. +handle_call(stop, _From, State) -> + {stop, normal, stopped, State}; +handle_call(_Request, _From, State) -> + {reply, ignored, State}. + +-spec handle_cast(_, State) -> {noreply, State} when State::#state{}. +handle_cast(_Msg, State) -> + {noreply, State}. + +-spec handle_info(any(), State) -> {noreply, State} when State::#state{}. +handle_info(update, #state{universaltime=Prev, rfc1123=B1, tref=TRef0}) -> + %% Cancel the timer in case an external process sent an update message. + _ = erlang:cancel_timer(TRef0), + T = erlang:universaltime(), + B2 = update_rfc1123(B1, Prev, T), + ets:insert(?MODULE, {rfc1123, B2}), + TRef = erlang:send_after(1000, self(), update), + {noreply, #state{universaltime=T, rfc1123=B2, tref=TRef}}; +handle_info(_Info, State) -> + {noreply, State}. + +-spec terminate(_, _) -> ok. +terminate(_Reason, _State) -> + ok. + +-spec code_change(_, State, _) -> {ok, State} when State::#state{}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% Internal. + +-spec update_rfc1123(binary(), undefined | calendar:datetime(), + calendar:datetime()) -> binary(). +update_rfc1123(Bin, Now, Now) -> + Bin; +update_rfc1123(<< Keep:23/binary, _/bits >>, + {Date, {H, M, _}}, {Date, {H, M, S}}) -> + << Keep/binary, (pad_int(S))/binary, " GMT" >>; +update_rfc1123(<< Keep:20/binary, _/bits >>, + {Date, {H, _, _}}, {Date, {H, M, S}}) -> + << Keep/binary, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>; +update_rfc1123(<< Keep:17/binary, _/bits >>, {Date, _}, {Date, {H, M, S}}) -> + << Keep/binary, (pad_int(H))/binary, $:, (pad_int(M))/binary, + $:, (pad_int(S))/binary, " GMT" >>; +update_rfc1123(<< _:7/binary, Keep:10/binary, _/bits >>, + {{Y, Mo, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) -> + Wday = calendar:day_of_the_week(Date), + << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, Keep/binary, + (pad_int(H))/binary, $:, (pad_int(M))/binary, + $:, (pad_int(S))/binary, " GMT" >>; +update_rfc1123(<< _:11/binary, Keep:6/binary, _/bits >>, + {{Y, _, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) -> + Wday = calendar:day_of_the_week(Date), + << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ", + (month(Mo))/binary, Keep/binary, + (pad_int(H))/binary, $:, (pad_int(M))/binary, + $:, (pad_int(S))/binary, " GMT" >>; +update_rfc1123(_, _, {Date = {Y, Mo, D}, {H, M, S}}) -> + Wday = calendar:day_of_the_week(Date), + << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ", + (month(Mo))/binary, " ", (integer_to_binary(Y))/binary, + " ", (pad_int(H))/binary, $:, (pad_int(M))/binary, + $:, (pad_int(S))/binary, " GMT" >>. + +%% Following suggestion by MononcQc on #erlounge. +-spec pad_int(0..59) -> binary(). +pad_int(X) when X < 10 -> + << $0, ($0 + X) >>; +pad_int(X) -> + integer_to_binary(X). + +-spec weekday(1..7) -> <<_:24>>. +weekday(1) -> <<"Mon">>; +weekday(2) -> <<"Tue">>; +weekday(3) -> <<"Wed">>; +weekday(4) -> <<"Thu">>; +weekday(5) -> <<"Fri">>; +weekday(6) -> <<"Sat">>; +weekday(7) -> <<"Sun">>. + +-spec month(1..12) -> <<_:24>>. +month( 1) -> <<"Jan">>; +month( 2) -> <<"Feb">>; +month( 3) -> <<"Mar">>; +month( 4) -> <<"Apr">>; +month( 5) -> <<"May">>; +month( 6) -> <<"Jun">>; +month( 7) -> <<"Jul">>; +month( 8) -> <<"Aug">>; +month( 9) -> <<"Sep">>; +month(10) -> <<"Oct">>; +month(11) -> <<"Nov">>; +month(12) -> <<"Dec">>. + +%% Tests. + +-ifdef(TEST). +update_rfc1123_test_() -> + Tests = [ + {<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined, + {{2011, 5, 14}, {14, 25, 33}}, <<>>}, + {<<"Sat, 14 May 2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}, + {{2011, 5, 14}, {14, 25, 33}}, <<"Sat, 14 May 2011 14:25:33 GMT">>}, + {<<"Sat, 14 May 2011 14:25:34 GMT">>, {{2011, 5, 14}, {14, 25, 33}}, + {{2011, 5, 14}, {14, 25, 34}}, <<"Sat, 14 May 2011 14:25:33 GMT">>}, + {<<"Sat, 14 May 2011 14:26:00 GMT">>, {{2011, 5, 14}, {14, 25, 59}}, + {{2011, 5, 14}, {14, 26, 0}}, <<"Sat, 14 May 2011 14:25:59 GMT">>}, + {<<"Sat, 14 May 2011 15:00:00 GMT">>, {{2011, 5, 14}, {14, 59, 59}}, + {{2011, 5, 14}, {15, 0, 0}}, <<"Sat, 14 May 2011 14:59:59 GMT">>}, + {<<"Sun, 15 May 2011 00:00:00 GMT">>, {{2011, 5, 14}, {23, 59, 59}}, + {{2011, 5, 15}, { 0, 0, 0}}, <<"Sat, 14 May 2011 23:59:59 GMT">>}, + {<<"Wed, 01 Jun 2011 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}}, + {{2011, 6, 1}, { 0, 0, 0}}, <<"Tue, 31 May 2011 23:59:59 GMT">>}, + {<<"Sun, 01 Jan 2012 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}}, + {{2012, 1, 1}, { 0, 0, 0}}, <<"Sat, 31 Dec 2011 23:59:59 GMT">>} + ], + [{R, fun() -> R = update_rfc1123(B, P, N) end} || {R, P, N, B} <- Tests]. + +pad_int_test_() -> + Tests = [ + { 0, <<"00">>}, { 1, <<"01">>}, { 2, <<"02">>}, { 3, <<"03">>}, + { 4, <<"04">>}, { 5, <<"05">>}, { 6, <<"06">>}, { 7, <<"07">>}, + { 8, <<"08">>}, { 9, <<"09">>}, {10, <<"10">>}, {11, <<"11">>}, + {12, <<"12">>}, {13, <<"13">>}, {14, <<"14">>}, {15, <<"15">>}, + {16, <<"16">>}, {17, <<"17">>}, {18, <<"18">>}, {19, <<"19">>}, + {20, <<"20">>}, {21, <<"21">>}, {22, <<"22">>}, {23, <<"23">>}, + {24, <<"24">>}, {25, <<"25">>}, {26, <<"26">>}, {27, <<"27">>}, + {28, <<"28">>}, {29, <<"29">>}, {30, <<"30">>}, {31, <<"31">>}, + {32, <<"32">>}, {33, <<"33">>}, {34, <<"34">>}, {35, <<"35">>}, + {36, <<"36">>}, {37, <<"37">>}, {38, <<"38">>}, {39, <<"39">>}, + {40, <<"40">>}, {41, <<"41">>}, {42, <<"42">>}, {43, <<"43">>}, + {44, <<"44">>}, {45, <<"45">>}, {46, <<"46">>}, {47, <<"47">>}, + {48, <<"48">>}, {49, <<"49">>}, {50, <<"50">>}, {51, <<"51">>}, + {52, <<"52">>}, {53, <<"53">>}, {54, <<"54">>}, {55, <<"55">>}, + {56, <<"56">>}, {57, <<"57">>}, {58, <<"58">>}, {59, <<"59">>} + ], + [{I, fun() -> O = pad_int(I) end} || {I, O} <- Tests]. +-endif. diff --git a/cowboy/src/cowboy_compress_h.erl b/cowboy/src/cowboy_compress_h.erl new file mode 100644 index 0000000..338ea9f --- /dev/null +++ b/cowboy/src/cowboy_compress_h.erl @@ -0,0 +1,267 @@ +%% Copyright (c) 2017-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_compress_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-record(state, { + next :: any(), + threshold :: non_neg_integer() | undefined, + compress = undefined :: undefined | gzip, + deflate = undefined :: undefined | zlib:zstream(), + deflate_flush = sync :: none | sync +}). + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {cowboy_stream:commands(), #state{}}. +init(StreamID, Req, Opts) -> + State0 = check_req(Req), + CompressThreshold = maps:get(compress_threshold, Opts, 300), + DeflateFlush = buffering_to_zflush(maps:get(compress_buffering, Opts, false)), + {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts), + fold(Commands0, State0#state{next=Next, + threshold=CompressThreshold, + deflate_flush=DeflateFlush}). + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +data(StreamID, IsFin, Data, State0=#state{next=Next0}) -> + {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), + fold(Commands0, State0#state{next=Next}). + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +info(StreamID, Info, State0=#state{next=Next0}) -> + {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), + fold(Commands0, State0#state{next=Next}). + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). +terminate(StreamID, Reason, #state{next=Next, deflate=Z}) -> + %% Clean the zlib:stream() in case something went wrong. + %% In the normal scenario the stream is already closed. + case Z of + undefined -> ok; + _ -> zlib:close(Z) + end, + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). +early_error(StreamID, Reason, PartialReq, Resp, Opts) -> + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). + +%% Internal. + +%% Check if the client supports decoding of gzip responses. +%% +%% A malformed accept-encoding header is ignored (no compression). +check_req(Req) -> + try cowboy_req:parse_header(<<"accept-encoding">>, Req) of + %% Client doesn't support any compression algorithm. + undefined -> + #state{compress=undefined}; + Encodings -> + %% We only support gzip so look for it specifically. + %% @todo A recipient SHOULD consider "x-gzip" to be + %% equivalent to "gzip". (RFC7230 4.2.3) + case [E || E={<<"gzip">>, Q} <- Encodings, Q =/= 0] of + [] -> + #state{compress=undefined}; + _ -> + #state{compress=gzip} + end + catch + _:_ -> + #state{compress=undefined} + end. + +%% Do not compress responses that contain the content-encoding header. +check_resp_headers(#{<<"content-encoding">> := _}, State) -> + State#state{compress=undefined}; +%% Do not compress responses that contain the etag header. +check_resp_headers(#{<<"etag">> := _}, State) -> + State#state{compress=undefined}; +check_resp_headers(_, State) -> + State. + +fold(Commands, State=#state{compress=undefined}) -> + fold_vary_only(Commands, State, []); +fold(Commands, State) -> + fold(Commands, State, []). + +fold([], State, Acc) -> + {lists:reverse(Acc), State}; +%% We do not compress full sendfile bodies. +fold([Response={response, _, _, {sendfile, _, _, _}}|Tail], State, Acc) -> + fold(Tail, State, [vary_response(Response)|Acc]); +%% We compress full responses directly, unless they are lower than +%% the configured threshold or we find we are not able to by looking at the headers. +fold([Response0={response, _, Headers, Body}|Tail], + State0=#state{threshold=CompressThreshold}, Acc) -> + case check_resp_headers(Headers, State0) of + State=#state{compress=undefined} -> + fold(Tail, State, [vary_response(Response0)|Acc]); + State1 -> + BodyLength = iolist_size(Body), + if + BodyLength =< CompressThreshold -> + fold(Tail, State1, [vary_response(Response0)|Acc]); + true -> + {Response, State} = gzip_response(Response0, State1), + fold(Tail, State, [vary_response(Response)|Acc]) + end + end; +%% Check headers and initiate compression... +fold([Response0={headers, _, Headers}|Tail], State0, Acc) -> + case check_resp_headers(Headers, State0) of + State=#state{compress=undefined} -> + fold(Tail, State, [vary_headers(Response0)|Acc]); + State1 -> + {Response, State} = gzip_headers(Response0, State1), + fold(Tail, State, [vary_headers(Response)|Acc]) + end; +%% then compress each data commands individually. +fold([Data0={data, _, _}|Tail], State0=#state{compress=gzip}, Acc) -> + {Data, State} = gzip_data(Data0, State0), + fold(Tail, State, [Data|Acc]); +%% When trailers are sent we need to end the compression. +%% This results in an extra data command being sent. +fold([Trailers={trailers, _}|Tail], State0=#state{compress=gzip}, Acc) -> + {{data, fin, Data}, State} = gzip_data({data, fin, <<>>}, State0), + fold(Tail, State, [Trailers, {data, nofin, Data}|Acc]); +%% All the options from this handler can be updated for the current stream. +%% The set_options command must be propagated as-is regardless. +fold([SetOptions={set_options, Opts}|Tail], State=#state{ + threshold=CompressThreshold0, deflate_flush=DeflateFlush0}, Acc) -> + CompressThreshold = maps:get(compress_threshold, Opts, CompressThreshold0), + DeflateFlush = case Opts of + #{compress_buffering := CompressBuffering} -> + buffering_to_zflush(CompressBuffering); + _ -> + DeflateFlush0 + end, + fold(Tail, State#state{threshold=CompressThreshold, deflate_flush=DeflateFlush}, + [SetOptions|Acc]); +%% Otherwise, we have an unrelated command or compression is disabled. +fold([Command|Tail], State, Acc) -> + fold(Tail, State, [Command|Acc]). + +fold_vary_only([], State, Acc) -> + {lists:reverse(Acc), State}; +fold_vary_only([Response={response, _, _, _}|Tail], State, Acc) -> + fold_vary_only(Tail, State, [vary_response(Response)|Acc]); +fold_vary_only([Response={headers, _, _}|Tail], State, Acc) -> + fold_vary_only(Tail, State, [vary_headers(Response)|Acc]); +fold_vary_only([Command|Tail], State, Acc) -> + fold_vary_only(Tail, State, [Command|Acc]). + +buffering_to_zflush(true) -> none; +buffering_to_zflush(false) -> sync. + +gzip_response({response, Status, Headers, Body}, State) -> + %% We can't call zlib:gzip/1 because it does an + %% iolist_to_binary(GzBody) at the end to return + %% a binary(). Therefore the code here is largely + %% a duplicate of the code of that function. + Z = zlib:open(), + GzBody = try + %% 31 = 16+?MAX_WBITS from zlib.erl + %% @todo It might be good to allow them to be configured? + zlib:deflateInit(Z, default, deflated, 31, 8, default), + Gz = zlib:deflate(Z, Body, finish), + zlib:deflateEnd(Z), + Gz + after + zlib:close(Z) + end, + {{response, Status, Headers#{ + <<"content-length">> => integer_to_binary(iolist_size(GzBody)), + <<"content-encoding">> => <<"gzip">> + }, GzBody}, State}. + +gzip_headers({headers, Status, Headers0}, State) -> + Z = zlib:open(), + %% We use the same arguments as when compressing the body fully. + %% @todo It might be good to allow them to be configured? + zlib:deflateInit(Z, default, deflated, 31, 8, default), + Headers = maps:remove(<<"content-length">>, Headers0), + {{headers, Status, Headers#{ + <<"content-encoding">> => <<"gzip">> + }}, State#state{deflate=Z}}. + +vary_response({response, Status, Headers, Body}) -> + {response, Status, vary(Headers), Body}. + +vary_headers({headers, Status, Headers}) -> + {headers, Status, vary(Headers)}. + +%% We must add content-encoding to vary if it's not already there. +vary(Headers=#{<<"vary">> := Vary}) -> + try cow_http_hd:parse_vary(iolist_to_binary(Vary)) of + '*' -> Headers; + List -> + case lists:member(<<"accept-encoding">>, List) of + true -> Headers; + false -> Headers#{<<"vary">> => [Vary, <<", accept-encoding">>]} + end + catch _:_ -> + %% The vary header is invalid. Probably empty. We replace it with ours. + Headers#{<<"vary">> => <<"accept-encoding">>} + end; +vary(Headers) -> + Headers#{<<"vary">> => <<"accept-encoding">>}. + +%% It is not possible to combine zlib and the sendfile +%% syscall as far as I can tell, because the zlib format +%% includes a checksum at the end of the stream. We have +%% to read the file in memory, making this not suitable for +%% large files. +gzip_data({data, nofin, Sendfile={sendfile, _, _, _}}, + State=#state{deflate=Z, deflate_flush=Flush}) -> + {ok, Data0} = read_file(Sendfile), + Data = zlib:deflate(Z, Data0, Flush), + {{data, nofin, Data}, State}; +gzip_data({data, fin, Sendfile={sendfile, _, _, _}}, State=#state{deflate=Z}) -> + {ok, Data0} = read_file(Sendfile), + Data = zlib:deflate(Z, Data0, finish), + zlib:deflateEnd(Z), + zlib:close(Z), + {{data, fin, Data}, State#state{deflate=undefined}}; +gzip_data({data, nofin, Data0}, State=#state{deflate=Z, deflate_flush=Flush}) -> + Data = zlib:deflate(Z, Data0, Flush), + {{data, nofin, Data}, State}; +gzip_data({data, fin, Data0}, State=#state{deflate=Z}) -> + Data = zlib:deflate(Z, Data0, finish), + zlib:deflateEnd(Z), + zlib:close(Z), + {{data, fin, Data}, State#state{deflate=undefined}}. + +read_file({sendfile, Offset, Bytes, Path}) -> + {ok, IoDevice} = file:open(Path, [read, raw, binary]), + try + _ = case Offset of + 0 -> ok; + _ -> file:position(IoDevice, {bof, Offset}) + end, + file:read(IoDevice, Bytes) + after + file:close(IoDevice) + end. diff --git a/cowboy/src/cowboy_constraints.erl b/cowboy/src/cowboy_constraints.erl new file mode 100644 index 0000000..33f0111 --- /dev/null +++ b/cowboy/src/cowboy_constraints.erl @@ -0,0 +1,174 @@ +%% Copyright (c) 2014-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_constraints). + +-export([validate/2]). +-export([reverse/2]). +-export([format_error/1]). + +-type constraint() :: int | nonempty | fun(). +-export_type([constraint/0]). + +-type reason() :: {constraint(), any(), any()}. +-export_type([reason/0]). + +-spec validate(binary(), constraint() | [constraint()]) + -> {ok, any()} | {error, reason()}. +validate(Value, Constraints) when is_list(Constraints) -> + apply_list(forward, Value, Constraints); +validate(Value, Constraint) -> + apply_list(forward, Value, [Constraint]). + +-spec reverse(any(), constraint() | [constraint()]) + -> {ok, binary()} | {error, reason()}. +reverse(Value, Constraints) when is_list(Constraints) -> + apply_list(reverse, Value, Constraints); +reverse(Value, Constraint) -> + apply_list(reverse, Value, [Constraint]). + +-spec format_error(reason()) -> iodata(). +format_error({Constraint, Reason, Value}) -> + apply_constraint(format_error, {Reason, Value}, Constraint). + +apply_list(_, Value, []) -> + {ok, Value}; +apply_list(Type, Value0, [Constraint|Tail]) -> + case apply_constraint(Type, Value0, Constraint) of + {ok, Value} -> + apply_list(Type, Value, Tail); + {error, Reason} -> + {error, {Constraint, Reason, Value0}} + end. + +%% @todo {int, From, To}, etc. +apply_constraint(Type, Value, int) -> + int(Type, Value); +apply_constraint(Type, Value, nonempty) -> + nonempty(Type, Value); +apply_constraint(Type, Value, F) when is_function(F) -> + F(Type, Value). + +%% Constraint functions. + +int(forward, Value) -> + try + {ok, binary_to_integer(Value)} + catch _:_ -> + {error, not_an_integer} + end; +int(reverse, Value) -> + try + {ok, integer_to_binary(Value)} + catch _:_ -> + {error, not_an_integer} + end; +int(format_error, {not_an_integer, Value}) -> + io_lib:format("The value ~p is not an integer.", [Value]). + +nonempty(Type, <<>>) when Type =/= format_error -> + {error, empty}; +nonempty(Type, Value) when Type =/= format_error, is_binary(Value) -> + {ok, Value}; +nonempty(format_error, {empty, Value}) -> + io_lib:format("The value ~p is empty.", [Value]). + +-ifdef(TEST). + +validate_test() -> + F = fun(_, Value) -> + try + {ok, binary_to_atom(Value, latin1)} + catch _:_ -> + {error, not_a_binary} + end + end, + %% Value, Constraints, Result. + Tests = [ + {<<>>, [], <<>>}, + {<<"123">>, int, 123}, + {<<"123">>, [int], 123}, + {<<"123">>, [nonempty, int], 123}, + {<<"123">>, [int, nonempty], 123}, + {<<>>, nonempty, error}, + {<<>>, [nonempty], error}, + {<<"hello">>, F, hello}, + {<<"hello">>, [F], hello}, + {<<"123">>, [F, int], error}, + {<<"123">>, [int, F], error}, + {<<"hello">>, [nonempty, F], hello}, + {<<"hello">>, [F, nonempty], hello} + ], + [{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() -> + case R of + error -> {error, _} = validate(V, C); + _ -> {ok, R} = validate(V, C) + end + end} || {V, C, R} <- Tests]. + +reverse_test() -> + F = fun(_, Value) -> + try + {ok, atom_to_binary(Value, latin1)} + catch _:_ -> + {error, not_an_atom} + end + end, + %% Value, Constraints, Result. + Tests = [ + {<<>>, [], <<>>}, + {123, int, <<"123">>}, + {123, [int], <<"123">>}, + {123, [nonempty, int], <<"123">>}, + {123, [int, nonempty], <<"123">>}, + {<<>>, nonempty, error}, + {<<>>, [nonempty], error}, + {hello, F, <<"hello">>}, + {hello, [F], <<"hello">>}, + {123, [F, int], error}, + {123, [int, F], error}, + {hello, [nonempty, F], <<"hello">>}, + {hello, [F, nonempty], <<"hello">>} + ], + [{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() -> + case R of + error -> {error, _} = reverse(V, C); + _ -> {ok, R} = reverse(V, C) + end + end} || {V, C, R} <- Tests]. + +int_format_error_test() -> + {error, Reason} = validate(<<"string">>, int), + Bin = iolist_to_binary(format_error(Reason)), + true = is_binary(Bin), + ok. + +nonempty_format_error_test() -> + {error, Reason} = validate(<<>>, nonempty), + Bin = iolist_to_binary(format_error(Reason)), + true = is_binary(Bin), + ok. + +fun_format_error_test() -> + F = fun + (format_error, {test, <<"value">>}) -> + formatted; + (_, _) -> + {error, test} + end, + {error, Reason} = validate(<<"value">>, F), + formatted = format_error(Reason), + ok. + +-endif. diff --git a/cowboy/src/cowboy_decompress_h.erl b/cowboy/src/cowboy_decompress_h.erl new file mode 100644 index 0000000..4e86e23 --- /dev/null +++ b/cowboy/src/cowboy_decompress_h.erl @@ -0,0 +1,257 @@ +%% Copyright (c) 2024, jdamanalo +%% Copyright (c) 2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_decompress_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-record(state, { + next :: any(), + enabled = true :: boolean(), + ratio_limit :: non_neg_integer() | undefined, + compress = undefined :: undefined | gzip, + inflate = undefined :: undefined | zlib:zstream(), + is_reading = false :: boolean(), + + %% We use a list of binaries to avoid doing unnecessary + %% memory allocations when inflating. We convert to binary + %% when we propagate the data. The data must be reversed + %% before converting to binary or inflating: this is done + %% via the buffer_to_binary/buffer_to_iovec functions. + read_body_buffer = [] :: [binary()], + read_body_is_fin = nofin :: nofin | {fin, non_neg_integer()} +}). + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {cowboy_stream:commands(), #state{}}. +init(StreamID, Req0, Opts) -> + Enabled = maps:get(decompress_enabled, Opts, true), + RatioLimit = maps:get(decompress_ratio_limit, Opts, 20), + {Req, State} = check_and_update_req(Req0), + Inflate = case State#state.compress of + undefined -> + undefined; + gzip -> + Z = zlib:open(), + zlib:inflateInit(Z, 31), + Z + end, + {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), + fold(Commands, State#state{next=Next, enabled=Enabled, + ratio_limit=RatioLimit, inflate=Inflate}). + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +data(StreamID, IsFin, Data, State=#state{next=Next0, inflate=undefined}) -> + {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), + fold(Commands, State#state{next=Next, read_body_is_fin=IsFin}); +data(StreamID, IsFin, Data, State=#state{next=Next0, enabled=false, read_body_buffer=Buffer}) -> + {Commands, Next} = cowboy_stream:data(StreamID, IsFin, + buffer_to_binary([Data|Buffer]), Next0), + fold(Commands, State#state{next=Next, read_body_is_fin=IsFin}); +data(StreamID, IsFin, Data, 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 + {error, ErrorType} -> + zlib:close(Z), + Status = case ErrorType of + data_error -> 400; + size_error -> 413 + end, + Commands = [ + {error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>}, + stop + ], + fold(Commands, State0#state{inflate=undefined, read_body_buffer=[]}); + {ok, Inflated} -> + State = case IsFin of + nofin -> + State0; + fin -> + zlib:close(Z), + State0#state{inflate=undefined} + end, + {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Inflated, Next0), + fold(Commands, State#state{next=Next, read_body_buffer=[], + read_body_is_fin=IsFin}) + end; +data(_, IsFin, Data, State=#state{read_body_buffer=Buffer}) -> + {[], State#state{read_body_buffer=[Data|Buffer], read_body_is_fin=IsFin}}. + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +info(StreamID, Info, State=#state{next=Next0, inflate=undefined}) -> + {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), + fold(Commands, State#state{next=Next}); +info(StreamID, Info={CommandTag, _, _, _, _}, State=#state{next=Next0, read_body_is_fin=IsFin}) + when CommandTag =:= read_body; CommandTag =:= read_body_timeout -> + {Commands0, Next1} = cowboy_stream:info(StreamID, Info, Next0), + {Commands, Next} = data(StreamID, IsFin, <<>>, State#state{next=Next1, is_reading=true}), + fold(Commands ++ Commands0, Next); +info(StreamID, Info={set_options, Opts}, State0=#state{next=Next0, + enabled=Enabled0, ratio_limit=RatioLimit0, is_reading=IsReading}) -> + Enabled = maps:get(decompress_enabled, Opts, Enabled0), + RatioLimit = maps:get(decompress_ratio_limit, Opts, RatioLimit0), + {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), + %% We can't change the enabled setting after we start reading, + %% otherwise the data becomes garbage. Changing the setting + %% is not treated as an error, it is just ignored. + State = case IsReading of + true -> State0; + false -> State0#state{enabled=Enabled} + end, + fold(Commands, State#state{next=Next, ratio_limit=RatioLimit}); +info(StreamID, Info, State=#state{next=Next0}) -> + {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), + fold(Commands, State#state{next=Next}). + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). +terminate(StreamID, Reason, #state{next=Next, inflate=Z}) -> + case Z of + undefined -> ok; + _ -> zlib:close(Z) + end, + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). +early_error(StreamID, Reason, PartialReq, Resp, Opts) -> + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). + +%% Internal. + +%% Check whether the request needs content decoding, and if it does +%% whether it fits our criteria for decoding. We also update the +%% Req to indicate whether content was decoded. +%% +%% We always set the content_decoded value in the Req because it +%% indicates whether content decoding was attempted. +%% +%% A malformed content-encoding header results in no decoding. +check_and_update_req(Req=#{headers := Headers}) -> + ContentDecoded = maps:get(content_decoded, Req, []), + try cowboy_req:parse_header(<<"content-encoding">>, Req) of + %% We only automatically decompress when gzip is the only + %% encoding used. Since it's the only encoding used, we + %% can remove the header entirely before passing the Req + %% forward. + [<<"gzip">>] -> + {Req#{ + headers => maps:remove(<<"content-encoding">>, Headers), + content_decoded => [<<"gzip">>|ContentDecoded] + }, #state{compress=gzip}}; + _ -> + {Req#{content_decoded => ContentDecoded}, + #state{compress=undefined}} + catch _:_ -> + {Req#{content_decoded => ContentDecoded}, + #state{compress=undefined}} + end. + +buffer_to_iovec(Buffer) -> + lists:reverse(Buffer). + +buffer_to_binary(Buffer) -> + iolist_to_binary(lists:reverse(Buffer)). + +fold(Commands, State) -> + fold(Commands, State, []). + +fold([], State, Acc) -> + {lists:reverse(Acc), State}; +fold([{response, Status, Headers0, Body}|Tail], State=#state{enabled=true}, Acc) -> + Headers = add_accept_encoding(Headers0), + fold(Tail, State, [{response, Status, Headers, Body}|Acc]); +fold([{headers, Status, Headers0} | Tail], State=#state{enabled=true}, Acc) -> + Headers = add_accept_encoding(Headers0), + fold(Tail, State, [{headers, Status, Headers}|Acc]); +fold([Command|Tail], State, Acc) -> + fold(Tail, State, [Command|Acc]). + +add_accept_encoding(Headers=#{<<"accept-encoding">> := AcceptEncoding}) -> + try cow_http_hd:parse_accept_encoding(iolist_to_binary(AcceptEncoding)) of + List -> + case lists:keyfind(<<"gzip">>, 1, List) of + %% gzip is excluded but this handler is enabled; we replace. + {_, 0} -> + Replaced = lists:keyreplace(<<"gzip">>, 1, List, {<<"gzip">>, 1000}), + Codings = build_accept_encoding(Replaced), + Headers#{<<"accept-encoding">> => Codings}; + {_, _} -> + Headers; + false -> + case lists:keyfind(<<"*">>, 1, List) of + %% Others are excluded along with gzip; we add. + {_, 0} -> + WithGzip = [{<<"gzip">>, 1000} | List], + Codings = build_accept_encoding(WithGzip), + Headers#{<<"accept-encoding">> => Codings}; + {_, _} -> + Headers; + false -> + Headers#{<<"accept-encoding">> => [AcceptEncoding, <<", gzip">>]} + end + end + catch _:_ -> + %% The accept-encoding header is invalid. Probably empty. We replace it with ours. + Headers#{<<"accept-encoding">> => <<"gzip">>} + end; +add_accept_encoding(Headers) -> + Headers#{<<"accept-encoding">> => <<"gzip">>}. + +%% @todo From cowlib, maybe expose? +qvalue_to_iodata(0) -> <<"0">>; +qvalue_to_iodata(Q) when Q < 10 -> [<<"0.00">>, integer_to_binary(Q)]; +qvalue_to_iodata(Q) when Q < 100 -> [<<"0.0">>, integer_to_binary(Q)]; +qvalue_to_iodata(Q) when Q < 1000 -> [<<"0.">>, integer_to_binary(Q)]; +qvalue_to_iodata(1000) -> <<"1">>. + +%% @todo Should be added to Cowlib. +build_accept_encoding([{ContentCoding, Q}|Tail]) -> + Weight = iolist_to_binary(qvalue_to_iodata(Q)), + Acc = <handler and handler_opts +%% environment values. The result of this execution is added to the +%% environment under the result value. +-module(cowboy_handler). +-behaviour(cowboy_middleware). + +-export([execute/2]). +-export([terminate/4]). + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), any()} + when Req::cowboy_req:req(). + +-callback terminate(any(), map(), any()) -> ok. +-optional_callbacks([terminate/3]). + +-spec execute(Req, Env) -> {ok, Req, Env} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) -> + try Handler:init(Req, HandlerOpts) of + {ok, Req2, State} -> + Result = terminate(normal, Req2, State, Handler), + {ok, Req2, Env#{result => Result}}; + {Mod, Req2, State} -> + Mod:upgrade(Req2, Env, Handler, State); + {Mod, Req2, State, Opts} -> + Mod:upgrade(Req2, Env, Handler, State, Opts) + catch Class:Reason:Stacktrace -> + terminate({crash, Class, Reason}, Req, HandlerOpts, Handler), + erlang:raise(Class, Reason, Stacktrace) + end. + +-spec terminate(any(), Req | undefined, any(), module()) -> ok when Req::cowboy_req:req(). +terminate(Reason, Req, State, Handler) -> + case erlang:function_exported(Handler, terminate, 3) of + true -> + Handler:terminate(Reason, Req, State); + false -> + ok + end. diff --git a/cowboy/src/cowboy_http.erl b/cowboy/src/cowboy_http.erl new file mode 100644 index 0000000..ee1e725 --- /dev/null +++ b/cowboy/src/cowboy_http.erl @@ -0,0 +1,1607 @@ +%% Copyright (c) 2016-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_http). + +-export([init/6]). + +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +-type opts() :: #{ + active_n => pos_integer(), + chunked => boolean(), + compress_buffering => boolean(), + compress_threshold => non_neg_integer(), + connection_type => worker | supervisor, + env => cowboy_middleware:env(), + http10_keepalive => boolean(), + idle_timeout => timeout(), + inactivity_timeout => timeout(), + initial_stream_flow_size => non_neg_integer(), + linger_timeout => timeout(), + logger => module(), + max_authority_length => non_neg_integer(), + max_empty_lines => non_neg_integer(), + max_header_name_length => non_neg_integer(), + max_header_value_length => non_neg_integer(), + max_headers => non_neg_integer(), + max_keepalive => non_neg_integer(), + max_method_length => non_neg_integer(), + max_request_line_length => non_neg_integer(), + metrics_callback => cowboy_metrics_h:metrics_callback(), + metrics_req_filter => fun((cowboy_req:req()) -> map()), + metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()), + middlewares => [module()], + proxy_header => boolean(), + request_timeout => timeout(), + reset_idle_timeout_on_send => boolean(), + sendfile => boolean(), + shutdown_timeout => timeout(), + stream_handlers => [module()], + tracer_callback => cowboy_tracer_h:tracer_callback(), + tracer_flags => [atom()], + tracer_match_specs => cowboy_tracer_h:tracer_match_specs(), + %% Open ended because configured stream handlers might add options. + _ => _ +}. +-export_type([opts/0]). + +-record(ps_request_line, { + empty_lines = 0 :: non_neg_integer() +}). + +-record(ps_header, { + method = undefined :: binary(), + authority = undefined :: binary() | undefined, + path = undefined :: binary(), + qs = undefined :: binary(), + version = undefined :: cowboy:http_version(), + headers = undefined :: cowboy:http_headers() | undefined, + name = undefined :: binary() | undefined +}). + +-record(ps_body, { + length :: non_neg_integer() | undefined, + received = 0 :: non_neg_integer(), + transfer_decode_fun :: fun((binary(), cow_http_te:state()) -> cow_http_te:decode_ret()), + transfer_decode_state :: cow_http_te:state() +}). + +-record(stream, { + id = undefined :: cowboy_stream:streamid(), + %% Stream handlers and their state. + state = undefined :: {module(), any()}, + %% Request method. + method = undefined :: binary(), + %% Client HTTP version for this stream. + version = undefined :: cowboy:http_version(), + %% Unparsed te header. Used to know if we can send trailers. + te :: undefined | binary(), + %% Expected body size. + local_expected_size = undefined :: undefined | non_neg_integer(), + %% Sent body size. + local_sent_size = 0 :: non_neg_integer(), + %% Commands queued. + queue = [] :: cowboy_stream:commands() +}). + +-type stream() :: #stream{}. + +-record(state, { + parent :: pid(), + ref :: ranch:ref(), + socket :: inet:socket(), + transport :: module(), + proxy_header :: undefined | ranch_proxy_header:proxy_info(), + opts = #{} :: cowboy:opts(), + buffer = <<>> :: binary(), + + %% Some options may be overriden for the current stream. + overriden_opts = #{} :: cowboy:opts(), + + %% Remote address and port for the connection. + peer = undefined :: {inet:ip_address(), inet:port_number()}, + + %% Local address and port for the connection. + sock = undefined :: {inet:ip_address(), inet:port_number()}, + + %% Client certificate (TLS only). + cert :: undefined | binary(), + + timer = undefined :: undefined | reference(), + + %% Whether we are currently receiving data from the socket. + active = true :: boolean(), + + %% Identifier for the stream currently being read (or waiting to be received). + in_streamid = 1 :: pos_integer(), + + %% Parsing state for the current stream or stream-to-be. + in_state = #ps_request_line{} :: #ps_request_line{} | #ps_header{} | #ps_body{}, + + %% Flow requested for the current stream. + flow = infinity :: non_neg_integer() | infinity, + + %% Identifier for the stream currently being written. + %% Note that out_streamid =< in_streamid. + out_streamid = 1 :: pos_integer(), + + %% Whether we finished writing data for the current stream. + out_state = wait :: wait | chunked | streaming | done, + + %% The connection will be closed after this stream. + last_streamid = undefined :: pos_integer(), + + %% Currently active HTTP/1.1 streams. + streams = [] :: [stream()], + + %% Children processes created by streams. + children = cowboy_children:init() :: cowboy_children:children() +}). + +-include_lib("cowlib/include/cow_inline.hrl"). +-include_lib("cowlib/include/cow_parse.hrl"). + +-spec init(pid(), ranch:ref(), inet:socket(), module(), + ranch_proxy_header:proxy_info(), cowboy:opts()) -> ok. +init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) -> + {ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket), + 'A socket error occurred when retrieving the peer name.'), + {ok, Sock} = maybe_socket_error(undefined, Transport:sockname(Socket), + 'A socket error occurred when retrieving the sock name.'), + CertResult = case Transport:name() of + ssl -> + case ssl:peercert(Socket) of + {error, no_peercert} -> + {ok, undefined}; + Cert0 -> + Cert0 + end; + _ -> + {ok, undefined} + end, + {ok, Cert} = maybe_socket_error(undefined, CertResult, + 'A socket error occurred when retrieving the client TLS certificate.'), + State = #state{ + parent=Parent, ref=Ref, socket=Socket, + transport=Transport, proxy_header=ProxyHeader, opts=Opts, + peer=Peer, sock=Sock, cert=Cert, + last_streamid=maps:get(max_keepalive, Opts, 1000)}, + safe_setopts_active(State), + loop(set_timeout(State, request_timeout)). + +setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) -> + N = maps:get(active_n, Opts, 100), + Transport:setopts(Socket, [{active, N}]). + +safe_setopts_active(State) -> + ok = maybe_socket_error(State, setopts_active(State)). + +active(State) -> + safe_setopts_active(State), + State#state{active=true}. + +passive(State=#state{socket=Socket, transport=Transport}) -> + ok = maybe_socket_error(State, Transport:setopts(Socket, [{active, false}])), + Messages = Transport:messages(), + flush_passive(Socket, Messages), + State#state{active=false}. + +flush_passive(Socket, Messages) -> + receive + {Passive, Socket} when Passive =:= element(4, Messages); + %% Hardcoded for compatibility with Ranch 1.x. + Passive =:= tcp_passive; Passive =:= ssl_passive -> + flush_passive(Socket, Messages) + after 0 -> + ok + end. + +loop(State=#state{parent=Parent, socket=Socket, transport=Transport, opts=Opts, + buffer=Buffer, timer=TimerRef, children=Children, in_streamid=InStreamID, + last_streamid=LastStreamID}) -> + Messages = Transport:messages(), + InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000), + receive + %% Discard data coming in after the last request + %% we want to process was received fully. + {OK, Socket, _} when OK =:= element(1, Messages), InStreamID > LastStreamID -> + loop(State); + %% Socket messages. + {OK, Socket, Data} when OK =:= element(1, Messages) -> + parse(<< Buffer/binary, Data/binary >>, State); + {Closed, Socket} when Closed =:= element(2, Messages) -> + terminate(State, {socket_error, closed, 'The socket has been closed.'}); + {Error, Socket, Reason} when Error =:= element(3, Messages) -> + terminate(State, {socket_error, Reason, 'An error has occurred on the socket.'}); + {Passive, Socket} when Passive =:= element(4, Messages); + %% Hardcoded for compatibility with Ranch 1.x. + Passive =:= tcp_passive; Passive =:= ssl_passive -> + safe_setopts_active(State), + loop(State); + %% Timeouts. + {timeout, Ref, {shutdown, Pid}} -> + cowboy_children:shutdown_timeout(Children, Ref, Pid), + loop(State); + {timeout, TimerRef, Reason} -> + timeout(State, Reason); + {timeout, _, _} -> + loop(State); + %% System messages. + {'EXIT', Parent, shutdown} -> + Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'}, + 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)); + %% Exit signal from children. + Msg = {'EXIT', Pid, _} -> + loop(down(State, Pid, Msg)); + %% Calls from supervisor module. + {'$gen_call', From, Call} -> + cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE), + loop(State); + %% Unknown messages. + Msg -> + cowboy:log(warning, "Received stray message ~p.~n", [Msg], Opts), + loop(State) + after InactivityTimeout -> + terminate(State, {internal_error, timeout, 'No message or data received before timeout.'}) + end. + +%% For HTTP/1.1 we have two types of timeouts: the request_timeout +%% is used when there is no currently ongoing request. This means +%% that we are not currently sending or receiving data and that +%% the next data to be received will be a new request. The +%% request_timeout is set once when we no longer have ongoing +%% requests, and runs until the full set of request headers +%% is received. It is not reset. +%% +%% After that point we use the idle_timeout. We continue using +%% the idle_timeout if pipelined requests come in: we are doing +%% work and just want to ensure the socket is not half-closed. +%% We continue using the idle_timeout up until there is no +%% ongoing request. This includes requests that were processed +%% and for which we only want to skip the body. Once the body +%% has been read fully we can go back to request_timeout. The +%% idle_timeout is reset every time we receive data and, +%% optionally, every time we send data. + +%% We do not set request_timeout if we are skipping a body. +set_timeout(State=#state{in_state=#ps_body{}}, request_timeout) -> + State; +%% We do not set idle_timeout if there are no active streams, +%% unless when we are skipping a body. +set_timeout(State=#state{streams=[], in_state=InState}, idle_timeout) + when element(1, InState) =/= ps_body -> + State; +%% Otherwise we can set the timeout. +set_timeout(State0=#state{opts=Opts, overriden_opts=Override}, Name) -> + State = cancel_timeout(State0), + Default = case Name of + request_timeout -> 5000; + idle_timeout -> 60000 + end, + Timeout = case Override of + %% The timeout may have been overriden for the current stream. + #{Name := Timeout0} -> Timeout0; + _ -> maps:get(Name, Opts, Default) + end, + TimerRef = case Timeout of + infinity -> undefined; + Timeout -> erlang:start_timer(Timeout, self(), Name) + end, + State#state{timer=TimerRef}. + +maybe_reset_idle_timeout(State=#state{opts=Opts}) -> + case maps:get(reset_idle_timeout_on_send, Opts, false) of + true -> + set_timeout(State, idle_timeout); + false -> + State + end. + +cancel_timeout(State=#state{timer=TimerRef}) -> + ok = case TimerRef of + undefined -> + ok; + _ -> + %% Do a synchronous cancel and remove the message if any + %% to avoid receiving stray messages. + _ = erlang:cancel_timer(TimerRef), + receive + {timeout, TimerRef, _} -> ok + after 0 -> + ok + end + end, + State#state{timer=undefined}. + +-spec timeout(_, _) -> no_return(). +timeout(State=#state{in_state=#ps_request_line{}}, request_timeout) -> + terminate(State, {connection_error, timeout, + 'No request-line received before timeout.'}); +timeout(State=#state{in_state=#ps_header{}}, request_timeout) -> + error_terminate(408, State, {connection_error, timeout, + 'Request headers not received before timeout.'}); +timeout(State, idle_timeout) -> + terminate(State, {connection_error, timeout, + 'Connection idle longer than configuration allows.'}). + +parse(<<>>, State) -> + 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= <<>>}); +parse(Buffer, State=#state{in_state=#ps_request_line{empty_lines=EmptyLines}}) -> + after_parse(parse_request(Buffer, State, EmptyLines)); +parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=undefined}}) -> + after_parse(parse_header(Buffer, + State#state{in_state=PS#ps_header{headers=undefined}}, + Headers)); +parse(Buffer, State=#state{in_state=PS=#ps_header{headers=Headers, name=Name}}) -> + after_parse(parse_hd_before_value(Buffer, + State#state{in_state=PS#ps_header{headers=undefined, name=undefined}}, + Headers, Name)); +parse(Buffer, State=#state{in_state=#ps_body{}}) -> + after_parse(parse_body(Buffer, State)). + +after_parse({request, Req=#{streamid := StreamID, method := Method, + headers := Headers, version := Version}, + State0=#state{opts=Opts, buffer=Buffer, streams=Streams0}}) -> + try cowboy_stream:init(StreamID, Req, Opts) of + {Commands, StreamState} -> + Flow = maps:get(initial_stream_flow_size, Opts, 65535), + TE = maps:get(<<"te">>, Headers, undefined), + Streams = [#stream{id=StreamID, state=StreamState, + method=Method, version=Version, te=TE}|Streams0], + State1 = State0#state{streams=Streams, flow=Flow}, + State2 = case maybe_req_close(State1, Headers, Version) of + close -> + State1#state{last_streamid=StreamID}; + keepalive -> + State1; + bad_connection_header -> + error_terminate(400, State1, {connection_error, protocol_error, + 'The Connection header is invalid. (RFC7230 6.1)'}) + end, + State = set_timeout(State2, idle_timeout), + parse(Buffer, commands(State, StreamID, Commands)) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(init, + [StreamID, Req, Opts], + Class, Exception, Stacktrace), Opts), + %% We do not reset the idle timeout on send here + %% because an error occurred in the application. While we + %% are keeping the connection open for further requests we + %% do not want to keep the connection up too long if no + %% additional requests come in. + early_error(500, State0, {internal_error, {Class, Exception}, + 'Unhandled exception in cowboy_stream:init/3.'}, Req), + parse(Buffer, State0) + end; +%% Streams are sequential so the body is always about the last stream created +%% unless that stream has terminated. +after_parse({data, StreamID, IsFin, Data, State0=#state{opts=Opts, buffer=Buffer, + streams=Streams0=[Stream=#stream{id=StreamID, state=StreamState0}|_]}}) -> + try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of + {Commands, StreamState} -> + Streams = lists:keyreplace(StreamID, #stream.id, Streams0, + Stream#stream{state=StreamState}), + State1 = set_timeout(State0, idle_timeout), + State = update_flow(IsFin, Data, State1#state{streams=Streams}), + parse(Buffer, commands(State, StreamID, Commands)) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(data, + [StreamID, IsFin, Data, StreamState0], + Class, Exception, Stacktrace), Opts), + %% @todo Should call parse after this. + stream_terminate(State0, StreamID, {internal_error, {Class, Exception}, + 'Unhandled exception in cowboy_stream:data/4.'}) + end; +%% No corresponding stream. We must skip the body of the previous request +%% in order to process the next one. +after_parse({data, _, IsFin, _, State}) -> + loop(set_timeout(State, case IsFin of + fin -> request_timeout; + nofin -> idle_timeout + end)); +after_parse({more, State}) -> + loop(set_timeout(State, idle_timeout)). + +update_flow(fin, _, State) -> + %% This function is only called after parsing, therefore we + %% are expecting to be in active mode already. + State#state{flow=infinity}; +update_flow(nofin, Data, State0=#state{flow=Flow0}) -> + Flow = Flow0 - byte_size(Data), + State = State0#state{flow=Flow}, + if + Flow0 > 0, Flow =< 0 -> + passive(State); + true -> + State + end. + +%% Request-line. + +-spec parse_request(Buffer, State, non_neg_integer()) + -> {request, cowboy_req:req(), State} + | {data, cowboy_stream:streamid(), cowboy_stream:fin(), binary(), State} + | {more, State} + when Buffer::binary(), State::#state{}. +%% Empty lines must be using \r\n. +parse_request(<< $\n, _/bits >>, State, _) -> + error_terminate(400, State, {connection_error, protocol_error, + 'Empty lines between requests must use the CRLF line terminator. (RFC7230 3.5)'}); +parse_request(<< $\s, _/bits >>, State, _) -> + error_terminate(400, State, {connection_error, protocol_error, + 'The request-line must not begin with a space. (RFC7230 3.1.1, RFC7230 3.5)'}); +%% We limit the length of the Request-line to MaxLength to avoid endlessly +%% reading from the socket and eventually crashing. +parse_request(Buffer, State=#state{opts=Opts, in_streamid=InStreamID}, EmptyLines) -> + MaxLength = maps:get(max_request_line_length, Opts, 8000), + MaxEmptyLines = maps:get(max_empty_lines, Opts, 5), + case match_eol(Buffer, 0) of + nomatch when byte_size(Buffer) > MaxLength -> + error_terminate(414, State, {connection_error, limit_reached, + 'The request-line length is larger than configuration allows. (RFC7230 3.1.1)'}); + nomatch -> + {more, State#state{buffer=Buffer, in_state=#ps_request_line{empty_lines=EmptyLines}}}; + 1 when EmptyLines =:= MaxEmptyLines -> + error_terminate(400, State, {connection_error, limit_reached, + 'More empty lines were received than configuration allows. (RFC7230 3.5)'}); + 1 -> + << _:16, Rest/bits >> = Buffer, + parse_request(Rest, State, EmptyLines + 1); + _ -> + case Buffer of + %% @todo * is only for server-wide OPTIONS request (RFC7230 5.3.4); tests + << "OPTIONS * ", Rest/bits >> -> + parse_version(Rest, State, <<"OPTIONS">>, undefined, <<"*">>, <<>>); + <<"CONNECT ", _/bits>> -> + error_terminate(501, State, {connection_error, no_error, + 'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'}); + <<"TRACE ", _/bits>> -> + error_terminate(501, State, {connection_error, no_error, + 'The TRACE method is currently not implemented. (RFC7231 4.3.8)'}); + %% Accept direct HTTP/2 only at the beginning of the connection. + << "PRI * HTTP/2.0\r\n", _/bits >> when InStreamID =:= 1 -> + %% @todo Might be worth throwing to get a clean stacktrace. + http2_upgrade(State, Buffer); + _ -> + parse_method(Buffer, State, <<>>, + maps:get(max_method_length, Opts, 32)) + end + end. + +match_eol(<< $\n, _/bits >>, N) -> + N; +match_eol(<< _, Rest/bits >>, N) -> + match_eol(Rest, N + 1); +match_eol(_, _) -> + nomatch. + +parse_method(_, State, _, 0) -> + error_terminate(501, State, {connection_error, limit_reached, + 'The method name is longer than configuration allows. (RFC7230 3.1.1)'}); +parse_method(<< C, Rest/bits >>, State, SoFar, Remaining) -> + case C of + $\r -> error_terminate(400, State, {connection_error, protocol_error, + 'The method name must not be followed with a line break. (RFC7230 3.1.1)'}); + $\s -> parse_uri(Rest, State, SoFar); + _ when ?IS_TOKEN(C) -> parse_method(Rest, State, << SoFar/binary, C >>, Remaining - 1); + _ -> error_terminate(400, State, {connection_error, protocol_error, + 'The method name must contain only valid token characters. (RFC7230 3.1.1)'}) + end. + +parse_uri(<< H, T, T, P, "://", Rest/bits >>, State, Method) + when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T; + P =:= $p orelse P =:= $P -> + parse_uri_authority(Rest, State, Method); +parse_uri(<< H, T, T, P, S, "://", Rest/bits >>, State, Method) + when H =:= $h orelse H =:= $H, T =:= $t orelse T =:= $T; + P =:= $p orelse P =:= $P; S =:= $s orelse S =:= $S -> + parse_uri_authority(Rest, State, Method); +parse_uri(<< $/, Rest/bits >>, State, Method) -> + parse_uri_path(Rest, State, Method, undefined, <<$/>>); +parse_uri(_, State, _) -> + error_terminate(400, State, {connection_error, protocol_error, + 'Invalid request-line or request-target. (RFC7230 3.1.1, RFC7230 5.3)'}). + +%% @todo We probably want to apply max_authority_length also +%% to the host header and to document this option. It might +%% also be useful for HTTP/2 requests. +parse_uri_authority(Rest, State=#state{opts=Opts}, Method) -> + parse_uri_authority(Rest, State, Method, <<>>, + maps:get(max_authority_length, Opts, 255)). + +parse_uri_authority(_, State, _, _, 0) -> + error_terminate(414, State, {connection_error, limit_reached, + 'The authority component of the absolute URI is longer than configuration allows. (RFC7230 2.7.1)'}); +parse_uri_authority(<>, State, Method, SoFar, Remaining) -> + case C of + $\r -> + error_terminate(400, State, {connection_error, protocol_error, + 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); + $@ -> + error_terminate(400, State, {connection_error, protocol_error, + 'Absolute URIs must not include a userinfo component. (RFC7230 2.7.1)'}); + C when SoFar =:= <<>> andalso + ((C =:= $/) orelse (C =:= $\s) orelse (C =:= $?) orelse (C =:= $#)) -> + error_terminate(400, State, {connection_error, protocol_error, + 'Absolute URIs must include a non-empty host component. (RFC7230 2.7.1)'}); + $: when SoFar =:= <<>> -> + error_terminate(400, State, {connection_error, protocol_error, + 'Absolute URIs must include a non-empty host component. (RFC7230 2.7.1)'}); + $/ -> parse_uri_path(Rest, State, Method, SoFar, <<"/">>); + $\s -> parse_version(Rest, State, Method, SoFar, <<"/">>, <<>>); + $? -> parse_uri_query(Rest, State, Method, SoFar, <<"/">>, <<>>); + $# -> skip_uri_fragment(Rest, State, Method, SoFar, <<"/">>, <<>>); + C -> parse_uri_authority(Rest, State, Method, <>, Remaining - 1) + end. + +parse_uri_path(<>, State, Method, Authority, SoFar) -> + case C of + $\r -> error_terminate(400, State, {connection_error, protocol_error, + 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); + $\s -> parse_version(Rest, State, Method, Authority, SoFar, <<>>); + $? -> parse_uri_query(Rest, State, Method, Authority, SoFar, <<>>); + $# -> skip_uri_fragment(Rest, State, Method, Authority, SoFar, <<>>); + _ -> parse_uri_path(Rest, State, Method, Authority, <>) + end. + +parse_uri_query(<>, State, M, A, P, SoFar) -> + case C of + $\r -> error_terminate(400, State, {connection_error, protocol_error, + 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); + $\s -> parse_version(Rest, State, M, A, P, SoFar); + $# -> skip_uri_fragment(Rest, State, M, A, P, SoFar); + _ -> parse_uri_query(Rest, State, M, A, P, <>) + end. + +skip_uri_fragment(<>, State, M, A, P, Q) -> + case C of + $\r -> error_terminate(400, State, {connection_error, protocol_error, + 'The request-target must not be followed by a line break. (RFC7230 3.1.1)'}); + $\s -> parse_version(Rest, State, M, A, P, Q); + _ -> skip_uri_fragment(Rest, State, M, A, P, Q) + end. + +parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, State, M, A, P, Q) -> + before_parse_headers(Rest, State, M, A, P, Q, 'HTTP/1.1'); +parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, State, M, A, P, Q) -> + before_parse_headers(Rest, State, M, A, P, Q, 'HTTP/1.0'); +parse_version(<< "HTTP/1.", _, C, _/bits >>, State, _, _, _, _) when C =:= $\s; C =:= $\t -> + error_terminate(400, State, {connection_error, protocol_error, + 'Whitespace is not allowed after the HTTP version. (RFC7230 3.1.1)'}); +parse_version(<< C, _/bits >>, State, _, _, _, _) when C =:= $\s; C =:= $\t -> + error_terminate(400, State, {connection_error, protocol_error, + 'The separator between request target and version must be a single SP. (RFC7230 3.1.1)'}); +parse_version(_, State, _, _, _, _) -> + error_terminate(505, State, {connection_error, protocol_error, + 'Unsupported HTTP version. (RFC7230 2.6)'}). + +before_parse_headers(Rest, State, M, A, P, Q, V) -> + parse_header(Rest, State#state{in_state=#ps_header{ + method=M, authority=A, path=P, qs=Q, version=V}}, #{}). + +%% Headers. + +%% We need two or more bytes in the buffer to continue. +parse_header(Rest, State=#state{in_state=PS}, Headers) when byte_size(Rest) < 2 -> + {more, State#state{buffer=Rest, in_state=PS#ps_header{headers=Headers}}}; +parse_header(<< $\r, $\n, Rest/bits >>, S, Headers) -> + request(Rest, S, Headers); +parse_header(Buffer, State=#state{opts=Opts, in_state=PS}, Headers) -> + MaxHeaders = maps:get(max_headers, Opts, 100), + NumHeaders = maps:size(Headers), + if + NumHeaders >= MaxHeaders -> + error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}}, + {connection_error, limit_reached, + 'The number of headers is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); + true -> + parse_header_colon(Buffer, State, Headers) + end. + +parse_header_colon(Buffer, State=#state{opts=Opts, in_state=PS}, Headers) -> + MaxLength = maps:get(max_header_name_length, Opts, 64), + case match_colon(Buffer, 0) of + nomatch when byte_size(Buffer) > MaxLength -> + error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}}, + {connection_error, limit_reached, + 'A header name is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); + nomatch -> + %% We don't have a colon but we might have an invalid header line, + %% so check if we have an LF and abort with an error if we do. + case match_eol(Buffer, 0) of + nomatch -> + {more, State#state{buffer=Buffer, in_state=PS#ps_header{headers=Headers}}}; + _ -> + error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, + {connection_error, protocol_error, + 'A header line is missing a colon separator. (RFC7230 3.2.4)'}) + end; + _ -> + parse_hd_name(Buffer, State, Headers, <<>>) + end. + +match_colon(<< $:, _/bits >>, N) -> + N; +match_colon(<< _, Rest/bits >>, N) -> + match_colon(Rest, N + 1); +match_colon(_, _) -> + nomatch. + +parse_hd_name(<< $:, Rest/bits >>, State, H, SoFar) -> + parse_hd_before_value(Rest, State, H, SoFar); +parse_hd_name(<< C, _/bits >>, State=#state{in_state=PS}, H, <<>>) when ?IS_WS(C) -> + error_terminate(400, State#state{in_state=PS#ps_header{headers=H}}, + {connection_error, protocol_error, + 'Whitespace is not allowed before the header name. (RFC7230 3.2)'}); +parse_hd_name(<< C, _/bits >>, State=#state{in_state=PS}, H, _) when ?IS_WS(C) -> + error_terminate(400, State#state{in_state=PS#ps_header{headers=H}}, + {connection_error, protocol_error, + 'Whitespace is not allowed between the header name and the colon. (RFC7230 3.2.4)'}); +parse_hd_name(<< C, Rest/bits >>, State, H, SoFar) -> + ?LOWER(parse_hd_name, Rest, State, H, SoFar). + +parse_hd_before_value(<< $\s, Rest/bits >>, S, H, N) -> + parse_hd_before_value(Rest, S, H, N); +parse_hd_before_value(<< $\t, Rest/bits >>, S, H, N) -> + parse_hd_before_value(Rest, S, H, N); +parse_hd_before_value(Buffer, State=#state{opts=Opts, in_state=PS}, H, N) -> + MaxLength = maps:get(max_header_value_length, Opts, 4096), + case match_eol(Buffer, 0) of + nomatch when byte_size(Buffer) > MaxLength -> + error_terminate(431, State#state{in_state=PS#ps_header{headers=H}}, + {connection_error, limit_reached, + 'A header value is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); + nomatch -> + {more, State#state{buffer=Buffer, in_state=PS#ps_header{headers=H, name=N}}}; + _ -> + parse_hd_value(Buffer, State, H, N, <<>>) + end. + +parse_hd_value(<< $\r, $\n, Rest/bits >>, S, Headers0, Name, SoFar) -> + Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1), + Headers = case maps:get(Name, Headers0, undefined) of + undefined -> Headers0#{Name => Value}; + %% The cookie header does not use proper HTTP header lists. + Value0 when Name =:= <<"cookie">> -> Headers0#{Name => << Value0/binary, "; ", Value/binary >>}; + Value0 -> Headers0#{Name => << Value0/binary, ", ", Value/binary >>} + end, + parse_header(Rest, S, Headers); +parse_hd_value(<< C, Rest/bits >>, S, H, N, SoFar) -> + parse_hd_value(Rest, S, H, N, << SoFar/binary, C >>). + +clean_value_ws_end(_, -1) -> + <<>>; +clean_value_ws_end(Value, N) -> + case binary:at(Value, N) of + $\s -> clean_value_ws_end(Value, N - 1); + $\t -> clean_value_ws_end(Value, N - 1); + _ -> + S = N + 1, + << Value2:S/binary, _/bits >> = Value, + Value2 + end. + +-ifdef(TEST). +clean_value_ws_end_test_() -> + Tests = [ + {<<>>, <<>>}, + {<<" ">>, <<>>}, + {<<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " + "text/html;level=2;q=0.4, */*;q=0.5 \t \t ">>, + <<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " + "text/html;level=2;q=0.4, */*;q=0.5">>} + ], + [{V, fun() -> R = clean_value_ws_end(V, byte_size(V) - 1) end} || {V, R} <- Tests]. + +horse_clean_value_ws_end() -> + horse:repeat(200000, + clean_value_ws_end( + <<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " + "text/html;level=2;q=0.4, */*;q=0.5 ">>, + byte_size(<<"text/*;q=0.3, text/html;q=0.7, text/html;level=1, " + "text/html;level=2;q=0.4, */*;q=0.5 ">>) - 1) + ). +-endif. + +request(Buffer, State=#state{transport=Transport, + in_state=PS=#ps_header{authority=Authority, version=Version}}, Headers) -> + case maps:get(<<"host">>, Headers, undefined) of + undefined when Version =:= 'HTTP/1.1' -> + %% @todo Might want to not close the connection on this and next one. + error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'}); + undefined -> + request(Buffer, State, Headers, <<>>, default_port(Transport:secure())); + %% @todo When CONNECT requests come in we need to ignore the RawHost + %% and instead use the Authority as the source of host. + RawHost when Authority =:= undefined; Authority =:= RawHost -> + request_parse_host(Buffer, State, Headers, RawHost); + %% RFC7230 does not explicitly ask us to reject requests + %% that have a different authority component and host header. + %% However it DOES ask clients to set them to the same value, + %% so we enforce that. + _ -> + error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'The host header is different than the absolute-form authority component. (RFC7230 5.4)'}) + end. + +request_parse_host(Buffer, State=#state{transport=Transport, in_state=PS}, Headers, RawHost) -> + try cow_http_hd:parse_host(RawHost) of + {Host, undefined} -> + request(Buffer, State, Headers, Host, default_port(Transport:secure())); + {Host, Port} when Port > 0, Port =< 65535 -> + request(Buffer, State, Headers, Host, Port); + _ -> + error_terminate(400, State, {stream_error, protocol_error, + 'The port component of the absolute-form is not in the range 0..65535. (RFC7230 2.7.1)'}) + catch _:_ -> + error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'The host header is invalid. (RFC7230 5.4)'}) + end. + +-spec default_port(boolean()) -> 80 | 443. +default_port(true) -> 443; +default_port(_) -> 80. + +%% End of request parsing. + +request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock, cert=Cert, + proxy_header=ProxyHeader, in_streamid=StreamID, in_state= + PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}}, + Headers, Host, Port) -> + Scheme = case Transport:secure() of + true -> <<"https">>; + false -> <<"http">> + end, + {HasBody, BodyLength, TDecodeFun, TDecodeState} = case Headers of + #{<<"transfer-encoding">> := _, <<"content-length">> := _} -> + error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'The request had both transfer-encoding and content-length headers. (RFC7230 3.3.3)'}); + #{<<"transfer-encoding">> := TransferEncoding0} -> + try cow_http_hd:parse_transfer_encoding(TransferEncoding0) of + [<<"chunked">>] -> + {true, undefined, fun cow_http_te:stream_chunked/2, {0, 0}}; + _ -> + error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'Cowboy only supports transfer-encoding: chunked. (RFC7230 3.3.1)'}) + catch _:_ -> + error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'The transfer-encoding header is invalid. (RFC7230 3.3.1)'}) + end; + #{<<"content-length">> := <<"0">>} -> + {false, 0, undefined, undefined}; + #{<<"content-length">> := BinLength} -> + Length = try + cow_http_hd:parse_content_length(BinLength) + catch _:_ -> + error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, protocol_error, + 'The content-length header is invalid. (RFC7230 3.3.2)'}) + end, + {true, Length, fun cow_http_te:stream_identity/2, {0, Length}}; + _ -> + {false, 0, undefined, undefined} + end, + Req0 = #{ + ref => Ref, + pid => self(), + streamid => StreamID, + peer => Peer, + sock => Sock, + cert => Cert, + method => Method, + scheme => Scheme, + host => Host, + port => Port, + path => Path, + qs => Qs, + version => Version, + %% We are transparently taking care of transfer-encodings so + %% the user code has no need to know about it. + headers => maps:remove(<<"transfer-encoding">>, Headers), + has_body => HasBody, + body_length => BodyLength + }, + %% We add the PROXY header information if any. + Req = case ProxyHeader of + undefined -> Req0; + _ -> Req0#{proxy_header => ProxyHeader} + end, + case is_http2_upgrade(Headers, Version) of + false -> + State = case HasBody of + true -> + State0#state{in_state=#ps_body{ + length = BodyLength, + transfer_decode_fun = TDecodeFun, + transfer_decode_state = TDecodeState + }}; + false -> + State0#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}} + end, + {request, Req, State#state{buffer=Buffer}}; + {true, HTTP2Settings} -> + %% We save the headers in case the upgrade will fail + %% and we need to pass them to cowboy_stream:early_error. + http2_upgrade(State0#state{in_state=PS#ps_header{headers=Headers}}, + Buffer, HTTP2Settings, Req) + end. + +%% HTTP/2 upgrade. + +%% @todo We must not upgrade to h2c over a TLS connection. +is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade, + <<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') -> + Conns = cow_http_hd:parse_connection(Conn), + case {lists:member(<<"upgrade">>, Conns), lists:member(<<"http2-settings">>, Conns)} of + {true, true} -> + Protocols = cow_http_hd:parse_upgrade(Upgrade), + case lists:member(<<"h2c">>, Protocols) of + true -> + {true, HTTP2Settings}; + false -> + false + end; + _ -> + false + end; +is_http2_upgrade(_, _) -> + false. + +%% Prior knowledge upgrade, without an HTTP/1.1 request. +http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, + proxy_header=ProxyHeader, opts=Opts, 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); + true -> + error_terminate(400, State, {connection_error, protocol_error, + 'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'}) + end. + +%% Upgrade via an HTTP/1.1 request. +http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, + proxy_header=ProxyHeader, opts=Opts, 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)'}) + end. + +%% Request body parsing. + +parse_body(Buffer, State=#state{in_streamid=StreamID, in_state= + PS=#ps_body{received=Received, transfer_decode_fun=TDecode, + transfer_decode_state=TState0}}) -> + %% @todo Proper trailers. + try TDecode(Buffer, TState0) of + more -> + {more, State#state{buffer=Buffer}}; + {more, Data, TState} -> + {data, StreamID, nofin, Data, State#state{buffer= <<>>, + in_state=PS#ps_body{received=Received + byte_size(Data), + transfer_decode_state=TState}}}; + {more, Data, _Length, TState} when is_integer(_Length) -> + {data, StreamID, nofin, Data, State#state{buffer= <<>>, + in_state=PS#ps_body{received=Received + byte_size(Data), + transfer_decode_state=TState}}}; + {more, Data, Rest, TState} -> + {data, StreamID, nofin, Data, State#state{buffer=Rest, + in_state=PS#ps_body{received=Received + byte_size(Data), + transfer_decode_state=TState}}}; + {done, _HasTrailers, Rest} -> + {data, StreamID, fin, <<>>, + State#state{buffer=Rest, in_streamid=StreamID + 1, in_state=#ps_request_line{}}}; + {done, Data, _HasTrailers, Rest} -> + {data, StreamID, fin, Data, + State#state{buffer=Rest, in_streamid=StreamID + 1, in_state=#ps_request_line{}}} + catch _:_ -> + Reason = {connection_error, protocol_error, + 'Failure to decode the content. (RFC7230 4)'}, + terminate(stream_terminate(State, StreamID, Reason), Reason) + end. + +%% Message handling. + +down(State=#state{opts=Opts, children=Children0}, Pid, Msg) -> + case cowboy_children:down(Children0, Pid) of + %% The stream was terminated already. + {ok, undefined, Children} -> + State#state{children=Children}; + %% The stream is still running. + {ok, StreamID, Children} -> + info(State#state{children=Children}, StreamID, Msg); + %% The process was unknown. + error -> + cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n", + [Msg, Pid], Opts), + State + end. + +info(State=#state{opts=Opts, streams=Streams0}, StreamID, Msg) -> + case lists:keyfind(StreamID, #stream.id, Streams0) of + Stream = #stream{state=StreamState0} -> + try cowboy_stream:info(StreamID, Msg, StreamState0) of + {Commands, StreamState} -> + Streams = lists:keyreplace(StreamID, #stream.id, Streams0, + Stream#stream{state=StreamState}), + commands(State#state{streams=Streams}, StreamID, Commands) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(info, + [StreamID, Msg, StreamState0], + Class, Exception, Stacktrace), Opts), + stream_terminate(State, StreamID, {internal_error, {Class, Exception}, + 'Unhandled exception in cowboy_stream:info/3.'}) + end; + false -> + cowboy:log(warning, "Received message ~p for unknown stream ~p.~n", + [Msg, StreamID], Opts), + State + end. + +%% Commands. +%% +%% The order in which the commands are given matters. Cowboy may +%% stop processing commands after the 'stop' command or when an +%% error occurred, such as a socket error. Critical commands such +%% as 'spawn' should always be given first. + +commands(State, _, []) -> + State; +%% Supervise a child process. +commands(State=#state{children=Children}, StreamID, [{spawn, Pid, Shutdown}|Tail]) -> + commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)}, + StreamID, Tail); +%% Error handling. +commands(State, StreamID, [Error = {internal_error, _, _}|Tail]) -> + commands(stream_terminate(State, StreamID, Error), StreamID, Tail); +%% Commands for a stream currently inactive. +commands(State=#state{out_streamid=Current, streams=Streams0}, StreamID, Commands) + when Current =/= StreamID -> + + %% @todo We still want to handle some commands... + + Stream = #stream{queue=Queue} = lists:keyfind(StreamID, #stream.id, Streams0), + Streams = lists:keyreplace(StreamID, #stream.id, Streams0, + Stream#stream{queue=Queue ++ Commands}), + State#state{streams=Streams}; +%% When we have finished reading the request body, do nothing. +commands(State=#state{flow=infinity}, StreamID, [{flow, _}|Tail]) -> + commands(State, StreamID, Tail); +%% Read the request body. +commands(State0=#state{flow=Flow0}, StreamID, [{flow, Size}|Tail]) -> + %% We must read *at least* Size of data otherwise functions + %% like cowboy_req:read_body/1,2 will wait indefinitely. + Flow = if + Flow0 < 0 -> Size; + true -> Flow0 + Size + end, + %% Reenable active mode if necessary. + State = if + Flow0 =< 0, Flow > 0 -> + active(State0); + true -> + State0 + end, + commands(State#state{flow=Flow}, StreamID, Tail); +%% Error responses are sent only if a response wasn't sent already. +commands(State=#state{out_state=wait, out_streamid=StreamID}, StreamID, + [{error_response, Status, Headers0, Body}|Tail]) -> + %% We close the connection when the error response is 408, as it + %% indicates a timeout and the RFC recommends that we stop here. (RFC7231 6.5.7) + Headers = case Status of + 408 -> Headers0#{<<"connection">> => <<"close">>}; + <<"408", _/bits>> -> Headers0#{<<"connection">> => <<"close">>}; + _ -> Headers0 + end, + commands(State, StreamID, [{response, Status, Headers, Body}|Tail]); +commands(State, StreamID, [{error_response, _, _, _}|Tail]) -> + commands(State, StreamID, Tail); +%% Send an informational response. +commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams}, + StreamID, [{inform, StatusCode, Headers}|Tail]) -> + %% @todo I'm pretty sure the last stream in the list is the one we want + %% considering all others are queued. + #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams), + _ = case Version of + 'HTTP/1.1' -> + ok = maybe_socket_error(State0, Transport:send(Socket, + cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)))); + %% Do not send informational responses to HTTP/1.0 clients. (RFC7231 6.2) + 'HTTP/1.0' -> + ok + end, + State = maybe_reset_idle_timeout(State0), + commands(State, StreamID, Tail); +%% Send a full response. +%% +%% @todo Kill the stream if it sent a response when one has already been sent. +%% @todo Keep IsFin in the state. +%% @todo Same two things above apply to DATA, possibly promise too. +commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams}, StreamID, + [{response, StatusCode, Headers0, Body}|Tail]) -> + %% @todo I'm pretty sure the last stream in the list is the one we want + %% considering all others are queued. + #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams), + {State1, Headers} = connection(State0, Headers0, StreamID, Version), + State2 = State1#state{out_state=done}, + %% @todo Ensure content-length is set. 204 must never have content-length set. + Response = cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)), + %% @todo 204 and 304 responses must not include a response body. (RFC7230 3.3.1, RFC7230 3.3.2) + case Body of + {sendfile, _, _, _} -> + ok = maybe_socket_error(State2, Transport:send(Socket, Response)), + sendfile(State2, Body); + _ -> + ok = maybe_socket_error(State2, Transport:send(Socket, [Response, Body])) + end, + State = maybe_reset_idle_timeout(State2), + commands(State, StreamID, Tail); +%% Send response headers and initiate chunked encoding or streaming. +commands(State0=#state{socket=Socket, transport=Transport, + opts=Opts, overriden_opts=Override, streams=Streams0, out_state=OutState}, + StreamID, [{headers, StatusCode, Headers0}|Tail]) -> + %% @todo Same as above (about the last stream in the list). + Stream = #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams0), + Status = cow_http:status_to_integer(StatusCode), + ContentLength = maps:get(<<"content-length">>, Headers0, undefined), + %% Chunked transfer-encoding can be disabled on a per-request basis. + Chunked = case Override of + #{chunked := Chunked0} -> Chunked0; + _ -> maps:get(chunked, Opts, true) + end, + {State1, Headers1} = case {Status, ContentLength, Version} of + {204, _, 'HTTP/1.1'} -> + {State0#state{out_state=done}, Headers0}; + {304, _, 'HTTP/1.1'} -> + {State0#state{out_state=done}, Headers0}; + {_, undefined, 'HTTP/1.1'} when Chunked -> + {State0#state{out_state=chunked}, Headers0#{<<"transfer-encoding">> => <<"chunked">>}}; + %% Close the connection after streaming without content-length + %% to all HTTP/1.0 clients and to HTTP/1.1 clients when chunked is disabled. + {_, undefined, _} -> + {State0#state{out_state=streaming, last_streamid=StreamID}, Headers0}; + %% Stream the response body without chunked transfer-encoding. + _ -> + ExpectedSize = cow_http_hd:parse_content_length(ContentLength), + Streams = lists:keyreplace(StreamID, #stream.id, Streams0, + Stream#stream{local_expected_size=ExpectedSize}), + {State0#state{out_state=streaming, streams=Streams}, Headers0} + end, + Headers2 = case stream_te(OutState, Stream) of + trailers -> Headers1; + _ -> maps:remove(<<"trailer">>, Headers1) + end, + {State2, Headers} = connection(State1, Headers2, StreamID, Version), + ok = maybe_socket_error(State2, Transport:send(Socket, + cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)))), + State = maybe_reset_idle_timeout(State2), + commands(State, StreamID, Tail); +%% Send a response body chunk. +%% @todo We need to kill the stream if it tries to send data before headers. +commands(State0=#state{socket=Socket, transport=Transport, streams=Streams0, out_state=OutState}, + StreamID, [{data, IsFin, Data}|Tail]) -> + %% Do not send anything when the user asks to send an empty + %% data frame, as that would break the protocol. + Size = case Data of + {sendfile, _, B, _} -> B; + _ -> iolist_size(Data) + end, + %% Depending on the current state we may need to send nothing, + %% the last chunk, chunked data with/without the last chunk, + %% or just the data as-is. + Stream = case lists:keyfind(StreamID, #stream.id, Streams0) of + Stream0=#stream{method= <<"HEAD">>} -> + Stream0; + Stream0 when Size =:= 0, IsFin =:= fin, OutState =:= chunked -> + ok = maybe_socket_error(State0, + Transport:send(Socket, <<"0\r\n\r\n">>)), + Stream0; + Stream0 when Size =:= 0 -> + Stream0; + Stream0 when is_tuple(Data), OutState =:= chunked -> + ok = maybe_socket_error(State0, + Transport:send(Socket, [integer_to_binary(Size, 16), <<"\r\n">>])), + sendfile(State0, Data), + ok = maybe_socket_error(State0, + Transport:send(Socket, + case IsFin of + fin -> <<"\r\n0\r\n\r\n">>; + nofin -> <<"\r\n">> + end) + ), + Stream0; + Stream0 when OutState =:= chunked -> + ok = maybe_socket_error(State0, + Transport:send(Socket, [ + integer_to_binary(Size, 16), <<"\r\n">>, Data, + case IsFin of + fin -> <<"\r\n0\r\n\r\n">>; + nofin -> <<"\r\n">> + end + ]) + ), + Stream0; + Stream0 when OutState =:= streaming -> + #stream{local_sent_size=SentSize0, local_expected_size=ExpectedSize} = Stream0, + SentSize = SentSize0 + Size, + if + %% ExpectedSize may be undefined, which is > any integer value. + SentSize > ExpectedSize -> + terminate(State0, response_body_too_large); + is_tuple(Data) -> + sendfile(State0, Data); + true -> + ok = maybe_socket_error(State0, Transport:send(Socket, Data)) + end, + Stream0#stream{local_sent_size=SentSize} + end, + State1 = case IsFin of + fin -> State0#state{out_state=done}; + nofin -> State0 + end, + State = maybe_reset_idle_timeout(State1), + Streams = lists:keyreplace(StreamID, #stream.id, Streams0, Stream), + commands(State#state{streams=Streams}, StreamID, Tail); +commands(State0=#state{socket=Socket, transport=Transport, streams=Streams, out_state=OutState}, + StreamID, [{trailers, Trailers}|Tail]) -> + case stream_te(OutState, lists:keyfind(StreamID, #stream.id, Streams)) of + trailers -> + ok = maybe_socket_error(State0, + Transport:send(Socket, [ + <<"0\r\n">>, + cow_http:headers(maps:to_list(Trailers)), + <<"\r\n">> + ]) + ); + no_trailers -> + ok = maybe_socket_error(State0, + Transport:send(Socket, <<"0\r\n\r\n">>)); + not_chunked -> + ok + end, + State = maybe_reset_idle_timeout(State0#state{out_state=done}), + commands(State, StreamID, Tail); +%% Protocol takeover. +commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transport, + out_state=OutState, opts=Opts, buffer=Buffer, children=Children}, StreamID, + [{switch_protocol, Headers, Protocol, InitialState}|_Tail]) -> + %% @todo If there's streams opened after this one, fail instead of 101. + State1 = cancel_timeout(State0), + %% Before we send the 101 response we need to stop receiving data + %% from the socket, otherwise the data might be receive before the + %% call to flush/0 and we end up inadvertently dropping a packet. + %% + %% @todo Handle cases where the request came with a body. We need + %% to process or skip the body before the upgrade can be completed. + State = passive(State1), + %% Send a 101 response if necessary, then terminate the stream. + #state{streams=Streams} = case OutState of + wait -> info(State, StreamID, {inform, 101, Headers}); + _ -> State + end, + #stream{state=StreamState} = lists:keyfind(StreamID, #stream.id, Streams), + stream_call_terminate(StreamID, switch_protocol, StreamState, State), + %% Terminate children processes and flush any remaining messages from the mailbox. + cowboy_children:terminate(Children), + flush(Parent), + %% Turn off the trap_exit process flag + %% since this process will no longer be a supervisor. + process_flag(trap_exit, false), + Protocol:takeover(Parent, Ref, Socket, Transport, Opts, 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}}, + idle_timeout); + _ -> + State0 + end, + State = case SetOpts of + #{chunked := Chunked} -> + State1#state{overriden_opts=Opts#{chunked => Chunked}}; + _ -> + State1 + end, + commands(State, StreamID, Tail); +%% Stream shutdown. +commands(State, StreamID, [stop|Tail]) -> + %% @todo Do we want to run the commands after a stop? + %% @todo We currently wait for the stop command before we + %% continue with the next request/response. In theory, if + %% the request body was read fully and the response body + %% was sent fully we should be able to start working on + %% the next request concurrently. This can be done as a + %% future optimization. + maybe_terminate(State, StreamID, Tail); +%% Log event. +commands(State=#state{opts=Opts}, StreamID, [Log={log, _, _, _}|Tail]) -> + cowboy:log(Log, Opts), + commands(State, StreamID, Tail); +%% HTTP/1.1 does not support push; ignore. +commands(State, StreamID, [{push, _, _, _, _, _, _, _}|Tail]) -> + commands(State, StreamID, Tail). + +%% The set-cookie header is special; we can only send one cookie per header. +headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) -> + Headers1 = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)), + Headers1 ++ [{<<"set-cookie">>, Value} || Value <- SetCookies]; +headers_to_list(Headers) -> + maps:to_list(Headers). + +%% We wrap the sendfile call into a try/catch because on OTP-20 +%% and earlier a few different crashes could occur for sockets +%% that were closing or closed. For example a badarg in +%% erlang:port_get_data(#Port<...>) or a badmatch like +%% {{badmatch,{error,einval}},[{prim_file,sendfile,8,[]}... +%% +%% OTP-21 uses a NIF instead of a port so the implementation +%% and behavior has dramatically changed and it is unclear +%% whether it will be necessary in the future. +%% +%% This try/catch prevents some noisy logs to be written +%% when these errors occur. +sendfile(State=#state{socket=Socket, transport=Transport, opts=Opts}, + {sendfile, Offset, Bytes, Path}) -> + try + %% When sendfile is disabled we explicitly use the fallback. + {ok, _} = maybe_socket_error(State, + case maps:get(sendfile, Opts, true) of + true -> Transport:sendfile(Socket, Path, Offset, Bytes); + false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, []) + end + ), + ok + catch _:_ -> + terminate(State, {socket_error, sendfile_crash, + 'An error occurred when using the sendfile function.'}) + end. + +%% Flush messages specific to cowboy_http before handing over the +%% connection to another protocol. +flush(Parent) -> + receive + {timeout, _, _} -> + flush(Parent); + {{Pid, _}, _} when Pid =:= self() -> + flush(Parent); + {'EXIT', Pid, _} when Pid =/= Parent -> + flush(Parent) + after 0 -> + ok + end. + +%% @todo In these cases I'm not sure if we should continue processing commands. +maybe_terminate(State=#state{last_streamid=StreamID}, StreamID, _Tail) -> + terminate(stream_terminate(State, StreamID, normal), normal); %% @todo Reason ok? +maybe_terminate(State, StreamID, _Tail) -> + stream_terminate(State, StreamID, normal). + +stream_terminate(State0=#state{opts=Opts, in_streamid=InStreamID, in_state=InState, + out_streamid=OutStreamID, out_state=OutState, streams=Streams0, + children=Children0}, StreamID, Reason) -> + #stream{version=Version, local_expected_size=ExpectedSize, local_sent_size=SentSize} + = lists:keyfind(StreamID, #stream.id, Streams0), + %% Send a response or terminate chunks depending on the current output state. + State1 = #state{streams=Streams1} = case OutState of + wait when element(1, Reason) =:= internal_error -> + info(State0, StreamID, {response, 500, #{<<"content-length">> => <<"0">>}, <<>>}); + wait when element(1, Reason) =:= connection_error -> + info(State0, StreamID, {response, 400, #{<<"content-length">> => <<"0">>}, <<>>}); + wait -> + info(State0, StreamID, {response, 204, #{}, <<>>}); + chunked when Version =:= 'HTTP/1.1' -> + info(State0, StreamID, {data, fin, <<>>}); + streaming when SentSize < ExpectedSize -> + terminate(State0, response_body_too_small); + _ -> %% done or Version =:= 'HTTP/1.0' + State0 + end, + %% Stop the stream, shutdown children and reset overriden options. + {value, #stream{state=StreamState}, Streams} + = lists:keytake(StreamID, #stream.id, Streams1), + stream_call_terminate(StreamID, Reason, StreamState, State1), + Children = cowboy_children:shutdown(Children0, StreamID), + State = State1#state{overriden_opts=#{}, streams=Streams, children=Children}, + %% We want to drop the connection if the body was not read fully + %% and we don't know its length or more remains to be read than + %% configuration allows. + MaxSkipBodyLength = maps:get(max_skip_body_length, Opts, 1000000), + case InState of + #ps_body{length=undefined} + when InStreamID =:= OutStreamID -> + terminate(State, skip_body_unknown_length); + #ps_body{length=Len, received=Received} + when InStreamID =:= OutStreamID, Received + MaxSkipBodyLength < Len -> + terminate(State, skip_body_too_large); + #ps_body{} when InStreamID =:= OutStreamID -> + stream_next(State#state{flow=infinity}); + _ -> + stream_next(State) + end. + +stream_next(State0=#state{opts=Opts, active=Active, out_streamid=OutStreamID, streams=Streams}) -> + NextOutStreamID = OutStreamID + 1, + case lists:keyfind(NextOutStreamID, #stream.id, Streams) of + false -> + State = State0#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}, + NextOutStreamID, Commands) + end. + +stream_call_terminate(StreamID, Reason, StreamState, #state{opts=Opts}) -> + try + cowboy_stream:terminate(StreamID, Reason, StreamState) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(terminate, + [StreamID, Reason, StreamState], + Class, Exception, Stacktrace), Opts) + end. + +maybe_req_close(#state{opts=#{http10_keepalive := false}}, _, 'HTTP/1.0') -> + close; +maybe_req_close(_, #{<<"connection">> := Conn}, 'HTTP/1.0') -> + try cow_http_hd:parse_connection(Conn) of + Conns -> + case lists:member(<<"keep-alive">>, Conns) of + true -> keepalive; + false -> close + end + catch _:_ -> + bad_connection_header + end; +maybe_req_close(_, _, 'HTTP/1.0') -> + close; +maybe_req_close(_, #{<<"connection">> := Conn}, 'HTTP/1.1') -> + try connection_hd_is_close(Conn) of + true -> close; + false -> keepalive + catch _:_ -> + bad_connection_header + end; +maybe_req_close(_, _, _) -> + keepalive. + +connection(State=#state{last_streamid=StreamID}, Headers=#{<<"connection">> := Conn}, StreamID, _) -> + case connection_hd_is_close(Conn) of + true -> {State, Headers}; + %% @todo Here we need to remove keep-alive and add close, not just add close. + false -> {State, Headers#{<<"connection">> => [<<"close, ">>, Conn]}} + end; +connection(State=#state{last_streamid=StreamID}, Headers, StreamID, _) -> + {State, Headers#{<<"connection">> => <<"close">>}}; +connection(State, Headers=#{<<"connection">> := Conn}, StreamID, _) -> + case connection_hd_is_close(Conn) of + true -> {State#state{last_streamid=StreamID}, Headers}; + %% @todo Here we need to set keep-alive only if it wasn't set before. + false -> {State, Headers} + end; +connection(State, Headers, _, 'HTTP/1.0') -> + {State, Headers#{<<"connection">> => <<"keep-alive">>}}; +connection(State, Headers, _, _) -> + {State, Headers}. + +connection_hd_is_close(Conn) -> + Conns = cow_http_hd:parse_connection(iolist_to_binary(Conn)), + lists:member(<<"close">>, Conns). + +stream_te(streaming, _) -> + not_chunked; +%% No TE header was sent. +stream_te(_, #stream{te=undefined}) -> + no_trailers; +stream_te(_, #stream{te=TE0}) -> + try cow_http_hd:parse_te(TE0) of + {TE1, _} -> TE1 + catch _:_ -> + %% If we can't parse the TE header, assume we can't send trailers. + no_trailers + end. + +%% This function is only called when an error occurs on a new stream. +-spec error_terminate(cowboy:http_status(), #state{}, _) -> no_return(). +error_terminate(StatusCode, State=#state{ref=Ref, peer=Peer, in_state=StreamState}, Reason) -> + PartialReq = case StreamState of + #ps_request_line{} -> #{ + ref => Ref, + peer => Peer + }; + #ps_header{method=Method, path=Path, qs=Qs, + version=Version, headers=ReqHeaders} -> #{ + ref => Ref, + peer => Peer, + method => Method, + path => Path, + qs => Qs, + version => Version, + headers => case ReqHeaders of + undefined -> #{}; + _ -> ReqHeaders + end + } + end, + early_error(StatusCode, State, Reason, PartialReq, #{<<"connection">> => <<"close">>}), + terminate(State, Reason). + +early_error(StatusCode, State, Reason, PartialReq) -> + early_error(StatusCode, State, Reason, PartialReq, #{}). + +early_error(StatusCode0, State=#state{socket=Socket, transport=Transport, + opts=Opts, in_streamid=StreamID}, Reason, PartialReq, RespHeaders0) -> + RespHeaders1 = RespHeaders0#{<<"content-length">> => <<"0">>}, + Resp = {response, StatusCode0, RespHeaders1, <<>>}, + try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of + {response, StatusCode, RespHeaders, RespBody} -> + ok = maybe_socket_error(State, + Transport:send(Socket, [ + cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(RespHeaders)), + %% @todo We shouldn't send the body when the method is HEAD. + %% @todo Technically we allow the sendfile tuple. + RespBody + ]) + ) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(early_error, + [StreamID, Reason, PartialReq, Resp, Opts], + Class, Exception, Stacktrace), Opts), + %% We still need to send an error response, so send what we initially + %% wanted to send. It's better than nothing. + ok = maybe_socket_error(State, + Transport:send(Socket, cow_http:response(StatusCode0, + 'HTTP/1.1', maps:to_list(RespHeaders1))) + ) + end. + +initiate_closing(State=#state{streams=[]}, Reason) -> + terminate(State, Reason); +initiate_closing(State=#state{streams=Streams, + out_streamid=OutStreamID}, Reason) -> + {value, LastStream, TerminatedStreams} + = lists:keytake(OutStreamID, #stream.id, Streams), + terminate_all_streams(State, TerminatedStreams, Reason), + State#state{streams=[LastStream], last_streamid=OutStreamID}. + +%% Function replicated in cowboy_http2. +maybe_socket_error(State, {error, closed}) -> + terminate(State, {socket_error, closed, 'The socket has been closed.'}); +maybe_socket_error(State, Reason) -> + maybe_socket_error(State, Reason, 'An error has occurred on the socket.'). + +maybe_socket_error(_, Result = ok, _) -> + Result; +maybe_socket_error(_, Result = {ok, _}, _) -> + Result; +maybe_socket_error(State, {error, Reason}, Human) -> + terminate(State, {socket_error, Reason, Human}). + +-spec terminate(_, _) -> no_return(). +terminate(undefined, Reason) -> + exit({shutdown, Reason}); +terminate(State=#state{streams=Streams, children=Children}, Reason) -> + terminate_all_streams(State, Streams, Reason), + cowboy_children:terminate(Children), + terminate_linger(State), + exit({shutdown, Reason}). + +terminate_all_streams(_, [], _) -> + ok; +terminate_all_streams(State, [#stream{id=StreamID, state=StreamState}|Tail], Reason) -> + stream_call_terminate(StreamID, Reason, StreamState, State), + terminate_all_streams(State, Tail, Reason). + +terminate_linger(State=#state{socket=Socket, transport=Transport, opts=Opts}) -> + case Transport:shutdown(Socket, write) of + ok -> + case maps:get(linger_timeout, Opts, 1000) of + 0 -> + ok; + infinity -> + terminate_linger_before_loop(State, undefined, Transport:messages()); + Timeout -> + TimerRef = erlang:start_timer(Timeout, self(), linger_timeout), + terminate_linger_before_loop(State, TimerRef, Transport:messages()) + end; + {error, _} -> + ok + end. + +terminate_linger_before_loop(State, TimerRef, Messages) -> + %% We may already be in active mode when we do this + %% but it's OK because we are shutting down anyway. + %% + %% We specially handle the socket error to terminate + %% when an error occurs. + case setopts_active(State) of + ok -> + terminate_linger_loop(State, TimerRef, Messages); + {error, _} -> + ok + end. + +terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) -> + receive + {OK, Socket, _} when OK =:= element(1, Messages) -> + terminate_linger_loop(State, TimerRef, Messages); + {Closed, Socket} when Closed =:= element(2, Messages) -> + ok; + {Error, Socket, _} when Error =:= element(3, Messages) -> + ok; + {Passive, Socket} when Passive =:= tcp_passive; Passive =:= ssl_passive -> + terminate_linger_before_loop(State, TimerRef, Messages); + {timeout, TimerRef, linger_timeout} -> + ok; + _ -> + terminate_linger_loop(State, TimerRef, Messages) + end. + +%% System callbacks. + +-spec system_continue(_, _, #state{}) -> ok. +system_continue(_, _, State) -> + 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)). + +-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}. +system_code_change(Misc, _, _, _) -> + {ok, Misc}. diff --git a/cowboy/src/cowboy_http2.erl b/cowboy/src/cowboy_http2.erl new file mode 100644 index 0000000..5b1f1e1 --- /dev/null +++ b/cowboy/src/cowboy_http2.erl @@ -0,0 +1,1354 @@ +%% Copyright (c) 2015-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_http2). + +-export([init/6]). +-export([init/10]). +-export([init/12]). + +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +-type opts() :: #{ + active_n => pos_integer(), + 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, + enable_connect_protocol => boolean(), + env => cowboy_middleware:env(), + goaway_initial_timeout => timeout(), + goaway_complete_timeout => timeout(), + idle_timeout => timeout(), + inactivity_timeout => timeout(), + initial_connection_window_size => 65535..16#7fffffff, + initial_stream_window_size => 0..16#7fffffff, + linger_timeout => timeout(), + logger => module(), + max_concurrent_streams => non_neg_integer() | infinity, + max_connection_buffer_size => non_neg_integer(), + max_connection_window_size => 0..16#7fffffff, + max_decode_table_size => non_neg_integer(), + max_encode_table_size => non_neg_integer(), + max_fragmented_header_block_size => 16384..16#7fffffff, + max_frame_size_received => 16384..16777215, + max_frame_size_sent => 16384..16777215 | infinity, + max_received_frame_rate => {pos_integer(), timeout()}, + max_reset_stream_rate => {pos_integer(), timeout()}, + max_cancel_stream_rate => {pos_integer(), timeout()}, + max_stream_buffer_size => non_neg_integer(), + max_stream_window_size => 0..16#7fffffff, + metrics_callback => cowboy_metrics_h:metrics_callback(), + metrics_req_filter => fun((cowboy_req:req()) -> map()), + metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()), + middlewares => [module()], + preface_timeout => timeout(), + proxy_header => boolean(), + reset_idle_timeout_on_send => boolean(), + sendfile => boolean(), + settings_timeout => timeout(), + shutdown_timeout => timeout(), + stream_handlers => [module()], + stream_window_data_threshold => 0..16#7fffffff, + stream_window_margin_size => 0..16#7fffffff, + stream_window_update_threshold => 0..16#7fffffff, + tracer_callback => cowboy_tracer_h:tracer_callback(), + tracer_flags => [atom()], + tracer_match_specs => cowboy_tracer_h:tracer_match_specs(), + %% Open ended because configured stream handlers might add options. + _ => _ +}. +-export_type([opts/0]). + +-record(stream, { + %% Whether the stream is currently stopping. + status = running :: running | stopping, + + %% Flow requested for this stream. + flow = 0 :: non_neg_integer(), + + %% Stream state. + state :: {module, any()} +}). + +-record(state, { + parent = undefined :: pid(), + ref :: ranch:ref(), + socket = undefined :: inet:socket(), + transport :: module(), + proxy_header :: undefined | ranch_proxy_header:proxy_info(), + opts = #{} :: opts(), + + %% Timer for idle_timeout; also used for goaway timers. + timer = undefined :: undefined | reference(), + + %% Remote address and port for the connection. + peer = undefined :: {inet:ip_address(), inet:port_number()}, + + %% Local address and port for the connection. + sock = undefined :: {inet:ip_address(), inet:port_number()}, + + %% Client certificate (TLS only). + cert :: undefined | binary(), + + %% HTTP/2 state machine. + http2_status :: sequence | settings | upgrade | connected | closing_initiated | closing, + http2_machine :: cow_http2_machine:http2_machine(), + + %% HTTP/2 frame rate flood protection. + frame_rate_num :: undefined | pos_integer(), + frame_rate_time :: undefined | integer(), + + %% HTTP/2 reset stream flood protection. + reset_rate_num :: undefined | pos_integer(), + reset_rate_time :: undefined | integer(), + + %% HTTP/2 rapid reset attack protection. + cancel_rate_num :: undefined | pos_integer(), + cancel_rate_time :: undefined | integer(), + + %% Flow requested for all streams. + flow = 0 :: non_neg_integer(), + + %% Currently active HTTP/2 streams. Streams may be initiated either + %% by the client or by the server through PUSH_PROMISE frames. + streams = #{} :: #{cow_http2:streamid() => #stream{}}, + + %% Streams can spawn zero or more children which are then managed + %% by this module if operating as a supervisor. + children = cowboy_children:init() :: cowboy_children:children() +}). + +-spec init(pid(), ranch:ref(), inet:socket(), module(), + ranch_proxy_header:proxy_info() | undefined, cowboy:opts()) -> ok. +init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) -> + {ok, Peer} = maybe_socket_error(undefined, Transport:peername(Socket), + 'A socket error occurred when retrieving the peer name.'), + {ok, Sock} = maybe_socket_error(undefined, Transport:sockname(Socket), + 'A socket error occurred when retrieving the sock name.'), + CertResult = case Transport:name() of + ssl -> + case ssl:peercert(Socket) of + {error, no_peercert} -> + {ok, undefined}; + Cert0 -> + Cert0 + end; + _ -> + {ok, undefined} + end, + {ok, Cert} = maybe_socket_error(undefined, CertResult, + 'A socket error occurred when retrieving the client TLS certificate.'), + init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, <<>>). + +-spec init(pid(), ranch:ref(), inet:socket(), module(), + ranch_proxy_header:proxy_info() | undefined, cowboy:opts(), + {inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()}, + binary() | undefined, binary()) -> ok. +init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) -> + {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, + http2_status=sequence, http2_machine=HTTP2Machine})), + safe_setopts_active(State), + case Buffer of + <<>> -> loop(State, Buffer); + _ -> parse(State, Buffer) + end. + +init_rate_limiting(State0) -> + CurrentTime = erlang:monotonic_time(millisecond), + State1 = init_frame_rate_limiting(State0, CurrentTime), + State2 = init_reset_rate_limiting(State1, CurrentTime), + init_cancel_rate_limiting(State2, CurrentTime). + +init_frame_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> + {FrameRateNum, FrameRatePeriod} = maps:get(max_received_frame_rate, Opts, {10000, 10000}), + State#state{ + frame_rate_num=FrameRateNum, frame_rate_time=add_period(CurrentTime, FrameRatePeriod) + }. + +init_reset_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> + {ResetRateNum, ResetRatePeriod} = maps:get(max_reset_stream_rate, Opts, {10, 10000}), + State#state{ + reset_rate_num=ResetRateNum, reset_rate_time=add_period(CurrentTime, ResetRatePeriod) + }. + +init_cancel_rate_limiting(State=#state{opts=Opts}, CurrentTime) -> + {CancelRateNum, CancelRatePeriod} = maps:get(max_cancel_stream_rate, Opts, {500, 10000}), + State#state{ + cancel_rate_num=CancelRateNum, cancel_rate_time=add_period(CurrentTime, CancelRatePeriod) + }. + +add_period(_, infinity) -> infinity; +add_period(Time, Period) -> Time + Period. + +%% @todo Add an argument for the request body. +-spec init(pid(), ranch:ref(), inet:socket(), module(), + ranch_proxy_header:proxy_info() | undefined, cowboy:opts(), + {inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()}, + binary() | undefined, binary(), map() | undefined, cowboy_req:req()) -> ok. +init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer, + _Settings, Req=#{method := Method}) -> + {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, + http2_status=upgrade, http2_machine=HTTP2Machine}, + State1 = headers_frame(State0#state{ + http2_machine=HTTP2Machine}, StreamID, Req), + %% We assume that the upgrade will be applied. A stream handler + %% must not prevent the normal operations of the server. + State2 = info(State1, 1, {switch_protocol, #{ + <<"connection">> => <<"Upgrade">>, + <<"upgrade">> => <<"h2c">> + }, ?MODULE, undefined}), %% @todo undefined or #{}? + State = set_idle_timeout(init_rate_limiting(State2#state{http2_status=sequence})), + %% In the case of HTTP/1.1 Upgrade we cannot send the Preface + %% until we send the 101 response. + ok = maybe_socket_error(State, Transport:send(Socket, Preface)), + safe_setopts_active(State), + case Buffer of + <<>> -> loop(State, Buffer); + _ -> parse(State, Buffer) + end. + +%% 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. +setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) -> + N = maps:get(active_n, Opts, 100), + Transport:setopts(Socket, [{active, N}]). + +safe_setopts_active(State) -> + ok = maybe_socket_error(State, setopts_active(State)). + +loop(State=#state{parent=Parent, socket=Socket, transport=Transport, + opts=Opts, timer=TimerRef, children=Children}, Buffer) -> + Messages = Transport:messages(), + InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000), + receive + %% Socket messages. + {OK, Socket, Data} when OK =:= element(1, Messages) -> + parse(set_idle_timeout(State), << Buffer/binary, Data/binary >>); + {Closed, Socket} when Closed =:= element(2, Messages) -> + Reason = case State#state.http2_status of + closing -> {stop, closed, 'The client is going away.'}; + _ -> {socket_error, closed, 'The socket has been closed.'} + end, + terminate(State, Reason); + {Error, Socket, Reason} when Error =:= element(3, Messages) -> + terminate(State, {socket_error, Reason, 'An error has occurred on the socket.'}); + {Passive, Socket} when Passive =:= element(4, Messages); + %% Hardcoded for compatibility with Ranch 1.x. + Passive =:= tcp_passive; Passive =:= ssl_passive -> + safe_setopts_active(State), + loop(State, Buffer); + %% System messages. + {'EXIT', Parent, shutdown} -> + Reason = {stop, {exit, shutdown}, 'Parent process requested shutdown.'}, + loop(initiate_closing(State, Reason), Buffer); + {'EXIT', Parent, Reason} -> + terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'}); + {system, From, Request} -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, Buffer}); + %% Timeouts. + {timeout, TimerRef, idle_timeout} -> + terminate(State, {stop, timeout, + 'Connection idle longer than configuration allows.'}); + {timeout, Ref, {shutdown, Pid}} -> + cowboy_children:shutdown_timeout(Children, Ref, Pid), + loop(State, Buffer); + {timeout, TRef, {cow_http2_machine, Name}} -> + loop(timeout(State, Name, TRef), Buffer); + {timeout, TimerRef, {goaway_initial_timeout, Reason}} -> + 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); + %% Exit signal from children. + Msg = {'EXIT', Pid, _} -> + 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); + Msg -> + cowboy:log(warning, "Received stray message ~p.", [Msg], Opts), + loop(State, Buffer) + after InactivityTimeout -> + terminate(State, {internal_error, timeout, 'No message or data received before timeout.'}) + end. + +set_idle_timeout(State=#state{http2_status=Status, timer=TimerRef}) + when Status =:= closing_initiated orelse Status =:= closing, + TimerRef =/= undefined -> + State; +set_idle_timeout(State=#state{opts=Opts}) -> + set_timeout(State, maps:get(idle_timeout, Opts, 60000), idle_timeout). + +set_timeout(State=#state{timer=TimerRef0}, Timeout, Message) -> + ok = case TimerRef0 of + undefined -> ok; + _ -> erlang:cancel_timer(TimerRef0, [{async, true}, {info, false}]) + end, + TimerRef = case Timeout of + infinity -> undefined; + Timeout -> erlang:start_timer(Timeout, self(), Message) + end, + State#state{timer=TimerRef}. + +maybe_reset_idle_timeout(State=#state{opts=Opts}) -> + case maps:get(reset_idle_timeout_on_send, Opts, false) of + true -> + set_idle_timeout(State); + false -> + State + end. + +%% HTTP/2 protocol parsing. + +parse(State=#state{http2_status=sequence}, Data) -> + case cow_http2:parse_sequence(Data) of + {ok, Rest} -> + parse(State#state{http2_status=settings}, Rest); + more -> + loop(State, Data); + Error = {connection_error, _, _} -> + terminate(State, Error) + end; +parse(State=#state{http2_status=Status, http2_machine=HTTP2Machine, streams=Streams}, Data) -> + MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), + case cow_http2:parse(Data, MaxFrameSize) of + {ok, Frame, Rest} -> + parse(frame_rate(State, Frame), Rest); + {ignore, Rest} -> + parse(frame_rate(State, ignore), Rest); + {stream_error, StreamID, Reason, Human, Rest} -> + parse(reset_stream(State, StreamID, {stream_error, Reason, Human}), Rest); + Error = {connection_error, _, _} -> + terminate(State, Error); + %% Terminate the connection if we are closing and all streams have completed. + more when Status =:= closing, Streams =:= #{} -> + terminate(State, {stop, normal, 'The connection is going away.'}); + more -> + loop(State, Data) + end. + +%% Frame rate flood protection. + +frame_rate(State0=#state{frame_rate_num=Num0, frame_rate_time=Time}, Frame) -> + {Result, State} = case Num0 - 1 of + 0 -> + CurrentTime = erlang:monotonic_time(millisecond), + if + CurrentTime < Time -> + {error, State0}; + true -> + %% When the option has a period of infinity we cannot reach this clause. + {ok, init_frame_rate_limiting(State0, CurrentTime)} + end; + Num -> + {ok, State0#state{frame_rate_num=Num}} + end, + case {Result, Frame} of + {ok, ignore} -> ignored_frame(State); + {ok, _} -> frame(State, Frame); + {error, _} -> terminate(State, {connection_error, enhance_your_calm, + 'Frame rate larger than configuration allows. Flood? (CVE-2019-9512, CVE-2019-9515, CVE-2019-9518)'}) + end. + +%% Frames received. + +%% We do nothing when receiving a lingering DATA frame. +%% We already removed the stream flow from the connection +%% flow and are therefore already accounting for the window +%% being reduced by these frames. +frame(State=#state{http2_machine=HTTP2Machine0}, Frame) -> + case cow_http2_machine:frame(Frame, HTTP2Machine0) of + {ok, HTTP2Machine} -> + maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame); + {ok, {data, StreamID, IsFin, Data}, HTTP2Machine} -> + data_frame(State#state{http2_machine=HTTP2Machine}, StreamID, IsFin, Data); + {ok, {headers, StreamID, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP2Machine} -> + headers_frame(State#state{http2_machine=HTTP2Machine}, + StreamID, IsFin, Headers, PseudoHeaders, BodyLen); + {ok, {trailers, _StreamID, _Trailers}, HTTP2Machine} -> + %% @todo Propagate trailers. + State#state{http2_machine=HTTP2Machine}; + {ok, {rst_stream, StreamID, Reason}, HTTP2Machine} -> + rst_stream_frame(State#state{http2_machine=HTTP2Machine}, StreamID, Reason); + {ok, GoAway={goaway, _, _, _}, HTTP2Machine} -> + goaway(State#state{http2_machine=HTTP2Machine}, GoAway); + {send, SendData, HTTP2Machine} -> + %% We may need to send an alarm for each of the streams sending data. + State1 = lists:foldl( + fun({StreamID, _, _}, S) -> maybe_send_data_alarm(S, HTTP2Machine0, StreamID) end, + send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData, []), + SendData), + maybe_reset_idle_timeout(State1); + {error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} -> + reset_stream(State#state{http2_machine=HTTP2Machine}, + StreamID, {stream_error, Reason, Human}); + {error, Error={connection_error, _, _}, HTTP2Machine} -> + terminate(State#state{http2_machine=HTTP2Machine}, Error) + end. + +%% We use this opportunity to mark the HTTP/2 status as connected +%% if we were still waiting for a SETTINGS frame. +maybe_ack(State=#state{http2_status=settings}, Frame) -> + maybe_ack(State#state{http2_status=connected}, Frame); +%% We do not reset the idle timeout on send here because we are +%% sending data as a consequence of receiving data, which means +%% we already resetted the idle timeout. +maybe_ack(State=#state{socket=Socket, transport=Transport}, Frame) -> + case Frame of + {settings, _} -> + ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:settings_ack())); + {ping, Opaque} -> + ok = maybe_socket_error(State, Transport:send(Socket, cow_http2:ping_ack(Opaque))); + _ -> ok + end, + State. + +data_frame(State0=#state{opts=Opts, flow=Flow0, streams=Streams}, StreamID, IsFin, Data) -> + case Streams of + #{StreamID := Stream=#stream{status=running, flow=StreamFlow, state=StreamState0}} -> + try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of + {Commands, StreamState} -> + %% Remove the amount of data received from the flow. + %% We may receive more data than we requested. We ensure + %% that the flow value doesn't go lower than 0. + Size = byte_size(Data), + Flow = max(0, Flow0 - Size), + %% We would normally update the window when changing the flow + %% value. But because we are running commands, which themselves + %% may update the window, and we want to avoid updating the + %% window twice in a row, we first run the commands and then + %% only update the window a flow command was executed. We know + %% that it was because the flow value changed in the state. + State1 = State0#state{flow=Flow, + streams=Streams#{StreamID => Stream#stream{ + flow=max(0, StreamFlow - Size), state=StreamState}}}, + State = commands(State1, StreamID, Commands), + case State of + %% No flow command was executed. We must update the window + %% because we changed the flow value earlier. + #state{flow=Flow} -> + update_window(State, StreamID); + %% Otherwise the window was updated already. + _ -> + State + end + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(data, + [StreamID, IsFin, Data, StreamState0], + Class, Exception, Stacktrace), Opts), + reset_stream(State0, StreamID, {internal_error, {Class, Exception}, + 'Unhandled exception in cowboy_stream:data/4.'}) + end; + %% We ignore DATA frames for streams that are stopping. + #{} -> + State0 + end. + +headers_frame(State, StreamID, IsFin, Headers, + PseudoHeaders=#{method := <<"CONNECT">>}, _) + when map_size(PseudoHeaders) =:= 2 -> + early_error(State, StreamID, IsFin, Headers, PseudoHeaders, 501, + 'The CONNECT method is currently not implemented. (RFC7231 4.3.6)'); +headers_frame(State, StreamID, IsFin, Headers, + PseudoHeaders=#{method := <<"TRACE">>}, _) -> + early_error(State, StreamID, IsFin, Headers, PseudoHeaders, 501, + 'The TRACE method is currently not implemented. (RFC7231 4.3.8)'); +headers_frame(State, StreamID, IsFin, Headers, PseudoHeaders=#{authority := Authority}, BodyLen) -> + headers_frame_parse_host(State, StreamID, IsFin, Headers, PseudoHeaders, BodyLen, Authority); +headers_frame(State, StreamID, IsFin, Headers, PseudoHeaders, BodyLen) -> + case lists:keyfind(<<"host">>, 1, Headers) of + {_, Authority} -> + headers_frame_parse_host(State, StreamID, IsFin, Headers, PseudoHeaders, BodyLen, Authority); + _ -> + reset_stream(State, StreamID, {stream_error, protocol_error, + 'Requests translated from HTTP/1.1 must include a host header. (RFC7540 8.1.2.3, RFC7230 5.4)'}) + end. + +headers_frame_parse_host(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert, proxy_header=ProxyHeader}, + StreamID, IsFin, Headers, PseudoHeaders=#{method := Method, scheme := Scheme, path := PathWithQs}, + BodyLen, Authority) -> + try cow_http_hd:parse_host(Authority) of + {Host, Port0} -> + Port = ensure_port(Scheme, Port0), + try cow_http:parse_fullpath(PathWithQs) of + {<<>>, _} -> + reset_stream(State, StreamID, {stream_error, protocol_error, + 'The path component must not be empty. (RFC7540 8.1.2.3)'}); + {Path, Qs} -> + Req0 = #{ + ref => Ref, + pid => self(), + streamid => StreamID, + peer => Peer, + sock => Sock, + cert => Cert, + method => Method, + scheme => Scheme, + host => Host, + port => Port, + path => Path, + qs => Qs, + version => 'HTTP/2', + headers => headers_to_map(Headers, #{}), + has_body => IsFin =:= nofin, + body_length => BodyLen + }, + %% We add the PROXY header information if any. + Req1 = case ProxyHeader of + undefined -> Req0; + _ -> Req0#{proxy_header => ProxyHeader} + end, + %% We add the protocol information for extended CONNECTs. + Req = case PseudoHeaders of + #{protocol := Protocol} -> Req1#{protocol => Protocol}; + _ -> Req1 + end, + headers_frame(State, StreamID, Req) + catch _:_ -> + reset_stream(State, StreamID, {stream_error, protocol_error, + 'The :path pseudo-header is invalid. (RFC7540 8.1.2.3)'}) + end + catch _:_ -> + reset_stream(State, StreamID, {stream_error, protocol_error, + 'The :authority pseudo-header is invalid. (RFC7540 8.1.2.3)'}) + end. + +ensure_port(<<"http">>, undefined) -> 80; +ensure_port(<<"https">>, undefined) -> 443; +ensure_port(_, Port) -> Port. + +%% This function is necessary to properly handle duplicate headers +%% and the special-case cookie header. +headers_to_map([], Acc) -> + Acc; +headers_to_map([{Name, Value}|Tail], Acc0) -> + Acc = case Acc0 of + %% The cookie header does not use proper HTTP header lists. + #{Name := Value0} when Name =:= <<"cookie">> -> + Acc0#{Name => << Value0/binary, "; ", Value/binary >>}; + #{Name := Value0} -> + Acc0#{Name => << Value0/binary, ", ", Value/binary >>}; + _ -> + Acc0#{Name => Value} + end, + headers_to_map(Tail, Acc). + +headers_frame(State=#state{opts=Opts, streams=Streams}, StreamID, Req) -> + try cowboy_stream:init(StreamID, Req, Opts) of + {Commands, StreamState} -> + commands(State#state{ + streams=Streams#{StreamID => #stream{state=StreamState}}}, + StreamID, Commands) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(init, + [StreamID, Req, Opts], + Class, Exception, Stacktrace), Opts), + reset_stream(State, StreamID, {internal_error, {Class, Exception}, + 'Unhandled exception in cowboy_stream:init/3.'}) + end. + +early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer}, + StreamID, _IsFin, Headers, #{method := Method}, + StatusCode0, HumanReadable) -> + %% We automatically terminate the stream but it is not an error + %% per se (at least not in the first implementation). + Reason = {stream_error, no_error, HumanReadable}, + %% The partial Req is minimal for now. We only have one case + %% where it can be called (when a method is completely disabled). + %% @todo Fill in the other elements. + PartialReq = #{ + ref => Ref, + peer => Peer, + method => Method, + headers => headers_to_map(Headers, #{}) + }, + Resp = {response, StatusCode0, RespHeaders0=#{<<"content-length">> => <<"0">>}, <<>>}, + try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of + {response, StatusCode, RespHeaders, RespBody} -> + send_response(State0, StreamID, StatusCode, RespHeaders, RespBody) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(early_error, + [StreamID, Reason, PartialReq, Resp, Opts], + Class, Exception, Stacktrace), Opts), + %% We still need to send an error response, so send what we initially + %% wanted to send. It's better than nothing. + send_headers(State0, StreamID, fin, StatusCode0, RespHeaders0) + end. + +rst_stream_frame(State=#state{streams=Streams0, children=Children0}, StreamID, Reason) -> + case maps:take(StreamID, Streams0) of + {#stream{state=StreamState}, Streams} -> + terminate_stream_handler(State, StreamID, Reason, StreamState), + Children = cowboy_children:shutdown(Children0, StreamID), + cancel_rate_limit(State#state{streams=Streams, children=Children}); + error -> + State + end. + +cancel_rate_limit(State0=#state{cancel_rate_num=Num0, cancel_rate_time=Time}) -> + case Num0 - 1 of + 0 -> + CurrentTime = erlang:monotonic_time(millisecond), + if + CurrentTime < Time -> + terminate(State0, {connection_error, enhance_your_calm, + 'Stream cancel rate larger than configuration allows. Flood? (CVE-2023-44487)'}); + true -> + %% When the option has a period of infinity we cannot reach this clause. + init_cancel_rate_limiting(State0, CurrentTime) + end; + Num -> + State0#state{cancel_rate_num=Num} + end. + +ignored_frame(State=#state{http2_machine=HTTP2Machine0}) -> + case cow_http2_machine:ignored_frame(HTTP2Machine0) of + {ok, HTTP2Machine} -> + State#state{http2_machine=HTTP2Machine}; + {error, Error={connection_error, _, _}, HTTP2Machine} -> + terminate(State#state{http2_machine=HTTP2Machine}, Error) + end. + +%% HTTP/2 timeouts. + +timeout(State=#state{http2_machine=HTTP2Machine0}, Name, TRef) -> + case cow_http2_machine:timeout(Name, TRef, HTTP2Machine0) of + {ok, HTTP2Machine} -> + State#state{http2_machine=HTTP2Machine}; + {error, Error={connection_error, _, _}, HTTP2Machine} -> + terminate(State#state{http2_machine=HTTP2Machine}, Error) + end. + +%% Erlang messages. + +down(State0=#state{opts=Opts, children=Children0}, Pid, Msg) -> + State = case cowboy_children:down(Children0, Pid) of + %% The stream was terminated already. + {ok, undefined, Children} -> + State0#state{children=Children}; + %% The stream is still running. + {ok, StreamID, Children} -> + info(State0#state{children=Children}, StreamID, Msg); + %% The process was unknown. + error -> + cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n", + [Msg, Pid], Opts), + State0 + end, + if + State#state.http2_status =:= closing, State#state.streams =:= #{} -> + terminate(State, {stop, normal, 'The connection is going away.'}); + true -> + State + end. + +info(State=#state{opts=Opts, http2_machine=HTTP2Machine, streams=Streams}, StreamID, Msg) -> + case Streams of + #{StreamID := Stream=#stream{state=StreamState0}} -> + try cowboy_stream:info(StreamID, Msg, StreamState0) of + {Commands, StreamState} -> + commands(State#state{streams=Streams#{StreamID => Stream#stream{state=StreamState}}}, + StreamID, Commands) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(info, + [StreamID, Msg, StreamState0], + Class, Exception, Stacktrace), Opts), + reset_stream(State, StreamID, {internal_error, {Class, Exception}, + 'Unhandled exception in cowboy_stream:info/3.'}) + end; + _ -> + case cow_http2_machine:is_lingering_stream(StreamID, HTTP2Machine) of + true -> + ok; + false -> + cowboy:log(warning, "Received message ~p for unknown stream ~p.", + [Msg, StreamID], Opts) + end, + State + end. + +%% Stream handler commands. +%% +%% @todo Kill the stream if it tries to send a response, headers, +%% data or push promise when the stream is closed or half-closed. + +commands(State, _, []) -> + State; +%% Error responses are sent only if a response wasn't sent already. +commands(State=#state{http2_machine=HTTP2Machine}, StreamID, + [{error_response, StatusCode, Headers, Body}|Tail]) -> + case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of + {ok, idle, _} -> + commands(State, StreamID, [{response, StatusCode, Headers, Body}|Tail]); + _ -> + commands(State, StreamID, Tail) + end; +%% Send an informational response. +commands(State0, StreamID, [{inform, StatusCode, Headers}|Tail]) -> + State1 = send_headers(State0, StreamID, idle, StatusCode, Headers), + State = maybe_reset_idle_timeout(State1), + commands(State, StreamID, Tail); +%% Send response headers. +commands(State0, StreamID, [{response, StatusCode, Headers, Body}|Tail]) -> + State1 = send_response(State0, StreamID, StatusCode, Headers, Body), + State = maybe_reset_idle_timeout(State1), + commands(State, StreamID, Tail); +%% Send response headers. +commands(State0, StreamID, [{headers, StatusCode, Headers}|Tail]) -> + State1 = send_headers(State0, StreamID, nofin, StatusCode, Headers), + State = maybe_reset_idle_timeout(State1), + commands(State, StreamID, Tail); +%% Send a response body chunk. +commands(State0, StreamID, [{data, IsFin, Data}|Tail]) -> + State = case maybe_send_data(State0, StreamID, IsFin, Data, []) of + {data_sent, State1} -> + maybe_reset_idle_timeout(State1); + {no_data_sent, State1} -> + State1 + end, + commands(State, StreamID, Tail); +%% Send trailers. +commands(State0, StreamID, [{trailers, Trailers}|Tail]) -> + State = case maybe_send_data(State0, StreamID, fin, + {trailers, maps:to_list(Trailers)}, []) of + {data_sent, State1} -> + maybe_reset_idle_timeout(State1); + {no_data_sent, State1} -> + State1 + end, + commands(State, StreamID, Tail); +%% Send a push promise. +%% +%% @todo Responses sent as a result of a push_promise request +%% must not send push_promise frames themselves. +%% +%% @todo We should not send push_promise frames when we are +%% in the closing http2_status. +commands(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, + StreamID, [{push, Method, Scheme, Host, Port, Path, Qs, Headers0}|Tail]) -> + Authority = case {Scheme, Port} of + {<<"http">>, 80} -> Host; + {<<"https">>, 443} -> Host; + _ -> iolist_to_binary([Host, $:, integer_to_binary(Port)]) + end, + PathWithQs = iolist_to_binary(case Qs of + <<>> -> Path; + _ -> [Path, $?, Qs] + end), + PseudoHeaders = #{ + method => Method, + scheme => Scheme, + authority => Authority, + path => PathWithQs + }, + %% We need to make sure the header value is binary before we can + %% create the Req object, as it expects them to be flat. + Headers = maps:to_list(maps:map(fun(_, V) -> iolist_to_binary(V) end, Headers0)), + State = case cow_http2_machine:prepare_push_promise(StreamID, HTTP2Machine0, + PseudoHeaders, Headers) of + {ok, PromisedStreamID, HeaderBlock, HTTP2Machine} -> + State1 = State0#state{http2_machine=HTTP2Machine}, + ok = maybe_socket_error(State1, Transport:send(Socket, + cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock))), + State2 = maybe_reset_idle_timeout(State1), + headers_frame(State2, PromisedStreamID, fin, Headers, PseudoHeaders, 0); + {error, no_push} -> + State0 + end, + commands(State, StreamID, Tail); +%% Read the request body. +commands(State0=#state{flow=Flow, streams=Streams}, StreamID, [{flow, Size}|Tail]) -> + #{StreamID := Stream=#stream{flow=StreamFlow}} = Streams, + State = update_window(State0#state{flow=Flow + Size, + streams=Streams#{StreamID => Stream#stream{flow=StreamFlow + Size}}}, + StreamID), + commands(State, StreamID, Tail); +%% Supervise a child process. +commands(State=#state{children=Children}, StreamID, [{spawn, Pid, Shutdown}|Tail]) -> + commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)}, + StreamID, Tail); +%% Error handling. +commands(State, StreamID, [Error = {internal_error, _, _}|_Tail]) -> + %% @todo Do we want to run the commands after an internal_error? + %% @todo Do we even allow commands after? + %% @todo Only reset when the stream still exists. + reset_stream(State, StreamID, Error); +%% Upgrade to HTTP/2. This is triggered by cowboy_http2 itself. +%% +%% We do not need to reset the idle timeout on send because it +%% hasn't been set yet. This is called from init/12. +commands(State=#state{socket=Socket, transport=Transport, http2_status=upgrade}, + StreamID, [{switch_protocol, Headers, ?MODULE, _}|Tail]) -> + %% @todo This 101 response needs to be passed through stream handlers. + ok = maybe_socket_error(State, Transport:send(Socket, + cow_http:response(101, 'HTTP/1.1', maps:to_list(Headers)))), + commands(State, StreamID, Tail); +%% Use a different protocol within the stream (CONNECT :protocol). +%% @todo Make sure we error out when the feature is disabled. +commands(State0, StreamID, [{switch_protocol, Headers, _Mod, _ModState}|Tail]) -> + State = info(State0, StreamID, {headers, 200, Headers}), + commands(State, StreamID, Tail); +%% Set options dynamically. +commands(State, StreamID, [{set_options, _Opts}|Tail]) -> + commands(State, StreamID, Tail); +commands(State, StreamID, [stop|_Tail]) -> + %% @todo Do we want to run the commands after a stop? + %% @todo Do we even allow commands after? + stop_stream(State, StreamID); +%% Log event. +commands(State=#state{opts=Opts}, StreamID, [Log={log, _, _, _}|Tail]) -> + cowboy:log(Log, Opts), + commands(State, StreamID, Tail). + +%% Tentatively update the window after the flow was updated. + +update_window(State0=#state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0, flow=Flow, streams=Streams}, StreamID) -> + {Data1, HTTP2Machine2} = case cow_http2_machine:ensure_window(Flow, HTTP2Machine0) of + ok -> {<<>>, HTTP2Machine0}; + {ok, Increment1, HTTP2Machine1} -> {cow_http2:window_update(Increment1), HTTP2Machine1} + end, + {Data2, HTTP2Machine} = case Streams of + #{StreamID := #stream{flow=StreamFlow}} -> + case cow_http2_machine:ensure_window(StreamID, StreamFlow, HTTP2Machine2) of + ok -> + {<<>>, HTTP2Machine2}; + {ok, Increment2, HTTP2Machine3} -> + {cow_http2:window_update(StreamID, Increment2), HTTP2Machine3} + end; + _ -> + %% Don't update the stream's window if it stopped. + {<<>>, HTTP2Machine2} + end, + State = State0#state{http2_machine=HTTP2Machine}, + case {Data1, Data2} of + {<<>>, <<>>} -> + State; + _ -> + ok = maybe_socket_error(State, Transport:send(Socket, [Data1, Data2])), + maybe_reset_idle_timeout(State) + end. + +%% Send the response, trailers or data. + +send_response(State0=#state{http2_machine=HTTP2Machine0}, StreamID, StatusCode, Headers, Body) -> + Size = case Body of + {sendfile, _, Bytes, _} -> Bytes; + _ -> iolist_size(Body) + end, + case Size of + 0 -> + State = send_headers(State0, StreamID, fin, StatusCode, Headers), + maybe_terminate_stream(State, StreamID, fin); + _ -> + %% @todo Add a test for HEAD to make sure we don't send the body when + %% returning {response...} from a stream handler (or {headers...} then {data...}). + {ok, _IsFin, HeaderBlock, HTTP2Machine} + = cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, nofin, + #{status => cow_http:status_to_integer(StatusCode)}, + headers_to_list(Headers)), + {_, State} = maybe_send_data(State0#state{http2_machine=HTTP2Machine}, + StreamID, fin, Body, [cow_http2:headers(StreamID, nofin, HeaderBlock)]), + State + end. + +send_headers(State0=#state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0}, StreamID, IsFin0, StatusCode, Headers) -> + {ok, IsFin, HeaderBlock, HTTP2Machine} + = cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, IsFin0, + #{status => cow_http:status_to_integer(StatusCode)}, + headers_to_list(Headers)), + State = State0#state{http2_machine=HTTP2Machine}, + ok = maybe_socket_error(State, Transport:send(Socket, + cow_http2:headers(StreamID, IsFin, HeaderBlock))), + State. + +%% The set-cookie header is special; we can only send one cookie per header. +headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) -> + Headers = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)), + Headers ++ [{<<"set-cookie">>, Value} || Value <- SetCookies]; +headers_to_list(Headers) -> + maps:to_list(Headers). + +maybe_send_data(State0=#state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0, Prefix) -> + Data = case is_tuple(Data0) of + false -> {data, Data0}; + true -> Data0 + end, + case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of + {ok, HTTP2Machine} -> + State1 = State0#state{http2_machine=HTTP2Machine}, + %% If we have prefix data (like a HEADERS frame) we need to send it + %% even if we do not send any DATA frames. + WasDataSent = case Prefix of + [] -> + no_data_sent; + _ -> + ok = maybe_socket_error(State1, Transport:send(Socket, Prefix)), + data_sent + end, + State = maybe_send_data_alarm(State1, HTTP2Machine0, StreamID), + {WasDataSent, State}; + {send, SendData, HTTP2Machine} -> + State = #state{http2_status=Status, streams=Streams} + = send_data(State0#state{http2_machine=HTTP2Machine}, SendData, Prefix), + %% Terminate the connection if we are closing and all streams have completed. + if + Status =:= closing, Streams =:= #{} -> + terminate(State, {stop, normal, 'The connection is going away.'}); + true -> + {data_sent, maybe_send_data_alarm(State, HTTP2Machine0, StreamID)} + end + end. + +send_data(State0=#state{socket=Socket, transport=Transport, opts=Opts}, SendData, Prefix) -> + {Acc, State} = prepare_data(State0, SendData, [], Prefix), + _ = [case Data of + {sendfile, Offset, Bytes, Path} -> + %% When sendfile is disabled we explicitly use the fallback. + {ok, _} = maybe_socket_error(State, + case maps:get(sendfile, Opts, true) of + true -> Transport:sendfile(Socket, Path, Offset, Bytes); + false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, []) + end + ), + ok; + _ -> + ok = maybe_socket_error(State, Transport:send(Socket, Data)) + end || Data <- Acc], + send_data_terminate(State, SendData). + +send_data_terminate(State, []) -> + State; +send_data_terminate(State0, [{StreamID, IsFin, _}|Tail]) -> + State = maybe_terminate_stream(State0, StreamID, IsFin), + send_data_terminate(State, Tail). + +prepare_data(State, [], Acc, []) -> + {lists:reverse(Acc), State}; +prepare_data(State, [], Acc, Buffer) -> + {lists:reverse([lists:reverse(Buffer)|Acc]), State}; +prepare_data(State0, [{StreamID, IsFin, SendData}|Tail], Acc0, Buffer0) -> + {Acc, Buffer, State} = prepare_data(State0, StreamID, IsFin, SendData, Acc0, Buffer0), + prepare_data(State, Tail, Acc, Buffer). + +prepare_data(State, _, _, [], Acc, Buffer) -> + {Acc, Buffer, State}; +prepare_data(State0, StreamID, IsFin, [FrameData|Tail], Acc, Buffer) -> + FrameIsFin = case Tail of + [] -> IsFin; + _ -> nofin + end, + case prepare_data_frame(State0, StreamID, FrameIsFin, FrameData) of + {{MoreData, Sendfile}, State} when is_tuple(Sendfile) -> + case Buffer of + [] -> + prepare_data(State, StreamID, IsFin, Tail, + [Sendfile, MoreData|Acc], []); + _ -> + prepare_data(State, StreamID, IsFin, Tail, + [Sendfile, lists:reverse([MoreData|Buffer])|Acc], []) + end; + {MoreData, State} -> + prepare_data(State, StreamID, IsFin, Tail, + Acc, [MoreData|Buffer]) + end. + +prepare_data_frame(State, StreamID, IsFin, {data, Data}) -> + {cow_http2:data(StreamID, IsFin, Data), + State}; +prepare_data_frame(State, StreamID, IsFin, Sendfile={sendfile, _, Bytes, _}) -> + {{cow_http2:data_header(StreamID, IsFin, Bytes), Sendfile}, + State}; +%% The stream is terminated in cow_http2_machine:prepare_trailers. +prepare_data_frame(State=#state{http2_machine=HTTP2Machine0}, + StreamID, nofin, {trailers, Trailers}) -> + {ok, HeaderBlock, HTTP2Machine} + = cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers), + {cow_http2:headers(StreamID, fin, HeaderBlock), + State#state{http2_machine=HTTP2Machine}}. + +%% After we have sent or queued data we may need to set or clear an alarm. +%% We do this by comparing the HTTP2Machine buffer state before/after for +%% the relevant streams. +maybe_send_data_alarm(State=#state{opts=Opts, http2_machine=HTTP2Machine}, HTTP2Machine0, StreamID) -> + ConnBufferSizeBefore = cow_http2_machine:get_connection_local_buffer_size(HTTP2Machine0), + ConnBufferSizeAfter = cow_http2_machine:get_connection_local_buffer_size(HTTP2Machine), + {ok, StreamBufferSizeBefore} = cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine0), + %% When the stream ends up closed after it finished sending data, + %% we do not want to trigger an alarm. We act as if the buffer + %% size did not change. + StreamBufferSizeAfter = case cow_http2_machine:get_stream_local_buffer_size(StreamID, HTTP2Machine) of + {ok, BSA} -> BSA; + {error, closed} -> StreamBufferSizeBefore + end, + MaxConnBufferSize = maps:get(max_connection_buffer_size, Opts, 16000000), + MaxStreamBufferSize = maps:get(max_stream_buffer_size, Opts, 8000000), + %% I do not want to document these internal events yet. I am not yet + %% convinced it should be {alarm, Name, on|off} and not {internal_event, E} + %% or something else entirely. Though alarms are probably right. + if + ConnBufferSizeBefore >= MaxConnBufferSize, ConnBufferSizeAfter < MaxConnBufferSize -> + connection_alarm(State, connection_buffer_full, off); + ConnBufferSizeBefore < MaxConnBufferSize, ConnBufferSizeAfter >= MaxConnBufferSize -> + connection_alarm(State, connection_buffer_full, on); + StreamBufferSizeBefore >= MaxStreamBufferSize, StreamBufferSizeAfter < MaxStreamBufferSize -> + stream_alarm(State, StreamID, stream_buffer_full, off); + StreamBufferSizeBefore < MaxStreamBufferSize, StreamBufferSizeAfter >= MaxStreamBufferSize -> + stream_alarm(State, StreamID, stream_buffer_full, on); + true -> + State + end. + +connection_alarm(State0=#state{streams=Streams}, Name, Value) -> + lists:foldl(fun(StreamID, State) -> + stream_alarm(State, StreamID, Name, Value) + end, State0, maps:keys(Streams)). + +stream_alarm(State, StreamID, Name, Value) -> + info(State, StreamID, {alarm, Name, Value}). + +%% Terminate a stream or the connection. + +%% We may have to cancel streams even if we receive multiple +%% GOAWAY frames as the LastStreamID value may be lower than +%% the one previously received. +%% +%% We do not reset the idle timeout on send here. We already +%% disabled it if we initiated shutdown; and we already reset +%% it if the client sent a GOAWAY frame. +goaway(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0, + http2_status=Status, streams=Streams0}, {goaway, LastStreamID, Reason, _}) + when Status =:= connected; Status =:= closing_initiated; Status =:= closing -> + Streams = goaway_streams(State0, maps:to_list(Streams0), LastStreamID, + {stop, {goaway, Reason}, 'The connection is going away.'}, []), + State1 = State0#state{streams=maps:from_list(Streams)}, + if + Status =:= connected; Status =:= closing_initiated -> + {OurLastStreamID, HTTP2Machine} = + cow_http2_machine:set_last_streamid(HTTP2Machine0), + State = State1#state{http2_status=closing, http2_machine=HTTP2Machine}, + ok = maybe_socket_error(State, Transport:send(Socket, + cow_http2:goaway(OurLastStreamID, no_error, <<>>))), + State; + true -> + State1 + end; +%% We terminate the connection immediately if it hasn't fully been initialized. +goaway(State, {goaway, _, Reason, _}) -> + terminate(State, {stop, {goaway, Reason}, 'The connection is going away.'}). + +%% Cancel client-initiated streams that are above LastStreamID. +goaway_streams(_, [], _, _, Acc) -> + Acc; +goaway_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], LastStreamID, Reason, Acc) + when StreamID > LastStreamID, (StreamID rem 2) =:= 0 -> + terminate_stream_handler(State, StreamID, Reason, StreamState), + goaway_streams(State, Tail, LastStreamID, Reason, Acc); +goaway_streams(State, [Stream|Tail], LastStreamID, Reason, Acc) -> + goaway_streams(State, Tail, LastStreamID, Reason, [Stream|Acc]). + +%% A server that is attempting to gracefully shut down a connection SHOULD send +%% an initial GOAWAY frame with the last stream identifier set to 2^31-1 and a +%% NO_ERROR code. This signals to the client that a shutdown is imminent and +%% that initiating further requests is prohibited. After allowing time for any +%% in-flight stream creation (at least one round-trip time), the server can send +%% another GOAWAY frame with an updated last stream identifier. This ensures +%% that a connection can be cleanly shut down without losing requests. +-spec initiate_closing(#state{}, _) -> #state{}. +initiate_closing(State=#state{http2_status=connected, socket=Socket, + transport=Transport, opts=Opts}, Reason) -> + ok = maybe_socket_error(State, Transport:send(Socket, + cow_http2:goaway(16#7fffffff, no_error, <<>>))), + Timeout = maps:get(goaway_initial_timeout, Opts, 1000), + Message = {goaway_initial_timeout, Reason}, + set_timeout(State#state{http2_status=closing_initiated}, Timeout, Message); +initiate_closing(State=#state{http2_status=Status}, _Reason) + when Status =:= closing_initiated; Status =:= closing -> + %% This happens if sys:terminate/2,3 is called twice or if the supervisor + %% tells us to shutdown after sys:terminate/2,3 is called or vice versa. + State; +initiate_closing(State, Reason) -> + terminate(State, {stop, stop_reason(Reason), 'The connection is going away.'}). + +%% Switch to 'closing' state and stop accepting new streams. +-spec closing(#state{}, Reason :: term()) -> #state{}. +closing(State=#state{streams=Streams}, Reason) when Streams =:= #{} -> + terminate(State, Reason); +closing(State0=#state{http2_status=closing_initiated, + http2_machine=HTTP2Machine0, socket=Socket, transport=Transport}, + Reason) -> + %% Stop accepting new streams. + {LastStreamID, HTTP2Machine} = + cow_http2_machine:set_last_streamid(HTTP2Machine0), + State = State0#state{http2_status=closing, http2_machine=HTTP2Machine}, + ok = maybe_socket_error(State, Transport:send(Socket, + cow_http2:goaway(LastStreamID, no_error, <<>>))), + closing(State, Reason); +closing(State=#state{http2_status=closing, opts=Opts}, Reason) -> + %% If client sent GOAWAY, we may already be in 'closing' but without the + %% goaway complete timeout set. + Timeout = maps:get(goaway_complete_timeout, Opts, 3000), + Message = {goaway_complete_timeout, Reason}, + set_timeout(State, Timeout, Message). + +stop_reason({stop, Reason, _}) -> Reason; +stop_reason(Reason) -> Reason. + +%% Function copied from cowboy_http. +maybe_socket_error(State, {error, closed}) -> + terminate(State, {socket_error, closed, 'The socket has been closed.'}); +maybe_socket_error(State, Reason) -> + maybe_socket_error(State, Reason, 'An error has occurred on the socket.'). + +maybe_socket_error(_, Result = ok, _) -> + Result; +maybe_socket_error(_, Result = {ok, _}, _) -> + Result; +maybe_socket_error(State, {error, Reason}, Human) -> + terminate(State, {socket_error, Reason, Human}). + +-spec terminate(#state{}, _) -> no_return(). +terminate(undefined, Reason) -> + exit({shutdown, Reason}); +terminate(State=#state{socket=Socket, transport=Transport, http2_status=Status, + http2_machine=HTTP2Machine, streams=Streams, children=Children}, Reason) + when Status =:= connected; Status =:= closing_initiated; Status =:= closing -> + %% @todo We might want to optionally send the Reason value + %% as debug data in the GOAWAY frame here. Perhaps more. + if + Status =:= connected; Status =:= closing_initiated -> + %% We are terminating so it's OK if we can't send the GOAWAY anymore. + _ = Transport:send(Socket, cow_http2:goaway( + cow_http2_machine:get_last_streamid(HTTP2Machine), + terminate_reason(Reason), <<>>)); + %% We already sent the GOAWAY frame. + Status =:= closing -> + ok + end, + terminate_all_streams(State, maps:to_list(Streams), Reason), + cowboy_children:terminate(Children), + %% @todo Don't linger on connection errors. + terminate_linger(State), + exit({shutdown, Reason}); +%% We are not fully connected so we can just terminate the connection. +terminate(_State, Reason) -> + exit({shutdown, Reason}). + +terminate_reason({connection_error, Reason, _}) -> Reason; +terminate_reason({stop, _, _}) -> no_error; +terminate_reason({socket_error, _, _}) -> internal_error; +terminate_reason({internal_error, _, _}) -> internal_error. + +terminate_all_streams(_, [], _) -> + ok; +terminate_all_streams(State, [{StreamID, #stream{state=StreamState}}|Tail], Reason) -> + terminate_stream_handler(State, StreamID, Reason, StreamState), + terminate_all_streams(State, Tail, Reason). + +%% This code is copied from cowboy_http. +terminate_linger(State=#state{socket=Socket, transport=Transport, opts=Opts}) -> + case Transport:shutdown(Socket, write) of + ok -> + case maps:get(linger_timeout, Opts, 1000) of + 0 -> + ok; + infinity -> + terminate_linger_before_loop(State, undefined, Transport:messages()); + Timeout -> + TimerRef = erlang:start_timer(Timeout, self(), linger_timeout), + terminate_linger_before_loop(State, TimerRef, Transport:messages()) + end; + {error, _} -> + ok + end. + +terminate_linger_before_loop(State, TimerRef, Messages) -> + %% We may already be in active mode when we do this + %% but it's OK because we are shutting down anyway. + %% + %% We specially handle the socket error to terminate + %% when an error occurs. + case setopts_active(State) of + ok -> + terminate_linger_loop(State, TimerRef, Messages); + {error, _} -> + ok + end. + +terminate_linger_loop(State=#state{socket=Socket}, TimerRef, Messages) -> + receive + {OK, Socket, _} when OK =:= element(1, Messages) -> + terminate_linger_loop(State, TimerRef, Messages); + {Closed, Socket} when Closed =:= element(2, Messages) -> + ok; + {Error, Socket, _} when Error =:= element(3, Messages) -> + ok; + {Passive, Socket} when Passive =:= tcp_passive; Passive =:= ssl_passive -> + terminate_linger_before_loop(State, TimerRef, Messages); + {timeout, TimerRef, linger_timeout} -> + ok; + _ -> + terminate_linger_loop(State, TimerRef, Messages) + end. + +%% @todo Don't send an RST_STREAM if one was already sent. +%% +%% When resetting the stream we are technically sending data +%% on the socket. However due to implementation complexities +%% we do not attempt to reset the idle timeout on send. +reset_stream(State0=#state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0}, StreamID, Error) -> + Reason = case Error of + {internal_error, _, _} -> internal_error; + {stream_error, Reason0, _} -> Reason0 + end, + ok = maybe_socket_error(State0, Transport:send(Socket, + cow_http2:rst_stream(StreamID, Reason))), + State1 = case cow_http2_machine:reset_stream(StreamID, HTTP2Machine0) of + {ok, HTTP2Machine} -> + terminate_stream(State0#state{http2_machine=HTTP2Machine}, StreamID, Error); + {error, not_found} -> + terminate_stream(State0, StreamID, Error) + end, + case reset_rate(State1) of + {ok, State} -> + State; + error -> + terminate(State1, {connection_error, enhance_your_calm, + 'Stream reset rate larger than configuration allows. Flood? (CVE-2019-9514)'}) + end. + +reset_rate(State0=#state{reset_rate_num=Num0, reset_rate_time=Time}) -> + case Num0 - 1 of + 0 -> + CurrentTime = erlang:monotonic_time(millisecond), + if + CurrentTime < Time -> + error; + true -> + %% When the option has a period of infinity we cannot reach this clause. + {ok, init_reset_rate_limiting(State0, CurrentTime)} + end; + Num -> + {ok, State0#state{reset_rate_num=Num}} + end. + +stop_stream(State=#state{http2_machine=HTTP2Machine}, StreamID) -> + case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of + %% When the stream terminates normally (without sending RST_STREAM) + %% and no response was sent, we need to send a proper response back to the client. + %% We delay the termination of the stream until the response is fully sent. + {ok, idle, _} -> + info(stopping(State, StreamID), StreamID, {response, 204, #{}, <<>>}); + %% When a response was sent but not terminated, we need to close the stream. + %% We delay the termination of the stream until the response is fully sent. + {ok, nofin, fin} -> + stopping(State, StreamID); + %% We only send a final DATA frame if there isn't one queued yet. + {ok, nofin, _} -> + info(stopping(State, StreamID), StreamID, {data, fin, <<>>}); + %% When a response was sent fully we can terminate the stream, + %% regardless of the stream being in half-closed or closed state. + _ -> + terminate_stream(State, StreamID) + end. + +stopping(State=#state{streams=Streams}, StreamID) -> + #{StreamID := Stream} = Streams, + State#state{streams=Streams#{StreamID => Stream#stream{status=stopping}}}. + +%% If we finished sending data and the stream is stopping, terminate it. +maybe_terminate_stream(State=#state{streams=Streams}, StreamID, fin) -> + case Streams of + #{StreamID := #stream{status=stopping}} -> + terminate_stream(State, StreamID); + _ -> + State + end; +maybe_terminate_stream(State, _, _) -> + State. + +%% When the stream stops normally without reading the request +%% body fully we need to tell the client to stop sending it. +%% We do this by sending an RST_STREAM with reason NO_ERROR. (RFC7540 8.1.0) +terminate_stream(State0=#state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0}, StreamID) -> + State = case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine0) of + {ok, fin, _} -> + ok = maybe_socket_error(State0, Transport:send(Socket, + cow_http2:rst_stream(StreamID, no_error))), + {ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0), + State0#state{http2_machine=HTTP2Machine}; + {error, closed} -> + State0 + end, + terminate_stream(State, StreamID, normal). + +%% We remove the stream flow from the connection flow. Any further +%% data received for this stream is therefore fully contained within +%% the extra window we allocated for this stream. +terminate_stream(State=#state{flow=Flow, streams=Streams0, children=Children0}, StreamID, Reason) -> + case maps:take(StreamID, Streams0) of + {#stream{flow=StreamFlow, state=StreamState}, Streams} -> + terminate_stream_handler(State, StreamID, Reason, StreamState), + Children = cowboy_children:shutdown(Children0, StreamID), + State#state{flow=Flow - StreamFlow, streams=Streams, children=Children}; + error -> + State + end. + +terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) -> + try + cowboy_stream:terminate(StreamID, Reason, StreamState) + catch Class:Exception:Stacktrace -> + cowboy:log(cowboy_stream:make_error_log(terminate, + [StreamID, Reason, StreamState], + Class, Exception, Stacktrace), Opts) + end. + +%% System callbacks. + +-spec system_continue(_, _, {#state{}, binary()}) -> ok. +system_continue(_, _, {State, Buffer}) -> + 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). + +-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}. +system_code_change(Misc, _, _, _) -> + {ok, Misc}. diff --git a/cowboy/src/cowboy_loop.erl b/cowboy/src/cowboy_loop.erl new file mode 100644 index 0000000..6859c82 --- /dev/null +++ b/cowboy/src/cowboy_loop.erl @@ -0,0 +1,117 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_loop). +-behaviour(cowboy_sub_protocol). + +-export([upgrade/4]). +-export([upgrade/5]). +-export([loop/5]). + +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +%% From gen_server. +-define(is_timeout(X), ((X) =:= infinity orelse (is_integer(X) andalso (X) >= 0))). + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), any()} + when Req::cowboy_req:req(). + +-callback info(any(), Req, State) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {stop, Req, State} + when Req::cowboy_req:req(), State::any(). + +-callback terminate(any(), cowboy_req:req(), any()) -> ok. +-optional_callbacks([terminate/3]). + +-spec upgrade(Req, Env, module(), any()) + -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade(Req, Env, Handler, HandlerState) -> + loop(Req, Env, Handler, HandlerState, infinity). + +-spec upgrade(Req, Env, module(), any(), hibernate | timeout()) + -> {suspend, ?MODULE, loop, [any()]} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade(Req, Env, Handler, HandlerState, hibernate) -> + suspend(Req, Env, Handler, HandlerState); +upgrade(Req, Env, Handler, HandlerState, Timeout) when ?is_timeout(Timeout) -> + loop(Req, Env, Handler, HandlerState, Timeout). + +-spec loop(Req, Env, module(), any(), timeout()) + -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +%% @todo Handle system messages. +loop(Req=#{pid := Parent}, Env, Handler, HandlerState, Timeout) -> + receive + %% System messages. + {'EXIT', Parent, Reason} -> + terminate(Req, Env, Handler, HandlerState, Reason); + {system, From, Request} -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], + {Req, Env, Handler, HandlerState, Timeout}); + %% Calls from supervisor module. + {'$gen_call', From, Call} -> + cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE), + loop(Req, Env, Handler, HandlerState, Timeout); + Message -> + call(Req, Env, Handler, HandlerState, Timeout, Message) + after Timeout -> + call(Req, Env, Handler, HandlerState, Timeout, timeout) + end. + +call(Req0, Env, Handler, HandlerState0, Timeout, Message) -> + try Handler:info(Message, Req0, HandlerState0) of + {ok, Req, HandlerState} -> + loop(Req, Env, Handler, HandlerState, Timeout); + {ok, Req, HandlerState, hibernate} -> + suspend(Req, Env, Handler, HandlerState); + {ok, Req, HandlerState, NewTimeout} when ?is_timeout(NewTimeout) -> + loop(Req, Env, Handler, HandlerState, NewTimeout); + {stop, Req, HandlerState} -> + terminate(Req, Env, Handler, HandlerState, stop) + catch Class:Reason:Stacktrace -> + cowboy_handler:terminate({crash, Class, Reason}, Req0, HandlerState0, Handler), + erlang:raise(Class, Reason, Stacktrace) + end. + +suspend(Req, Env, Handler, HandlerState) -> + {suspend, ?MODULE, loop, [Req, Env, Handler, HandlerState, infinity]}. + +terminate(Req, Env, Handler, HandlerState, Reason) -> + Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler), + {ok, Req, Env#{result => Result}}. + +%% System callbacks. + +-spec system_continue(_, _, {Req, Env, module(), any(), timeout()}) + -> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +system_continue(_, _, {Req, Env, Handler, HandlerState, Timeout}) -> + loop(Req, Env, Handler, HandlerState, Timeout). + +-spec system_terminate(any(), _, _, {Req, Env, module(), any(), timeout()}) + -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +system_terminate(Reason, _, _, {Req, Env, Handler, HandlerState, _}) -> + terminate(Req, Env, Handler, HandlerState, Reason). + +-spec system_code_change(Misc, _, _, _) -> {ok, Misc} + when Misc::{cowboy_req:req(), cowboy_middleware:env(), module(), any()}. +system_code_change(Misc, _, _, _) -> + {ok, Misc}. diff --git a/cowboy/src/cowboy_metrics_h.erl b/cowboy/src/cowboy_metrics_h.erl new file mode 100644 index 0000000..27f14d4 --- /dev/null +++ b/cowboy/src/cowboy_metrics_h.erl @@ -0,0 +1,331 @@ +%% Copyright (c) 2017-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_metrics_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-type proc_metrics() :: #{pid() => #{ + %% Time at which the process spawned. + spawn := integer(), + + %% Time at which the process exited. + exit => integer(), + + %% Reason for the process exit. + reason => any() +}}. + +-type informational_metrics() :: #{ + %% Informational response status. + status := cowboy:http_status(), + + %% Headers sent with the informational response. + headers := cowboy:http_headers(), + + %% Time when the informational response was sent. + time := integer() +}. + +-type metrics() :: #{ + %% The identifier for this listener. + ref := ranch:ref(), + + %% The pid for this connection. + pid := pid(), + + %% The streamid also indicates the total number of requests on + %% this connection (StreamID div 2 + 1). + streamid := cowboy_stream:streamid(), + + %% The terminate reason is always useful. + reason := cowboy_stream:reason(), + + %% A filtered Req object or a partial Req object + %% depending on how far the request got to. + req => cowboy_req:req(), + partial_req => cowboy_stream:partial_req(), + + %% Response status. + resp_status := cowboy:http_status(), + + %% Filtered response headers. + resp_headers := cowboy:http_headers(), + + %% Start/end of the processing of the request. + %% + %% This represents the time from this stream handler's init + %% to terminate. + req_start => integer(), + req_end => integer(), + + %% Start/end of the receiving of the request body. + %% Begins when the first packet has been received. + req_body_start => integer(), + req_body_end => integer(), + + %% Start/end of the sending of the response. + %% Begins when we send the headers and ends on the final + %% packet of the response body. If everything is sent at + %% once these values are identical. + resp_start => integer(), + resp_end => integer(), + + %% For early errors all we get is the time we received it. + early_error_time => integer(), + + %% Start/end of spawned processes. This is where most of + %% the user code lies, excluding stream handlers. On a + %% default Cowboy configuration there should be only one + %% process: the request process. + procs => proc_metrics(), + + %% Informational responses sent before the final response. + informational => [informational_metrics()], + + %% Length of the request and response bodies. This does + %% not include the framing. + req_body_length => non_neg_integer(), + resp_body_length => non_neg_integer(), + + %% Additional metadata set by the user. + user_data => map() +}. +-export_type([metrics/0]). + +-type metrics_callback() :: fun((metrics()) -> any()). +-export_type([metrics_callback/0]). + +-record(state, { + next :: any(), + callback :: fun((metrics()) -> any()), + resp_headers_filter :: undefined | fun((cowboy:http_headers()) -> cowboy:http_headers()), + req :: map(), + resp_status :: undefined | cowboy:http_status(), + resp_headers :: undefined | cowboy:http_headers(), + ref :: ranch:ref(), + req_start :: integer(), + req_end :: undefined | integer(), + req_body_start :: undefined | integer(), + req_body_end :: undefined | integer(), + resp_start :: undefined | integer(), + resp_end :: undefined | integer(), + procs = #{} :: proc_metrics(), + informational = [] :: [informational_metrics()], + req_body_length = 0 :: non_neg_integer(), + resp_body_length = 0 :: non_neg_integer(), + user_data = #{} :: map() +}). + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {[{spawn, pid(), timeout()}], #state{}}. +init(StreamID, Req=#{ref := Ref}, Opts=#{metrics_callback := Fun}) -> + ReqStart = erlang:monotonic_time(), + {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), + FilteredReq = case maps:get(metrics_req_filter, Opts, undefined) of + undefined -> Req; + ReqFilter -> ReqFilter(Req) + end, + RespHeadersFilter = maps:get(metrics_resp_headers_filter, Opts, undefined), + {Commands, fold(Commands, #state{ + next=Next, + callback=Fun, + resp_headers_filter=RespHeadersFilter, + req=FilteredReq, + ref=Ref, + req_start=ReqStart + })}. + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +data(StreamID, IsFin=fin, Data, State=#state{req_body_start=undefined}) -> + ReqBody = erlang:monotonic_time(), + do_data(StreamID, IsFin, Data, State#state{ + req_body_start=ReqBody, + req_body_end=ReqBody, + req_body_length=byte_size(Data) + }); +data(StreamID, IsFin=fin, Data, State=#state{req_body_length=ReqBodyLen}) -> + ReqBodyEnd = erlang:monotonic_time(), + do_data(StreamID, IsFin, Data, State#state{ + req_body_end=ReqBodyEnd, + req_body_length=ReqBodyLen + byte_size(Data) + }); +data(StreamID, IsFin, Data, State=#state{req_body_start=undefined}) -> + ReqBodyStart = erlang:monotonic_time(), + do_data(StreamID, IsFin, Data, State#state{ + req_body_start=ReqBodyStart, + req_body_length=byte_size(Data) + }); +data(StreamID, IsFin, Data, State=#state{req_body_length=ReqBodyLen}) -> + do_data(StreamID, IsFin, Data, State#state{ + req_body_length=ReqBodyLen + byte_size(Data) + }). + +do_data(StreamID, IsFin, Data, State0=#state{next=Next0}) -> + {Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), + {Commands, fold(Commands, State0#state{next=Next})}. + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +info(StreamID, Info={'EXIT', Pid, Reason}, State0=#state{procs=Procs}) -> + ProcEnd = erlang:monotonic_time(), + P = maps:get(Pid, Procs), + State = State0#state{procs=Procs#{Pid => P#{ + exit => ProcEnd, + reason => Reason + }}}, + do_info(StreamID, Info, State); +info(StreamID, Info, State) -> + do_info(StreamID, Info, State). + +do_info(StreamID, Info, State0=#state{next=Next0}) -> + {Commands, Next} = cowboy_stream:info(StreamID, Info, Next0), + {Commands, fold(Commands, State0#state{next=Next})}. + +fold([], State) -> + State; +fold([{spawn, Pid, _}|Tail], State0=#state{procs=Procs}) -> + ProcStart = erlang:monotonic_time(), + State = State0#state{procs=Procs#{Pid => #{spawn => ProcStart}}}, + fold(Tail, State); +fold([{inform, Status, Headers}|Tail], + State=#state{informational=Infos}) -> + Time = erlang:monotonic_time(), + fold(Tail, State#state{informational=[#{ + status => Status, + headers => Headers, + time => Time + }|Infos]}); +fold([{response, Status, Headers, Body}|Tail], + State=#state{resp_headers_filter=RespHeadersFilter}) -> + Resp = erlang:monotonic_time(), + fold(Tail, State#state{ + resp_status=Status, + resp_headers=case RespHeadersFilter of + undefined -> Headers; + _ -> RespHeadersFilter(Headers) + end, + resp_start=Resp, + resp_end=Resp, + resp_body_length=resp_body_length(Body) + }); +fold([{error_response, Status, Headers, Body}|Tail], + State=#state{resp_status=RespStatus}) -> + %% The error_response command only results in a response + %% if no response was sent before. + case RespStatus of + undefined -> + fold([{response, Status, Headers, Body}|Tail], State); + _ -> + fold(Tail, State) + end; +fold([{headers, Status, Headers}|Tail], + State=#state{resp_headers_filter=RespHeadersFilter}) -> + RespStart = erlang:monotonic_time(), + fold(Tail, State#state{ + resp_status=Status, + resp_headers=case RespHeadersFilter of + undefined -> Headers; + _ -> RespHeadersFilter(Headers) + end, + resp_start=RespStart + }); +%% @todo It might be worthwhile to keep the sendfile information around, +%% especially if these frames ultimately result in a sendfile syscall. +fold([{data, nofin, Data}|Tail], State=#state{resp_body_length=RespBodyLen}) -> + fold(Tail, State#state{ + resp_body_length=RespBodyLen + resp_body_length(Data) + }); +fold([{data, fin, Data}|Tail], State=#state{resp_body_length=RespBodyLen}) -> + RespEnd = erlang:monotonic_time(), + fold(Tail, State#state{ + resp_end=RespEnd, + resp_body_length=RespBodyLen + resp_body_length(Data) + }); +fold([{set_options, SetOpts}|Tail], State0=#state{user_data=OldUserData}) -> + State = case SetOpts of + #{metrics_user_data := NewUserData} -> + State0#state{user_data=maps:merge(OldUserData, NewUserData)}; + _ -> + State0 + end, + fold(Tail, State); +fold([_|Tail], State) -> + fold(Tail, State). + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any(). +terminate(StreamID, Reason, #state{next=Next, callback=Fun, + req=Req, resp_status=RespStatus, resp_headers=RespHeaders, ref=Ref, + req_start=ReqStart, req_body_start=ReqBodyStart, + req_body_end=ReqBodyEnd, resp_start=RespStart, resp_end=RespEnd, + procs=Procs, informational=Infos, user_data=UserData, + req_body_length=ReqBodyLen, resp_body_length=RespBodyLen}) -> + Res = cowboy_stream:terminate(StreamID, Reason, Next), + ReqEnd = erlang:monotonic_time(), + Metrics = #{ + ref => Ref, + pid => self(), + streamid => StreamID, + reason => Reason, + req => Req, + resp_status => RespStatus, + resp_headers => RespHeaders, + req_start => ReqStart, + req_end => ReqEnd, + req_body_start => ReqBodyStart, + req_body_end => ReqBodyEnd, + resp_start => RespStart, + resp_end => RespEnd, + procs => Procs, + informational => lists:reverse(Infos), + req_body_length => ReqBodyLen, + resp_body_length => RespBodyLen, + user_data => UserData + }, + Fun(Metrics), + Res. + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). +early_error(StreamID, Reason, PartialReq=#{ref := Ref}, Resp0, Opts=#{metrics_callback := Fun}) -> + Time = erlang:monotonic_time(), + Resp = {response, RespStatus, RespHeaders, RespBody} + = cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp0, Opts), + %% As far as metrics go we are limited in what we can provide + %% in this case. + Metrics = #{ + ref => Ref, + pid => self(), + streamid => StreamID, + reason => Reason, + partial_req => PartialReq, + resp_status => RespStatus, + resp_headers => RespHeaders, + early_error_time => Time, + resp_body_length => resp_body_length(RespBody) + }, + Fun(Metrics), + Resp. + +resp_body_length({sendfile, _, Len, _}) -> + Len; +resp_body_length(Data) -> + iolist_size(Data). diff --git a/cowboy/src/cowboy_middleware.erl b/cowboy/src/cowboy_middleware.erl new file mode 100644 index 0000000..efeef4f --- /dev/null +++ b/cowboy/src/cowboy_middleware.erl @@ -0,0 +1,24 @@ +%% Copyright (c) 2013-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_middleware). + +-type env() :: #{atom() => any()}. +-export_type([env/0]). + +-callback execute(Req, Env) + -> {ok, Req, Env} + | {suspend, module(), atom(), [any()]} + | {stop, Req} + when Req::cowboy_req:req(), Env::env(). diff --git a/cowboy/src/cowboy_req.erl b/cowboy/src/cowboy_req.erl new file mode 100644 index 0000000..3f87677 --- /dev/null +++ b/cowboy/src/cowboy_req.erl @@ -0,0 +1,1054 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% Copyright (c) 2011, Anthony Ramine +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_req). + +%% Request. +-export([method/1]). +-export([version/1]). +-export([peer/1]). +-export([sock/1]). +-export([cert/1]). +-export([scheme/1]). +-export([host/1]). +-export([host_info/1]). +-export([port/1]). +-export([path/1]). +-export([path_info/1]). +-export([qs/1]). +-export([parse_qs/1]). +-export([match_qs/2]). +-export([uri/1]). +-export([uri/2]). +-export([binding/2]). +-export([binding/3]). +-export([bindings/1]). +-export([header/2]). +-export([header/3]). +-export([headers/1]). +-export([parse_header/2]). +-export([parse_header/3]). +-export([filter_cookies/2]). +-export([parse_cookies/1]). +-export([match_cookies/2]). + +%% Request body. +-export([has_body/1]). +-export([body_length/1]). +-export([read_body/1]). +-export([read_body/2]). +-export([read_urlencoded_body/1]). +-export([read_urlencoded_body/2]). +-export([read_and_match_urlencoded_body/2]). +-export([read_and_match_urlencoded_body/3]). + +%% Multipart. +-export([read_part/1]). +-export([read_part/2]). +-export([read_part_body/1]). +-export([read_part_body/2]). + +%% Response. +-export([set_resp_cookie/3]). +-export([set_resp_cookie/4]). +-export([resp_header/2]). +-export([resp_header/3]). +-export([resp_headers/1]). +-export([set_resp_header/3]). +-export([set_resp_headers/2]). +-export([has_resp_header/2]). +-export([delete_resp_header/2]). +-export([set_resp_body/2]). +%% @todo set_resp_body/3 with a ContentType or even Headers argument, to set content headers. +-export([has_resp_body/1]). +-export([inform/2]). +-export([inform/3]). +-export([reply/2]). +-export([reply/3]). +-export([reply/4]). +-export([stream_reply/2]). +-export([stream_reply/3]). +%% @todo stream_body/2 (nofin) +-export([stream_body/3]). +%% @todo stream_events/2 (nofin) +-export([stream_events/3]). +-export([stream_trailers/2]). +-export([push/3]). +-export([push/4]). + +%% Stream handlers. +-export([cast/2]). + +%% Internal. +-export([response_headers/2]). + +-type read_body_opts() :: #{ + length => non_neg_integer() | infinity, + period => non_neg_integer(), + timeout => timeout() +}. +-export_type([read_body_opts/0]). + +%% While sendfile allows a Len of 0 that means "everything past Offset", +%% Cowboy expects the real length as it is used as metadata. +-type resp_body() :: iodata() + | {sendfile, non_neg_integer(), non_neg_integer(), file:name_all()}. +-export_type([resp_body/0]). + +-type push_opts() :: #{ + method => binary(), + scheme => binary(), + host => binary(), + port => inet:port_number(), + qs => binary() +}. +-export_type([push_opts/0]). + +-type req() :: #{ + %% Public interface. + method := binary(), + version := cowboy:http_version() | atom(), + scheme := binary(), + host := binary(), + port := inet:port_number(), + path := binary(), + qs := binary(), + headers := cowboy:http_headers(), + peer := {inet:ip_address(), inet:port_number()}, + sock := {inet:ip_address(), inet:port_number()}, + cert := binary() | undefined, + + %% Private interface. + ref := ranch:ref(), + pid := pid(), + streamid := cowboy_stream:streamid(), + + host_info => cowboy_router:tokens(), + path_info => cowboy_router:tokens(), + bindings => cowboy_router:bindings(), + + has_body := boolean(), + body_length := non_neg_integer() | undefined, + has_read_body => true, + multipart => {binary(), binary()} | done, + + has_sent_resp => headers | true, + resp_cookies => #{iodata() => iodata()}, + resp_headers => #{binary() => iodata()}, + resp_body => resp_body(), + + proxy_header => ranch_proxy_header:proxy_info(), + media_type => {binary(), binary(), [{binary(), binary()}]}, + language => binary() | undefined, + charset => binary() | undefined, + range => {binary(), binary() + | [{non_neg_integer(), non_neg_integer() | infinity} | neg_integer()]}, + websocket_version => 7 | 8 | 13, + + %% The user is encouraged to use the Req to store information + %% when no better solution is available. + _ => _ +}. +-export_type([req/0]). + +%% Request. + +-spec method(req()) -> binary(). +method(#{method := Method}) -> + Method. + +-spec version(req()) -> cowboy:http_version(). +version(#{version := Version}) -> + Version. + +-spec peer(req()) -> {inet:ip_address(), inet:port_number()}. +peer(#{peer := Peer}) -> + Peer. + +-spec sock(req()) -> {inet:ip_address(), inet:port_number()}. +sock(#{sock := Sock}) -> + Sock. + +-spec cert(req()) -> binary() | undefined. +cert(#{cert := Cert}) -> + Cert. + +-spec scheme(req()) -> binary(). +scheme(#{scheme := Scheme}) -> + Scheme. + +-spec host(req()) -> binary(). +host(#{host := Host}) -> + Host. + +%% @todo The host_info is undefined if cowboy_router isn't used. Do we want to crash? +-spec host_info(req()) -> cowboy_router:tokens() | undefined. +host_info(#{host_info := HostInfo}) -> + HostInfo. + +-spec port(req()) -> inet:port_number(). +port(#{port := Port}) -> + Port. + +-spec path(req()) -> binary(). +path(#{path := Path}) -> + Path. + +%% @todo The path_info is undefined if cowboy_router isn't used. Do we want to crash? +-spec path_info(req()) -> cowboy_router:tokens() | undefined. +path_info(#{path_info := PathInfo}) -> + PathInfo. + +-spec qs(req()) -> binary(). +qs(#{qs := Qs}) -> + Qs. + +%% @todo Might be useful to limit the number of keys. +-spec parse_qs(req()) -> [{binary(), binary() | true}]. +parse_qs(#{qs := Qs}) -> + try + cow_qs:parse_qs(Qs) + catch _:_:Stacktrace -> + erlang:raise(exit, {request_error, qs, + 'Malformed query string; application/x-www-form-urlencoded expected.' + }, Stacktrace) + end. + +-spec match_qs(cowboy:fields(), req()) -> map(). +match_qs(Fields, Req) -> + case filter(Fields, kvlist_to_map(Fields, parse_qs(Req))) of + {ok, Map} -> + Map; + {error, Errors} -> + exit({request_error, {match_qs, Errors}, + 'Query string validation constraints failed for the reasons provided.'}) + end. + +-spec uri(req()) -> iodata(). +uri(Req) -> + uri(Req, #{}). + +-spec uri(req(), map()) -> iodata(). +uri(#{scheme := Scheme0, host := Host0, port := Port0, + path := Path0, qs := Qs0}, Opts) -> + Scheme = case maps:get(scheme, Opts, Scheme0) of + S = undefined -> S; + S -> iolist_to_binary(S) + end, + Host = maps:get(host, Opts, Host0), + Port = maps:get(port, Opts, Port0), + {Path, Qs} = case maps:get(path, Opts, Path0) of + <<"*">> -> {<<>>, <<>>}; + P -> {P, maps:get(qs, Opts, Qs0)} + end, + Fragment = maps:get(fragment, Opts, undefined), + [uri_host(Scheme, Scheme0, Port, Host), uri_path(Path), uri_qs(Qs), uri_fragment(Fragment)]. + +uri_host(_, _, _, undefined) -> <<>>; +uri_host(Scheme, Scheme0, Port, Host) -> + case iolist_size(Host) of + 0 -> <<>>; + _ -> [uri_scheme(Scheme), <<"//">>, Host, uri_port(Scheme, Scheme0, Port)] + end. + +uri_scheme(undefined) -> <<>>; +uri_scheme(Scheme) -> + case iolist_size(Scheme) of + 0 -> Scheme; + _ -> [Scheme, $:] + end. + +uri_port(_, _, undefined) -> <<>>; +uri_port(undefined, <<"http">>, 80) -> <<>>; +uri_port(undefined, <<"https">>, 443) -> <<>>; +uri_port(<<"http">>, _, 80) -> <<>>; +uri_port(<<"https">>, _, 443) -> <<>>; +uri_port(_, _, Port) -> + [$:, integer_to_binary(Port)]. + +uri_path(undefined) -> <<>>; +uri_path(Path) -> Path. + +uri_qs(undefined) -> <<>>; +uri_qs(Qs) -> + case iolist_size(Qs) of + 0 -> Qs; + _ -> [$?, Qs] + end. + +uri_fragment(undefined) -> <<>>; +uri_fragment(Fragment) -> + case iolist_size(Fragment) of + 0 -> Fragment; + _ -> [$#, Fragment] + end. + +-ifdef(TEST). +uri1_test() -> + <<"http://localhost/path">> = iolist_to_binary(uri(#{ + scheme => <<"http">>, host => <<"localhost">>, port => 80, + path => <<"/path">>, qs => <<>>})), + <<"http://localhost:443/path">> = iolist_to_binary(uri(#{ + scheme => <<"http">>, host => <<"localhost">>, port => 443, + path => <<"/path">>, qs => <<>>})), + <<"http://localhost:8080/path">> = iolist_to_binary(uri(#{ + scheme => <<"http">>, host => <<"localhost">>, port => 8080, + path => <<"/path">>, qs => <<>>})), + <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(#{ + scheme => <<"http">>, host => <<"localhost">>, port => 8080, + path => <<"/path">>, qs => <<"dummy=2785">>})), + <<"https://localhost/path">> = iolist_to_binary(uri(#{ + scheme => <<"https">>, host => <<"localhost">>, port => 443, + path => <<"/path">>, qs => <<>>})), + <<"https://localhost:8443/path">> = iolist_to_binary(uri(#{ + scheme => <<"https">>, host => <<"localhost">>, port => 8443, + path => <<"/path">>, qs => <<>>})), + <<"https://localhost:8443/path?dummy=2785">> = iolist_to_binary(uri(#{ + scheme => <<"https">>, host => <<"localhost">>, port => 8443, + path => <<"/path">>, qs => <<"dummy=2785">>})), + ok. + +uri2_test() -> + Req = #{ + scheme => <<"http">>, host => <<"localhost">>, port => 8080, + path => <<"/path">>, qs => <<"dummy=2785">> + }, + <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{})), + %% Disable individual components. + <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => undefined})), + <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => undefined})), + <<"http://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req, #{port => undefined})), + <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => undefined})), + <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => undefined})), + <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => undefined})), + <<"http://localhost:8080">> = iolist_to_binary(uri(Req, #{path => undefined, qs => undefined})), + <<>> = iolist_to_binary(uri(Req, #{host => undefined, path => undefined, qs => undefined})), + %% Empty values. + <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => <<>>})), + <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => ""})), + <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => [<<>>]})), + <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => <<>>})), + <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => ""})), + <<"/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => [<<>>]})), + <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => <<>>})), + <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => ""})), + <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req, #{path => [<<>>]})), + <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => <<>>})), + <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => ""})), + <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req, #{qs => [<<>>]})), + <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => <<>>})), + <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => ""})), + <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{fragment => [<<>>]})), + %% Port is integer() | undefined. + {'EXIT', _} = (catch iolist_to_binary(uri(Req, #{port => <<>>}))), + {'EXIT', _} = (catch iolist_to_binary(uri(Req, #{port => ""}))), + {'EXIT', _} = (catch iolist_to_binary(uri(Req, #{port => [<<>>]}))), + %% Update components. + <<"https://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => "https"})), + <<"http://example.org:8080/path?dummy=2785">> = iolist_to_binary(uri(Req, #{host => "example.org"})), + <<"http://localhost:123/path?dummy=2785">> = iolist_to_binary(uri(Req, #{port => 123})), + <<"http://localhost:8080/custom?dummy=2785">> = iolist_to_binary(uri(Req, #{path => "/custom"})), + <<"http://localhost:8080/path?smart=42">> = iolist_to_binary(uri(Req, #{qs => "smart=42"})), + <<"http://localhost:8080/path?dummy=2785#intro">> = iolist_to_binary(uri(Req, #{fragment => "intro"})), + %% Interesting combinations. + <<"http://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req, #{port => 80})), + <<"https://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req, #{scheme => "https", port => 443})), + ok. +-endif. + +-spec binding(atom(), req()) -> any() | undefined. +binding(Name, Req) -> + binding(Name, Req, undefined). + +-spec binding(atom(), req(), Default) -> any() | Default when Default::any(). +binding(Name, #{bindings := Bindings}, Default) when is_atom(Name) -> + case Bindings of + #{Name := Value} -> Value; + _ -> Default + end; +binding(Name, _, Default) when is_atom(Name) -> + Default. + +-spec bindings(req()) -> cowboy_router:bindings(). +bindings(#{bindings := Bindings}) -> + Bindings; +bindings(_) -> + #{}. + +-spec header(binary(), req()) -> binary() | undefined. +header(Name, Req) -> + header(Name, Req, undefined). + +-spec header(binary(), req(), Default) -> binary() | Default when Default::any(). +header(Name, #{headers := Headers}, Default) -> + maps:get(Name, Headers, Default). + +-spec headers(req()) -> cowboy:http_headers(). +headers(#{headers := Headers}) -> + Headers. + +-spec parse_header(binary(), Req) -> any() when Req::req(). +parse_header(Name = <<"content-length">>, Req) -> + parse_header(Name, Req, 0); +parse_header(Name = <<"cookie">>, Req) -> + parse_header(Name, Req, []); +parse_header(Name, Req) -> + parse_header(Name, Req, undefined). + +-spec parse_header(binary(), Req, any()) -> any() when Req::req(). +parse_header(Name, Req, Default) -> + try + parse_header(Name, Req, Default, parse_header_fun(Name)) + catch _:_:Stacktrace -> + erlang:raise(exit, {request_error, {header, Name}, + 'Malformed header. Please consult the relevant specification.' + }, Stacktrace) + end. + +parse_header_fun(<<"accept">>) -> fun cow_http_hd:parse_accept/1; +parse_header_fun(<<"accept-charset">>) -> fun cow_http_hd:parse_accept_charset/1; +parse_header_fun(<<"accept-encoding">>) -> fun cow_http_hd:parse_accept_encoding/1; +parse_header_fun(<<"accept-language">>) -> fun cow_http_hd:parse_accept_language/1; +parse_header_fun(<<"access-control-request-headers">>) -> fun cow_http_hd:parse_access_control_request_headers/1; +parse_header_fun(<<"access-control-request-method">>) -> fun cow_http_hd:parse_access_control_request_method/1; +parse_header_fun(<<"authorization">>) -> fun cow_http_hd:parse_authorization/1; +parse_header_fun(<<"connection">>) -> fun cow_http_hd:parse_connection/1; +parse_header_fun(<<"content-encoding">>) -> fun cow_http_hd:parse_content_encoding/1; +parse_header_fun(<<"content-language">>) -> fun cow_http_hd:parse_content_language/1; +parse_header_fun(<<"content-length">>) -> fun cow_http_hd:parse_content_length/1; +parse_header_fun(<<"content-type">>) -> fun cow_http_hd:parse_content_type/1; +parse_header_fun(<<"cookie">>) -> fun cow_cookie:parse_cookie/1; +parse_header_fun(<<"expect">>) -> fun cow_http_hd:parse_expect/1; +parse_header_fun(<<"if-match">>) -> fun cow_http_hd:parse_if_match/1; +parse_header_fun(<<"if-modified-since">>) -> fun cow_http_hd:parse_if_modified_since/1; +parse_header_fun(<<"if-none-match">>) -> fun cow_http_hd:parse_if_none_match/1; +parse_header_fun(<<"if-range">>) -> fun cow_http_hd:parse_if_range/1; +parse_header_fun(<<"if-unmodified-since">>) -> fun cow_http_hd:parse_if_unmodified_since/1; +parse_header_fun(<<"max-forwards">>) -> fun cow_http_hd:parse_max_forwards/1; +parse_header_fun(<<"origin">>) -> fun cow_http_hd:parse_origin/1; +parse_header_fun(<<"proxy-authorization">>) -> fun cow_http_hd:parse_proxy_authorization/1; +parse_header_fun(<<"range">>) -> fun cow_http_hd:parse_range/1; +parse_header_fun(<<"sec-websocket-extensions">>) -> fun cow_http_hd:parse_sec_websocket_extensions/1; +parse_header_fun(<<"sec-websocket-protocol">>) -> fun cow_http_hd:parse_sec_websocket_protocol_req/1; +parse_header_fun(<<"sec-websocket-version">>) -> fun cow_http_hd:parse_sec_websocket_version_req/1; +parse_header_fun(<<"trailer">>) -> fun cow_http_hd:parse_trailer/1; +parse_header_fun(<<"upgrade">>) -> fun cow_http_hd:parse_upgrade/1; +parse_header_fun(<<"x-forwarded-for">>) -> fun cow_http_hd:parse_x_forwarded_for/1. + +parse_header(Name, Req, Default, ParseFun) -> + case header(Name, Req) of + undefined -> Default; + Value -> ParseFun(Value) + end. + +-spec filter_cookies([atom() | binary()], Req) -> Req when Req::req(). +filter_cookies(Names0, Req=#{headers := Headers}) -> + Names = [if + is_atom(N) -> atom_to_binary(N, utf8); + true -> N + end || N <- Names0], + case header(<<"cookie">>, Req) of + undefined -> Req; + Value0 -> + Cookies0 = binary:split(Value0, <<$;>>), + Cookies = lists:filter(fun(Cookie) -> + lists:member(cookie_name(Cookie), Names) + end, Cookies0), + Value = iolist_to_binary(lists:join($;, Cookies)), + Req#{headers => Headers#{<<"cookie">> => Value}} + end. + +%% This is a specialized function to extract a cookie name +%% regardless of whether the name is valid or not. We skip +%% whitespace at the beginning and take whatever's left to +%% be the cookie name, up to the = sign. +cookie_name(<<$\s, Rest/binary>>) -> cookie_name(Rest); +cookie_name(<<$\t, Rest/binary>>) -> cookie_name(Rest); +cookie_name(Name) -> cookie_name(Name, <<>>). + +cookie_name(<<>>, Name) -> Name; +cookie_name(<<$=, _/bits>>, Name) -> Name; +cookie_name(<>, Acc) -> cookie_name(Rest, <>). + +-spec parse_cookies(req()) -> [{binary(), binary()}]. +parse_cookies(Req) -> + parse_header(<<"cookie">>, Req). + +-spec match_cookies(cowboy:fields(), req()) -> map(). +match_cookies(Fields, Req) -> + case filter(Fields, kvlist_to_map(Fields, parse_cookies(Req))) of + {ok, Map} -> + Map; + {error, Errors} -> + exit({request_error, {match_cookies, Errors}, + 'Cookie validation constraints failed for the reasons provided.'}) + end. + +%% Request body. + +-spec has_body(req()) -> boolean(). +has_body(#{has_body := HasBody}) -> + HasBody. + +%% The length may not be known if HTTP/1.1 with a transfer-encoding; +%% or HTTP/2 with no content-length header. The length is always +%% known once the body has been completely read. +-spec body_length(req()) -> undefined | non_neg_integer(). +body_length(#{body_length := Length}) -> + Length. + +-spec read_body(Req) -> {ok, binary(), Req} | {more, binary(), Req} when Req::req(). +read_body(Req) -> + read_body(Req, #{}). + +-spec read_body(Req, read_body_opts()) -> {ok, binary(), Req} | {more, binary(), Req} when Req::req(). +read_body(Req=#{has_body := false}, _) -> + {ok, <<>>, Req}; +read_body(Req=#{has_read_body := true}, _) -> + {ok, <<>>, Req}; +read_body(Req, Opts) -> + Length = maps:get(length, Opts, 8000000), + Period = maps:get(period, Opts, 15000), + DefaultTimeout = case Period of + infinity -> infinity; %% infinity + 1000 = infinity. + _ -> Period + 1000 + end, + Timeout = maps:get(timeout, Opts, DefaultTimeout), + Ref = make_ref(), + cast({read_body, self(), Ref, Length, Period}, Req), + receive + {request_body, Ref, nofin, Body} -> + {more, Body, Req}; + {request_body, Ref, fin, BodyLength, Body} -> + {ok, Body, set_body_length(Req, BodyLength)} + after Timeout -> + exit(timeout) + end. + +set_body_length(Req=#{headers := Headers}, BodyLength) -> + Req#{ + headers => Headers#{<<"content-length">> => integer_to_binary(BodyLength)}, + body_length => BodyLength, + has_read_body => true + }. + +-spec read_urlencoded_body(Req) -> {ok, [{binary(), binary() | true}], Req} when Req::req(). +read_urlencoded_body(Req) -> + read_urlencoded_body(Req, #{length => 64000, period => 5000}). + +-spec read_urlencoded_body(Req, read_body_opts()) -> {ok, [{binary(), binary() | true}], Req} when Req::req(). +read_urlencoded_body(Req0, Opts) -> + case read_body(Req0, Opts) of + {ok, Body, Req} -> + try + {ok, cow_qs:parse_qs(Body), Req} + catch _:_:Stacktrace -> + erlang:raise(exit, {request_error, urlencoded_body, + 'Malformed body; application/x-www-form-urlencoded expected.' + }, Stacktrace) + end; + {more, Body, _} -> + Length = maps:get(length, Opts, 64000), + if + byte_size(Body) < Length -> + exit({request_error, timeout, + 'The request body was not received within the configured time.'}); + true -> + exit({request_error, payload_too_large, + 'The request body is larger than allowed by configuration.'}) + end + end. + +-spec read_and_match_urlencoded_body(cowboy:fields(), Req) + -> {ok, map(), Req} when Req::req(). +read_and_match_urlencoded_body(Fields, Req) -> + read_and_match_urlencoded_body(Fields, Req, #{length => 64000, period => 5000}). + +-spec read_and_match_urlencoded_body(cowboy:fields(), Req, read_body_opts()) + -> {ok, map(), Req} when Req::req(). +read_and_match_urlencoded_body(Fields, Req0, Opts) -> + {ok, Qs, Req} = read_urlencoded_body(Req0, Opts), + case filter(Fields, kvlist_to_map(Fields, Qs)) of + {ok, Map} -> + {ok, Map, Req}; + {error, Errors} -> + exit({request_error, {read_and_match_urlencoded_body, Errors}, + 'Urlencoded request body validation constraints failed for the reasons provided.'}) + end. + +%% Multipart. + +-spec read_part(Req) + -> {ok, cowboy:http_headers(), Req} | {done, Req} + when Req::req(). +read_part(Req) -> + read_part(Req, #{length => 64000, period => 5000}). + +-spec read_part(Req, read_body_opts()) + -> {ok, cowboy:http_headers(), Req} | {done, Req} + when Req::req(). +read_part(Req, Opts) -> + case maps:is_key(multipart, Req) of + true -> + {Data, Req2} = stream_multipart(Req, Opts, headers), + read_part(Data, Opts, Req2); + false -> + read_part(init_multipart(Req), Opts) + end. + +read_part(Buffer, Opts, Req=#{multipart := {Boundary, _}}) -> + try cow_multipart:parse_headers(Buffer, Boundary) of + more -> + {Data, Req2} = stream_multipart(Req, Opts, headers), + read_part(<< Buffer/binary, Data/binary >>, Opts, Req2); + {more, Buffer2} -> + {Data, Req2} = stream_multipart(Req, Opts, headers), + read_part(<< Buffer2/binary, Data/binary >>, Opts, Req2); + {ok, Headers0, Rest} -> + Headers = maps:from_list(Headers0), + %% Reject multipart content containing duplicate headers. + true = map_size(Headers) =:= length(Headers0), + {ok, Headers, Req#{multipart => {Boundary, Rest}}}; + %% Ignore epilogue. + {done, _} -> + {done, Req#{multipart => done}} + catch _:_:Stacktrace -> + erlang:raise(exit, {request_error, {multipart, headers}, + 'Malformed body; multipart expected.' + }, Stacktrace) + end. + +-spec read_part_body(Req) + -> {ok, binary(), Req} | {more, binary(), Req} + when Req::req(). +read_part_body(Req) -> + read_part_body(Req, #{}). + +-spec read_part_body(Req, read_body_opts()) + -> {ok, binary(), Req} | {more, binary(), Req} + when Req::req(). +read_part_body(Req, Opts) -> + case maps:is_key(multipart, Req) of + true -> + read_part_body(<<>>, Opts, Req, <<>>); + false -> + read_part_body(init_multipart(Req), Opts) + end. + +read_part_body(Buffer, Opts, Req=#{multipart := {Boundary, _}}, Acc) -> + Length = maps:get(length, Opts, 8000000), + case byte_size(Acc) > Length of + true -> + {more, Acc, Req#{multipart => {Boundary, Buffer}}}; + false -> + {Data, Req2} = stream_multipart(Req, Opts, body), + case cow_multipart:parse_body(<< Buffer/binary, Data/binary >>, Boundary) of + {ok, Body} -> + read_part_body(<<>>, Opts, Req2, << Acc/binary, Body/binary >>); + {ok, Body, Rest} -> + read_part_body(Rest, Opts, Req2, << Acc/binary, Body/binary >>); + done -> + {ok, Acc, Req2}; + {done, Body} -> + {ok, << Acc/binary, Body/binary >>, Req2}; + {done, Body, Rest} -> + {ok, << Acc/binary, Body/binary >>, + Req2#{multipart => {Boundary, Rest}}} + end + end. + +init_multipart(Req) -> + {<<"multipart">>, _, Params} = parse_header(<<"content-type">>, Req), + case lists:keyfind(<<"boundary">>, 1, Params) of + {_, Boundary} -> + Req#{multipart => {Boundary, <<>>}}; + false -> + exit({request_error, {multipart, boundary}, + 'Missing boundary parameter for multipart media type.'}) + end. + +stream_multipart(Req=#{multipart := done}, _, _) -> + {<<>>, Req}; +stream_multipart(Req=#{multipart := {_, <<>>}}, Opts, Type) -> + case read_body(Req, Opts) of + {more, Data, Req2} -> + {Data, Req2}; + %% We crash when the data ends unexpectedly. + {ok, <<>>, _} -> + exit({request_error, {multipart, Type}, + 'Malformed body; multipart expected.'}); + {ok, Data, Req2} -> + {Data, Req2} + end; +stream_multipart(Req=#{multipart := {Boundary, Buffer}}, _, _) -> + {Buffer, Req#{multipart => {Boundary, <<>>}}}. + +%% Response. + +-spec set_resp_cookie(iodata(), iodata(), Req) + -> Req when Req::req(). +set_resp_cookie(Name, Value, Req) -> + set_resp_cookie(Name, Value, Req, #{}). + +%% The cookie name cannot contain any of the following characters: +%% =,;\s\t\r\n\013\014 +%% +%% The cookie value cannot contain any of the following characters: +%% ,; \t\r\n\013\014 +-spec set_resp_cookie(binary(), iodata(), Req, cow_cookie:cookie_opts()) + -> Req when Req::req(). +set_resp_cookie(Name, Value, Req, Opts) -> + Cookie = cow_cookie:setcookie(Name, Value, Opts), + RespCookies = maps:get(resp_cookies, Req, #{}), + Req#{resp_cookies => RespCookies#{Name => Cookie}}. + +%% @todo We could add has_resp_cookie and unset_resp_cookie now. + +-spec set_resp_header(binary(), iodata(), Req) + -> Req when Req::req(). +set_resp_header(<<"set-cookie">>, _, _) -> + exit({response_error, invalid_header, + 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); +set_resp_header(Name, Value, Req=#{resp_headers := RespHeaders}) -> + Req#{resp_headers => RespHeaders#{Name => Value}}; +set_resp_header(Name,Value, Req) -> + Req#{resp_headers => #{Name => Value}}. + +-spec set_resp_headers(cowboy:http_headers(), Req) + -> Req when Req::req(). +set_resp_headers(#{<<"set-cookie">> := _}, _) -> + exit({response_error, invalid_header, + 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); +set_resp_headers(Headers, Req=#{resp_headers := RespHeaders}) -> + Req#{resp_headers => maps:merge(RespHeaders, Headers)}; +set_resp_headers(Headers, Req) -> + Req#{resp_headers => Headers}. + +-spec resp_header(binary(), req()) -> binary() | undefined. +resp_header(Name, Req) -> + resp_header(Name, Req, undefined). + +-spec resp_header(binary(), req(), Default) + -> binary() | Default when Default::any(). +resp_header(Name, #{resp_headers := Headers}, Default) -> + maps:get(Name, Headers, Default); +resp_header(_, #{}, Default) -> + Default. + +-spec resp_headers(req()) -> cowboy:http_headers(). +resp_headers(#{resp_headers := RespHeaders}) -> + RespHeaders; +resp_headers(#{}) -> + #{}. + +-spec set_resp_body(resp_body(), Req) -> Req when Req::req(). +set_resp_body(Body, Req) -> + Req#{resp_body => Body}. + +-spec has_resp_header(binary(), req()) -> boolean(). +has_resp_header(Name, #{resp_headers := RespHeaders}) -> + maps:is_key(Name, RespHeaders); +has_resp_header(_, _) -> + false. + +-spec has_resp_body(req()) -> boolean(). +has_resp_body(#{resp_body := {sendfile, _, _, _}}) -> + true; +has_resp_body(#{resp_body := RespBody}) -> + iolist_size(RespBody) > 0; +has_resp_body(_) -> + false. + +-spec delete_resp_header(binary(), Req) + -> Req when Req::req(). +delete_resp_header(Name, Req=#{resp_headers := RespHeaders}) -> + Req#{resp_headers => maps:remove(Name, RespHeaders)}; +%% There are no resp headers so we have nothing to delete. +delete_resp_header(_, Req) -> + Req. + +-spec inform(cowboy:http_status(), req()) -> ok. +inform(Status, Req) -> + inform(Status, #{}, Req). + +-spec inform(cowboy:http_status(), cowboy:http_headers(), req()) -> ok. +inform(_, _, #{has_sent_resp := _}) -> + exit({response_error, response_already_sent, + 'The final response has already been sent.'}); +inform(_, #{<<"set-cookie">> := _}, _) -> + exit({response_error, invalid_header, + 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); +inform(Status, Headers, Req) when is_integer(Status); is_binary(Status) -> + cast({inform, Status, Headers}, Req). + +-spec reply(cowboy:http_status(), Req) -> Req when Req::req(). +reply(Status, Req) -> + reply(Status, #{}, Req). + +-spec reply(cowboy:http_status(), cowboy:http_headers(), Req) + -> Req when Req::req(). +reply(Status, Headers, Req=#{resp_body := Body}) -> + reply(Status, Headers, Body, Req); +reply(Status, Headers, Req) -> + reply(Status, Headers, <<>>, Req). + +-spec reply(cowboy:http_status(), cowboy:http_headers(), resp_body(), Req) + -> Req when Req::req(). +reply(_, _, _, #{has_sent_resp := _}) -> + exit({response_error, response_already_sent, + 'The final response has already been sent.'}); +reply(_, #{<<"set-cookie">> := _}, _, _) -> + exit({response_error, invalid_header, + 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); +reply(Status, Headers, {sendfile, _, 0, _}, Req) + when is_integer(Status); is_binary(Status) -> + do_reply(Status, Headers#{ + <<"content-length">> => <<"0">> + }, <<>>, Req); +reply(Status, Headers, SendFile = {sendfile, _, Len, _}, Req) + when is_integer(Status); is_binary(Status) -> + do_reply(Status, Headers#{ + <<"content-length">> => integer_to_binary(Len) + }, SendFile, Req); +%% 204 responses must not include content-length. 304 responses may +%% but only when set explicitly. (RFC7230 3.3.1, RFC7230 3.3.2) +%% Neither status code must include a response body. (RFC7230 3.3) +reply(Status, Headers, Body, Req) + when Status =:= 204; Status =:= 304 -> + do_reply_ensure_no_body(Status, Headers, Body, Req); +reply(Status = <<"204",_/bits>>, Headers, Body, Req) -> + do_reply_ensure_no_body(Status, Headers, Body, Req); +reply(Status = <<"304",_/bits>>, Headers, Body, Req) -> + do_reply_ensure_no_body(Status, Headers, Body, Req); +reply(Status, Headers, Body, Req) + when is_integer(Status); is_binary(Status) -> + do_reply(Status, Headers#{ + <<"content-length">> => integer_to_binary(iolist_size(Body)) + }, Body, Req). + +do_reply_ensure_no_body(Status, Headers, Body, Req) -> + case iolist_size(Body) of + 0 -> + do_reply(Status, Headers, Body, Req); + _ -> + exit({response_error, payload_too_large, + '204 and 304 responses must not include a body. (RFC7230 3.3)'}) + end. + +%% Don't send any body for HEAD responses. While the protocol code is +%% supposed to enforce this rule, we prefer to avoid copying too much +%% data around if we can avoid it. +do_reply(Status, Headers, _, Req=#{method := <<"HEAD">>}) -> + cast({response, Status, response_headers(Headers, Req), <<>>}, Req), + done_replying(Req, true); +do_reply(Status, Headers, Body, Req) -> + cast({response, Status, response_headers(Headers, Req), Body}, Req), + done_replying(Req, true). + +done_replying(Req, HasSentResp) -> + maps:without([resp_cookies, resp_headers, resp_body], Req#{has_sent_resp => HasSentResp}). + +-spec stream_reply(cowboy:http_status(), Req) -> Req when Req::req(). +stream_reply(Status, Req) -> + stream_reply(Status, #{}, Req). + +-spec stream_reply(cowboy:http_status(), cowboy:http_headers(), Req) + -> Req when Req::req(). +stream_reply(_, _, #{has_sent_resp := _}) -> + exit({response_error, response_already_sent, + 'The final response has already been sent.'}); +stream_reply(_, #{<<"set-cookie">> := _}, _) -> + exit({response_error, invalid_header, + 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); +%% 204 and 304 responses must NOT send a body. We therefore +%% transform the call to a full response and expect the user +%% to NOT call stream_body/3 afterwards. (RFC7230 3.3) +stream_reply(Status, Headers=#{}, Req) + when Status =:= 204; Status =:= 304 -> + reply(Status, Headers, <<>>, Req); +stream_reply(Status = <<"204",_/bits>>, Headers=#{}, Req) -> + reply(Status, Headers, <<>>, Req); +stream_reply(Status = <<"304",_/bits>>, Headers=#{}, Req) -> + reply(Status, Headers, <<>>, Req); +stream_reply(Status, Headers=#{}, Req) when is_integer(Status); is_binary(Status) -> + cast({headers, Status, response_headers(Headers, Req)}, Req), + done_replying(Req, headers). + +-spec stream_body(resp_body(), fin | nofin, req()) -> ok. +%% Error out if headers were not sent. +%% Don't send any body for HEAD responses. +stream_body(_, _, #{method := <<"HEAD">>, has_sent_resp := headers}) -> + ok; +%% Don't send a message if the data is empty, except for the +%% very last message with IsFin=fin. When using sendfile this +%% is converted to a data tuple, however. +stream_body({sendfile, _, 0, _}, nofin, _) -> + ok; +stream_body({sendfile, _, 0, _}, IsFin=fin, Req=#{has_sent_resp := headers}) -> + stream_body({data, self(), IsFin, <<>>}, Req); +stream_body({sendfile, O, B, P}, IsFin, Req=#{has_sent_resp := headers}) + when is_integer(O), O >= 0, is_integer(B), B > 0 -> + stream_body({data, self(), IsFin, {sendfile, O, B, P}}, Req); +stream_body(Data, IsFin=nofin, Req=#{has_sent_resp := headers}) + when not is_tuple(Data) -> + case iolist_size(Data) of + 0 -> ok; + _ -> stream_body({data, self(), IsFin, Data}, Req) + end; +stream_body(Data, IsFin, Req=#{has_sent_resp := headers}) + when not is_tuple(Data) -> + stream_body({data, self(), IsFin, Data}, Req). + +%% @todo Do we need a timeout? +stream_body(Msg, Req=#{pid := Pid}) -> + cast(Msg, Req), + receive {data_ack, Pid} -> ok end. + +-spec stream_events(cow_sse:event() | [cow_sse:event()], fin | nofin, req()) -> ok. +stream_events(Event, IsFin, Req) when is_map(Event) -> + stream_events([Event], IsFin, Req); +stream_events(Events, IsFin, Req=#{has_sent_resp := headers}) -> + stream_body({data, self(), IsFin, cow_sse:events(Events)}, Req). + +-spec stream_trailers(cowboy:http_headers(), req()) -> ok. +stream_trailers(#{<<"set-cookie">> := _}, _) -> + exit({response_error, invalid_header, + 'Response cookies must be set using cowboy_req:set_resp_cookie/3,4.'}); +stream_trailers(Trailers, Req=#{has_sent_resp := headers}) -> + cast({trailers, Trailers}, Req). + +-spec push(iodata(), cowboy:http_headers(), req()) -> ok. +push(Path, Headers, Req) -> + push(Path, Headers, Req, #{}). + +%% @todo Optimization: don't send anything at all for HTTP/1.0 and HTTP/1.1. +%% @todo Path, Headers, Opts, everything should be in proper binary, +%% or normalized when creating the Req object. +-spec push(iodata(), cowboy:http_headers(), req(), push_opts()) -> ok. +push(_, _, #{has_sent_resp := _}, _) -> + exit({response_error, response_already_sent, + 'The final response has already been sent.'}); +push(Path, Headers, Req=#{scheme := Scheme0, host := Host0, port := Port0}, Opts) -> + Method = maps:get(method, Opts, <<"GET">>), + Scheme = maps:get(scheme, Opts, Scheme0), + Host = maps:get(host, Opts, Host0), + Port = maps:get(port, Opts, Port0), + Qs = maps:get(qs, Opts, <<>>), + cast({push, Method, Scheme, Host, Port, Path, Qs, Headers}, Req). + +%% Stream handlers. + +-spec cast(any(), req()) -> ok. +cast(Msg, #{pid := Pid, streamid := StreamID}) -> + Pid ! {{Pid, StreamID}, Msg}, + ok. + +%% Internal. + +%% @todo What about set-cookie headers set through set_resp_header or reply? +-spec response_headers(Headers, req()) -> Headers when Headers::cowboy:http_headers(). +response_headers(Headers0, Req) -> + RespHeaders = maps:get(resp_headers, Req, #{}), + Headers = maps:merge(#{ + <<"date">> => cowboy_clock:rfc1123(), + <<"server">> => <<"Cowboy">> + }, maps:merge(RespHeaders, Headers0)), + %% The set-cookie header is special; we can only send one cookie per header. + %% We send the list of values for many cookies in one key of the map, + %% and let the protocols deal with it directly. + case maps:get(resp_cookies, Req, undefined) of + undefined -> Headers; + RespCookies -> Headers#{<<"set-cookie">> => maps:values(RespCookies)} + end. + +%% Create map, convert keys to atoms and group duplicate keys into lists. +%% Keys that are not found in the user provided list are entirely skipped. +%% @todo Can probably be done directly while parsing. +kvlist_to_map(Fields, KvList) -> + Keys = [case K of + {Key, _} -> Key; + {Key, _, _} -> Key; + Key -> Key + end || K <- Fields], + kvlist_to_map(Keys, KvList, #{}). + +kvlist_to_map(_, [], Map) -> + Map; +kvlist_to_map(Keys, [{Key, Value}|Tail], Map) -> + try binary_to_existing_atom(Key, utf8) of + Atom -> + case lists:member(Atom, Keys) of + true -> + case maps:find(Atom, Map) of + {ok, MapValue} when is_list(MapValue) -> + kvlist_to_map(Keys, Tail, + Map#{Atom => [Value|MapValue]}); + {ok, MapValue} -> + kvlist_to_map(Keys, Tail, + Map#{Atom => [Value, MapValue]}); + error -> + kvlist_to_map(Keys, Tail, + Map#{Atom => Value}) + end; + false -> + kvlist_to_map(Keys, Tail, Map) + end + catch error:badarg -> + kvlist_to_map(Keys, Tail, Map) + end. + +filter(Fields, Map0) -> + filter(Fields, Map0, #{}). + +%% Loop through fields, if value is missing and no default, +%% record the error; else if value is missing and has a +%% default, set default; otherwise apply constraints. If +%% constraint fails, record the error. +%% +%% When there is an error at the end, crash. +filter([], Map, Errors) -> + case maps:size(Errors) of + 0 -> {ok, Map}; + _ -> {error, Errors} + end; +filter([{Key, Constraints}|Tail], Map, Errors) -> + case maps:find(Key, Map) of + {ok, Value} -> + filter_constraints(Tail, Map, Errors, Key, Value, Constraints); + error -> + filter(Tail, Map, Errors#{Key => required}) + end; +filter([{Key, Constraints, Default}|Tail], Map, Errors) -> + case maps:find(Key, Map) of + {ok, Value} -> + filter_constraints(Tail, Map, Errors, Key, Value, Constraints); + error -> + filter(Tail, Map#{Key => Default}, Errors) + end; +filter([Key|Tail], Map, Errors) -> + case maps:is_key(Key, Map) of + true -> + filter(Tail, Map, Errors); + false -> + filter(Tail, Map, Errors#{Key => required}) + end. + +filter_constraints(Tail, Map, Errors, Key, Value0, Constraints) -> + case cowboy_constraints:validate(Value0, Constraints) of + {ok, Value} -> + filter(Tail, Map#{Key => Value}, Errors); + {error, Reason} -> + filter(Tail, Map, Errors#{Key => Reason}) + end. diff --git a/cowboy/src/cowboy_rest.erl b/cowboy/src/cowboy_rest.erl new file mode 100644 index 0000000..fcea71c --- /dev/null +++ b/cowboy/src/cowboy_rest.erl @@ -0,0 +1,1644 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Originally based on the Webmachine Diagram from Alan Dean and +%% Justin Sheehy. +-module(cowboy_rest). +-behaviour(cowboy_sub_protocol). + +-export([upgrade/4]). +-export([upgrade/5]). + +-type switch_handler() :: {switch_handler, module()} + | {switch_handler, module(), any()}. + +%% Common handler callbacks. + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), any()} + when Req::cowboy_req:req(). + +-callback terminate(any(), cowboy_req:req(), any()) -> ok. +-optional_callbacks([terminate/3]). + +%% REST handler callbacks. + +-callback allowed_methods(Req, State) + -> {[binary()], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([allowed_methods/2]). + +-callback allow_missing_post(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([allow_missing_post/2]). + +-callback charsets_provided(Req, State) + -> {[binary()], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([charsets_provided/2]). + +-callback content_types_accepted(Req, State) + -> {[{'*' | binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([content_types_accepted/2]). + +-callback content_types_provided(Req, State) + -> {[{binary() | {binary(), binary(), '*' | [{binary(), binary()}]}, atom()}], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([content_types_provided/2]). + +-callback delete_completed(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([delete_completed/2]). + +-callback delete_resource(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([delete_resource/2]). + +-callback expires(Req, State) + -> {calendar:datetime() | binary() | undefined, Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([expires/2]). + +-callback forbidden(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([forbidden/2]). + +-callback generate_etag(Req, State) + -> {binary() | {weak | strong, binary()} | undefined, Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([generate_etag/2]). + +-callback is_authorized(Req, State) + -> {true | {false, iodata()}, Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([is_authorized/2]). + +-callback is_conflict(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([is_conflict/2]). + +-callback known_methods(Req, State) + -> {[binary()], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([known_methods/2]). + +-callback languages_provided(Req, State) + -> {[binary()], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([languages_provided/2]). + +-callback last_modified(Req, State) + -> {calendar:datetime(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([last_modified/2]). + +-callback malformed_request(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([malformed_request/2]). + +-callback moved_permanently(Req, State) + -> {{true, iodata()} | false, Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([moved_permanently/2]). + +-callback moved_temporarily(Req, State) + -> {{true, iodata()} | false, Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([moved_temporarily/2]). + +-callback multiple_choices(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([multiple_choices/2]). + +-callback options(Req, State) + -> {ok, Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([options/2]). + +-callback previously_existed(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([previously_existed/2]). + +-callback range_satisfiable(Req, State) + -> {boolean() | {false, non_neg_integer() | iodata()}, Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([range_satisfiable/2]). + +-callback ranges_provided(Req, State) + -> {[{binary(), atom()}], Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([ranges_provided/2]). + +-callback rate_limited(Req, State) + -> {{true, non_neg_integer() | calendar:datetime()} | false, Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([rate_limited/2]). + +-callback resource_exists(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([resource_exists/2]). + +-callback service_available(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([service_available/2]). + +-callback uri_too_long(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([uri_too_long/2]). + +-callback valid_content_headers(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([valid_content_headers/2]). + +-callback valid_entity_length(Req, State) + -> {boolean(), Req, State} + | {stop, Req, State} + | {switch_handler(), Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([valid_entity_length/2]). + +-callback variances(Req, State) + -> {[binary()], Req, State} + when Req::cowboy_req:req(), State::any(). +-optional_callbacks([variances/2]). + +%% End of REST callbacks. Whew! + +-record(state, { + method = undefined :: binary(), + + %% Handler. + handler :: atom(), + handler_state :: any(), + + %% Allowed methods. Only used for OPTIONS requests. + allowed_methods :: [binary()] | undefined, + + %% Media type. + content_types_p = [] :: + [{binary() | {binary(), binary(), [{binary(), binary()}] | '*'}, + atom()}], + content_type_a :: undefined + | {binary() | {binary(), binary(), [{binary(), binary()}] | '*'}, + atom()}, + + %% Language. + languages_p = [] :: [binary()], + language_a :: undefined | binary(), + + %% Charset. + charsets_p = undefined :: undefined | [binary()], + charset_a :: undefined | binary(), + + %% Range units. + ranges_a = [] :: [{binary(), atom()}], + + %% Whether the resource exists. + exists = false :: boolean(), + + %% Cached resource calls. + etag :: undefined | no_call | {strong | weak, binary()}, + last_modified :: undefined | no_call | calendar:datetime(), + expires :: undefined | no_call | calendar:datetime() | binary() +}). + +-spec upgrade(Req, Env, module(), any()) + -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade(Req0, Env, Handler, HandlerState0) -> + Method = cowboy_req:method(Req0), + case service_available(Req0, #state{method=Method, + handler=Handler, handler_state=HandlerState0}) of + {ok, Req, Result} -> + {ok, Req, Env#{result => Result}}; + {Mod, Req, HandlerState} -> + Mod:upgrade(Req, Env, Handler, HandlerState); + {Mod, Req, HandlerState, Opts} -> + Mod:upgrade(Req, Env, Handler, HandlerState, Opts) + end. + +-spec upgrade(Req, Env, module(), any(), any()) + -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +%% cowboy_rest takes no options. +upgrade(Req, Env, Handler, HandlerState, _Opts) -> + upgrade(Req, Env, Handler, HandlerState). + +service_available(Req, State) -> + expect(Req, State, service_available, true, fun known_methods/2, 503). + +%% known_methods/2 should return a list of binary methods. +known_methods(Req, State=#state{method=Method}) -> + case call(Req, State, known_methods) of + no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>; + Method =:= <<"POST">>; Method =:= <<"PUT">>; + Method =:= <<"PATCH">>; Method =:= <<"DELETE">>; + Method =:= <<"OPTIONS">> -> + next(Req, State, fun uri_too_long/2); + no_call -> + next(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) + end + end. + +uri_too_long(Req, State) -> + expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414). + +%% allowed_methods/2 should return a list of binary methods. +allowed_methods(Req, State=#state{method=Method}) -> + case call(Req, State, allowed_methods) of + no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> + 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 -> + method_not_allowed(Req, State, + [<<"HEAD">>, <<"GET">>, <<"OPTIONS">>]); + {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 when Method =:= <<"OPTIONS">> -> + next(Req2, State2#state{allowed_methods=List}, + fun malformed_request/2); + true -> + next(Req2, State2, fun malformed_request/2); + false -> + method_not_allowed(Req2, State2, List) + 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). + +%% is_authorized/2 should return true or {false, WwwAuthenticateHeader}. +is_authorized(Req, State) -> + case call(Req, State, is_authorized) of + no_call -> + forbidden(Req, State); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {true, Req2, State2} -> + forbidden(Req2, State2); + {{false, AuthHead}, Req2, State2} -> + Req3 = cowboy_req:set_resp_header( + <<"www-authenticate">>, AuthHead, Req2), + respond(Req3, State2, 401) + end. + +forbidden(Req, State) -> + expect(Req, State, forbidden, false, fun rate_limited/2, 403). + +rate_limited(Req, State) -> + case call(Req, State, rate_limited) of + no_call -> + valid_content_headers(Req, State); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {false, Req2, State2} -> + valid_content_headers(Req2, State2); + {{true, RetryAfter0}, Req2, State2} -> + RetryAfter = if + is_integer(RetryAfter0), RetryAfter0 >= 0 -> + integer_to_binary(RetryAfter0); + is_tuple(RetryAfter0) -> + cowboy_clock:rfc1123(RetryAfter0) + end, + Req3 = cowboy_req:set_resp_header(<<"retry-after">>, RetryAfter, Req2), + respond(Req3, State2, 429) + end. + +valid_content_headers(Req, State) -> + expect(Req, State, valid_content_headers, true, + fun valid_entity_length/2, 501). + +valid_entity_length(Req, State) -> + expect(Req, State, valid_entity_length, true, fun options/2, 413). + +%% If you need to add additional headers to the response at this point, +%% you should do it directly in the options/2 call using set_resp_headers. +options(Req, State=#state{allowed_methods=Methods, 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); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {ok, Req2, State2} -> + respond(Req2, State2, 200) + end; +options(Req, State) -> + content_types_provided(Req, State). + +%% content_types_provided/2 should return a list of content types and their +%% associated callback function as a tuple: {{Type, SubType, Params}, Fun}. +%% Type and SubType are the media type as binary. Params is a list of +%% Key/Value tuple, with Key and Value a binary. Fun is the name of the +%% callback that will be used to return the content of the response. It is +%% given as an atom. +%% +%% An example of such return value would be: +%% {{<<"text">>, <<"html">>, []}, to_html} +%% +%% Note that it is also possible to return a binary content type that will +%% then be parsed by Cowboy. However note that while this may make your +%% resources a little more readable, this is a lot less efficient. +%% +%% An example of such return value would be: +%% {<<"text/html">>, to_html} +content_types_provided(Req, State) -> + case call(Req, State, content_types_provided) of + no_call -> + State2 = State#state{ + content_types_p=[{{<<"text">>, <<"html">>, '*'}, to_html}]}, + try cowboy_req:parse_header(<<"accept">>, Req) of + undefined -> + languages_provided( + Req#{media_type => {<<"text">>, <<"html">>, []}}, + State2#state{content_type_a={{<<"text">>, <<"html">>, []}, to_html}}); + Accept -> + choose_media_type(Req, State2, prioritize_accept(Accept)) + catch _:_ -> + respond(Req, State2, 400) + end; + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {[], Req2, State2} -> + not_acceptable(Req2, State2); + {CTP, Req2, State2} -> + CTP2 = [normalize_content_types(P) || P <- CTP], + State3 = State2#state{content_types_p=CTP2}, + try cowboy_req:parse_header(<<"accept">>, Req2) of + undefined -> + {PMT0, _Fun} = HeadCTP = hd(CTP2), + %% We replace the wildcard by an empty list of parameters. + PMT = case PMT0 of + {Type, SubType, '*'} -> {Type, SubType, []}; + _ -> PMT0 + end, + languages_provided( + Req2#{media_type => PMT}, + State3#state{content_type_a=HeadCTP}); + Accept -> + choose_media_type(Req2, State3, prioritize_accept(Accept)) + catch _:_ -> + respond(Req2, State3, 400) + end + end. + +normalize_content_types({ContentType, Callback}) + when is_binary(ContentType) -> + {cow_http_hd:parse_content_type(ContentType), Callback}; +normalize_content_types(Normalized) -> + Normalized. + +prioritize_accept(Accept) -> + lists:sort( + fun ({MediaTypeA, Quality, _AcceptParamsA}, + {MediaTypeB, Quality, _AcceptParamsB}) -> + %% Same quality, check precedence in more details. + prioritize_mediatype(MediaTypeA, MediaTypeB); + ({_MediaTypeA, QualityA, _AcceptParamsA}, + {_MediaTypeB, QualityB, _AcceptParamsB}) -> + %% Just compare the quality. + QualityA > QualityB + end, Accept). + +%% Media ranges can be overridden by more specific media ranges or +%% specific media types. If more than one media range applies to a given +%% type, the most specific reference has precedence. +%% +%% We always choose B over A when we can't decide between the two. +prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) -> + case TypeB of + TypeA -> + case SubTypeB of + SubTypeA -> length(ParamsA) > length(ParamsB); + <<"*">> -> true; + _Any -> false + end; + <<"*">> -> true; + _Any -> false + end. + +%% Ignoring the rare AcceptParams. Not sure what should be done about them. +choose_media_type(Req, State, []) -> + not_acceptable(Req, State); +choose_media_type(Req, State=#state{content_types_p=CTP}, + [MediaType|Tail]) -> + match_media_type(Req, State, Tail, CTP, MediaType). + +match_media_type(Req, State, Accept, [], _MediaType) -> + choose_media_type(Req, State, Accept); +match_media_type(Req, State, Accept, CTP, + MediaType = {{<<"*">>, <<"*">>, _Params_A}, _QA, _APA}) -> + match_media_type_params(Req, State, Accept, CTP, MediaType); +match_media_type(Req, State, Accept, + CTP = [{{Type, SubType_P, _PP}, _Fun}|_Tail], + MediaType = {{Type, SubType_A, _PA}, _QA, _APA}) + when SubType_P =:= SubType_A; SubType_A =:= <<"*">> -> + match_media_type_params(Req, State, Accept, CTP, MediaType); +match_media_type(Req, State, Accept, [_Any|Tail], MediaType) -> + match_media_type(Req, State, Accept, Tail, MediaType). + +match_media_type_params(Req, State, Accept, + [Provided = {{TP, STP, '*'}, _Fun}|Tail], + MediaType = {{TA, _STA, Params_A0}, _QA, _APA}) -> + case lists:keytake(<<"charset">>, 1, Params_A0) of + {value, {_, Charset}, Params_A} when TA =:= <<"text">> -> + %% When we match against a wildcard, the media type is text + %% and has a charset parameter, we call charsets_provided + %% and check that the charset is provided. If the callback + %% is not exported, we accept inconditionally but ignore + %% the given charset so as to not send a wrong value back. + case call(Req, State, charsets_provided) of + no_call -> + languages_provided(Req#{media_type => {TP, STP, Params_A0}}, + State#state{content_type_a=Provided}); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {CP, Req2, State2} -> + State3 = State2#state{charsets_p=CP}, + case lists:member(Charset, CP) of + false -> + match_media_type(Req2, State3, Accept, Tail, MediaType); + true -> + languages_provided(Req2#{media_type => {TP, STP, Params_A}}, + State3#state{content_type_a=Provided, + charset_a=Charset}) + end + end; + _ -> + languages_provided(Req#{media_type => {TP, STP, Params_A0}}, + State#state{content_type_a=Provided}) + end; +match_media_type_params(Req, State, Accept, + [Provided = {PMT = {TP, STP, Params_P0}, Fun}|Tail], + MediaType = {{_TA, _STA, Params_A}, _QA, _APA}) -> + case lists:sort(Params_P0) =:= lists:sort(Params_A) of + true when TP =:= <<"text">> -> + %% When a charset was provided explicitly in both the charset header + %% and the media types provided and the negotiation is successful, + %% we keep the charset and don't call charsets_provided. This only + %% applies to text media types, however. + {Charset, Params_P} = case lists:keytake(<<"charset">>, 1, Params_P0) of + false -> {undefined, Params_P0}; + {value, {_, Charset0}, Params_P1} -> {Charset0, Params_P1} + end, + languages_provided(Req#{media_type => {TP, STP, Params_P}}, + State#state{content_type_a={{TP, STP, Params_P}, Fun}, + charset_a=Charset}); + true -> + languages_provided(Req#{media_type => PMT}, + State#state{content_type_a=Provided}); + false -> + match_media_type(Req, State, Accept, Tail, MediaType) + end. + +%% languages_provided should return a list of binary values indicating +%% which languages are accepted by the resource. +%% +%% @todo I suppose we should also ask the resource if it wants to +%% set a language itself or if it wants it to be automatically chosen. +languages_provided(Req, State) -> + case call(Req, State, languages_provided) of + no_call -> + charsets_provided(Req, State); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {[], Req2, State2} -> + not_acceptable(Req2, State2); + {LP, Req2, State2} -> + State3 = State2#state{languages_p=LP}, + case cowboy_req:parse_header(<<"accept-language">>, Req2) of + undefined -> + set_language(Req2, State3#state{language_a=hd(LP)}); + AcceptLanguage -> + AcceptLanguage2 = prioritize_languages(AcceptLanguage), + choose_language(Req2, State3, AcceptLanguage2) + end + end. + +%% A language-range matches a language-tag if it exactly equals the tag, +%% or if it exactly equals a prefix of the tag such that the first tag +%% character following the prefix is "-". The special range "*", if +%% present in the Accept-Language field, matches every tag not matched +%% by any other range present in the Accept-Language field. +%% +%% @todo The last sentence probably means we should always put '*' +%% at the end of the list. +prioritize_languages(AcceptLanguages) -> + lists:sort( + fun ({_TagA, QualityA}, {_TagB, QualityB}) -> + QualityA > QualityB + end, AcceptLanguages). + +choose_language(Req, State, []) -> + not_acceptable(Req, State); +choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) -> + match_language(Req, State, Tail, LP, Language). + +match_language(Req, State, Accept, [], _Language) -> + choose_language(Req, State, Accept); +match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) -> + set_language(Req, State#state{language_a=Provided}); +match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) -> + set_language(Req, State#state{language_a=Provided}); +match_language(Req, State, Accept, [Provided|Tail], + Language = {Tag, _Quality}) -> + Length = byte_size(Tag), + case Provided of + << Tag:Length/binary, $-, _Any/bits >> -> + set_language(Req, State#state{language_a=Provided}); + _Any -> + match_language(Req, State, Accept, Tail, Language) + end. + +set_language(Req, State=#state{language_a=Language}) -> + Req2 = cowboy_req:set_resp_header(<<"content-language">>, Language, Req), + charsets_provided(Req2#{language => Language}, State). + +%% charsets_provided should return a list of binary values indicating +%% which charsets are accepted by the resource. +%% +%% A charset may have been selected while negotiating the accept header. +%% There's no need to select one again. +charsets_provided(Req, State=#state{charset_a=Charset}) + when Charset =/= undefined -> + set_content_type(Req, State); +%% If charsets_p is defined, use it instead of calling charsets_provided +%% again. We also call this clause during normal execution to avoid +%% duplicating code. +charsets_provided(Req, State=#state{charsets_p=[]}) -> + not_acceptable(Req, State); +charsets_provided(Req, State=#state{charsets_p=CP}) + when CP =/= undefined -> + case cowboy_req:parse_header(<<"accept-charset">>, Req) of + undefined -> + set_content_type(Req, State#state{charset_a=hd(CP)}); + AcceptCharset0 -> + AcceptCharset = prioritize_charsets(AcceptCharset0), + choose_charset(Req, State, AcceptCharset) + end; +charsets_provided(Req, State) -> + case call(Req, State, charsets_provided) of + no_call -> + set_content_type(Req, State); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {CP, Req2, State2} -> + charsets_provided(Req2, State2#state{charsets_p=CP}) + end. + +prioritize_charsets(AcceptCharsets) -> + lists:sort( + fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) -> + QualityA > QualityB + end, AcceptCharsets). + +choose_charset(Req, State, []) -> + not_acceptable(Req, State); +%% A q-value of 0 means not acceptable. +choose_charset(Req, State, [{_, 0}|Tail]) -> + choose_charset(Req, State, Tail); +choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) -> + match_charset(Req, State, Tail, CP, Charset). + +match_charset(Req, State, Accept, [], _Charset) -> + choose_charset(Req, State, Accept); +match_charset(Req, State, _Accept, [Provided|_], {<<"*">>, _}) -> + set_content_type(Req, State#state{charset_a=Provided}); +match_charset(Req, State, _Accept, [Provided|_], {Provided, _}) -> + set_content_type(Req, State#state{charset_a=Provided}); +match_charset(Req, State, Accept, [_|Tail], Charset) -> + match_charset(Req, State, Accept, Tail, Charset). + +set_content_type(Req, State=#state{ + content_type_a={{Type, SubType, Params}, _Fun}, + charset_a=Charset}) -> + ParamsBin = set_content_type_build_params(Params, []), + ContentType = [Type, <<"/">>, SubType, ParamsBin], + ContentType2 = case {Type, Charset} of + {<<"text">>, Charset} when Charset =/= undefined -> + [ContentType, <<"; charset=">>, Charset]; + _ -> + ContentType + end, + Req2 = cowboy_req:set_resp_header(<<"content-type">>, ContentType2, Req), + encodings_provided(Req2#{charset => Charset}, State). + +set_content_type_build_params('*', []) -> + <<>>; +set_content_type_build_params([], []) -> + <<>>; +set_content_type_build_params([], Acc) -> + lists:reverse(Acc); +set_content_type_build_params([{Attr, Value}|Tail], Acc) -> + set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]). + +%% @todo Match for identity as we provide nothing else for now. +%% @todo Don't forget to set the Content-Encoding header when we reply a body +%% and the found encoding is something other than identity. +encodings_provided(Req, State) -> + ranges_provided(Req, State). + +not_acceptable(Req, State) -> + respond(Req, State, 406). + +ranges_provided(Req, State) -> + case call(Req, State, ranges_provided) of + no_call -> + variances(Req, State); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {[], Req2, State2} -> + Req3 = cowboy_req:set_resp_header(<<"accept-ranges">>, <<"none">>, Req2), + variances(Req3, State2#state{ranges_a=[]}); + {RP, Req2, State2} -> + <<", ", AcceptRanges/binary>> = <<<<", ", R/binary>> || {R, _} <- RP>>, + Req3 = cowboy_req:set_resp_header(<<"accept-ranges">>, AcceptRanges, Req2), + variances(Req3, State2#state{ranges_a=RP}) + end. + +%% variances/2 should return a list of headers that will be added +%% to the Vary response header. The Accept, Accept-Language, +%% Accept-Charset and Accept-Encoding headers do not need to be +%% specified. +%% +%% @todo Do Accept-Encoding too when we handle it. +%% @todo Does the order matter? +variances(Req, State=#state{content_types_p=CTP, + languages_p=LP, charsets_p=CP}) -> + Variances = case CTP of + [] -> []; + [_] -> []; + [_|_] -> [<<"accept">>] + end, + Variances2 = case LP of + [] -> Variances; + [_] -> Variances; + [_|_] -> [<<"accept-language">>|Variances] + end, + Variances3 = case CP of + undefined -> Variances2; + [] -> Variances2; + [_] -> Variances2; + [_|_] -> [<<"accept-charset">>|Variances2] + end, + try variances(Req, State, Variances3) of + {Variances4, Req2, State2} -> + case [[<<", ">>, V] || V <- Variances4] of + [] -> + resource_exists(Req2, State2); + [[<<", ">>, H]|Variances5] -> + Req3 = cowboy_req:set_resp_header( + <<"vary">>, [H|Variances5], Req2), + resource_exists(Req3, State2) + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +variances(Req, State, Variances) -> + case unsafe_call(Req, State, variances) of + no_call -> + {Variances, Req, State}; + {HandlerVariances, Req2, State2} -> + {Variances ++ HandlerVariances, Req2, State2} + end. + +resource_exists(Req, State) -> + expect(Req, State, resource_exists, true, + fun if_match_exists/2, fun if_match_must_not_exist/2). + +if_match_exists(Req, State) -> + State2 = State#state{exists=true}, + case cowboy_req:parse_header(<<"if-match">>, Req) of + undefined -> + if_unmodified_since_exists(Req, State2); + '*' -> + if_unmodified_since_exists(Req, State2); + ETagsList -> + if_match(Req, State2, ETagsList) + end. + +if_match(Req, State, EtagsList) -> + try generate_etag(Req, State) of + %% Strong Etag comparison: weak Etag never matches. + {{weak, _}, Req2, State2} -> + precondition_failed(Req2, State2); + {Etag, Req2, State2} -> + case lists:member(Etag, EtagsList) of + true -> if_none_match_exists(Req2, State2); + %% Etag may be `undefined' which cannot be a member. + false -> precondition_failed(Req2, State2) + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +if_match_must_not_exist(Req, State) -> + case cowboy_req:header(<<"if-match">>, Req) of + undefined -> is_put_to_missing_resource(Req, State); + _ -> precondition_failed(Req, State) + end. + +if_unmodified_since_exists(Req, State) -> + try cowboy_req:parse_header(<<"if-unmodified-since">>, Req) of + undefined -> + if_none_match_exists(Req, State); + IfUnmodifiedSince -> + if_unmodified_since(Req, State, IfUnmodifiedSince) + catch _:_ -> + if_none_match_exists(Req, State) + end. + +%% If LastModified is the atom 'no_call', we continue. +if_unmodified_since(Req, State, IfUnmodifiedSince) -> + try last_modified(Req, State) of + {LastModified, Req2, State2} -> + case LastModified > IfUnmodifiedSince of + true -> precondition_failed(Req2, State2); + false -> if_none_match_exists(Req2, State2) + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +if_none_match_exists(Req, State) -> + case cowboy_req:parse_header(<<"if-none-match">>, Req) of + undefined -> + if_modified_since_exists(Req, State); + '*' -> + precondition_is_head_get(Req, State); + EtagsList -> + if_none_match(Req, State, EtagsList) + end. + +if_none_match(Req, State, EtagsList) -> + try generate_etag(Req, State) of + {Etag, Req2, State2} -> + case Etag of + undefined -> + precondition_failed(Req2, State2); + Etag -> + case is_weak_match(Etag, EtagsList) of + true -> precondition_is_head_get(Req2, State2); + false -> method(Req2, State2) + end + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +%% Weak Etag comparison: only check the opaque tag. +is_weak_match(_, []) -> + false; +is_weak_match({_, Tag}, [{_, Tag}|_]) -> + true; +is_weak_match(Etag, [_|Tail]) -> + is_weak_match(Etag, Tail). + +precondition_is_head_get(Req, State=#state{method=Method}) + when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> + not_modified(Req, State); +precondition_is_head_get(Req, State) -> + precondition_failed(Req, State). + +if_modified_since_exists(Req, State) -> + try cowboy_req:parse_header(<<"if-modified-since">>, Req) of + undefined -> + method(Req, State); + IfModifiedSince -> + if_modified_since_now(Req, State, IfModifiedSince) + catch _:_ -> + method(Req, State) + end. + +if_modified_since_now(Req, State, IfModifiedSince) -> + case IfModifiedSince > erlang:universaltime() of + true -> method(Req, State); + false -> if_modified_since(Req, State, IfModifiedSince) + end. + +if_modified_since(Req, State, IfModifiedSince) -> + try last_modified(Req, State) of + {undefined, Req2, State2} -> + method(Req2, State2); + {LastModified, Req2, State2} -> + case LastModified > IfModifiedSince of + true -> method(Req2, State2); + false -> not_modified(Req2, State2) + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +not_modified(Req, State) -> + Req2 = cowboy_req:delete_resp_header(<<"content-type">>, Req), + try set_resp_etag(Req2, State) of + {Req3, State2} -> + try set_resp_expires(Req3, State2) of + {Req4, State3} -> + respond(Req4, State3, 304) + catch Class:Reason:Stacktrace -> + error_terminate(Req, State2, Class, Reason, Stacktrace) + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +precondition_failed(Req, State) -> + respond(Req, State, 412). + +is_put_to_missing_resource(Req, State=#state{method= <<"PUT">>}) -> + moved_permanently(Req, State, fun is_conflict/2); +is_put_to_missing_resource(Req, State) -> + previously_existed(Req, State). + +%% moved_permanently/2 should return either false or {true, Location} +%% with Location the full new URI of the resource. +moved_permanently(Req, State, OnFalse) -> + case call(Req, State, moved_permanently) of + {{true, Location}, Req2, State2} -> + Req3 = cowboy_req:set_resp_header( + <<"location">>, Location, Req2), + respond(Req3, State2, 301); + {false, Req2, State2} -> + OnFalse(Req2, State2); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + no_call -> + OnFalse(Req, State) + end. + +previously_existed(Req, State) -> + expect(Req, State, previously_existed, false, + fun (R, S) -> is_post_to_missing_resource(R, S, 404) end, + fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end). + +%% moved_temporarily/2 should return either false or {true, Location} +%% with Location the full new URI of the resource. +moved_temporarily(Req, State) -> + case call(Req, State, moved_temporarily) of + {{true, Location}, Req2, State2} -> + Req3 = cowboy_req:set_resp_header( + <<"location">>, Location, Req2), + respond(Req3, State2, 307); + {false, Req2, State2} -> + is_post_to_missing_resource(Req2, State2, 410); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + no_call -> + is_post_to_missing_resource(Req, State, 410) + end. + +is_post_to_missing_resource(Req, State=#state{method= <<"POST">>}, OnFalse) -> + allow_missing_post(Req, State, OnFalse); +is_post_to_missing_resource(Req, State, OnFalse) -> + respond(Req, State, OnFalse). + +allow_missing_post(Req, State, OnFalse) -> + expect(Req, State, allow_missing_post, true, fun accept_resource/2, OnFalse). + +method(Req, State=#state{method= <<"DELETE">>}) -> + delete_resource(Req, State); +method(Req, State=#state{method= <<"PUT">>}) -> + is_conflict(Req, State); +method(Req, State=#state{method=Method}) + when Method =:= <<"POST">>; Method =:= <<"PATCH">> -> + accept_resource(Req, State); +method(Req, State=#state{method=Method}) + when Method =:= <<"GET">>; Method =:= <<"HEAD">> -> + set_resp_body_etag(Req, State); +method(Req, State) -> + multiple_choices(Req, State). + +%% delete_resource/2 should start deleting the resource and return. +delete_resource(Req, State) -> + expect(Req, State, delete_resource, false, 500, fun delete_completed/2). + +%% delete_completed/2 indicates whether the resource has been deleted yet. +delete_completed(Req, State) -> + expect(Req, State, delete_completed, true, fun has_resp_body/2, 202). + +is_conflict(Req, State) -> + expect(Req, State, is_conflict, false, fun accept_resource/2, 409). + +%% content_types_accepted should return a list of media types and their +%% associated callback functions in the same format as content_types_provided. +%% +%% The callback will then be called and is expected to process the content +%% pushed to the resource in the request body. +%% +%% content_types_accepted SHOULD return a different list +%% for each HTTP method. +accept_resource(Req, State) -> + case call(Req, State, content_types_accepted) of + no_call -> + respond(Req, State, 415); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {CTA, Req2, State2} -> + CTA2 = [normalize_content_types(P) || P <- CTA], + try cowboy_req:parse_header(<<"content-type">>, Req2) of + %% We do not match against the boundary parameter for multipart. + {Type = <<"multipart">>, SubType, Params} -> + ContentType = {Type, SubType, lists:keydelete(<<"boundary">>, 1, Params)}, + choose_content_type(Req2, State2, ContentType, CTA2); + ContentType -> + choose_content_type(Req2, State2, ContentType, CTA2) + catch _:_ -> + respond(Req2, State2, 415) + end + end. + +%% The special content type '*' will always match. It can be used as a +%% catch-all content type for accepting any kind of request content. +%% Note that because it will always match, it should be the last of the +%% list of content types, otherwise it'll shadow the ones following. +choose_content_type(Req, State, _ContentType, []) -> + respond(Req, State, 415); +choose_content_type(Req, State, ContentType, [{Accepted, Fun}|_Tail]) + when Accepted =:= '*'; Accepted =:= ContentType -> + process_content_type(Req, State, Fun); +%% The special parameter '*' will always match any kind of content type +%% parameters. +%% Note that because it will always match, it should be the last of the +%% list for specific content type, otherwise it'll shadow the ones following. +choose_content_type(Req, State, {Type, SubType, Param}, + [{{Type, SubType, AcceptedParam}, Fun}|_Tail]) + when AcceptedParam =:= '*'; AcceptedParam =:= Param -> + process_content_type(Req, State, Fun); +choose_content_type(Req, State, ContentType, [_Any|Tail]) -> + choose_content_type(Req, State, ContentType, Tail). + +process_content_type(Req, State=#state{method=Method, exists=Exists}, Fun) -> + try case call(Req, State, Fun) of + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {true, Req2, State2} when Exists -> + next(Req2, State2, fun has_resp_body/2); + {true, Req2, State2} -> + next(Req2, State2, fun maybe_created/2); + {false, Req2, State2} -> + respond(Req2, State2, 400); + {{created, ResURL}, Req2, State2} when Method =:= <<"POST">> -> + Req3 = cowboy_req:set_resp_header( + <<"location">>, ResURL, Req2), + respond(Req3, State2, 201); + {{see_other, ResURL}, Req2, State2} when Method =:= <<"POST">> -> + Req3 = cowboy_req:set_resp_header( + <<"location">>, ResURL, Req2), + respond(Req3, State2, 303); + {{true, ResURL}, Req2, State2} when Method =:= <<"POST">> -> + Req3 = cowboy_req:set_resp_header( + <<"location">>, ResURL, Req2), + if + Exists -> respond(Req3, State2, 303); + true -> respond(Req3, State2, 201) + end + end catch Class:Reason = {case_clause, no_call}:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +%% If PUT was used then the resource has been created at the current URL. +%% Otherwise, if a location header has been set then the resource has been +%% created at a new URL. If not, send a 200 or 204 as expected from a +%% POST or PATCH request. +maybe_created(Req, State=#state{method= <<"PUT">>}) -> + respond(Req, State, 201); +maybe_created(Req, State) -> + case cowboy_req:has_resp_header(<<"location">>, Req) of + true -> respond(Req, State, 201); + false -> has_resp_body(Req, State) + end. + +has_resp_body(Req, State) -> + case cowboy_req:has_resp_body(Req) of + true -> multiple_choices(Req, State); + false -> respond(Req, State, 204) + end. + +%% Set the Etag header if any for the response provided. +set_resp_body_etag(Req, State) -> + try set_resp_etag(Req, State) of + {Req2, State2} -> + set_resp_body_last_modified(Req2, State2) + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +%% Set the Last-Modified header if any for the response provided. +set_resp_body_last_modified(Req, State) -> + try last_modified(Req, State) of + {LastModified, Req2, State2} -> + case LastModified of + LastModified when is_atom(LastModified) -> + set_resp_body_expires(Req2, State2); + LastModified -> + LastModifiedBin = cowboy_clock:rfc1123(LastModified), + Req3 = cowboy_req:set_resp_header( + <<"last-modified">>, LastModifiedBin, Req2), + set_resp_body_expires(Req3, State2) + end + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +%% Set the Expires header if any for the response provided. +set_resp_body_expires(Req, State) -> + try set_resp_expires(Req, State) of + {Req2, State2} -> + if_range(Req2, State2) + catch Class:Reason:Stacktrace -> + error_terminate(Req, State, Class, Reason, Stacktrace) + end. + +%% When both the if-range and range headers are set, we perform +%% a strong comparison. If it fails, we send a full response. +if_range(Req=#{headers := #{<<"if-range">> := _, <<"range">> := _}}, + State=#state{etag=Etag}) -> + try cowboy_req:parse_header(<<"if-range">>, Req) of + %% Strong etag comparison is an exact match with the generate_etag result. + Etag={strong, _} -> + range(Req, State); + %% We cannot do a strong date comparison because we have + %% no way of knowing whether the representation changed + %% twice during the second covered by the presented + %% validator. (RFC7232 2.2.2) + _ -> + set_resp_body(Req, State) + catch _:_ -> + set_resp_body(Req, State) + end; +if_range(Req, State) -> + range(Req, State). + +%% @todo This can probably be moved to if_range directly. +range(Req, State=#state{ranges_a=[]}) -> + set_resp_body(Req, State); +range(Req, State) -> + try cowboy_req:parse_header(<<"range">>, Req) of + undefined -> + set_resp_body(Req, State); + %% @todo Maybe change parse_header to return <<"bytes">> in 3.0. + {bytes, BytesRange} -> + choose_range(Req, State, {<<"bytes">>, BytesRange}); + Range -> + choose_range(Req, State, Range) + catch _:_ -> + %% We send a 416 response back when we can't parse the + %% range header at all. I'm not sure this is the right + %% way to go but at least this can help clients identify + %% what went wrong when their range requests never work. + range_not_satisfiable(Req, State, undefined) + end. + +choose_range(Req, State=#state{ranges_a=RangesAccepted}, Range={RangeUnit, _}) -> + case lists:keyfind(RangeUnit, 1, RangesAccepted) of + {_, Callback} -> + %% We pass the selected range onward in the Req. + range_satisfiable(Req#{range => Range}, State, Callback); + false -> + set_resp_body(Req, State) + end. + +range_satisfiable(Req, State, Callback) -> + case call(Req, State, range_satisfiable) of + no_call -> + set_ranged_body(Req, State, Callback); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {true, Req2, State2} -> + set_ranged_body(Req2, State2, Callback); + {false, Req2, State2} -> + range_not_satisfiable(Req2, State2, undefined); + {{false, Int}, Req2, State2} when is_integer(Int) -> + range_not_satisfiable(Req2, State2, [<<"*/">>, integer_to_binary(Int)]); + {{false, Iodata}, Req2, State2} when is_binary(Iodata); is_list(Iodata) -> + range_not_satisfiable(Req2, State2, Iodata) + end. + +%% When the callback selected is 'auto' and the range unit +%% is bytes, we call the normal provide callback and split +%% the content automatically. +set_ranged_body(Req=#{range := {<<"bytes">>, _}}, State, auto) -> + set_ranged_body_auto(Req, State); +set_ranged_body(Req, State, Callback) -> + set_ranged_body_callback(Req, State, Callback). + +set_ranged_body_auto(Req, State=#state{handler=Handler, content_type_a={_, Callback}}) -> + try case call(Req, State, Callback) of + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {Body, Req2, State2} -> + maybe_set_ranged_body_auto(Req2, State2, Body) + end catch Class:{case_clause, no_call}:Stacktrace -> + error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, + 'A callback specified in content_types_provided/2 is not exported.'}, + Stacktrace) + end. + +maybe_set_ranged_body_auto(Req=#{range := {_, Ranges}}, State, Body) -> + Size = case Body of + {sendfile, _, Bytes, _} -> Bytes; + _ -> iolist_size(Body) + end, + Checks = [case Range of + {From, infinity} -> From < Size; + {From, To} -> (From < Size) andalso (From =< To) andalso (To =< Size); + Neg -> (Neg =/= 0) andalso (-Neg < Size) + end || Range <- Ranges], + case lists:usort(Checks) of + [true] -> set_ranged_body_auto(Req, State, Body); + _ -> range_not_satisfiable(Req, State, [<<"*/">>, integer_to_binary(Size)]) + end. + +%% We might also want to have some checks about range order, +%% number of ranges, and perhaps also join ranges that are +%% too close into one contiguous range. Some of these can +%% be done before calling the ProvideCallback. + +set_ranged_body_auto(Req=#{range := {_, Ranges}}, State, Body) -> + Parts = [ranged_partition(Range, Body) || Range <- Ranges], + case Parts of + [OnePart] -> set_one_ranged_body(Req, State, OnePart); + _ when is_tuple(Body) -> send_multipart_ranged_body(Req, State, Parts); + _ -> set_multipart_ranged_body(Req, State, Parts) + end. + +ranged_partition(Range, {sendfile, Offset0, Bytes0, Path}) -> + {From, To, Offset, Bytes} = case Range of + {From0, infinity} -> {From0, Bytes0 - 1, Offset0 + From0, Bytes0 - From0}; + {From0, To0} -> {From0, To0, Offset0 + From0, 1 + To0 - From0}; + Neg -> {Bytes0 + Neg, Bytes0 - 1, Offset0 + Bytes0 + Neg, -Neg} + end, + {{From, To, Bytes0}, {sendfile, Offset, Bytes, Path}}; +ranged_partition(Range, Data0) -> + Total = iolist_size(Data0), + {From, To, Data} = case Range of + {From0, infinity} -> + {_, Data1} = cow_iolists:split(From0, Data0), + {From0, Total - 1, Data1}; + {From0, To0} -> + {_, Data1} = cow_iolists:split(From0, Data0), + {Data2, _} = cow_iolists:split(To0 - From0 + 1, Data1), + {From0, To0, Data2}; + Neg -> + {_, Data1} = cow_iolists:split(Total + Neg, Data0), + {Total + Neg, Total - 1, Data1} + end, + {{From, To, Total}, Data}. + +-ifdef(TEST). +ranged_partition_test_() -> + Tests = [ + %% Sendfile with open-ended range. + {{0, infinity}, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, + {{6, infinity}, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, + {{11, infinity}, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, + %% Sendfile with open-ended range. Sendfile tuple has an offset originally. + {{0, infinity}, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, + {{6, infinity}, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, + {{11, infinity}, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, + %% Sendfile with a specific range. + {{0, 11}, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, + {{6, 11}, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, + {{11, 11}, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, + {{1, 10}, {sendfile, 0, 12, "t"}, {{1, 10, 12}, {sendfile, 1, 10, "t"}}}, + %% Sendfile with a specific range. Sendfile tuple has an offset originally. + {{0, 11}, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, + {{6, 11}, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, + {{11, 11}, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, + {{1, 10}, {sendfile, 3, 12, "t"}, {{1, 10, 12}, {sendfile, 4, 10, "t"}}}, + %% Sendfile with negative range. + {-12, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, + {-6, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, + {-1, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, + %% Sendfile with negative range. Sendfile tuple has an offset originally. + {-12, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, + {-6, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, + {-1, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, + %% Iodata with open-ended range. + {{0, infinity}, <<"Hello world!">>, {{0, 11, 12}, <<"Hello world!">>}}, + {{6, infinity}, <<"Hello world!">>, {{6, 11, 12}, <<"world!">>}}, + {{11, infinity}, <<"Hello world!">>, {{11, 11, 12}, <<"!">>}}, + %% Iodata with a specific range. The resulting data is + %% wrapped in a list because of how cow_iolists:split/2 works. + {{0, 11}, <<"Hello world!">>, {{0, 11, 12}, [<<"Hello world!">>]}}, + {{6, 11}, <<"Hello world!">>, {{6, 11, 12}, [<<"world!">>]}}, + {{11, 11}, <<"Hello world!">>, {{11, 11, 12}, [<<"!">>]}}, + {{1, 10}, <<"Hello world!">>, {{1, 10, 12}, [<<"ello world">>]}}, + %% Iodata with negative range. + {-12, <<"Hello world!">>, {{0, 11, 12}, <<"Hello world!">>}}, + {-6, <<"Hello world!">>, {{6, 11, 12}, <<"world!">>}}, + {-1, <<"Hello world!">>, {{11, 11, 12}, <<"!">>}} + ], + [{iolist_to_binary(io_lib:format("range ~p data ~p", [VR, VD])), + fun() -> R = ranged_partition(VR, VD) end} || {VR, VD, R} <- Tests]. +-endif. + +set_ranged_body_callback(Req, State=#state{handler=Handler}, Callback) -> + try case call(Req, State, Callback) of + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + %% When we receive a single range, we send it directly. + {[OneRange], Req2, State2} -> + set_one_ranged_body(Req2, State2, OneRange); + %% When we receive multiple ranges we have to send them as multipart/byteranges. + %% This also applies to non-bytes units. (RFC7233 A) If users don't want to use + %% this for non-bytes units they can always return a single range with a binary + %% content-range information. + {Ranges, Req2, State2} when length(Ranges) > 1 -> + %% We have to check whether there are sendfile tuples in the + %% ranges to be sent. If there are we must use stream_reply. + HasSendfile = [] =/= [true || {_, {sendfile, _, _, _}} <- Ranges], + case HasSendfile of + true -> send_multipart_ranged_body(Req2, State2, Ranges); + false -> set_multipart_ranged_body(Req2, State2, Ranges) + end + end catch Class:{case_clause, no_call}:Stacktrace -> + error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, + 'A callback specified in ranges_provided/2 is not exported.'}, + Stacktrace) + end. + +set_one_ranged_body(Req0, State, OneRange) -> + {ContentRange, Body} = prepare_range(Req0, OneRange), + Req1 = cowboy_req:set_resp_header(<<"content-range">>, ContentRange, Req0), + Req = cowboy_req:set_resp_body(Body, Req1), + respond(Req, State, 206). + +set_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> + Boundary = cow_multipart:boundary(), + ContentType = cowboy_req:resp_header(<<"content-type">>, Req), + {FirstContentRange, FirstPartBody} = prepare_range(Req, FirstRange), + FirstPartHead = cow_multipart:first_part(Boundary, [ + {<<"content-type">>, ContentType}, + {<<"content-range">>, FirstContentRange} + ]), + MoreParts = [begin + {NextContentRange, NextPartBody} = prepare_range(Req, NextRange), + NextPartHead = cow_multipart:part(Boundary, [ + {<<"content-type">>, ContentType}, + {<<"content-range">>, NextContentRange} + ]), + [NextPartHead, NextPartBody] + end || NextRange <- MoreRanges], + Body = [FirstPartHead, FirstPartBody, MoreParts, cow_multipart:close(Boundary)], + Req2 = cowboy_req:set_resp_header(<<"content-type">>, + [<<"multipart/byteranges; boundary=">>, Boundary], Req), + Req3 = cowboy_req:set_resp_body(Body, Req2), + respond(Req3, State, 206). + +%% Similar to set_multipart_ranged_body except we have to stream +%% the data because the parts contain sendfile tuples. +send_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> + Boundary = cow_multipart:boundary(), + ContentType = cowboy_req:resp_header(<<"content-type">>, Req), + Req2 = cowboy_req:set_resp_header(<<"content-type">>, + [<<"multipart/byteranges; boundary=">>, Boundary], Req), + Req3 = cowboy_req:stream_reply(206, Req2), + {FirstContentRange, FirstPartBody} = prepare_range(Req, FirstRange), + FirstPartHead = cow_multipart:first_part(Boundary, [ + {<<"content-type">>, ContentType}, + {<<"content-range">>, FirstContentRange} + ]), + cowboy_req:stream_body(FirstPartHead, nofin, Req3), + cowboy_req:stream_body(FirstPartBody, nofin, Req3), + _ = [begin + {NextContentRange, NextPartBody} = prepare_range(Req, NextRange), + NextPartHead = cow_multipart:part(Boundary, [ + {<<"content-type">>, ContentType}, + {<<"content-range">>, NextContentRange} + ]), + cowboy_req:stream_body(NextPartHead, nofin, Req3), + cowboy_req:stream_body(NextPartBody, nofin, Req3), + [NextPartHead, NextPartBody] + end || NextRange <- MoreRanges], + cowboy_req:stream_body(cow_multipart:close(Boundary), fin, Req3), + terminate(Req3, State). + +prepare_range(#{range := {RangeUnit, _}}, {{From, To, Total0}, Body}) -> + Total = case Total0 of + '*' -> <<"*">>; + _ -> integer_to_binary(Total0) + end, + ContentRange = [RangeUnit, $\s, integer_to_binary(From), + $-, integer_to_binary(To), $/, Total], + {ContentRange, Body}; +prepare_range(#{range := {RangeUnit, _}}, {RangeData, Body}) -> + {[RangeUnit, $\s, RangeData], Body}. + +%% We send the content-range header when we can on error. +range_not_satisfiable(Req, State, undefined) -> + respond(Req, State, 416); +range_not_satisfiable(Req0=#{range := {RangeUnit, _}}, State, RangeData) -> + Req = cowboy_req:set_resp_header(<<"content-range">>, + [RangeUnit, $\s, RangeData], Req0), + respond(Req, State, 416). + +%% Set the response headers and call the callback found using +%% content_types_provided/2 to obtain the request body and add +%% it to the response. +set_resp_body(Req, State=#state{handler=Handler, content_type_a={_, Callback}}) -> + try case call(Req, State, Callback) of + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {Body, Req2, State2} -> + Req3 = cowboy_req:set_resp_body(Body, Req2), + multiple_choices(Req3, State2) + end catch Class:{case_clause, no_call}:Stacktrace -> + error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, + 'A callback specified in content_types_provided/2 is not exported.'}, + Stacktrace) + end. + +multiple_choices(Req, State) -> + expect(Req, State, multiple_choices, false, 200, 300). + +%% Response utility functions. + +set_resp_etag(Req, State) -> + {Etag, Req2, State2} = generate_etag(Req, State), + case Etag of + undefined -> + {Req2, State2}; + Etag -> + Req3 = cowboy_req:set_resp_header( + <<"etag">>, encode_etag(Etag), Req2), + {Req3, State2} + end. + +-spec encode_etag({strong | weak, binary()}) -> iolist(). +encode_etag({strong, Etag}) -> [$",Etag,$"]; +encode_etag({weak, Etag}) -> ["W/\"",Etag,$"]. + +set_resp_expires(Req, State) -> + {Expires, Req2, State2} = expires(Req, State), + case Expires of + Expires when is_atom(Expires) -> + {Req2, State2}; + Expires when is_binary(Expires) -> + Req3 = cowboy_req:set_resp_header( + <<"expires">>, Expires, Req2), + {Req3, State2}; + Expires -> + ExpiresBin = cowboy_clock:rfc1123(Expires), + Req3 = cowboy_req:set_resp_header( + <<"expires">>, ExpiresBin, Req2), + {Req3, State2} + end. + +%% Info retrieval. No logic. + +generate_etag(Req, State=#state{etag=no_call}) -> + {undefined, Req, State}; +generate_etag(Req, State=#state{etag=undefined}) -> + case unsafe_call(Req, State, generate_etag) of + no_call -> + {undefined, Req, State#state{etag=no_call}}; + %% We allow the callback to return 'undefined' + %% to allow conditionally generating etags. We + %% handle 'undefined' the same as if the function + %% was not exported. + {undefined, Req2, State2} -> + {undefined, Req2, State2#state{etag=no_call}}; + {Etag, Req2, State2} when is_binary(Etag) -> + Etag2 = cow_http_hd:parse_etag(Etag), + {Etag2, Req2, State2#state{etag=Etag2}}; + {Etag, Req2, State2} -> + {Etag, Req2, State2#state{etag=Etag}} + end; +generate_etag(Req, State=#state{etag=Etag}) -> + {Etag, Req, State}. + +last_modified(Req, State=#state{last_modified=no_call}) -> + {undefined, Req, State}; +last_modified(Req, State=#state{last_modified=undefined}) -> + case unsafe_call(Req, State, last_modified) of + no_call -> + {undefined, Req, State#state{last_modified=no_call}}; + {LastModified, Req2, State2} -> + {LastModified, Req2, State2#state{last_modified=LastModified}} + end; +last_modified(Req, State=#state{last_modified=LastModified}) -> + {LastModified, Req, State}. + +expires(Req, State=#state{expires=no_call}) -> + {undefined, Req, State}; +expires(Req, State=#state{expires=undefined}) -> + case unsafe_call(Req, State, expires) of + no_call -> + {undefined, Req, State#state{expires=no_call}}; + {Expires, Req2, State2} -> + {Expires, Req2, State2#state{expires=Expires}} + end; +expires(Req, State=#state{expires=Expires}) -> + {Expires, Req, State}. + +%% REST primitives. + +expect(Req, State, Callback, Expected, OnTrue, OnFalse) -> + case call(Req, State, Callback) of + no_call -> + next(Req, State, OnTrue); + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {Expected, Req2, State2} -> + next(Req2, State2, OnTrue); + {_Unexpected, Req2, State2} -> + next(Req2, State2, OnFalse) + end. + +call(Req0, State=#state{handler=Handler, + handler_state=HandlerState0}, Callback) -> + case erlang:function_exported(Handler, Callback, 2) of + true -> + try Handler:Callback(Req0, HandlerState0) of + no_call -> + no_call; + {Result, Req, HandlerState} -> + {Result, Req, State#state{handler_state=HandlerState}} + catch Class:Reason:Stacktrace -> + error_terminate(Req0, State, Class, Reason, Stacktrace) + end; + false -> + no_call + end. + +unsafe_call(Req0, State=#state{handler=Handler, + handler_state=HandlerState0}, Callback) -> + case erlang:function_exported(Handler, Callback, 2) of + false -> + no_call; + true -> + case Handler:Callback(Req0, HandlerState0) of + no_call -> + no_call; + {Result, Req, HandlerState} -> + {Result, Req, State#state{handler_state=HandlerState}} + end + end. + +next(Req, State, Next) when is_function(Next) -> + Next(Req, State); +next(Req, State, StatusCode) when is_integer(StatusCode) -> + respond(Req, State, StatusCode). + +respond(Req0, State, StatusCode) -> + %% We remove the content-type header when there is no body, + %% except when the status code is 200 because it might have + %% been intended (for example sending an empty file). + Req = case cowboy_req:has_resp_body(Req0) of + true when StatusCode =:= 200 -> Req0; + true -> Req0; + false -> cowboy_req:delete_resp_header(<<"content-type">>, Req0) + end, + terminate(cowboy_req:reply(StatusCode, Req), State). + +switch_handler({switch_handler, Mod}, Req, #state{handler_state=HandlerState}) -> + {Mod, Req, HandlerState}; +switch_handler({switch_handler, Mod, Opts}, Req, #state{handler_state=HandlerState}) -> + {Mod, Req, HandlerState, Opts}. + +-spec error_terminate(cowboy_req:req(), #state{}, atom(), any(), any()) -> no_return(). +error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class, Reason, Stacktrace) -> + cowboy_handler:terminate({crash, Class, Reason}, Req, HandlerState, Handler), + erlang:raise(Class, Reason, Stacktrace). + +terminate(Req, #state{handler=Handler, handler_state=HandlerState}) -> + Result = cowboy_handler:terminate(normal, Req, HandlerState, Handler), + {ok, Req, Result}. diff --git a/cowboy/src/cowboy_router.erl b/cowboy/src/cowboy_router.erl new file mode 100644 index 0000000..61c9012 --- /dev/null +++ b/cowboy/src/cowboy_router.erl @@ -0,0 +1,603 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Routing middleware. +%% +%% Resolve the handler to be used for the request based on the +%% routing information found in the dispatch environment value. +%% When found, the handler module and associated data are added to +%% the environment as the handler and handler_opts values +%% respectively. +%% +%% If the route cannot be found, processing stops with either +%% a 400 or a 404 reply. +-module(cowboy_router). +-behaviour(cowboy_middleware). + +-export([compile/1]). +-export([execute/2]). + +-type bindings() :: #{atom() => any()}. +-type tokens() :: [binary()]. +-export_type([bindings/0]). +-export_type([tokens/0]). + +-type route_match() :: '_' | iodata(). +-type route_path() :: {Path::route_match(), Handler::module(), Opts::any()} + | {Path::route_match(), cowboy:fields(), Handler::module(), Opts::any()}. +-type route_rule() :: {Host::route_match(), Paths::[route_path()]} + | {Host::route_match(), cowboy:fields(), Paths::[route_path()]}. +-type routes() :: [route_rule()]. +-export_type([routes/0]). + +-type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()]. +-type dispatch_path() :: {dispatch_match(), cowboy:fields(), module(), any()}. +-type dispatch_rule() :: {Host::dispatch_match(), cowboy:fields(), Paths::[dispatch_path()]}. +-opaque dispatch_rules() :: [dispatch_rule()]. +-export_type([dispatch_rules/0]). + +-spec compile(routes()) -> dispatch_rules(). +compile(Routes) -> + compile(Routes, []). + +compile([], Acc) -> + lists:reverse(Acc); +compile([{Host, Paths}|Tail], Acc) -> + compile([{Host, [], Paths}|Tail], Acc); +compile([{HostMatch, Fields, Paths}|Tail], Acc) -> + HostRules = case HostMatch of + '_' -> '_'; + _ -> compile_host(HostMatch) + end, + PathRules = compile_paths(Paths, []), + Hosts = case HostRules of + '_' -> [{'_', Fields, PathRules}]; + _ -> [{R, Fields, PathRules} || R <- HostRules] + end, + compile(Tail, Hosts ++ Acc). + +compile_host(HostMatch) when is_list(HostMatch) -> + compile_host(list_to_binary(HostMatch)); +compile_host(HostMatch) when is_binary(HostMatch) -> + compile_rules(HostMatch, $., [], [], <<>>). + +compile_paths([], Acc) -> + lists:reverse(Acc); +compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) -> + compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc); +compile_paths([{PathMatch, Fields, Handler, Opts}|Tail], Acc) + when is_list(PathMatch) -> + compile_paths([{iolist_to_binary(PathMatch), + Fields, Handler, Opts}|Tail], Acc); +compile_paths([{'_', Fields, Handler, Opts}|Tail], Acc) -> + compile_paths(Tail, [{'_', Fields, Handler, Opts}] ++ Acc); +compile_paths([{<<"*">>, Fields, Handler, Opts}|Tail], Acc) -> + compile_paths(Tail, [{<<"*">>, Fields, Handler, Opts}|Acc]); +compile_paths([{<< $/, PathMatch/bits >>, Fields, Handler, Opts}|Tail], + Acc) -> + PathRules = compile_rules(PathMatch, $/, [], [], <<>>), + Paths = [{lists:reverse(R), Fields, Handler, Opts} || R <- PathRules], + compile_paths(Tail, Paths ++ Acc); +compile_paths([{PathMatch, _, _, _}|_], _) -> + error({badarg, "The following route MUST begin with a slash: " + ++ binary_to_list(PathMatch)}). + +compile_rules(<<>>, _, Segments, Rules, <<>>) -> + [Segments|Rules]; +compile_rules(<<>>, _, Segments, Rules, Acc) -> + [[Acc|Segments]|Rules]; +compile_rules(<< S, Rest/bits >>, S, Segments, Rules, <<>>) -> + compile_rules(Rest, S, Segments, Rules, <<>>); +compile_rules(<< S, Rest/bits >>, S, Segments, Rules, Acc) -> + compile_rules(Rest, S, [Acc|Segments], Rules, <<>>); +%% Colon on path segment start is special, otherwise allow. +compile_rules(<< $:, Rest/bits >>, S, Segments, Rules, <<>>) -> + {NameBin, Rest2} = compile_binding(Rest, S, <<>>), + Name = binary_to_atom(NameBin, utf8), + compile_rules(Rest2, S, Segments, Rules, Name); +compile_rules(<< $[, $., $., $., $], Rest/bits >>, S, Segments, Rules, Acc) + when Acc =:= <<>> -> + compile_rules(Rest, S, ['...'|Segments], Rules, Acc); +compile_rules(<< $[, $., $., $., $], Rest/bits >>, S, Segments, Rules, Acc) -> + compile_rules(Rest, S, ['...', Acc|Segments], Rules, Acc); +compile_rules(<< $[, S, Rest/bits >>, S, Segments, Rules, Acc) -> + compile_brackets(Rest, S, [Acc|Segments], Rules); +compile_rules(<< $[, Rest/bits >>, S, Segments, Rules, <<>>) -> + compile_brackets(Rest, S, Segments, Rules); +%% Open bracket in the middle of a segment. +compile_rules(<< $[, _/bits >>, _, _, _, _) -> + error(badarg); +%% Missing an open bracket. +compile_rules(<< $], _/bits >>, _, _, _, _) -> + error(badarg); +compile_rules(<< C, Rest/bits >>, S, Segments, Rules, Acc) -> + compile_rules(Rest, S, Segments, Rules, << Acc/binary, C >>). + +%% Everything past $: until the segment separator ($. for hosts, +%% $/ for paths) or $[ or $] or end of binary is the binding name. +compile_binding(<<>>, _, <<>>) -> + error(badarg); +compile_binding(Rest = <<>>, _, Acc) -> + {Acc, Rest}; +compile_binding(Rest = << C, _/bits >>, S, Acc) + when C =:= S; C =:= $[; C =:= $] -> + {Acc, Rest}; +compile_binding(<< C, Rest/bits >>, S, Acc) -> + compile_binding(Rest, S, << Acc/binary, C >>). + +compile_brackets(Rest, S, Segments, Rules) -> + {Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0), + Rules1 = compile_rules(Rest2, S, Segments, [], <<>>), + Rules2 = compile_rules(<< Bracket/binary, Rest2/binary >>, + S, Segments, [], <<>>), + Rules ++ Rules2 ++ Rules1. + +%% Missing a close bracket. +compile_brackets_split(<<>>, _, _) -> + error(badarg); +%% Make sure we don't confuse the closing bracket we're looking for. +compile_brackets_split(<< C, Rest/bits >>, Acc, N) when C =:= $[ -> + compile_brackets_split(Rest, << Acc/binary, C >>, N + 1); +compile_brackets_split(<< C, Rest/bits >>, Acc, N) when C =:= $], N > 0 -> + compile_brackets_split(Rest, << Acc/binary, C >>, N - 1); +%% That's the right one. +compile_brackets_split(<< $], Rest/bits >>, Acc, 0) -> + {Acc, Rest}; +compile_brackets_split(<< C, Rest/bits >>, Acc, N) -> + compile_brackets_split(Rest, << Acc/binary, C >>, N). + +-spec execute(Req, Env) + -> {ok, Req, Env} | {stop, Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +execute(Req=#{host := Host, path := Path}, Env=#{dispatch := Dispatch0}) -> + Dispatch = case Dispatch0 of + {persistent_term, Key} -> persistent_term:get(Key); + _ -> Dispatch0 + end, + case match(Dispatch, Host, Path) of + {ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} -> + {ok, Req#{ + host_info => HostInfo, + path_info => PathInfo, + bindings => Bindings + }, Env#{ + handler => Handler, + handler_opts => HandlerOpts + }}; + {error, notfound, host} -> + {stop, cowboy_req:reply(400, Req)}; + {error, badrequest, path} -> + {stop, cowboy_req:reply(400, Req)}; + {error, notfound, path} -> + {stop, cowboy_req:reply(404, Req)} + end. + +%% Internal. + +%% Match hostname tokens and path tokens against dispatch rules. +%% +%% It is typically used for matching tokens for the hostname and path of +%% the request against a global dispatch rule for your listener. +%% +%% Dispatch rules are a list of {Hostname, PathRules} tuples, with +%% PathRules being a list of {Path, HandlerMod, HandlerOpts}. +%% +%% Hostname and Path are match rules and can be either the +%% atom '_', which matches everything, `<<"*">>', which match the +%% wildcard path, or a list of tokens. +%% +%% Each token can be either a binary, the atom '_', +%% the atom '...' or a named atom. A binary token must match exactly, +%% '_' matches everything for a single token, '...' matches +%% everything for the rest of the tokens and a named atom will bind the +%% corresponding token value and return it. +%% +%% The list of hostname tokens is reversed before matching. For example, if +%% we were to match "www.ninenines.eu", we would first match "eu", then +%% "ninenines", then "www". This means that in the context of hostnames, +%% the '...' atom matches properly the lower levels of the domain +%% as would be expected. +%% +%% When a result is found, this function will return the handler module and +%% options found in the dispatch list, a key-value list of bindings and +%% the tokens that were matched by the '...' atom for both the +%% hostname and path. +-spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary()) + -> {ok, module(), any(), bindings(), + HostInfo::undefined | tokens(), + PathInfo::undefined | tokens()} + | {error, notfound, host} | {error, notfound, path} + | {error, badrequest, path}. +match([], _, _) -> + {error, notfound, host}; +%% If the host is '_' then there can be no constraints. +match([{'_', [], PathMatchs}|_Tail], _, Path) -> + match_path(PathMatchs, undefined, Path, #{}); +match([{HostMatch, Fields, PathMatchs}|Tail], Tokens, Path) + when is_list(Tokens) -> + case list_match(Tokens, HostMatch, #{}) of + false -> + match(Tail, Tokens, Path); + {true, Bindings, HostInfo} -> + HostInfo2 = case HostInfo of + undefined -> undefined; + _ -> lists:reverse(HostInfo) + end, + case check_constraints(Fields, Bindings) of + {ok, Bindings2} -> + match_path(PathMatchs, HostInfo2, Path, Bindings2); + nomatch -> + match(Tail, Tokens, Path) + end + end; +match(Dispatch, Host, Path) -> + match(Dispatch, split_host(Host), Path). + +-spec match_path([dispatch_path()], + HostInfo::undefined | tokens(), binary() | tokens(), bindings()) + -> {ok, module(), any(), bindings(), + HostInfo::undefined | tokens(), + PathInfo::undefined | tokens()} + | {error, notfound, path} | {error, badrequest, path}. +match_path([], _, _, _) -> + {error, notfound, path}; +%% If the path is '_' then there can be no constraints. +match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) -> + {ok, Handler, Opts, Bindings, HostInfo, undefined}; +match_path([{<<"*">>, _, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) -> + {ok, Handler, Opts, Bindings, HostInfo, undefined}; +match_path([_|Tail], HostInfo, <<"*">>, Bindings) -> + match_path(Tail, HostInfo, <<"*">>, Bindings); +match_path([{PathMatch, Fields, Handler, Opts}|Tail], HostInfo, Tokens, + Bindings) when is_list(Tokens) -> + case list_match(Tokens, PathMatch, Bindings) of + false -> + match_path(Tail, HostInfo, Tokens, Bindings); + {true, PathBinds, PathInfo} -> + case check_constraints(Fields, PathBinds) of + {ok, PathBinds2} -> + {ok, Handler, Opts, PathBinds2, HostInfo, PathInfo}; + nomatch -> + match_path(Tail, HostInfo, Tokens, Bindings) + end + end; +match_path(_Dispatch, _HostInfo, badrequest, _Bindings) -> + {error, badrequest, path}; +match_path(Dispatch, HostInfo, Path, Bindings) -> + match_path(Dispatch, HostInfo, split_path(Path), Bindings). + +check_constraints([], Bindings) -> + {ok, Bindings}; +check_constraints([Field|Tail], Bindings) when is_atom(Field) -> + check_constraints(Tail, Bindings); +check_constraints([Field|Tail], Bindings) -> + Name = element(1, Field), + case Bindings of + #{Name := Value0} -> + Constraints = element(2, Field), + case cowboy_constraints:validate(Value0, Constraints) of + {ok, Value} -> + check_constraints(Tail, Bindings#{Name => Value}); + {error, _} -> + nomatch + end; + _ -> + check_constraints(Tail, Bindings) + end. + +-spec split_host(binary()) -> tokens(). +split_host(Host) -> + split_host(Host, []). + +split_host(Host, Acc) -> + case binary:match(Host, <<".">>) of + nomatch when Host =:= <<>> -> + Acc; + nomatch -> + [Host|Acc]; + {Pos, _} -> + << Segment:Pos/binary, _:8, Rest/bits >> = Host, + false = byte_size(Segment) == 0, + split_host(Rest, [Segment|Acc]) + end. + +%% Following RFC2396, this function may return path segments containing any +%% character, including / if, and only if, a / was escaped +%% and part of a path segment. +-spec split_path(binary()) -> tokens() | badrequest. +split_path(<< $/, Path/bits >>) -> + split_path(Path, []); +split_path(_) -> + badrequest. + +split_path(Path, Acc) -> + try + case binary:match(Path, <<"/">>) of + nomatch when Path =:= <<>> -> + remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- Acc]), []); + nomatch -> + remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- [Path|Acc]]), []); + {Pos, _} -> + << Segment:Pos/binary, _:8, Rest/bits >> = Path, + split_path(Rest, [Segment|Acc]) + end + catch error:_ -> + badrequest + end. + +remove_dot_segments([], Acc) -> + lists:reverse(Acc); +remove_dot_segments([<<".">>|Segments], Acc) -> + remove_dot_segments(Segments, Acc); +remove_dot_segments([<<"..">>|Segments], Acc=[]) -> + remove_dot_segments(Segments, Acc); +remove_dot_segments([<<"..">>|Segments], [_|Acc]) -> + remove_dot_segments(Segments, Acc); +remove_dot_segments([S|Segments], Acc) -> + remove_dot_segments(Segments, [S|Acc]). + +-ifdef(TEST). +remove_dot_segments_test_() -> + Tests = [ + {[<<"a">>, <<"b">>, <<"c">>, <<".">>, <<"..">>, <<"..">>, <<"g">>], [<<"a">>, <<"g">>]}, + {[<<"mid">>, <<"content=5">>, <<"..">>, <<"6">>], [<<"mid">>, <<"6">>]}, + {[<<"..">>, <<"a">>], [<<"a">>]} + ], + [fun() -> R = remove_dot_segments(S, []) end || {S, R} <- Tests]. +-endif. + +-spec list_match(tokens(), dispatch_match(), bindings()) + -> {true, bindings(), undefined | tokens()} | false. +%% Atom '...' matches any trailing path, stop right now. +list_match(List, ['...'], Binds) -> + {true, Binds, List}; +%% Atom '_' matches anything, continue. +list_match([_E|Tail], ['_'|TailMatch], Binds) -> + list_match(Tail, TailMatch, Binds); +%% Both values match, continue. +list_match([E|Tail], [E|TailMatch], Binds) -> + list_match(Tail, TailMatch, Binds); +%% Bind E to the variable name V and continue, +%% unless V was already defined and E isn't identical to the previous value. +list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) -> + case Binds of + %% @todo This isn't right, the constraint must be applied FIRST + %% otherwise we can't check for example ints in both host/path. + #{V := E} -> + list_match(Tail, TailMatch, Binds); + #{V := _} -> + false; + _ -> + list_match(Tail, TailMatch, Binds#{V => E}) + end; +%% Match complete. +list_match([], [], Binds) -> + {true, Binds, undefined}; +%% Values don't match, stop. +list_match(_List, _Match, _Binds) -> + false. + +%% Tests. + +-ifdef(TEST). +compile_test_() -> + Tests = [ + %% Match any host and path. + {[{'_', [{'_', h, o}]}], + [{'_', [], [{'_', [], h, o}]}]}, + {[{"cowboy.example.org", + [{"/", ha, oa}, {"/path/to/resource", hb, ob}]}], + [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [ + {[], [], ha, oa}, + {[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]}, + {[{'_', [{"/path/to/resource/", h, o}]}], + [{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]}, + % Cyrillic from a latin1 encoded file. + {[{'_', [{[47,208,191,209,131,209,130,209,140,47,208,186,47,209,128, + 208,181,209,129,209,131,209,128,209,129,209,131,47], h, o}]}], + [{'_', [], [{[<<208,191,209,131,209,130,209,140>>, <<208,186>>, + <<209,128,208,181,209,129,209,131,209,128,209,129,209,131>>], + [], h, o}]}]}, + {[{"cowboy.example.org.", [{'_', h, o}]}], + [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]}, + {[{".cowboy.example.org", [{'_', h, o}]}], + [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]}, + % Cyrillic from a latin1 encoded file. + {[{[208,189,208,181,208,186,208,184,208,185,46,209,129,208,176, + 208,185,209,130,46,209,128,209,132,46], [{'_', h, o}]}], + [{[<<209,128,209,132>>, <<209,129,208,176,208,185,209,130>>, + <<208,189,208,181,208,186,208,184,208,185>>], + [], [{'_', [], h, o}]}]}, + {[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}], + [{[<<"org">>, <<"example">>, subdomain], [], [ + {[<<"hats">>, name, <<"prices">>], [], h, o}]}]}, + {[{"ninenines.:_", [{"/hats/:_", h, o}]}], + [{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]}, + {[{"[www.]ninenines.eu", + [{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [ + {[<<"eu">>, <<"ninenines">>], [], [ + {[<<"horses">>], [], h, o}, + {[<<"hats">>], [], h, o}, + {[<<"hats">>, <<"page">>, number], [], h, o}]}, + {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [ + {[<<"horses">>], [], h, o}, + {[<<"hats">>], [], h, o}, + {[<<"hats">>, <<"page">>, number], [], h, o}]}]}, + {[{'_', [{"/hats/:page/:number", h, o}]}], [{'_', [], [ + {[<<"hats">>, page, number], [], h, o}]}]}, + {[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [ + {[<<"hats">>], [], h, o}, + {[<<"hats">>, <<"page">>], [], h, o}, + {[<<"hats">>, <<"page">>, number], [], h, o}]}]}, + {[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}], + [{[<<"eu">>, <<"ninenines">>, '...'], [], [ + {[<<"hats">>, '...'], [], h, o}]}]}, + %% Path segment containing a colon. + {[{'_', [{"/foo/bar:blah", h, o}]}], [{'_', [], [ + {[<<"foo">>, <<"bar:blah">>], [], h, o}]}]} + ], + [{lists:flatten(io_lib:format("~p", [Rt])), + fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests]. + +split_host_test_() -> + Tests = [ + {<<"">>, []}, + {<<"*">>, [<<"*">>]}, + {<<"cowboy.ninenines.eu">>, + [<<"eu">>, <<"ninenines">>, <<"cowboy">>]}, + {<<"ninenines.eu">>, + [<<"eu">>, <<"ninenines">>]}, + {<<"ninenines.eu.">>, + [<<"eu">>, <<"ninenines">>]}, + {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>, + [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>, + <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>, + <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>, + <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]} + ], + [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests]. + +split_path_test_() -> + Tests = [ + {<<"/">>, []}, + {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]}, + {<<"/users">>, [<<"users">>]}, + {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]}, + {<<"/users/a%20b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]} + ], + [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests]. + +match_test_() -> + Dispatch = [ + {[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [ + {[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []} + ]}, + {[<<"eu">>, <<"ninenines">>], [], [ + {[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []}, + {'_', [], match_extend, []} + ]}, + {[var, <<"ninenines">>], [], [ + {[<<"threads">>, var], [], match_duplicate_vars, + [we, {expect, two}, var, here]} + ]}, + {[ext, <<"erlang">>], [], [ + {'_', [], match_erlang_ext, []} + ]}, + {'_', [], [ + {[<<"users">>, id, <<"friends">>], [], match_users_friends, []}, + {'_', [], match_any, []} + ]} + ], + Tests = [ + {<<"any">>, <<"/">>, {ok, match_any, [], #{}}}, + {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>, + {ok, match_any_subdomain_users, [], #{}}}, + {<<"www.ninenines.eu">>, <<"/users/42/mails">>, + {ok, match_any, [], #{}}}, + {<<"www.ninenines.eu">>, <<"/">>, + {ok, match_any, [], #{}}}, + {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>, + {error, notfound, path}}, + {<<"ninenines.eu">>, <<"/">>, + {ok, match_extend, [], #{}}}, + {<<"ninenines.eu">>, <<"/users/42/friends">>, + {ok, match_extend_users_friends, [], #{id => <<"42">>}}}, + {<<"erlang.fr">>, '_', + {ok, match_erlang_ext, [], #{ext => <<"fr">>}}}, + {<<"any">>, <<"/users/444/friends">>, + {ok, match_users_friends, [], #{id => <<"444">>}}}, + {<<"any">>, <<"/users//friends">>, + {ok, match_users_friends, [], #{id => <<>>}}} + ], + [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> + {ok, Handler, Opts, Binds, undefined, undefined} + = match(Dispatch, H, P) + end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests]. + +match_info_test_() -> + Dispatch = [ + {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [ + {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []} + ]}, + {[<<"eu">>, <<"ninenines">>, '...'], [], [ + {'_', [], match_any, []} + ]} + ], + Tests = [ + {<<"ninenines.eu">>, <<"/">>, + {ok, match_any, [], #{}, [], undefined}}, + {<<"bugs.ninenines.eu">>, <<"/">>, + {ok, match_any, [], #{}, [<<"bugs">>], undefined}}, + {<<"cowboy.bugs.ninenines.eu">>, <<"/">>, + {ok, match_any, [], #{}, [<<"cowboy">>, <<"bugs">>], undefined}}, + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>, + {ok, match_path, [], #{}, undefined, []}}, + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>, + {ok, match_path, [], #{}, undefined, [<<"path_info">>]}}, + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>, + {ok, match_path, [], #{}, undefined, [<<"foo">>, <<"bar">>]}} + ], + [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> + R = match(Dispatch, H, P) + end} || {H, P, R} <- Tests]. + +match_constraints_test() -> + Dispatch0 = [{'_', [], + [{[<<"path">>, value], [{value, int}], match, []}]}], + {ok, _, [], #{value := 123}, _, _} = match(Dispatch0, + <<"ninenines.eu">>, <<"/path/123">>), + {ok, _, [], #{value := 123}, _, _} = match(Dispatch0, + <<"ninenines.eu">>, <<"/path/123/">>), + {error, notfound, path} = match(Dispatch0, + <<"ninenines.eu">>, <<"/path/NaN/">>), + Dispatch1 = [{'_', [], + [{[<<"path">>, value, <<"more">>], [{value, nonempty}], match, []}]}], + {ok, _, [], #{value := <<"something">>}, _, _} = match(Dispatch1, + <<"ninenines.eu">>, <<"/path/something/more">>), + {error, notfound, path} = match(Dispatch1, + <<"ninenines.eu">>, <<"/path//more">>), + Dispatch2 = [{'_', [], [{[<<"path">>, username], + [{username, fun(_, Value) -> + case cowboy_bstr:to_lower(Value) of + Value -> {ok, Value}; + _ -> {error, not_lowercase} + end end}], + match, []}]}], + {ok, _, [], #{username := <<"essen">>}, _, _} = match(Dispatch2, + <<"ninenines.eu">>, <<"/path/essen">>), + {error, notfound, path} = match(Dispatch2, + <<"ninenines.eu">>, <<"/path/ESSEN">>), + ok. + +match_same_bindings_test() -> + Dispatch = [{[same, same], [], [{'_', [], match, []}]}], + {ok, _, [], #{same := <<"eu">>}, _, _} = match(Dispatch, + <<"eu.eu">>, <<"/">>), + {error, notfound, host} = match(Dispatch, + <<"ninenines.eu">>, <<"/">>), + Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [], + [{[<<"path">>, user], [], match, []}]}], + {ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2, + <<"essen.ninenines.eu">>, <<"/path/essen">>), + {ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2, + <<"essen.ninenines.eu">>, <<"/path/essen/">>), + {error, notfound, path} = match(Dispatch2, + <<"essen.ninenines.eu">>, <<"/path/notessen">>), + Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}], + {ok, _, [], #{same := <<"path">>}, _, _} = match(Dispatch3, + <<"ninenines.eu">>, <<"/path/path">>), + {error, notfound, path} = match(Dispatch3, + <<"ninenines.eu">>, <<"/path/to">>), + ok. +-endif. diff --git a/cowboy/src/cowboy_static.erl b/cowboy/src/cowboy_static.erl new file mode 100644 index 0000000..a185ef1 --- /dev/null +++ b/cowboy/src/cowboy_static.erl @@ -0,0 +1,418 @@ +%% Copyright (c) 2013-2024, Loïc Hoguin +%% Copyright (c) 2011, Magnus Klaar +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_static). + +-export([init/2]). +-export([malformed_request/2]). +-export([forbidden/2]). +-export([content_types_provided/2]). +-export([charsets_provided/2]). +-export([ranges_provided/2]). +-export([resource_exists/2]). +-export([last_modified/2]). +-export([generate_etag/2]). +-export([get_file/2]). + +-type extra_charset() :: {charset, module(), function()} | {charset, binary()}. +-type extra_etag() :: {etag, module(), function()} | {etag, false}. +-type extra_mimetypes() :: {mimetypes, module(), function()} + | {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}. +-type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()]. +-type opts() :: {file | dir, string() | binary()} + | {file | dir, string() | binary(), extra()} + | {priv_file | priv_dir, atom(), string() | binary()} + | {priv_file | priv_dir, atom(), string() | binary(), extra()}. +-export_type([opts/0]). + +-include_lib("kernel/include/file.hrl"). + +-type state() :: {binary(), {direct | archive, #file_info{}} + | {error, atom()}, extra()}. + +%% Resolve the file that will be sent and get its file information. +%% If the handler is configured to manage a directory, check that the +%% requested file is inside the configured directory. + +-spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req::cowboy_req:req(). +init(Req, {Name, Path}) -> + init_opts(Req, {Name, Path, []}); +init(Req, {Name, App, Path}) + when Name =:= priv_file; Name =:= priv_dir -> + init_opts(Req, {Name, App, Path, []}); +init(Req, Opts) -> + init_opts(Req, Opts). + +init_opts(Req, {priv_file, App, Path, Extra}) -> + {PrivPath, HowToAccess} = priv_path(App, Path), + init_info(Req, absname(PrivPath), HowToAccess, Extra); +init_opts(Req, {file, Path, Extra}) -> + init_info(Req, absname(Path), direct, Extra); +init_opts(Req, {priv_dir, App, Path, Extra}) -> + {PrivPath, HowToAccess} = priv_path(App, Path), + init_dir(Req, PrivPath, HowToAccess, Extra); +init_opts(Req, {dir, Path, Extra}) -> + init_dir(Req, Path, direct, Extra). + +priv_path(App, Path) -> + case code:priv_dir(App) of + {error, bad_name} -> + error({badarg, "Can't resolve the priv_dir of application " + ++ atom_to_list(App)}); + PrivDir when is_list(Path) -> + { + PrivDir ++ "/" ++ Path, + how_to_access_app_priv(PrivDir) + }; + PrivDir when is_binary(Path) -> + { + << (list_to_binary(PrivDir))/binary, $/, Path/binary >>, + how_to_access_app_priv(PrivDir) + } + end. + +how_to_access_app_priv(PrivDir) -> + %% If the priv directory is not a directory, it must be + %% inside an Erlang application .ez archive. We call + %% how_to_access_app_priv1() to find the corresponding archive. + case filelib:is_dir(PrivDir) of + true -> direct; + false -> how_to_access_app_priv1(PrivDir) + end. + +how_to_access_app_priv1(Dir) -> + %% We go "up" by one path component at a time and look for a + %% regular file. + Archive = filename:dirname(Dir), + case Archive of + Dir -> + %% filename:dirname() returned its argument: + %% we reach the root directory. We found no + %% archive so we return 'direct': the given priv + %% directory doesn't exist. + direct; + _ -> + case filelib:is_regular(Archive) of + true -> {archive, Archive}; + false -> how_to_access_app_priv1(Archive) + end + end. + +absname(Path) when is_list(Path) -> + filename:absname(list_to_binary(Path)); +absname(Path) when is_binary(Path) -> + filename:absname(Path). + +init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) -> + init_dir(Req, list_to_binary(Path), HowToAccess, Extra); +init_dir(Req, Path, HowToAccess, Extra) -> + Dir = fullpath(filename:absname(Path)), + case cowboy_req:path_info(Req) of + %% When dir/priv_dir are used and there is no path_info + %% this is a configuration error and we abort immediately. + undefined -> + {ok, cowboy_req:reply(500, Req), error}; + PathInfo -> + case validate_reserved(PathInfo) of + error -> + {cowboy_rest, Req, error}; + ok -> + Filepath = filename:join([Dir|PathInfo]), + Len = byte_size(Dir), + case fullpath(Filepath) of + << Dir:Len/binary, $/, _/binary >> -> + init_info(Req, Filepath, HowToAccess, Extra); + << Dir:Len/binary >> -> + init_info(Req, Filepath, HowToAccess, Extra); + _ -> + {cowboy_rest, Req, error} + end + end + end. + +validate_reserved([]) -> + ok; +validate_reserved([P|Tail]) -> + case validate_reserved1(P) of + ok -> validate_reserved(Tail); + error -> error + end. + +%% We always reject forward slash, backward slash and NUL as +%% those have special meanings across the supported platforms. +%% We could support the backward slash on some platforms but +%% for the sake of consistency and simplicity we don't. +validate_reserved1(<<>>) -> + ok; +validate_reserved1(<<$/, _/bits>>) -> + error; +validate_reserved1(<<$\\, _/bits>>) -> + error; +validate_reserved1(<<0, _/bits>>) -> + error; +validate_reserved1(<<_, Rest/bits>>) -> + validate_reserved1(Rest). + +fullpath(Path) -> + fullpath(filename:split(Path), []). +fullpath([], Acc) -> + filename:join(lists:reverse(Acc)); +fullpath([<<".">>|Tail], Acc) -> + fullpath(Tail, Acc); +fullpath([<<"..">>|Tail], Acc=[_]) -> + fullpath(Tail, Acc); +fullpath([<<"..">>|Tail], [_|Acc]) -> + fullpath(Tail, Acc); +fullpath([Segment|Tail], Acc) -> + fullpath(Tail, [Segment|Acc]). + +init_info(Req, Path, HowToAccess, Extra) -> + Info = read_file_info(Path, HowToAccess), + {cowboy_rest, Req, {Path, Info, Extra}}. + +read_file_info(Path, direct) -> + case file:read_file_info(Path, [{time, universal}]) of + {ok, Info} -> {direct, Info}; + Error -> Error + end; +read_file_info(Path, {archive, Archive}) -> + case file:read_file_info(Archive, [{time, universal}]) of + {ok, ArchiveInfo} -> + %% The Erlang application archive is fine. + %% Now check if the requested file is in that + %% archive. We also need the file_info to merge + %% them with the archive's one. + PathS = binary_to_list(Path), + case erl_prim_loader:read_file_info(PathS) of + {ok, ContainedFileInfo} -> + Info = fix_archived_file_info( + ArchiveInfo, + ContainedFileInfo), + {archive, Info}; + error -> + {error, enoent} + end; + Error -> + Error + end. + +fix_archived_file_info(ArchiveInfo, ContainedFileInfo) -> + %% We merge the archive and content #file_info because we are + %% interested by the timestamps of the archive, but the type and + %% size of the contained file/directory. + %% + %% We reset the access to 'read', because we won't rewrite the + %% archive. + ArchiveInfo#file_info{ + size = ContainedFileInfo#file_info.size, + type = ContainedFileInfo#file_info.type, + access = read + }. + +-ifdef(TEST). +fullpath_test_() -> + Tests = [ + {<<"/home/cowboy">>, <<"/home/cowboy">>}, + {<<"/home/cowboy">>, <<"/home/cowboy/">>}, + {<<"/home/cowboy">>, <<"/home/cowboy/./">>}, + {<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>}, + {<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>}, + {<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>}, + {<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>}, + {<<"/">>, <<"/home/cowboy/../../../../../..">>}, + {<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>} + ], + [{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests]. + +good_path_check_test_() -> + Tests = [ + <<"/home/cowboy/file">>, + <<"/home/cowboy/file/">>, + <<"/home/cowboy/./file">>, + <<"/home/cowboy/././././././file">>, + <<"/home/cowboy/abc/../file">>, + <<"/home/cowboy/abc/../file">>, + <<"/home/cowboy/abc/./.././file">> + ], + [{P, fun() -> + case fullpath(P) of + << "/home/cowboy/", _/bits >> -> ok + end + end} || P <- Tests]. + +bad_path_check_test_() -> + Tests = [ + <<"/home/cowboy/../../../../../../file">>, + <<"/home/cowboy/../../etc/passwd">> + ], + [{P, fun() -> + error = case fullpath(P) of + << "/home/cowboy/", _/bits >> -> ok; + _ -> error + end + end} || P <- Tests]. + +good_path_win32_check_test_() -> + Tests = case os:type() of + {unix, _} -> + []; + {win32, _} -> + [ + <<"c:/home/cowboy/file">>, + <<"c:/home/cowboy/file/">>, + <<"c:/home/cowboy/./file">>, + <<"c:/home/cowboy/././././././file">>, + <<"c:/home/cowboy/abc/../file">>, + <<"c:/home/cowboy/abc/../file">>, + <<"c:/home/cowboy/abc/./.././file">> + ] + end, + [{P, fun() -> + case fullpath(P) of + << "c:/home/cowboy/", _/bits >> -> ok + end + end} || P <- Tests]. + +bad_path_win32_check_test_() -> + Tests = case os:type() of + {unix, _} -> + []; + {win32, _} -> + [ + <<"c:/home/cowboy/../../secretfile.bat">>, + <<"c:/home/cowboy/c:/secretfile.bat">>, + <<"c:/home/cowboy/..\\..\\secretfile.bat">>, + <<"c:/home/cowboy/c:\\secretfile.bat">> + ] + end, + [{P, fun() -> + error = case fullpath(P) of + << "c:/home/cowboy/", _/bits >> -> ok; + _ -> error + end + end} || P <- Tests]. +-endif. + +%% Reject requests that tried to access a file outside +%% the target directory, or used reserved characters. + +-spec malformed_request(Req, State) + -> {boolean(), Req, State}. +malformed_request(Req, State) -> + {State =:= error, Req, State}. + +%% Directories, files that can't be accessed at all and +%% files with no read flag are forbidden. + +-spec forbidden(Req, State) + -> {boolean(), Req, State} + when State::state(). +forbidden(Req, State={_, {_, #file_info{type=directory}}, _}) -> + {true, Req, State}; +forbidden(Req, State={_, {error, eacces}, _}) -> + {true, Req, State}; +forbidden(Req, State={_, {_, #file_info{access=Access}}, _}) + when Access =:= write; Access =:= none -> + {true, Req, State}; +forbidden(Req, State) -> + {false, Req, State}. + +%% Detect the mimetype of the file. + +-spec content_types_provided(Req, State) + -> {[{binary(), get_file}], Req, State} + when State::state(). +content_types_provided(Req, State={Path, _, Extra}) when is_list(Extra) -> + case lists:keyfind(mimetypes, 1, Extra) of + false -> + {[{cow_mimetypes:web(Path), get_file}], Req, State}; + {mimetypes, Module, Function} -> + {[{Module:Function(Path), get_file}], Req, State}; + {mimetypes, Type} -> + {[{Type, get_file}], Req, State} + end. + +%% Detect the charset of the file. + +-spec charsets_provided(Req, State) + -> {[binary()], Req, State} + when State::state(). +charsets_provided(Req, State={Path, _, Extra}) -> + case lists:keyfind(charset, 1, Extra) of + %% We simulate the callback not being exported. + false -> + no_call; + {charset, Module, Function} -> + {[Module:Function(Path)], Req, State}; + {charset, Charset} when is_binary(Charset) -> + {[Charset], Req, State} + end. + +%% Enable support for range requests. + +-spec ranges_provided(Req, State) + -> {[{binary(), auto}], Req, State} + when State::state(). +ranges_provided(Req, State) -> + {[{<<"bytes">>, auto}], Req, State}. + +%% Assume the resource doesn't exist if it's not a regular file. + +-spec resource_exists(Req, State) + -> {boolean(), Req, State} + when State::state(). +resource_exists(Req, State={_, {_, #file_info{type=regular}}, _}) -> + {true, Req, State}; +resource_exists(Req, State) -> + {false, Req, State}. + +%% Generate an etag for the file. + +-spec generate_etag(Req, State) + -> {{strong | weak, binary()}, Req, State} + when State::state(). +generate_etag(Req, State={Path, {_, #file_info{size=Size, mtime=Mtime}}, + Extra}) -> + case lists:keyfind(etag, 1, Extra) of + false -> + {generate_default_etag(Size, Mtime), Req, State}; + {etag, Module, Function} -> + {Module:Function(Path, Size, Mtime), Req, State}; + {etag, false} -> + {undefined, Req, State} + end. + +generate_default_etag(Size, Mtime) -> + {strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}. + +%% Return the time of last modification of the file. + +-spec last_modified(Req, State) + -> {calendar:datetime(), Req, State} + when State::state(). +last_modified(Req, State={_, {_, #file_info{mtime=Modified}}, _}) -> + {Modified, Req, State}. + +%% Stream the file. + +-spec get_file(Req, State) + -> {{sendfile, 0, non_neg_integer(), binary()}, Req, State} + when State::state(). +get_file(Req, State={Path, {direct, #file_info{size=Size}}, _}) -> + {{sendfile, 0, Size, Path}, Req, State}; +get_file(Req, State={Path, {archive, _}, _}) -> + PathS = binary_to_list(Path), + {ok, Bin, _} = erl_prim_loader:get_file(PathS), + {Bin, Req, State}. diff --git a/cowboy/src/cowboy_stream.erl b/cowboy/src/cowboy_stream.erl new file mode 100644 index 0000000..6ceb5ba --- /dev/null +++ b/cowboy/src/cowboy_stream.erl @@ -0,0 +1,193 @@ +%% Copyright (c) 2015-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_stream). + +-type state() :: any(). +-type human_reason() :: atom(). + +-type streamid() :: any(). +-export_type([streamid/0]). + +-type fin() :: fin | nofin. +-export_type([fin/0]). + +%% @todo Perhaps it makes more sense to have resp_body in this module? + +-type resp_command() + :: {response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()}. +-export_type([resp_command/0]). + +-type commands() :: [{inform, cowboy:http_status(), cowboy:http_headers()} + | resp_command() + | {headers, cowboy:http_status(), cowboy:http_headers()} + | {data, fin(), cowboy_req:resp_body()} + | {trailers, cowboy:http_headers()} + | {push, binary(), binary(), binary(), inet:port_number(), + binary(), binary(), cowboy:http_headers()} + | {flow, pos_integer()} + | {spawn, pid(), timeout()} + | {error_response, cowboy:http_status(), cowboy:http_headers(), iodata()} + | {switch_protocol, cowboy:http_headers(), module(), state()} + | {internal_error, any(), human_reason()} + | {set_options, map()} + | {log, logger:level(), io:format(), list()} + | stop]. +-export_type([commands/0]). + +-type reason() :: normal | switch_protocol + | {internal_error, timeout | {error | exit | throw, any()}, human_reason()} + | {socket_error, closed | atom(), human_reason()} + | {stream_error, cow_http2:error(), human_reason()} + | {connection_error, cow_http2:error(), human_reason()} + | {stop, cow_http2:frame() | {exit, any()}, human_reason()}. +-export_type([reason/0]). + +-type partial_req() :: map(). %% @todo Take what's in cowboy_req with everything? optional. +-export_type([partial_req/0]). + +-callback init(streamid(), cowboy_req:req(), cowboy:opts()) -> {commands(), state()}. +-callback data(streamid(), fin(), binary(), State) -> {commands(), State} when State::state(). +-callback info(streamid(), any(), State) -> {commands(), State} when State::state(). +-callback terminate(streamid(), reason(), state()) -> any(). +-callback early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts()) + -> Resp when Resp::resp_command(). + +%% @todo To optimize the number of active timers we could have a command +%% that enables a timeout that is called in the absence of any other call, +%% similar to what gen_server does. However the nice thing about this is +%% that the connection process can keep a single timer around (the same +%% one that would be used to detect half-closed sockets) and use this +%% timer and other events to trigger the timeout in streams at their +%% intended time. +%% +%% This same timer can be used to try and send PING frames to help detect +%% that the connection is indeed unresponsive. + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). +-export([make_error_log/5]). + +%% Note that this and other functions in this module do NOT catch +%% exceptions. We want the exception to go all the way down to the +%% protocol code. +%% +%% OK the failure scenario is not so clear. The problem is +%% that the failure at any point in init/3 will result in the +%% corresponding state being lost. I am unfortunately not +%% confident we can do anything about this. If the crashing +%% handler just created a process, we'll never know about it. +%% Therefore at this time I choose to leave all failure handling +%% to the protocol process. +%% +%% Note that a failure in init/3 will result in terminate/3 +%% NOT being called. This is because the state is not available. + +-spec init(streamid(), cowboy_req:req(), cowboy:opts()) + -> {commands(), {module(), state()} | undefined}. +init(StreamID, Req, Opts) -> + case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of + [] -> + {[], undefined}; + [Handler|Tail] -> + %% We call the next handler and remove it from the list of + %% stream handlers. This means that handlers that run after + %% it have no knowledge it exists. Should user require this + %% knowledge they can just define a separate option that will + %% be left untouched. + {Commands, State} = Handler:init(StreamID, Req, Opts#{stream_handlers => Tail}), + {Commands, {Handler, State}} + end. + +-spec data(streamid(), fin(), binary(), {Handler, State} | undefined) + -> {commands(), {Handler, State} | undefined} + when Handler::module(), State::state(). +data(_, _, _, undefined) -> + {[], undefined}; +data(StreamID, IsFin, Data, {Handler, State0}) -> + {Commands, State} = Handler:data(StreamID, IsFin, Data, State0), + {Commands, {Handler, State}}. + +-spec info(streamid(), any(), {Handler, State} | undefined) + -> {commands(), {Handler, State} | undefined} + when Handler::module(), State::state(). +info(_, _, undefined) -> + {[], undefined}; +info(StreamID, Info, {Handler, State0}) -> + {Commands, State} = Handler:info(StreamID, Info, State0), + {Commands, {Handler, State}}. + +-spec terminate(streamid(), reason(), {module(), state()} | undefined) -> ok. +terminate(_, _, undefined) -> + ok; +terminate(StreamID, Reason, {Handler, State}) -> + _ = Handler:terminate(StreamID, Reason, State), + ok. + +-spec early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts()) + -> Resp when Resp::resp_command(). +early_error(StreamID, Reason, PartialReq, Resp, Opts) -> + case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of + [] -> + Resp; + [Handler|Tail] -> + %% This is the same behavior as in init/3. + Handler:early_error(StreamID, Reason, + PartialReq, Resp, Opts#{stream_handlers => Tail}) + end. + +-spec make_error_log(init | data | info | terminate | early_error, + list(), error | exit | throw, any(), list()) + -> {log, error, string(), list()}. +make_error_log(init, [StreamID, Req, Opts], Class, Exception, Stacktrace) -> + {log, error, + "Unhandled exception ~p:~p in cowboy_stream:init(~p, Req, Opts)~n" + "Stacktrace: ~p~n" + "Req: ~p~n" + "Opts: ~p~n", + [Class, Exception, StreamID, Stacktrace, Req, Opts]}; +make_error_log(data, [StreamID, IsFin, Data, State], Class, Exception, Stacktrace) -> + {log, error, + "Unhandled exception ~p:~p in cowboy_stream:data(~p, ~p, Data, State)~n" + "Stacktrace: ~p~n" + "Data: ~p~n" + "State: ~p~n", + [Class, Exception, StreamID, IsFin, Stacktrace, Data, State]}; +make_error_log(info, [StreamID, Msg, State], Class, Exception, Stacktrace) -> + {log, error, + "Unhandled exception ~p:~p in cowboy_stream:info(~p, Msg, State)~n" + "Stacktrace: ~p~n" + "Msg: ~p~n" + "State: ~p~n", + [Class, Exception, StreamID, Stacktrace, Msg, State]}; +make_error_log(terminate, [StreamID, Reason, State], Class, Exception, Stacktrace) -> + {log, error, + "Unhandled exception ~p:~p in cowboy_stream:terminate(~p, Reason, State)~n" + "Stacktrace: ~p~n" + "Reason: ~p~n" + "State: ~p~n", + [Class, Exception, StreamID, Stacktrace, Reason, State]}; +make_error_log(early_error, [StreamID, Reason, PartialReq, Resp, Opts], + Class, Exception, Stacktrace) -> + {log, error, + "Unhandled exception ~p:~p in cowboy_stream:early_error(~p, Reason, PartialReq, Resp, Opts)~n" + "Stacktrace: ~p~n" + "Reason: ~p~n" + "PartialReq: ~p~n" + "Resp: ~p~n" + "Opts: ~p~n", + [Class, Exception, StreamID, Stacktrace, Reason, PartialReq, Resp, Opts]}. diff --git a/cowboy/src/cowboy_stream_h.erl b/cowboy/src/cowboy_stream_h.erl new file mode 100644 index 0000000..842bd8d --- /dev/null +++ b/cowboy/src/cowboy_stream_h.erl @@ -0,0 +1,324 @@ +%% Copyright (c) 2016-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_stream_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-export([request_process/3]). +-export([resume/5]). + +-record(state, { + next :: any(), + ref = undefined :: ranch:ref(), + pid = undefined :: pid(), + expect = undefined :: undefined | continue, + read_body_pid = undefined :: pid() | undefined, + read_body_ref = undefined :: reference() | undefined, + read_body_timer_ref = undefined :: reference() | undefined, + read_body_length = 0 :: non_neg_integer() | infinity | auto, + read_body_is_fin = nofin :: nofin | {fin, non_neg_integer()}, + read_body_buffer = <<>> :: binary(), + body_length = 0 :: non_neg_integer(), + stream_body_pid = undefined :: pid() | undefined, + stream_body_status = normal :: normal | blocking | blocked +}). + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {[{spawn, pid(), timeout()}], #state{}}. +init(StreamID, Req=#{ref := Ref}, Opts) -> + Env = maps:get(env, Opts, #{}), + Middlewares = maps:get(middlewares, Opts, [cowboy_router, cowboy_handler]), + Shutdown = maps:get(shutdown_timeout, Opts, 5000), + Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env, Middlewares]), + Expect = expect(Req), + {Commands, Next} = cowboy_stream:init(StreamID, Req, Opts), + {[{spawn, Pid, Shutdown}|Commands], + #state{next=Next, ref=Ref, pid=Pid, expect=Expect}}. + +%% Ignore the expect header in HTTP/1.0. +expect(#{version := 'HTTP/1.0'}) -> + undefined; +expect(Req) -> + try cowboy_req:parse_header(<<"expect">>, Req) of + Expect -> + Expect + catch _:_ -> + undefined + end. + +%% If we receive data and stream is waiting for data: +%% If we accumulated enough data or IsFin=fin, send it. +%% If we are in auto mode, send it and update flow control. +%% If not, buffer it. +%% If not, buffer it. +%% +%% We always reset the expect field when we receive data, +%% since the client started sending the request body before +%% we could send a 100 continue response. + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +%% Stream isn't waiting for data. +data(StreamID, IsFin, Data, State=#state{ + read_body_ref=undefined, read_body_buffer=Buffer, body_length=BodyLen}) -> + do_data(StreamID, IsFin, Data, [], State#state{ + expect=undefined, + read_body_is_fin=IsFin, + read_body_buffer= << Buffer/binary, Data/binary >>, + body_length=BodyLen + byte_size(Data) + }); +%% Stream is waiting for data using auto mode. +%% +%% There is no buffering done in auto mode. +data(StreamID, IsFin, Data, State=#state{read_body_pid=Pid, read_body_ref=Ref, + read_body_length=auto, body_length=BodyLen}) -> + send_request_body(Pid, Ref, IsFin, BodyLen, Data), + do_data(StreamID, IsFin, Data, [{flow, byte_size(Data)}], State#state{ + read_body_ref=undefined, + %% @todo This is wrong, it's missing byte_size(Data). + body_length=BodyLen + }); +%% Stream is waiting for data but we didn't receive enough to send yet. +data(StreamID, IsFin=nofin, Data, State=#state{ + read_body_length=ReadLen, read_body_buffer=Buffer, body_length=BodyLen}) + when byte_size(Data) + byte_size(Buffer) < ReadLen -> + do_data(StreamID, IsFin, Data, [], State#state{ + expect=undefined, + read_body_buffer= << Buffer/binary, Data/binary >>, + body_length=BodyLen + byte_size(Data) + }); +%% Stream is waiting for data and we received enough to send. +data(StreamID, IsFin, Data, State=#state{read_body_pid=Pid, read_body_ref=Ref, + read_body_timer_ref=TRef, read_body_buffer=Buffer, body_length=BodyLen0}) -> + BodyLen = BodyLen0 + byte_size(Data), + ok = erlang:cancel_timer(TRef, [{async, true}, {info, false}]), + send_request_body(Pid, Ref, IsFin, BodyLen, <>), + do_data(StreamID, IsFin, Data, [], State#state{ + expect=undefined, + read_body_ref=undefined, + read_body_timer_ref=undefined, + read_body_buffer= <<>>, + body_length=BodyLen + }). + +do_data(StreamID, IsFin, Data, Commands1, State=#state{next=Next0}) -> + {Commands2, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), + {Commands1 ++ Commands2, State#state{next=Next}}. + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::#state{}. +info(StreamID, Info={'EXIT', Pid, normal}, State=#state{pid=Pid}) -> + do_info(StreamID, Info, [stop], State); +info(StreamID, Info={'EXIT', Pid, {{request_error, Reason, _HumanReadable}, _}}, + State=#state{pid=Pid}) -> + Status = case Reason of + timeout -> 408; + payload_too_large -> 413; + _ -> 400 + end, + %% @todo Headers? Details in body? Log the crash? More stuff in debug only? + do_info(StreamID, Info, [ + {error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>}, + stop + ], State); +info(StreamID, Exit={'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref, pid=Pid}) -> + Commands0 = [{internal_error, Exit, 'Stream process crashed.'}], + Commands = case Reason of + normal -> Commands0; + shutdown -> Commands0; + {shutdown, _} -> Commands0; + _ -> [{log, error, + "Ranch listener ~p, connection process ~p, stream ~p " + "had its request process ~p exit with reason " + "~999999p and stacktrace ~999999p~n", + [Ref, self(), StreamID, Pid, Reason, Stacktrace]} + |Commands0] + end, + do_info(StreamID, Exit, [ + {error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>} + |Commands], State); +%% Request body, auto mode, no body buffered. +info(StreamID, Info={read_body, Pid, Ref, auto, infinity}, State=#state{read_body_buffer= <<>>}) -> + do_info(StreamID, Info, [], State#state{ + read_body_pid=Pid, + read_body_ref=Ref, + read_body_length=auto + }); +%% Request body, auto mode, body buffered or complete. +info(StreamID, Info={read_body, Pid, Ref, auto, infinity}, State=#state{ + read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) -> + send_request_body(Pid, Ref, IsFin, BodyLen, Buffer), + do_info(StreamID, Info, [{flow, byte_size(Buffer)}], + State#state{read_body_buffer= <<>>}); +%% Request body, body buffered large enough or complete. +%% +%% We do not send a 100 continue response if the client +%% already started sending the body. +info(StreamID, Info={read_body, Pid, Ref, Length, _}, State=#state{ + read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) + when IsFin =:= fin; byte_size(Buffer) >= Length -> + send_request_body(Pid, Ref, IsFin, BodyLen, Buffer), + do_info(StreamID, Info, [], State#state{read_body_buffer= <<>>}); +%% Request body, not enough to send yet. +info(StreamID, Info={read_body, Pid, Ref, Length, Period}, State=#state{expect=Expect}) -> + Commands = case Expect of + continue -> [{inform, 100, #{}}, {flow, Length}]; + undefined -> [{flow, Length}] + end, + TRef = erlang:send_after(Period, self(), {{self(), StreamID}, {read_body_timeout, Ref}}), + do_info(StreamID, Info, Commands, State#state{ + read_body_pid=Pid, + read_body_ref=Ref, + read_body_timer_ref=TRef, + read_body_length=Length + }); +%% Request body reading timeout; send what we got. +info(StreamID, Info={read_body_timeout, Ref}, State=#state{read_body_pid=Pid, read_body_ref=Ref, + read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) -> + send_request_body(Pid, Ref, IsFin, BodyLen, Buffer), + do_info(StreamID, Info, [], State#state{ + read_body_ref=undefined, + read_body_timer_ref=undefined, + read_body_buffer= <<>> + }); +info(StreamID, Info={read_body_timeout, _}, State) -> + do_info(StreamID, Info, [], State); +%% Response. +%% +%% We reset the expect field when a 100 continue response +%% is sent or when any final response is sent. +info(StreamID, Inform={inform, Status, _}, State0) -> + State = case cow_http:status_to_integer(Status) of + 100 -> State0#state{expect=undefined}; + _ -> State0 + end, + do_info(StreamID, Inform, [Inform], State); +info(StreamID, Response={response, _, _, _}, State) -> + do_info(StreamID, Response, [Response], State#state{expect=undefined}); +info(StreamID, Headers={headers, _, _}, State) -> + do_info(StreamID, Headers, [Headers], State#state{expect=undefined}); +%% Sending data involves the data message, the stream_buffer_full alarm +%% and the connection_buffer_full alarm. We stop sending acks when an alarm is on. +%% +%% We only apply backpressure when the message includes a pid. Otherwise +%% it is a message from Cowboy, or the user circumventing the backpressure. +%% +%% We currently do not support sending data from multiple processes concurrently. +info(StreamID, Data={data, _, _}, State) -> + do_info(StreamID, Data, [Data], State); +info(StreamID, Data0={data, Pid, _, _}, State0=#state{stream_body_status=Status}) -> + State = case Status of + normal -> + Pid ! {data_ack, self()}, + State0; + blocking -> + State0#state{stream_body_pid=Pid, stream_body_status=blocked}; + blocked -> + State0 + end, + Data = erlang:delete_element(2, Data0), + do_info(StreamID, Data, [Data], State); +info(StreamID, Alarm={alarm, Name, on}, State0=#state{stream_body_status=Status}) + when Name =:= connection_buffer_full; Name =:= stream_buffer_full -> + State = case Status of + normal -> State0#state{stream_body_status=blocking}; + _ -> State0 + end, + do_info(StreamID, Alarm, [], State); +info(StreamID, Alarm={alarm, Name, off}, State=#state{stream_body_pid=Pid, stream_body_status=Status}) + when Name =:= connection_buffer_full; Name =:= stream_buffer_full -> + _ = case Status of + normal -> ok; + blocking -> ok; + blocked -> Pid ! {data_ack, self()} + end, + do_info(StreamID, Alarm, [], State#state{stream_body_pid=undefined, stream_body_status=normal}); +info(StreamID, Trailers={trailers, _}, State) -> + do_info(StreamID, Trailers, [Trailers], State); +info(StreamID, Push={push, _, _, _, _, _, _, _}, State) -> + do_info(StreamID, Push, [Push], State); +info(StreamID, SwitchProtocol={switch_protocol, _, _, _}, State) -> + do_info(StreamID, SwitchProtocol, [SwitchProtocol], State#state{expect=undefined}); +%% Convert the set_options message to a command. +info(StreamID, SetOptions={set_options, _}, State) -> + do_info(StreamID, SetOptions, [SetOptions], State); +%% Unknown message, either stray or meant for a handler down the line. +info(StreamID, Info, State) -> + do_info(StreamID, Info, [], State). + +do_info(StreamID, Info, Commands1, State0=#state{next=Next0}) -> + {Commands2, Next} = cowboy_stream:info(StreamID, Info, Next0), + {Commands1 ++ Commands2, State0#state{next=Next}}. + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> ok. +terminate(StreamID, Reason, #state{next=Next}) -> + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). +early_error(StreamID, Reason, PartialReq, Resp, Opts) -> + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). + +send_request_body(Pid, Ref, nofin, _, Data) -> + Pid ! {request_body, Ref, nofin, Data}, + ok; +send_request_body(Pid, Ref, fin, BodyLen, Data) -> + Pid ! {request_body, Ref, fin, BodyLen, Data}, + ok. + +%% Request process. + +%% We add the stacktrace to exit exceptions here in order +%% to simplify the debugging of errors. The proc_lib library +%% already adds the stacktrace to other types of exceptions. +-spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok. +request_process(Req, Env, Middlewares) -> + try + execute(Req, Env, Middlewares) + catch + exit:Reason={shutdown, _}:Stacktrace -> + erlang:raise(exit, Reason, Stacktrace); + exit:Reason:Stacktrace when Reason =/= normal, Reason =/= shutdown -> + erlang:raise(exit, {Reason, Stacktrace}, Stacktrace) + end. + +execute(_, _, []) -> + ok; +execute(Req, Env, [Middleware|Tail]) -> + case Middleware:execute(Req, Env) of + {ok, Req2, Env2} -> + execute(Req2, Env2, Tail); + {suspend, Module, Function, Args} -> + proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module, Function, Args]); + {stop, _Req2} -> + ok + end. + +-spec resume(cowboy_middleware:env(), [module()], module(), atom(), [any()]) -> ok. +resume(Env, Tail, Module, Function, Args) -> + case apply(Module, Function, Args) of + {ok, Req2, Env2} -> + execute(Req2, Env2, Tail); + {suspend, Module2, Function2, Args2} -> + proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module2, Function2, Args2]); + {stop, _Req2} -> + ok + end. diff --git a/cowboy/src/cowboy_sub_protocol.erl b/cowboy/src/cowboy_sub_protocol.erl new file mode 100644 index 0000000..062fd38 --- /dev/null +++ b/cowboy/src/cowboy_sub_protocol.erl @@ -0,0 +1,24 @@ +%% Copyright (c) 2013-2024, Loïc Hoguin +%% Copyright (c) 2013, James Fish +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_sub_protocol). + +-callback upgrade(Req, Env, module(), any()) + -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). + +-callback upgrade(Req, Env, module(), any(), any()) + -> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). diff --git a/cowboy/src/cowboy_sup.erl b/cowboy/src/cowboy_sup.erl new file mode 100644 index 0000000..e37f4cf --- /dev/null +++ b/cowboy/src/cowboy_sup.erl @@ -0,0 +1,30 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_sup). +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +-spec init([]) + -> {ok, {{supervisor:strategy(), 10, 10}, [supervisor:child_spec()]}}. +init([]) -> + Procs = [{cowboy_clock, {cowboy_clock, start_link, []}, + permanent, 5000, worker, [cowboy_clock]}], + {ok, {{one_for_one, 10, 10}, Procs}}. diff --git a/cowboy/src/cowboy_tls.erl b/cowboy/src/cowboy_tls.erl new file mode 100644 index 0000000..60ab2ed --- /dev/null +++ b/cowboy/src/cowboy_tls.erl @@ -0,0 +1,58 @@ +%% Copyright (c) 2015-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_tls). +-behavior(ranch_protocol). + +-export([start_link/3]). +-export([start_link/4]). +-export([connection_process/4]). + +%% Ranch 1. +-spec start_link(ranch:ref(), ssl:sslsocket(), module(), cowboy:opts()) -> {ok, pid()}. +start_link(Ref, _Socket, Transport, Opts) -> + start_link(Ref, Transport, Opts). + +%% Ranch 2. +-spec start_link(ranch:ref(), module(), cowboy:opts()) -> {ok, pid()}. +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, connection_process, + [self(), Ref, Transport, Opts]), + {ok, Pid}. + +-spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok. +connection_process(Parent, Ref, Transport, Opts) -> + ProxyInfo = get_proxy_info(Ref, Opts), + {ok, Socket} = ranch:handshake(Ref), + case ssl:negotiated_protocol(Socket) of + {ok, <<"h2">>} -> + init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http2); + _ -> %% http/1.1 or no protocol negotiated. + init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http) + end. + +init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) -> + _ = case maps:get(connection_type, Opts, supervisor) of + worker -> ok; + supervisor -> process_flag(trap_exit, true) + end, + Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts). + +get_proxy_info(Ref, #{proxy_header := true}) -> + case ranch:recv_proxy_header(Ref, 1000) of + {ok, ProxyInfo} -> ProxyInfo; + {error, closed} -> exit({shutdown, closed}) + end; +get_proxy_info(_, _) -> + undefined. diff --git a/cowboy/src/cowboy_tracer_h.erl b/cowboy/src/cowboy_tracer_h.erl new file mode 100644 index 0000000..b1196fe --- /dev/null +++ b/cowboy/src/cowboy_tracer_h.erl @@ -0,0 +1,192 @@ +%% Copyright (c) 2017-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(cowboy_tracer_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-export([set_trace_patterns/0]). + +-export([tracer_process/3]). +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +-type match_predicate() + :: fun((cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> boolean()). + +-type tracer_match_specs() :: [match_predicate() + | {method, binary()} + | {host, binary()} + | {path, binary()} + | {path_start, binary()} + | {header, binary()} + | {header, binary(), binary()} + | {peer_ip, inet:ip_address()} +]. +-export_type([tracer_match_specs/0]). + +-type tracer_callback() :: fun((init | terminate | tuple(), any()) -> any()). +-export_type([tracer_callback/0]). + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {cowboy_stream:commands(), any()}. +init(StreamID, Req, Opts) -> + init_tracer(StreamID, Req, Opts), + cowboy_stream:init(StreamID, Req, Opts). + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::any(). +data(StreamID, IsFin, Data, Next) -> + cowboy_stream:data(StreamID, IsFin, Data, Next). + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::any(). +info(StreamID, Info, Next) -> + cowboy_stream:info(StreamID, Info, Next). + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), any()) -> any(). +terminate(StreamID, Reason, Next) -> + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). +early_error(StreamID, Reason, PartialReq, Resp, Opts) -> + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts). + +%% API. + +%% These trace patterns are most likely not suitable for production. +-spec set_trace_patterns() -> ok. +set_trace_patterns() -> + erlang:trace_pattern({'_', '_', '_'}, [{'_', [], [{return_trace}]}], [local]), + erlang:trace_pattern(on_load, [{'_', [], [{return_trace}]}], [local]), + ok. + +%% Internal. + +init_tracer(StreamID, Req, Opts=#{tracer_match_specs := List, tracer_callback := _}) -> + case match(List, StreamID, Req, Opts) of + false -> + ok; + true -> + start_tracer(StreamID, Req, Opts) + end; +%% When the options tracer_match_specs or tracer_callback +%% are not provided we do not enable tracing. +init_tracer(_, _, _) -> + ok. + +match([], _, _, _) -> + true; +match([Predicate|Tail], StreamID, Req, Opts) when is_function(Predicate) -> + case Predicate(StreamID, Req, Opts) of + true -> match(Tail, StreamID, Req, Opts); + false -> false + end; +match([{method, Value}|Tail], StreamID, Req=#{method := Value}, Opts) -> + match(Tail, StreamID, Req, Opts); +match([{host, Value}|Tail], StreamID, Req=#{host := Value}, Opts) -> + match(Tail, StreamID, Req, Opts); +match([{path, Value}|Tail], StreamID, Req=#{path := Value}, Opts) -> + match(Tail, StreamID, Req, Opts); +match([{path_start, PathStart}|Tail], StreamID, Req=#{path := Path}, Opts) -> + Len = byte_size(PathStart), + case Path of + <> -> match(Tail, StreamID, Req, Opts); + _ -> false + end; +match([{header, Name}|Tail], StreamID, Req=#{headers := Headers}, Opts) -> + case Headers of + #{Name := _} -> match(Tail, StreamID, Req, Opts); + _ -> false + end; +match([{header, Name, Value}|Tail], StreamID, Req=#{headers := Headers}, Opts) -> + case Headers of + #{Name := Value} -> match(Tail, StreamID, Req, Opts); + _ -> false + end; +match([{peer_ip, IP}|Tail], StreamID, Req=#{peer := {IP, _}}, Opts) -> + match(Tail, StreamID, Req, Opts); +match(_, _, _, _) -> + false. + +%% We only start the tracer if one wasn't started before. +start_tracer(StreamID, Req, Opts) -> + case erlang:trace_info(self(), tracer) of + {tracer, []} -> + TracerPid = proc_lib:spawn_link(?MODULE, tracer_process, [StreamID, Req, Opts]), + %% The default flags are probably not suitable for production. + Flags = maps:get(tracer_flags, Opts, [ + send, 'receive', call, return_to, + procs, ports, monotonic_timestamp, + %% The set_on_spawn flag is necessary to catch events + %% from request processes. + set_on_spawn + ]), + erlang:trace(self(), true, [{tracer, TracerPid}|Flags]), + ok; + _ -> + ok + end. + +%% Tracer process. + +-spec tracer_process(_, _, _) -> no_return(). +tracer_process(StreamID, Req=#{pid := Parent}, Opts=#{tracer_callback := Fun}) -> + %% This is necessary because otherwise the tracer could stop + %% before it has finished processing the events in its queue. + process_flag(trap_exit, true), + State = Fun(init, {StreamID, Req, Opts}), + tracer_loop(Parent, Opts, State). + +tracer_loop(Parent, Opts=#{tracer_callback := Fun}, State0) -> + receive + Msg when element(1, Msg) =:= trace; element(1, Msg) =:= trace_ts -> + State = Fun(Msg, State0), + tracer_loop(Parent, Opts, State); + {'EXIT', Parent, Reason} -> + tracer_terminate(Reason, Opts, State0); + {system, From, Request} -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Opts, State0}); + Msg -> + cowboy:log(warning, "~p: Tracer process received stray message ~9999p~n", + [?MODULE, Msg], Opts), + tracer_loop(Parent, Opts, State0) + end. + +-spec tracer_terminate(_, _, _) -> no_return(). +tracer_terminate(Reason, #{tracer_callback := Fun}, State) -> + _ = Fun(terminate, State), + exit(Reason). + +%% System callbacks. + +-spec system_continue(pid(), _, {cowboy:opts(), any()}) -> no_return(). +system_continue(Parent, _, {Opts, State}) -> + tracer_loop(Parent, Opts, State). + +-spec system_terminate(any(), _, _, _) -> no_return(). +system_terminate(Reason, _, _, {Opts, State}) -> + tracer_terminate(Reason, Opts, State). + +-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::any(). +system_code_change(Misc, _, _, _) -> + {ok, Misc}. diff --git a/cowboy/src/cowboy_websocket.erl b/cowboy/src/cowboy_websocket.erl new file mode 100644 index 0000000..5b98b43 --- /dev/null +++ b/cowboy/src/cowboy_websocket.erl @@ -0,0 +1,707 @@ +%% Copyright (c) 2011-2024, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Cowboy supports versions 7 through 17 of the Websocket drafts. +%% It also supports RFC6455, the proposed standard for Websocket. +-module(cowboy_websocket). +-behaviour(cowboy_sub_protocol). + +-export([is_upgrade_request/1]). +-export([upgrade/4]). +-export([upgrade/5]). +-export([takeover/7]). +-export([loop/3]). + +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +-type commands() :: [cow_ws:frame() + | {active, boolean()} + | {deflate, boolean()} + | {set_options, map()} + | {shutdown_reason, any()} +]. +-export_type([commands/0]). + +-type call_result(State) :: {commands(), State} | {commands(), State, hibernate}. + +-type deprecated_call_result(State) :: {ok, State} + | {ok, State, hibernate} + | {reply, cow_ws:frame() | [cow_ws:frame()], State} + | {reply, cow_ws:frame() | [cow_ws:frame()], State, hibernate} + | {stop, State}. + +-type terminate_reason() :: normal | stop | timeout + | remote | {remote, cow_ws:close_code(), binary()} + | {error, badencoding | badframe | closed | atom()} + | {crash, error | exit | throw, any()}. + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), any()} + when Req::cowboy_req:req(). + +-callback websocket_init(State) + -> call_result(State) | deprecated_call_result(State) when State::any(). +-optional_callbacks([websocket_init/1]). + +-callback websocket_handle(ping | pong | {text | binary | ping | pong, binary()}, State) + -> call_result(State) | deprecated_call_result(State) when State::any(). +-callback websocket_info(any(), State) + -> call_result(State) | deprecated_call_result(State) when State::any(). + +-callback terminate(any(), cowboy_req:req(), any()) -> ok. +-optional_callbacks([terminate/3]). + +-type opts() :: #{ + active_n => pos_integer(), + compress => boolean(), + deflate_opts => cow_ws:deflate_opts(), + idle_timeout => timeout(), + max_frame_size => non_neg_integer() | infinity, + req_filter => fun((cowboy_req:req()) -> map()), + validate_utf8 => boolean() +}. +-export_type([opts/0]). + +-record(state, { + parent :: undefined | pid(), + ref :: ranch:ref(), + socket = undefined :: inet:socket() | {pid(), cowboy_stream:streamid()} | undefined, + transport = undefined :: module() | undefined, + opts = #{} :: opts(), + active = true :: boolean(), + handler :: module(), + key = undefined :: undefined | binary(), + timeout_ref = undefined :: undefined | reference(), + messages = undefined :: undefined | {atom(), atom(), atom()} + | {atom(), atom(), atom(), atom()}, + hibernate = false :: boolean(), + frag_state = undefined :: cow_ws:frag_state(), + frag_buffer = <<>> :: binary(), + utf8_state :: cow_ws:utf8_state(), + deflate = true :: boolean(), + extensions = #{} :: map(), + req = #{} :: map(), + shutdown_reason = normal :: any() +}). + +%% Because the HTTP/1.1 and HTTP/2 handshakes are so different, +%% this function is necessary to figure out whether a request +%% is trying to upgrade to the Websocket protocol. + +-spec is_upgrade_request(cowboy_req:req()) -> boolean(). +is_upgrade_request(#{version := 'HTTP/2', method := <<"CONNECT">>, protocol := Protocol}) -> + <<"websocket">> =:= cowboy_bstr:to_lower(Protocol); +is_upgrade_request(Req=#{version := 'HTTP/1.1', method := <<"GET">>}) -> + ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []), + case lists:member(<<"upgrade">>, ConnTokens) of + false -> + false; + true -> + UpgradeTokens = cowboy_req:parse_header(<<"upgrade">>, Req), + lists:member(<<"websocket">>, UpgradeTokens) + end; +is_upgrade_request(_) -> + false. + +%% Stream process. + +-spec upgrade(Req, Env, module(), any()) + -> {ok, Req, Env} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +upgrade(Req, Env, Handler, HandlerState) -> + upgrade(Req, Env, Handler, HandlerState, #{}). + +-spec upgrade(Req, Env, module(), any(), opts()) + -> {ok, Req, Env} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +%% @todo Immediately crash if a response has already been sent. +upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) -> + FilteredReq = case maps:get(req_filter, Opts, undefined) of + undefined -> maps:with([method, version, scheme, host, port, path, qs, peer], Req0); + FilterFun -> FilterFun(Req0) + end, + Utf8State = case maps:get(validate_utf8, Opts, true) of + true -> 0; + false -> undefined + end, + State0 = #state{opts=Opts, handler=Handler, utf8_state=Utf8State, req=FilteredReq}, + try websocket_upgrade(State0, Req0) of + {ok, State, Req} -> + websocket_handshake(State, Req, HandlerState, Env); + %% The status code 426 is specific to HTTP/1.1 connections. + {error, upgrade_required} when Version =:= 'HTTP/1.1' -> + {ok, cowboy_req:reply(426, #{ + <<"connection">> => <<"upgrade">>, + <<"upgrade">> => <<"websocket">> + }, Req0), Env}; + %% Use a generic 400 error for HTTP/2. + {error, upgrade_required} -> + {ok, cowboy_req:reply(400, Req0), Env} + catch _:_ -> + %% @todo Probably log something here? + %% @todo Test that we can have 2 /ws 400 status code in a row on the same connection. + %% @todo Does this even work? + {ok, cowboy_req:reply(400, Req0), Env} + end. + +websocket_upgrade(State, Req=#{version := Version}) -> + case is_upgrade_request(Req) of + false -> + {error, upgrade_required}; + true when Version =:= 'HTTP/1.1' -> + Key = cowboy_req:header(<<"sec-websocket-key">>, Req), + false = Key =:= undefined, + websocket_version(State#state{key=Key}, Req); + true -> + websocket_version(State, Req) + end. + +websocket_version(State, Req) -> + WsVersion = cowboy_req:parse_header(<<"sec-websocket-version">>, Req), + case WsVersion of + 7 -> ok; + 8 -> ok; + 13 -> ok + end, + websocket_extensions(State, Req#{websocket_version => WsVersion}). + +websocket_extensions(State=#state{opts=Opts}, Req) -> + %% @todo We want different options for this. For example + %% * compress everything auto + %% * compress only text auto + %% * compress only binary auto + %% * compress nothing auto (but still enabled it) + %% * disable compression + Compress = maps:get(compress, Opts, false), + case {Compress, cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req)} of + {true, Extensions} when Extensions =/= undefined -> + websocket_extensions(State, Req, Extensions, []); + _ -> + {ok, State, Req} + end. + +websocket_extensions(State, Req, [], []) -> + {ok, State, Req}; +websocket_extensions(State, Req, [], [<<", ">>|RespHeader]) -> + {ok, State, cowboy_req:set_resp_header(<<"sec-websocket-extensions">>, lists:reverse(RespHeader), Req)}; +%% For HTTP/2 we ARE on the controlling process and do NOT want to update the owner. +websocket_extensions(State=#state{opts=Opts, extensions=Extensions}, + Req=#{pid := Pid, version := Version}, + [{<<"permessage-deflate">>, Params}|Tail], RespHeader) -> + DeflateOpts0 = maps:get(deflate_opts, Opts, #{}), + DeflateOpts = case Version of + 'HTTP/1.1' -> DeflateOpts0#{owner => Pid}; + _ -> DeflateOpts0 + end, + try cow_ws:negotiate_permessage_deflate(Params, Extensions, DeflateOpts) of + {ok, RespExt, Extensions2} -> + websocket_extensions(State#state{extensions=Extensions2}, + Req, Tail, [<<", ">>, RespExt|RespHeader]); + ignore -> + websocket_extensions(State, Req, Tail, RespHeader) + catch exit:{error, incompatible_zlib_version, _} -> + websocket_extensions(State, Req, Tail, RespHeader) + end; +websocket_extensions(State=#state{opts=Opts, extensions=Extensions}, + Req=#{pid := Pid, version := Version}, + [{<<"x-webkit-deflate-frame">>, Params}|Tail], RespHeader) -> + DeflateOpts0 = maps:get(deflate_opts, Opts, #{}), + DeflateOpts = case Version of + 'HTTP/1.1' -> DeflateOpts0#{owner => Pid}; + _ -> DeflateOpts0 + end, + try cow_ws:negotiate_x_webkit_deflate_frame(Params, Extensions, DeflateOpts) of + {ok, RespExt, Extensions2} -> + websocket_extensions(State#state{extensions=Extensions2}, + Req, Tail, [<<", ">>, RespExt|RespHeader]); + ignore -> + websocket_extensions(State, Req, Tail, RespHeader) + catch exit:{error, incompatible_zlib_version, _} -> + websocket_extensions(State, Req, Tail, RespHeader) + end; +websocket_extensions(State, Req, [_|Tail], RespHeader) -> + websocket_extensions(State, Req, Tail, RespHeader). + +-spec websocket_handshake(#state{}, Req, any(), Env) + -> {ok, Req, Env} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +websocket_handshake(State=#state{key=Key}, + Req=#{version := 'HTTP/1.1', pid := Pid, streamid := StreamID}, + HandlerState, Env) -> + Challenge = base64:encode(crypto:hash(sha, + << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)), + %% @todo We don't want date and server headers. + Headers = cowboy_req:response_headers(#{ + <<"connection">> => <<"Upgrade">>, + <<"upgrade">> => <<"websocket">>, + <<"sec-websocket-accept">> => Challenge + }, Req), + Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, {State, HandlerState}}}, + {ok, Req, Env}; +%% For HTTP/2 we do not let the process die, we instead keep it +%% for the Websocket stream. This is because in HTTP/2 we only +%% have a stream, it doesn't take over the whole connection. +websocket_handshake(State, Req=#{ref := Ref, pid := Pid, streamid := StreamID}, + HandlerState, _Env) -> + %% @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, <<>>, + {State, HandlerState}). + +%% Connection process. + +-record(ps_header, { + buffer = <<>> :: binary() +}). + +-record(ps_payload, { + type :: cow_ws:frame_type(), + len :: non_neg_integer(), + mask_key :: cow_ws:mask_key(), + rsv :: cow_ws:rsv(), + close_code = undefined :: undefined | cow_ws:close_code(), + unmasked = <<>> :: binary(), + unmasked_len = 0 :: non_neg_integer(), + buffer = <<>> :: binary() +}). + +-type parse_state() :: #ps_header{} | #ps_payload{}. + +-spec takeover(pid(), ranch:ref(), inet:socket() | {pid(), cowboy_stream:streamid()}, + module() | undefined, any(), binary(), + {#state{}, any()}) -> no_return(). +takeover(Parent, Ref, Socket, Transport, _Opts, Buffer, + {State0=#state{handler=Handler}, HandlerState}) -> + %% @todo We should have an option to disable this behavior. + ranch:remove_connection(Ref), + Messages = case Transport of + undefined -> undefined; + _ -> Transport:messages() + end, + State = loop_timeout(State0#state{parent=Parent, + ref=Ref, socket=Socket, transport=Transport, + key=undefined, messages=Messages}), + %% We call parse_header/3 immediately because there might be + %% some data in the buffer that was sent along with the handshake. + %% While it is not allowed by the protocol to send frames immediately, + %% we still want to process that data if any. + case erlang:function_exported(Handler, websocket_init, 1) of + true -> handler_call(State, HandlerState, #ps_header{buffer=Buffer}, + websocket_init, undefined, fun after_init/3); + false -> after_init(State, HandlerState, #ps_header{buffer=Buffer}) + end. + +after_init(State=#state{active=true}, HandlerState, ParseState) -> + %% Enable active,N for HTTP/1.1, and auto read_body for HTTP/2. + %% We must do this only after calling websocket_init/1 (if any) + %% to give the handler a chance to disable active mode immediately. + setopts_active(State), + maybe_read_body(State), + parse_header(State, HandlerState, ParseState); +after_init(State, HandlerState, ParseState) -> + parse_header(State, HandlerState, ParseState). + +%% We have two ways of reading the body for Websocket. For HTTP/1.1 +%% we have full control of the socket and can therefore use active,N. +%% For HTTP/2 we are just a stream, and are instead using read_body +%% (automatic mode). Technically HTTP/2 will only go passive after +%% receiving the next data message, while HTTP/1.1 goes passive +%% immediately but there might still be data to be processed in +%% the message queue. + +setopts_active(#state{transport=undefined}) -> + ok; +setopts_active(#state{socket=Socket, transport=Transport, opts=Opts}) -> + N = maps:get(active_n, Opts, 100), + Transport:setopts(Socket, [{active, N}]). + +maybe_read_body(#state{socket=Stream={Pid, _}, transport=undefined, active=true}) -> + %% @todo Keep Ref around. + ReadBodyRef = make_ref(), + Pid ! {Stream, {read_body, self(), ReadBodyRef, auto, infinity}}, + ok; +maybe_read_body(_) -> + ok. + +active(State) -> + setopts_active(State), + maybe_read_body(State), + State#state{active=true}. + +passive(State=#state{transport=undefined}) -> + %% Unfortunately we cannot currently cancel read_body. + %% But that's OK, we will just stop reading the body + %% after the next message. + State#state{active=false}; +passive(State=#state{socket=Socket, transport=Transport, messages=Messages}) -> + Transport:setopts(Socket, [{active, false}]), + flush_passive(Socket, Messages), + State#state{active=false}. + +flush_passive(Socket, Messages) -> + receive + {Passive, Socket} when Passive =:= element(4, Messages); + %% Hardcoded for compatibility with Ranch 1.x. + Passive =:= tcp_passive; Passive =:= ssl_passive -> + flush_passive(Socket, Messages) + after 0 -> + ok + end. + +before_loop(State=#state{hibernate=true}, HandlerState, ParseState) -> + proc_lib:hibernate(?MODULE, loop, + [State#state{hibernate=false}, HandlerState, ParseState]); +before_loop(State, HandlerState, ParseState) -> + loop(State, HandlerState, ParseState). + +-spec loop_timeout(#state{}) -> #state{}. +loop_timeout(State=#state{opts=Opts, timeout_ref=PrevRef}) -> + _ = case PrevRef of + undefined -> ignore; + PrevRef -> erlang:cancel_timer(PrevRef) + end, + case maps:get(idle_timeout, Opts, 60000) of + infinity -> + State#state{timeout_ref=undefined}; + Timeout -> + TRef = erlang:start_timer(Timeout, self(), ?MODULE), + State#state{timeout_ref=TRef} + end. + +-spec loop(#state{}, any(), parse_state()) -> no_return(). +loop(State=#state{parent=Parent, socket=Socket, messages=Messages, + timeout_ref=TRef}, HandlerState, ParseState) -> + receive + %% Socket messages. (HTTP/1.1) + {OK, Socket, Data} when OK =:= element(1, Messages) -> + State2 = loop_timeout(State), + parse(State2, HandlerState, ParseState, Data); + {Closed, Socket} when Closed =:= element(2, Messages) -> + terminate(State, HandlerState, {error, closed}); + {Error, Socket, Reason} when Error =:= element(3, Messages) -> + terminate(State, HandlerState, {error, Reason}); + {Passive, Socket} when Passive =:= element(4, Messages); + %% Hardcoded for compatibility with Ranch 1.x. + Passive =:= tcp_passive; Passive =:= ssl_passive -> + setopts_active(State), + loop(State, HandlerState, ParseState); + %% Body reading messages. (HTTP/2) + {request_body, _Ref, nofin, Data} -> + maybe_read_body(State), + State2 = loop_timeout(State), + parse(State2, HandlerState, ParseState, Data); + %% @todo We need to handle this case as if it was an {error, closed} + %% but not before we finish processing frames. We probably should have + %% a check in before_loop to let us stop looping if a flag is set. + {request_body, _Ref, fin, _, Data} -> + maybe_read_body(State), + State2 = loop_timeout(State), + parse(State2, HandlerState, ParseState, Data); + %% Timeouts. + {timeout, TRef, ?MODULE} -> + websocket_close(State, HandlerState, timeout); + {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, ParseState}); + %% Calls from supervisor module. + {'$gen_call', From, Call} -> + cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE), + before_loop(State, HandlerState, ParseState); + Message -> + handler_call(State, HandlerState, ParseState, + websocket_info, Message, fun before_loop/3) + end. + +parse(State, HandlerState, PS=#ps_header{buffer=Buffer}, Data) -> + parse_header(State, HandlerState, PS#ps_header{ + buffer= <>}); +parse(State, HandlerState, PS=#ps_payload{buffer=Buffer}, Data) -> + parse_payload(State, HandlerState, PS#ps_payload{buffer= <<>>}, + <>). + +parse_header(State=#state{opts=Opts, frag_state=FragState, extensions=Extensions}, + HandlerState, ParseState=#ps_header{buffer=Data}) -> + MaxFrameSize = maps:get(max_frame_size, Opts, infinity), + case cow_ws:parse_header(Data, Extensions, FragState) of + %% All frames sent from the client to the server are masked. + {_, _, _, _, undefined, _} -> + websocket_close(State, HandlerState, {error, badframe}); + {_, _, _, Len, _, _} when Len > MaxFrameSize -> + websocket_close(State, HandlerState, {error, badsize}); + {Type, FragState2, Rsv, Len, MaskKey, Rest} -> + parse_payload(State#state{frag_state=FragState2}, HandlerState, + #ps_payload{type=Type, len=Len, mask_key=MaskKey, rsv=Rsv}, Rest); + more -> + before_loop(State, HandlerState, ParseState); + error -> + websocket_close(State, HandlerState, {error, badframe}) + end. + +parse_payload(State=#state{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) -> + case cow_ws:parse_payload(Data, MaskKey, Incomplete, UnmaskedLen, + Type, Len, FragState, Extensions, Rsv) of + {ok, CloseCode, Payload, Utf8State, Rest} -> + dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState, + ParseState#ps_payload{unmasked= <>, + close_code=CloseCode}, Rest); + {ok, Payload, Utf8State, Rest} -> + dispatch_frame(State#state{utf8_state=Utf8State}, HandlerState, + ParseState#ps_payload{unmasked= <>}, + Rest); + {more, CloseCode, Payload, Utf8State} -> + before_loop(State#state{utf8_state=Utf8State}, HandlerState, + ParseState#ps_payload{len=Len - byte_size(Data), close_code=CloseCode, + unmasked= <>, + unmasked_len=UnmaskedLen + byte_size(Data)}); + {more, Payload, Utf8State} -> + before_loop(State#state{utf8_state=Utf8State}, HandlerState, + ParseState#ps_payload{len=Len - byte_size(Data), + unmasked= <>, + unmasked_len=UnmaskedLen + byte_size(Data)}); + Error = {error, _Reason} -> + websocket_close(State, HandlerState, Error) + end. + +dispatch_frame(State=#state{opts=Opts, frag_state=FragState, frag_buffer=SoFar}, HandlerState, + #ps_payload{type=Type0, unmasked=Payload0, close_code=CloseCode0}, RemainingData) -> + MaxFrameSize = maps:get(max_frame_size, Opts, infinity), + case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of + %% @todo Allow receiving fragments. + {fragment, _, _, Payload} when byte_size(Payload) + byte_size(SoFar) > MaxFrameSize -> + websocket_close(State, HandlerState, {error, badsize}); + {fragment, nofin, _, Payload} -> + parse_header(State#state{frag_buffer= << SoFar/binary, Payload/binary >>}, + HandlerState, #ps_header{buffer=RemainingData}); + {fragment, fin, Type, Payload} -> + handler_call(State#state{frag_state=undefined, frag_buffer= <<>>}, HandlerState, + #ps_header{buffer=RemainingData}, + websocket_handle, {Type, << SoFar/binary, Payload/binary >>}, + fun parse_header/3); + close -> + websocket_close(State, HandlerState, remote); + {close, CloseCode, Payload} -> + websocket_close(State, HandlerState, {remote, CloseCode, Payload}); + Frame = ping -> + transport_send(State, nofin, frame(pong, State)), + handler_call(State, HandlerState, + #ps_header{buffer=RemainingData}, + websocket_handle, Frame, fun parse_header/3); + Frame = {ping, Payload} -> + transport_send(State, nofin, frame({pong, Payload}, State)), + handler_call(State, HandlerState, + #ps_header{buffer=RemainingData}, + websocket_handle, Frame, fun parse_header/3); + Frame -> + handler_call(State, HandlerState, + #ps_header{buffer=RemainingData}, + websocket_handle, Frame, fun parse_header/3) + end. + +handler_call(State=#state{handler=Handler}, HandlerState, + ParseState, Callback, Message, NextState) -> + try case Callback of + websocket_init -> Handler:websocket_init(HandlerState); + _ -> Handler:Callback(Message, HandlerState) + end of + {Commands, HandlerState2} when is_list(Commands) -> + handler_call_result(State, + HandlerState2, ParseState, NextState, Commands); + {Commands, HandlerState2, hibernate} when is_list(Commands) -> + handler_call_result(State#state{hibernate=true}, + HandlerState2, ParseState, NextState, Commands); + %% The following call results are deprecated. + {ok, HandlerState2} -> + NextState(State, HandlerState2, ParseState); + {ok, HandlerState2, hibernate} -> + NextState(State#state{hibernate=true}, HandlerState2, ParseState); + {reply, Payload, HandlerState2} -> + case websocket_send(Payload, State) of + ok -> + NextState(State, HandlerState2, ParseState); + stop -> + terminate(State, HandlerState2, stop); + Error = {error, _} -> + terminate(State, HandlerState2, Error) + end; + {reply, Payload, HandlerState2, hibernate} -> + case websocket_send(Payload, State) of + ok -> + NextState(State#state{hibernate=true}, + HandlerState2, ParseState); + stop -> + terminate(State, HandlerState2, stop); + Error = {error, _} -> + terminate(State, HandlerState2, Error) + end; + {stop, HandlerState2} -> + websocket_close(State, HandlerState2, stop) + catch Class:Reason:Stacktrace -> + websocket_send_close(State, {crash, Class, Reason}), + handler_terminate(State, HandlerState, {crash, Class, Reason}), + erlang:raise(Class, Reason, Stacktrace) + end. + +-spec handler_call_result(#state{}, any(), parse_state(), fun(), commands()) -> no_return(). +handler_call_result(State0, HandlerState, ParseState, NextState, Commands) -> + case commands(Commands, State0, []) of + {ok, State} -> + NextState(State, HandlerState, ParseState); + {stop, State} -> + terminate(State, HandlerState, stop); + {Error = {error, _}, State} -> + terminate(State, HandlerState, Error) + end. + +commands([], State, []) -> + {ok, State}; +commands([], State, Data) -> + Result = transport_send(State, nofin, lists:reverse(Data)), + {Result, State}; +commands([{active, Active}|Tail], State0=#state{active=Active0}, Data) when is_boolean(Active) -> + State = if + Active, not Active0 -> + active(State0); + Active0, not Active -> + passive(State0); + true -> + State0 + end, + commands(Tail, State#state{active=Active}, Data); +commands([{deflate, Deflate}|Tail], State, Data) when is_boolean(Deflate) -> + commands(Tail, State#state{deflate=Deflate}, Data); +commands([{set_options, SetOpts}|Tail], State0=#state{opts=Opts}, Data) -> + State = case SetOpts of + #{idle_timeout := IdleTimeout} -> + loop_timeout(State0#state{opts=Opts#{idle_timeout => IdleTimeout}}); + _ -> + State0 + end, + commands(Tail, State, Data); +commands([{shutdown_reason, ShutdownReason}|Tail], State, Data) -> + commands(Tail, State#state{shutdown_reason=ShutdownReason}, Data); +commands([Frame|Tail], State, Data0) -> + Data = [frame(Frame, State)|Data0], + case is_close_frame(Frame) of + true -> + _ = transport_send(State, fin, lists:reverse(Data)), + {stop, State}; + false -> + commands(Tail, State, Data) + end. + +transport_send(#state{socket=Stream={Pid, _}, transport=undefined}, IsFin, Data) -> + Pid ! {Stream, {data, IsFin, Data}}, + ok; +transport_send(#state{socket=Socket, transport=Transport}, _, Data) -> + Transport:send(Socket, Data). + +-spec websocket_send(cow_ws:frame(), #state{}) -> ok | stop | {error, atom()}. +websocket_send(Frames, State) when is_list(Frames) -> + websocket_send_many(Frames, State, []); +websocket_send(Frame, State) -> + Data = frame(Frame, State), + case is_close_frame(Frame) of + true -> + _ = transport_send(State, fin, Data), + stop; + false -> + transport_send(State, nofin, Data) + end. + +websocket_send_many([], State, Acc) -> + transport_send(State, nofin, lists:reverse(Acc)); +websocket_send_many([Frame|Tail], State, Acc0) -> + Acc = [frame(Frame, State)|Acc0], + case is_close_frame(Frame) of + true -> + _ = transport_send(State, fin, lists:reverse(Acc)), + stop; + false -> + websocket_send_many(Tail, State, Acc) + end. + +is_close_frame(close) -> true; +is_close_frame({close, _}) -> true; +is_close_frame({close, _, _}) -> true; +is_close_frame(_) -> false. + +-spec websocket_close(#state{}, any(), terminate_reason()) -> no_return(). +websocket_close(State, HandlerState, Reason) -> + websocket_send_close(State, Reason), + terminate(State, HandlerState, Reason). + +websocket_send_close(State, Reason) -> + _ = case Reason of + Normal when Normal =:= stop; Normal =:= timeout -> + transport_send(State, fin, frame({close, 1000, <<>>}, State)); + {error, badframe} -> + transport_send(State, fin, frame({close, 1002, <<>>}, State)); + {error, badencoding} -> + transport_send(State, fin, frame({close, 1007, <<>>}, State)); + {error, badsize} -> + transport_send(State, fin, frame({close, 1009, <<>>}, State)); + {crash, _, _} -> + transport_send(State, fin, frame({close, 1011, <<>>}, State)); + remote -> + transport_send(State, fin, frame(close, State)); + {remote, Code, _} -> + transport_send(State, fin, frame({close, Code, <<>>}, State)) + end, + ok. + +%% Don't compress frames while deflate is disabled. +frame(Frame, #state{deflate=false, extensions=Extensions}) -> + cow_ws:frame(Frame, Extensions#{deflate => false}); +frame(Frame, #state{extensions=Extensions}) -> + cow_ws:frame(Frame, Extensions). + +-spec terminate(#state{}, any(), terminate_reason()) -> no_return(). +terminate(State=#state{shutdown_reason=Shutdown}, HandlerState, Reason) -> + handler_terminate(State, HandlerState, Reason), + case Shutdown of + normal -> exit(normal); + _ -> exit({shutdown, Shutdown}) + end. + +handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) -> + cowboy_handler:terminate(Reason, Req, HandlerState, Handler). + +%% System callbacks. + +-spec system_continue(_, _, {#state{}, any(), parse_state()}) -> no_return(). +system_continue(_, _, {State, HandlerState, ParseState}) -> + loop(State, HandlerState, ParseState). + +-spec system_terminate(any(), _, _, {#state{}, any(), parse_state()}) -> no_return(). +system_terminate(Reason, _, _, {State, HandlerState, _}) -> + %% @todo We should exit gracefully, if possible. + terminate(State, HandlerState, Reason). + +-spec system_code_change(Misc, _, _, _) + -> {ok, Misc} when Misc::{#state{}, any(), parse_state()}. +system_code_change(Misc, _, _, _) -> + {ok, Misc}.