123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899 |
- %% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
- %% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
- %%
- %% Permission to use, copy, modify, and/or distribute this software for any
- %% 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([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]).
- %% @todo read_and_match_urlencoded_body?
- %% 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_event/2,3
- -export([stream_trailers/2]).
- -export([push/3]).
- -export([push/4]).
- %% Internal.
- -export([response_headers/2]).
- %% @todo Get rid of this type, use cow_cookie directly.
- -type cookie_opts() :: map().
- -export_type([cookie_opts/0]).
- -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.
- %% @todo We should probably explicitly reject it.
- -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 => binary(),
- qs => binary()
- }.
- -export_type([push_opts/0]).
- -type req() :: map(). %% @todo
- % ref := ranch:ref(),
- % pid := pid(),
- % streamid := cowboy_stream:streamid(),
- % peer := {inet:ip_address(), inet:port_number()},
- %
- % method := binary(), %% case sensitive
- % version := cowboy:http_version() | atom(),
- % scheme := binary(), %% <<"http">> or <<"https">>
- % host := binary(), %% lowercase; case insensitive
- % port := inet:port_number(),
- % path := binary(), %% case sensitive
- % qs := binary(), %% case sensitive
- % headers := cowboy:http_headers(),
- %
- % host_info => cowboy_router:tokens(),
- % path_info => cowboy_router:tokens(),
- % bindings => cowboy_router:bindings(),
- %
- % has_body := boolean(),
- % has_read_body => true,
- % body_length := undefined | non_neg_integer()
- %
- %% @todo resp_*
- %}.
- -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 _:_ ->
- erlang:raise(exit, {request_error, qs,
- 'Malformed query string; application/x-www-form-urlencoded expected.'
- }, erlang:get_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(
- 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;
- _ -> [$
- 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,
- <<"/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080">> = iolist_to_binary(uri(Req,
- <<>> = iolist_to_binary(uri(Req,
- %% Empty values.
- <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"//localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- %% Port is integer() | undefined.
- {'EXIT', _} = (catch iolist_to_binary(uri(Req,
- {'EXIT', _} = (catch iolist_to_binary(uri(Req,
- {'EXIT', _} = (catch iolist_to_binary(uri(Req,
- %% Update components.
- <<"https://localhost:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://example.org:8080/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:123/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/custom?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path?smart=42">> = iolist_to_binary(uri(Req,
- <<"http://localhost:8080/path?dummy=2785#intro">> = iolist_to_binary(uri(Req,
- %% Interesting combinations.
- <<"http://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req,
- <<"https://localhost/path?dummy=2785">> = iolist_to_binary(uri(Req,
- 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
-
- _ -> 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 _:_ ->
- erlang:raise(exit, {request_error, {header, Name},
- 'Malformed header. Please consult the relevant specification.'
- }, erlang:get_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(<<"authorization">>) -> fun cow_http_hd:parse_authorization/1;
- parse_header_fun(<<"connection">>) -> fun cow_http_hd:parse_connection/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-unmodified-since">>) -> fun cow_http_hd:parse_if_unmodified_since/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(<<"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 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=#{pid := Pid, streamid := StreamID}, Opts) ->
- Length = maps:get(length, Opts, 8000000),
- Period = maps:get(period, Opts, 15000),
- Timeout = maps:get(timeout, Opts, Period + 1000),
- Ref = make_ref(),
- Pid ! {{Pid, StreamID}, {read_body, Ref, Length, Period}},
- 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
- 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,
- -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 _:_ ->
- erlang:raise(exit, {request_error, urlencoded_body,
- 'Malformed body; application/x-www-form-urlencoded expected.'
- }, erlang:get_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.
- %% Multipart.
- -spec read_part(Req)
- -> {ok, cow_multipart:headers(), Req} | {done, Req}
- when Req::req().
- read_part(Req) ->
- read_part(Req,
- -spec read_part(Req, read_body_opts())
- -> {ok,
- 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
- %% Ignore epilogue.
- {done, _} ->
- {done, Req
- catch _:_ ->
- erlang:raise(exit, {request_error, {multipart, headers},
- 'Malformed body; multipart expected.'
- }, erlang:get_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
- 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
- end
- end.
- init_multipart(Req) ->
- {<<"multipart">>, _, Params} = parse_header(<<"content-type">>, Req),
- case lists:keyfind(<<"boundary">>, 1, Params) of
- {_, Boundary} ->
- Req
- 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
- %% 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
- %% @todo Fix the cookie_opts() type.
- -spec set_resp_cookie(binary(), iodata(), Req, cookie_opts())
- -> Req when Req::req().
- set_resp_cookie(Name, Value, Req, Opts) ->
- Cookie = cow_cookie:setcookie(Name, Value, maps:to_list(Opts)),
- RespCookies = maps:get(resp_cookies, Req,
- Req
- %% @todo We could add has_resp_cookie and delete_resp_cookie now.
- -spec set_resp_header(binary(), iodata(), Req)
- -> Req when Req::req().
- set_resp_header(Name, Value, Req=#{resp_headers := RespHeaders}) ->
- Req
- set_resp_header(Name,Value, Req) ->
- Req
- -spec set_resp_headers(cowboy:http_headers(), Req)
- -> Req when Req::req().
- set_resp_headers(Headers, Req=#{resp_headers := RespHeaders}) ->
- Req
- set_resp_headers(Headers, Req) ->
- Req
- -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
- -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
- -spec inform(cowboy:http_status(), req()) -> ok.
- inform(Status, Req) ->
- inform(Status,
- -spec inform(cowboy:http_status(), cowboy:http_headers(), req()) -> ok.
- inform(_, _, #{has_sent_resp := _}) ->
- error(function_clause); %% @todo Better error message.
- inform(Status, Headers,
- when is_integer(Status); is_binary(Status) ->
- Pid ! {{Pid, StreamID}, {inform, Status, Headers}},
- ok.
- -spec reply(cowboy:http_status(), Req) -> Req when Req::req().
- reply(Status, Req) ->
- reply(Status,
- -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 := _}) ->
- error(function_clause); %% @todo Better error message.
- 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. (RFC7230 3.3.1, RFC7230 3.3.2)
- reply(Status=204, Headers, Body, Req) ->
- do_reply(Status, Headers, Body, Req);
- reply(Status= <<"204",_/bits>>, Headers, Body, Req) ->
- do_reply(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).
- %% 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=#{pid := Pid, streamid := StreamID, method := <<"HEAD">>}) ->
- Pid ! {{Pid, StreamID}, {response, Status, response_headers(Headers, Req), <<>>}},
- done_replying(Req, true);
- do_reply(Status, Headers, Body, Req=#{pid := Pid, streamid := StreamID}) ->
- Pid ! {{Pid, StreamID}, {response, Status, response_headers(Headers, Req), Body}},
- 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 := _}) ->
- error(function_clause);
- stream_reply(Status, Headers=#{}, Req=#{pid := Pid, streamid := StreamID})
- when is_integer(Status); is_binary(Status) ->
- Pid ! {{Pid, StreamID}, {headers, Status, response_headers(Headers, Req)}},
- done_replying(Req, headers).
- -spec stream_body(iodata(), 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.
- stream_body(Data, IsFin=nofin, #{pid := Pid, streamid := StreamID, has_sent_resp := headers}) ->
- case iolist_size(Data) of
- 0 -> ok;
- _ ->
- Pid ! {{Pid, StreamID}, {data, IsFin, Data}},
- ok
- end;
- stream_body(Data, IsFin, #{pid := Pid, streamid := StreamID, has_sent_resp := headers}) ->
- Pid ! {{Pid, StreamID}, {data, IsFin, Data}},
- ok.
- -spec stream_trailers(cowboy:http_headers(), req()) -> ok.
- stream_trailers(Trailers, #{pid := Pid, streamid := StreamID, has_sent_resp := headers}) ->
- Pid ! {{Pid, StreamID}, {trailers, Trailers}},
- ok.
- -spec push(binary(), 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(Path, Headers,
- 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, <<>>),
- Pid ! {{Pid, StreamID}, {push, Method, Scheme, Host, Port, Path, Qs, Headers}},
- 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
- 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
- {ok, MapValue} ->
- kvlist_to_map(Keys, Tail,
- Map
- error ->
- kvlist_to_map(Keys, Tail,
- Map
- 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) ->
- filter_constraints(Tail, Map, Errors, Key, maps:get(Key, Map), Constraints);
- 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
- end;
- filter([Key|Tail], Map, Errors) ->
- case maps:is_key(Key, Map) of
- true ->
- filter(Tail, Map, Errors);
- false ->
- filter(Tail, Map, Errors
- end.
- filter_constraints(Tail, Map, Errors, Key, Value0, Constraints) ->
- case cowboy_constraints:validate(Value0, Constraints) of
- {ok, Value} ->
- filter(Tail, Map
- {error, Reason} ->
- filter(Tail, Map, Errors
- end.
|