aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-11-11 13:57:26 +0100
committerLoïc Hoguin <[email protected]>2018-11-11 13:57:26 +0100
commitdd0fbab6b79e1cefa31adf6e9647f0d76bbade06 (patch)
tree595eb0d4ff3af00427598b8d2bb57c371b919d25
parentd7b7580b3913c17b404319cc4c153748d5e59194 (diff)
downloadcowboy-dd0fbab6b79e1cefa31adf6e9647f0d76bbade06.tar.gz
cowboy-dd0fbab6b79e1cefa31adf6e9647f0d76bbade06.tar.bz2
cowboy-dd0fbab6b79e1cefa31adf6e9647f0d76bbade06.zip
Add automatic ranged request handling for bytes units
Returning the atom auto instead of a callback informs Cowboy that it needs to handle range requests automatically. This changes the behavior so that the ProvideCallback function is called and then Cowboy splits the data on its own and sends the response without any other user involvement other than defining the ranges_provided/2 callback. This is a quick and dirty way to add range request support to resources, and will be good enough for many cases including for cowboy_static as it also works when the normal response body is a sendfile tuple.
-rw-r--r--src/cowboy_rest.erl177
-rw-r--r--test/handlers/provide_range_callback_h.erl4
-rw-r--r--test/handlers/ranges_provided_auto_h.erl27
-rw-r--r--test/rest_handler_SUITE.erl173
4 files changed, 357 insertions, 24 deletions
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index 8a574cd..d908b40 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -1229,15 +1229,127 @@ range_satisfiable(Req, State, Callback) ->
range_not_satisfiable(Req2, State2, Iodata)
end.
-%% 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).
+%% 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} ->
+ error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}},
+ 'A callback specified in content_types_provided/2 is not exported.'})
+ 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(Req, State=#state{handler=Handler}, Callback) ->
+set_ranged_body_callback(Req, State=#state{handler=Handler}, Callback) ->
try case call(Req, State, Callback) of
{stop, Req2, State2} ->
terminate(Req2, State2);
@@ -1245,10 +1357,7 @@ set_ranged_body(Req, State=#state{handler=Handler}, Callback) ->
switch_handler(Switch, Req2, State2);
%% When we receive a single range, we send it directly.
{[OneRange], Req2, State2} ->
- {ContentRange, Body} = prepare_range(Req2, OneRange),
- Req3 = cowboy_req:set_resp_header(<<"content-range">>, ContentRange, Req2),
- Req4 = cowboy_req:set_resp_body(Body, Req3),
- respond(Req4, State2, 206);
+ 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
@@ -1257,9 +1366,15 @@ set_ranged_body(Req, State=#state{handler=Handler}, Callback) ->
set_multipart_ranged_body(Req2, State2, Ranges)
end catch Class:{case_clause, no_call} ->
error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}},
- 'A callback specified in ranges_accepted/2 is not exported.'})
+ 'A callback specified in ranges_provided/2 is not exported.'})
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),
@@ -1282,6 +1397,34 @@ set_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) ->
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
'*' -> <<"*">>;
@@ -1293,6 +1436,14 @@ prepare_range(#{range := {RangeUnit, _}}, {{From, To, Total0}, 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.
diff --git a/test/handlers/provide_range_callback_h.erl b/test/handlers/provide_range_callback_h.erl
index f14a544..136e37e 100644
--- a/test/handlers/provide_range_callback_h.erl
+++ b/test/handlers/provide_range_callback_h.erl
@@ -1,4 +1,4 @@
-%% This module defines the range_satisfiable callback
+%% This module defines many callbacks relevant to range requests
%% and return something different depending on query string.
-module(provide_range_callback_h).
@@ -41,7 +41,7 @@ get_text_plain(Req, State) ->
%% Simulate the callback being missing, otherwise expect true/false.
get_text_plain_bytes(#{qs := <<"missing">>}, _) ->
- ct_helper_error_h:ignore(cowboy_rest, set_ranged_body, 3),
+ ct_helper_error_h:ignore(cowboy_rest, set_ranged_body_callback, 3),
no_call;
get_text_plain_bytes(Req=#{range := {_, [{From=0, infinity}]}}, State) ->
%% We send everything in one part.
diff --git a/test/handlers/ranges_provided_auto_h.erl b/test/handlers/ranges_provided_auto_h.erl
new file mode 100644
index 0000000..f7e6595
--- /dev/null
+++ b/test/handlers/ranges_provided_auto_h.erl
@@ -0,0 +1,27 @@
+%% This module defines the ranges_provided callback
+%% which returns the auto option for bytes ranges
+%% and the normal ProvideCallback that returns
+%% something different depending on query string.
+
+-module(ranges_provided_auto_h).
+
+-export([init/2]).
+-export([content_types_provided/2]).
+-export([ranges_provided/2]).
+-export([get_text_plain/2]).
+
+init(Req, State) ->
+ {cowboy_rest, Req, State}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}.
+
+ranges_provided(Req, State) ->
+ {[{<<"bytes">>, auto}], Req, State}.
+
+get_text_plain(Req=#{qs := <<"data">>}, State) ->
+ {<<"This is ranged REST!">>, Req, State};
+get_text_plain(Req=#{qs := <<"sendfile">>}, State) ->
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ {{sendfile, 0, Size, Path}, Req, State}.
diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl
index 6116830..58898b2 100644
--- a/test/rest_handler_SUITE.erl
+++ b/test/rest_handler_SUITE.erl
@@ -52,6 +52,7 @@ init_dispatch(_) ->
{"/provide_range_callback", provide_range_callback_h, []},
{"/range_satisfiable", range_satisfiable_h, []},
{"/ranges_provided", ranges_provided_h, []},
+ {"/ranges_provided_auto", ranges_provided_auto_h, []},
{"/rate_limited", rate_limited_h, []},
{"/stop_handler", stop_handler_h, []},
{"/switch_handler", switch_handler_h, run},
@@ -431,7 +432,15 @@ provide_range_callback_multipart(Config) ->
= lists:keyfind(<<"content-type">>, 1, Headers),
{ok, Body0} = gun:await_body(ConnPid, Ref),
Body = do_decode(Headers, Body0),
- do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>).
+ {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
+ [
+ {bytes, 0, 3, 20},
+ {bytes, 5, 6, 20},
+ {bytes, 8, 13, 20},
+ {bytes, 15, 19, 20}
+ ] = ContentRanges,
+ <<"ThisisrangedREST!">> = BodyAcc,
+ ok.
do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyAcc) ->
case cow_multipart:parse_headers(Rest, Boundary) of
@@ -450,14 +459,7 @@ do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyA
<<BodyAcc/binary, Body/binary>>)
end;
{done, <<>>} ->
- [
- {bytes, 0, 3, 20},
- {bytes, 5, 6, 20},
- {bytes, 8, 13, 20},
- {bytes, 15, 19, 20}
- ] = lists:reverse(ContentRangesAcc),
- <<"ThisisrangedREST!">> = BodyAcc,
- ok
+ {lists:reverse(ContentRangesAcc), BodyAcc}
end.
provide_range_callback_metadata(Config) ->
@@ -598,6 +600,159 @@ ranges_provided_accept_ranges(Config) ->
{_, <<"bytes, pages, chapters">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
ok.
+%% @todo Probably should have options to do this automatically for auto at least.
+%%
+%% A server that supports range requests MAY ignore or reject a Range
+%% header field that consists of more than two overlapping ranges, or a
+%% set of many small ranges that are not listed in ascending order,
+%% since both are indications of either a broken client or a deliberate
+%% denial-of-service attack (Section 6.1).
+
+%% @todo Probably should have options for auto as well to join ranges that
+%% are very close from each other.
+
+ranges_provided_auto_data(Config) ->
+ doc("When the unit range is bytes and the callback is 'auto' "
+ "Cowboy will call the normal ProvideCallback and perform "
+ "the range calculations automatically."),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ {<<"range">>, <<"bytes=8-">>}
+ ]),
+ {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
+ {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
+ {_, <<"bytes 8-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
+ {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+ {ok, <<"ranged REST!">>} = gun:await_body(ConnPid, Ref),
+ ok.
+
+ranges_provided_auto_sendfile(Config) ->
+ doc("When the unit range is bytes and the callback is 'auto' "
+ "Cowboy will call the normal ProvideCallback and perform "
+ "the range calculations automatically."),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ {<<"range">>, <<"bytes=8-">>}
+ ]),
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ {ok, <<_:8/binary, Body/bits>>} = file:read_file(Path),
+ {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
+ {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
+ {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers),
+ ContentRange = iolist_to_binary([
+ <<"bytes 8-">>,
+ integer_to_binary(Size - 1),
+ <<"/">>,
+ integer_to_binary(Size)
+ ]),
+ {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
+ {ok, Body} = gun:await_body(ConnPid, Ref),
+ ok.
+
+ranges_provided_auto_multipart_data(Config) ->
+ doc("When the unit range is bytes and the callback is 'auto' "
+ "Cowboy will call the normal ProvideCallback and perform "
+ "the range calculations automatically."),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ %% This range selects everything except the space characters.
+ {<<"range">>, <<"bytes=0-3, 5-6, 8-13, 15-">>}
+ ]),
+ {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
+ {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
+ false = lists:keyfind(<<"content-range">>, 1, Headers),
+ {_, <<"multipart/byteranges; boundary=", Boundary/bits>>}
+ = lists:keyfind(<<"content-type">>, 1, Headers),
+ {ok, Body0} = gun:await_body(ConnPid, Ref),
+ Body = do_decode(Headers, Body0),
+ %% We will receive the ranges in the same order as requested.
+ {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
+ [
+ {bytes, 0, 3, 20},
+ {bytes, 5, 6, 20},
+ {bytes, 8, 13, 20},
+ {bytes, 15, 19, 20}
+ ] = ContentRanges,
+ <<"ThisisrangedREST!">> = BodyAcc,
+ ok.
+
+ranges_provided_auto_multipart_sendfile(Config) ->
+ doc("When the unit range is bytes and the callback is 'auto' "
+ "Cowboy will call the normal ProvideCallback and perform "
+ "the range calculations automatically."),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ %% This range selects a few random chunks of the file.
+ {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>}
+ ]),
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ Skip = Size - 399,
+ {ok, <<
+ _:50/binary, Body1:50/binary,
+ _:50/binary, Body2:50/binary,
+ _:50/binary, Body3:50/binary,
+ _:Skip/binary, Body4/bits>>} = file:read_file(Path),
+ {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
+ {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
+ false = lists:keyfind(<<"content-range">>, 1, Headers),
+ {_, <<"multipart/byteranges; boundary=", Boundary/bits>>}
+ = lists:keyfind(<<"content-type">>, 1, Headers),
+ {ok, Body0} = gun:await_body(ConnPid, Ref),
+ Body = do_decode(Headers, Body0),
+ %% We will receive the ranges in the same order as requested.
+ {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
+ LastFrom = 300 + Skip,
+ LastTo = Size - 1,
+ [
+ {bytes, 50, 99, Size},
+ {bytes, 150, 199, Size},
+ {bytes, 250, 299, Size},
+ {bytes, LastFrom, LastTo, Size}
+ ] = ContentRanges,
+ BodyAcc = <<Body1/binary, Body2/binary, Body3/binary, Body4/binary>>,
+ ok.
+
+ranges_provided_auto_not_satisfiable_data(Config) ->
+ doc("When the unit range is bytes and the callback is 'auto' "
+ "Cowboy will call the normal ProvideCallback and perform "
+ "the range calculations automatically. When the requested "
+ "range is not satisfiable a 416 range not satisfiable response "
+ "is expected. The content-range header will be set. (RFC7233 4.4)"),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ {<<"range">>, <<"bytes=1000-">>}
+ ]),
+ {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
+ {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
+ {_, <<"bytes */20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
+ ok.
+
+ranges_provided_auto_not_satisfiable_sendfile(Config) ->
+ doc("When the unit range is bytes and the callback is 'auto' "
+ "Cowboy will call the normal ProvideCallback and perform "
+ "the range calculations automatically. When the requested "
+ "range is not satisfiable a 416 range not satisfiable response "
+ "is expected. The content-range header will be set. (RFC7233 4.4)"),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ {<<"range">>, <<"bytes=1000-">>}
+ ]),
+ {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
+ {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ ContentRange = iolist_to_binary([<<"bytes */">>, integer_to_binary(Size)]),
+ {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers),
+ ok.
+
ranges_provided_empty_accept_ranges_none(Config) ->
doc("When the ranges_provided callback exists but returns an empty list "
"the accept-ranges header is sent in the response with the value none. (RFC7233 2.3)"),