%% ``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. %% %% The Initial Developer of the Original Code is Ericsson Utvecklings AB. %% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings %% AB. All Rights Reserved.'' %% %% $Id: mod_range.erl,v 1.1 2008/12/17 09:53:35 mikpe Exp $ %% -module(mod_range). -export([do/1]). -include("httpd.hrl"). %% do do(Info) -> ?DEBUG("do -> entry",[]), case Info#mod.method of "GET" -> case httpd_util:key1search(Info#mod.data,status) of %% A status code has been generated! {StatusCode,PhraseArgs,Reason} -> {proceed,Info#mod.data}; %% No status code has been generated! undefined -> case httpd_util:key1search(Info#mod.data,response) of %% No response has been generated! undefined -> case httpd_util:key1search(Info#mod.parsed_header,"range") 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 httpd_util:key1search(Info#mod.data,if_range) 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"), Date = httpd_util:rfc1123_date(), {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)}, {last_modified,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 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"), Date = httpd_util:rfc1123_date(), Size = get_range_size(Start,Stop,FileInfo), case valid_range(Start,Stop,FileInfo) of {true,StartByte,EndByte,TotByte}-> Head=[{code,206},{content_type, MimeType}, {last_modified, LastModified}, {etag,httpd_util:create_etag(FileInfo)}, {content_range,["bytes=",integer_to_list(StartByte),"-", integer_to_list(EndByte),"/",integer_to_list(TotByte)]}, {content_length,Size}], 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 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= case FileInfo#file_info.size of FileSize when Start< FileSize -> case FileInfo#file_info.size of Size when End {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} -> {FileInfo0, httpd_util:rfc1123_date(FileInfo0#file_info.mtime)}; _ -> {#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([$\ ,$b,$y,$t,$e,$s,$\=|Ranges])-> parse_ranges([$b,$y,$t,$e,$s,$\=|Ranges]); parse_ranges([$b,$y,$t,$e,$s,$\=|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.