%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2001-2010. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
-module(mod_range).
-export([do/1]).
-include("httpd.hrl").
-include("httpd_internal.hrl").
%% do
do(Info) ->
?DEBUG("do -> entry",[]),
case Info#mod.method of
"GET" ->
case proplists:get_value(status, Info#mod.data) of
%% A status code has been generated!
{_StatusCode, _PhraseArgs, _Reason} ->
{proceed,Info#mod.data};
%% No status code has been generated!
undefined ->
case proplists:get_value(response, Info#mod.data) of
%% No response has been generated!
undefined ->
case proplists:get_value("range",
Info#mod.parsed_header) of
undefined ->
%Not a range response
{proceed,Info#mod.data};
Range ->
%%Control that there weren't a
%%if-range field that stopped The
%%range request in favor for the
%%whole file
case proplists:get_value(if_range,
Info#mod.data) of
send_file ->
{proceed,Info#mod.data};
_undefined ->
do_get_range(Info,Range)
end
end;
%% A response has been generated or sent!
_Response ->
{proceed, Info#mod.data}
end
end;
%% Not a GET method!
_ ->
{proceed,Info#mod.data}
end.
do_get_range(Info,Ranges) ->
?DEBUG("do_get_range -> Request URI: ~p",[Info#mod.request_uri]),
Path = mod_alias:path(Info#mod.data, Info#mod.config_db,
Info#mod.request_uri),
{FileInfo, LastModified} = get_modification_date(Path),
send_range_response(Path, Info, Ranges, FileInfo, LastModified).
send_range_response(Path, Info, Ranges, FileInfo, LastModified)->
case parse_ranges(Ranges) of
error->
?ERROR("send_range_response-> Unparsable range request",[]),
{proceed,Info#mod.data};
{multipart,RangeList}->
send_multi_range_response(Path, Info, RangeList);
{Start,Stop}->
send_range_response(Path, Info, Start, Stop, FileInfo,
LastModified)
end.
%%More than one range specified
%%Send a multipart reponse to the user
%
%%An example of an multipart range response
% HTTP/1.1 206 Partial Content
% Date:Wed 15 Nov 1995 04:08:23 GMT
% Last-modified:Wed 14 Nov 1995 04:08:23 GMT
% Content-type: multipart/byteranges; boundary="SeparatorString"
%
% --"SeparatorString"
% Content-Type: application/pdf
% Content-Range: bytes 500-600/1010
% .... The data..... 101 bytes
%
% --"SeparatorString"
% Content-Type: application/pdf
% Content-Range: bytes 700-1009/1010
% .... The data.....
send_multi_range_response(Path,Info,RangeList)->
case file:open(Path, [raw,binary]) of
{ok, FileDescriptor} ->
file:close(FileDescriptor),
?DEBUG("send_multi_range_response -> FileDescriptor: ~p",
[FileDescriptor]),
Suffix = httpd_util:suffix(Path),
PartMimeType = httpd_util:lookup_mime_default(Info#mod.config_db,
Suffix,"text/plain"),
{FileInfo, LastModified} = get_modification_date(Path),
case valid_ranges(RangeList,Path,FileInfo) of
{ValidRanges,true}->
?DEBUG("send_multi_range_response ->Ranges are valid:",[]),
%Apache breaks the standard by sending the size
%field in the Header.
Header =
[{code,206},
{content_type, "multipart/byteranges;boundary"
"=RangeBoundarySeparator"},
{etag, httpd_util:create_etag(FileInfo)} |
LastModified],
?DEBUG("send_multi_range_response -> Valid Ranges: ~p",
[RagneList]),
Body = {fun send_multiranges/4,
[ValidRanges, Info, PartMimeType, Path]},
{proceed,[{response,
{response, Header, Body}} | Info#mod.data]};
_ ->
{proceed, [{status, {416, "Range not valid",
bad_range_boundaries }}]}
end;
{error, _Reason} ->
?ERROR("do_get -> failed open file: ~p",[_Reason]),
{proceed,Info#mod.data}
end.
send_multiranges(ValidRanges,Info,PartMimeType,Path)->
?DEBUG("send_multiranges -> Start sending the ranges",[]),
case file:open(Path, [raw,binary]) of
{ok,FileDescriptor} ->
lists:foreach(fun(Range)->
send_multipart_start(Range,
Info,
PartMimeType,
FileDescriptor)
end,ValidRanges),
file:close(FileDescriptor),
%%Sends an end of the multipart
httpd_socket:deliver(Info#mod.socket_type,Info#mod.socket,
"\r\n--RangeBoundarySeparator--"),
sent;
_ ->
close
end.
send_multipart_start({{Start,End},{StartByte,EndByte,Size}},Info,
PartMimeType,FileDescriptor)
when StartByte < Size ->
PartHeader=["\r\n--RangeBoundarySeparator\r\n","Content-type: ",
PartMimeType,"\r\n",
"Content-Range:bytes=",integer_to_list(StartByte),"-",
integer_to_list(EndByte),"/",
integer_to_list(Size),"\r\n\r\n"],
send_part_start(Info#mod.socket_type, Info#mod.socket, PartHeader,
FileDescriptor, Start, End);
send_multipart_start({{Start,End},{StartByte,EndByte,Size}}, Info,
PartMimeType, FileDescriptor)->
PartHeader=["\r\n--RangeBoundarySeparator\r\n","Content-type: ",
PartMimeType,"\r\n",
"Content-Range:bytes=",integer_to_list(Size-(StartByte-Size)),
"-",integer_to_list(EndByte),"/",
integer_to_list(Size),"\r\n\r\n"],
send_part_start(Info#mod.socket_type, Info#mod.socket, PartHeader,
FileDescriptor, Start, End).
send_part_start(SocketType, Socket, PartHeader, FileDescriptor, Start, End)->
case httpd_socket:deliver(SocketType, Socket, PartHeader) of
ok ->
send_part_start(SocketType,Socket,FileDescriptor,Start,End);
_ ->
close
end.
send_range_response(Path, Info, Start, Stop, FileInfo, LastModified)->
case file:open(Path, [raw,binary]) of
{ok, FileDescriptor} ->
file:close(FileDescriptor),
?DEBUG("send_range_response -> FileDescriptor: ~p",
[FileDescriptor]),
Suffix = httpd_util:suffix(Path),
MimeType = httpd_util:lookup_mime_default(Info#mod.config_db,
Suffix,"text/plain"),
Size = get_range_size(Start,Stop,FileInfo),
case valid_range(Start,Stop,FileInfo) of
{true,StartByte,EndByte,TotByte}->
Head =[{code,206},{content_type, MimeType},
{etag, httpd_util:create_etag(FileInfo)},
{content_range,["bytes=",
integer_to_list(StartByte),"-",
integer_to_list(EndByte),"/",
integer_to_list(TotByte)]},
{content_length, Size} | LastModified],
BodyFunc = fun send_range_body/5,
Arg = [Info#mod.socket_type,
Info#mod.socket, Path, Start, Stop],
{proceed,[{response,{response ,Head, {BodyFunc,Arg}}}|
Info#mod.data]};
{false,Reason} ->
{proceed, [{status, {416, Reason, bad_range_boundaries }}]}
end;
{error, _Reason} ->
?ERROR("send_range_response -> failed open file: ~p",[_Reason]),
{proceed,Info#mod.data}
end.
send_range_body(SocketType,Socket,Path,Start,End) ->
?DEBUG("mod_range -> send_range_body",[]),
case file:open(Path, [raw,binary]) of
{ok,FileDescriptor} ->
send_part_start(SocketType,Socket,FileDescriptor,Start,End),
file:close(FileDescriptor);
_ ->
close
end.
send_part_start(SocketType,Socket,FileDescriptor,Start,End) ->
case Start of
from_end ->
file:position(FileDescriptor,{eof,End}),
send_body(SocketType,Socket,FileDescriptor);
from_start ->
file:position(FileDescriptor,{bof,End}),
send_body(SocketType,Socket,FileDescriptor);
Byte when is_integer(Byte) ->
file:position(FileDescriptor,{bof,Start}),
send_part(SocketType,Socket,FileDescriptor,End)
end,
sent.
%%This function could replace send_body by calling it with Start=0 end
%%=FileSize But i gues it would be stupid when we look at performance
send_part(SocketType,Socket,FileDescriptor,End)->
case file:position(FileDescriptor,{cur,0}) of
{ok,NewPos} ->
if
NewPos > End ->
ok;
true ->
Size = get_file_chunk_size(NewPos,End,?FILE_CHUNK_SIZE),
case file:read(FileDescriptor,Size) of
eof ->
ok;
{error, _Reason} ->
ok;
{ok,Binary} ->
case httpd_socket:deliver(SocketType,Socket,
Binary) of
socket_closed ->
?LOG("send_range of body -> socket "
"closed while sending",[]),
socket_close;
_ ->
send_part(SocketType,Socket,
FileDescriptor,End)
end
end
end;
_->
ok
end.
%% validate that the range is in the limits of the file
valid_ranges(RangeList, _Path, FileInfo)->
lists:mapfoldl(fun({Start,End},Acc)->
case Acc of
true ->
case valid_range(Start,End,FileInfo) of
{true,StartB,EndB,Size}->
{{{Start,End},
{StartB,EndB,Size}},true};
_ ->
false
end;
_ ->
{false,false}
end
end,true,RangeList).
valid_range(from_end,End,FileInfo)->
Size=FileInfo#file_info.size,
if
End < Size ->
{true,(Size+End),Size-1,Size};
true ->
false
end;
valid_range(from_start,End,FileInfo)->
Size=FileInfo#file_info.size,
if
End < Size ->
{true,End,Size-1,Size};
true ->
false
end;
valid_range(Start,End,FileInfo) when Start =< End ->
case FileInfo#file_info.size of
FileSize when Start< FileSize ->
case FileInfo#file_info.size of
Size when End<Size ->
{true,Start,End,FileInfo#file_info.size};
Size ->
{true,Start,Size-1,Size}
end;
_->
{false,"The size of the range is negative"}
end;
valid_range(_Start,_End,_FileInfo)->
{false,"Range starts out of file boundaries"}.
%% Find the modification date of the file
get_modification_date(Path)->
case file:read_file_info(Path) of
{ok, FileInfo0} ->
case (catch httpd_util:rfc1123_date(FileInfo0#file_info.mtime)) of
Date when is_list(Date) ->
{FileInfo0, [{last_modified, Date}]};
_ ->
{FileInfo0, []}
end;
_ ->
{#file_info{}, []}
end.
%Calculate the size of the chunk to read
get_file_chunk_size(Position, End, DefaultChunkSize)
when (Position+DefaultChunkSize) =< End ->
DefaultChunkSize;
get_file_chunk_size(Position, End, _DefaultChunkSize) ->
(End-Position) +1.
%Get the size of the range to send. Remember that
%A range is from startbyte up to endbyte which means that
%the nuber of byte in a range is (StartByte-EndByte)+1
get_range_size(from_end, Stop, _FileInfo)->
integer_to_list(-1*Stop);
get_range_size(from_start, StartByte, FileInfo) ->
integer_to_list((((FileInfo#file_info.size)-StartByte)));
get_range_size(StartByte, EndByte, _FileInfo) ->
integer_to_list((EndByte-StartByte)+1).
parse_ranges("\bytes\=" ++ Ranges)->
parse_ranges("bytes\=" ++ Ranges);
parse_ranges("bytes\=" ++ Ranges)->
case string:tokens(Ranges,", ") of
[Range] ->
parse_range(Range);
[Range1|SplittedRanges]->
{multipart,lists:map(fun parse_range/1,[Range1|SplittedRanges])}
end;
%Bad unit
parse_ranges(Ranges)->
io:format("Bad Ranges : ~p",[Ranges]),
error.
%Parse the range specification from the request to {Start,End}
%Start=End : Numreric string | []
parse_range(Range)->
format_range(split_range(Range,[],[])).
format_range({[],BytesFromEnd})->
{from_end,-1*(list_to_integer(BytesFromEnd))};
format_range({StartByte,[]})->
{from_start,list_to_integer(StartByte)};
format_range({StartByte,EndByte})->
{list_to_integer(StartByte),list_to_integer(EndByte)}.
%Last case return the splitted range
split_range([],Current,Other)->
{lists:reverse(Other),lists:reverse(Current)};
split_range([$-|Rest],Current,Other)->
split_range(Rest,Other,Current);
split_range([N|Rest],Current,End) ->
split_range(Rest,[N|Current],End).
send_body(SocketType,Socket,FileDescriptor) ->
case file:read(FileDescriptor,?FILE_CHUNK_SIZE) of
{ok,Binary} ->
?DEBUG("send_body -> send another chunk: ~p",[size(Binary)]),
case httpd_socket:deliver(SocketType,Socket,Binary) of
socket_closed ->
?LOG("send_body -> socket closed while sending",[]),
socket_close;
_ ->
send_body(SocketType,Socket,FileDescriptor)
end;
eof ->
?DEBUG("send_body -> done with this file",[]),
eof
end.