aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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)"),