1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2001-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21-module(mod_range).
22-export([do/1]).
23-include("httpd.hrl").
24-include("httpd_internal.hrl").
25%% do
26
27do(Info) ->
28    case Info#mod.method of
29	"GET" ->
30	    case proplists:get_value(status, Info#mod.data) of
31		%% A status code has been generated!
32		{_StatusCode,  _PhraseArgs, _Reason} ->
33		    {proceed,Info#mod.data};
34		%% No status code has been generated!
35		undefined ->
36		    case proplists:get_value(response, Info#mod.data) of
37			%% No response has been generated!
38			undefined ->
39			    case proplists:get_value("range",
40						     Info#mod.parsed_header) of
41				undefined ->
42				    %Not a range response
43				    {proceed,Info#mod.data};
44				Range ->
45				    %%Control that there weren't a
46				    %%if-range field that stopped The
47				    %%range request in favor for the
48				    %%whole file
49				    case proplists:get_value(if_range,
50							     Info#mod.data) of
51					send_file ->
52					    {proceed,Info#mod.data};
53					_undefined ->
54					    do_get_range(Info,Range)
55				    end
56			    end;
57			%% A response has been generated or sent!
58			_Response ->
59			    {proceed, Info#mod.data}
60		    end
61	    end;
62	%% Not a GET method!
63	_ ->
64	    {proceed,Info#mod.data}
65    end.
66
67do_get_range(Info,Ranges) ->
68     Path = mod_alias:path(Info#mod.data, Info#mod.config_db,
69			  Info#mod.request_uri),
70    {FileInfo, LastModified} = get_modification_date(Path),
71    send_range_response(Path, Info, Ranges, FileInfo, LastModified).
72
73
74send_range_response(Path, Info, Ranges, FileInfo, LastModified)->
75    case parse_ranges(Ranges) of
76	error->
77	    {proceed,Info#mod.data};
78	{multipart,RangeList}->
79	    send_multi_range_response(Path, Info, RangeList);
80	{Start,Stop}->
81	    send_range_response(Path, Info, Start, Stop, FileInfo,
82				LastModified)
83    end.
84%%More than one range specified
85%%Send a multipart reponse to the user
86%
87%%An example of an multipart range response
88
89% HTTP/1.1 206 Partial Content
90% Date:Wed 15 Nov 1995 04:08:23 GMT
91% Last-modified:Wed 14 Nov 1995 04:08:23 GMT
92% Content-type: multipart/byteranges; boundary="SeparatorString"
93%
94% --"SeparatorString"
95% Content-Type: application/pdf
96% Content-Range: bytes 500-600/1010
97% .... The data..... 101 bytes
98%
99% --"SeparatorString"
100% Content-Type: application/pdf
101% Content-Range: bytes 700-1009/1010
102% .... The data.....
103
104
105
106send_multi_range_response(Path,Info,RangeList)->
107    case file:open(Path, [raw,binary]) of
108	{ok, FileDescriptor} ->
109	    file:close(FileDescriptor),
110	    Suffix = httpd_util:strip_extension_dot(Path),
111	    PartMimeType = httpd_util:lookup_mime_default(Info#mod.config_db,
112							  Suffix,"text/plain"),
113	    {FileInfo,  LastModified} = get_modification_date(Path),
114	    case valid_ranges(RangeList,Path,FileInfo) of
115		{ValidRanges,true}->
116		    %Apache breaks the standard by sending the size
117		    %field in the Header.
118		    Header =
119			[{code,206},
120			 {content_type, "multipart/byteranges;boundary"
121			  "=RangeBoundarySeparator"},
122			 {etag, httpd_util:create_etag(FileInfo)} |
123			 LastModified],
124		    Body = {fun send_multiranges/4,
125			    [ValidRanges, Info, PartMimeType, Path]},
126		    {proceed,[{response,
127			       {response, Header, Body}} | Info#mod.data]};
128		_ ->
129		    {proceed, [{status, {416, "Range not valid",
130					 bad_range_boundaries }}]}
131	    end;
132	{error, _Reason} ->
133	    {proceed,Info#mod.data}
134    end.
135
136send_multiranges(ValidRanges,Info,PartMimeType,Path)->
137    case file:open(Path, [raw,binary]) of
138	{ok,FileDescriptor} ->
139	    lists:foreach(fun(Range)->
140				  send_multipart_start(Range,
141						       Info,
142						       PartMimeType,
143						       FileDescriptor)
144			  end,ValidRanges),
145	    file:close(FileDescriptor),
146	    %%Sends an end of the multipart
147	    httpd_socket:deliver(Info#mod.socket_type,Info#mod.socket,
148				 "\r\n--RangeBoundarySeparator--"),
149	    sent;
150	_ ->
151	    close
152    end.
153
154send_multipart_start({{Start,End},{StartByte,EndByte,Size}},Info,
155		     PartMimeType,FileDescriptor)
156  when StartByte < Size ->
157    PartHeader=["\r\n--RangeBoundarySeparator\r\n","Content-type: ",
158		PartMimeType,"\r\n",
159                "Content-Range:bytes=",integer_to_list(StartByte),"-",
160		integer_to_list(EndByte),"/",
161		integer_to_list(Size),"\r\n\r\n"],
162    send_part_start(Info#mod.socket_type, Info#mod.socket, PartHeader,
163		    FileDescriptor, Start, End);
164
165
166send_multipart_start({{Start,End},{StartByte,EndByte,Size}}, Info,
167		     PartMimeType, FileDescriptor)->
168    PartHeader=["\r\n--RangeBoundarySeparator\r\n","Content-type: ",
169		PartMimeType,"\r\n",
170                "Content-Range:bytes=",integer_to_list(Size-(StartByte-Size)),
171		"-",integer_to_list(EndByte),"/",
172		integer_to_list(Size),"\r\n\r\n"],
173    send_part_start(Info#mod.socket_type, Info#mod.socket, PartHeader,
174		    FileDescriptor, Start, End).
175
176send_part_start(SocketType, Socket, PartHeader, FileDescriptor, Start, End)->
177    case httpd_socket:deliver(SocketType, Socket, PartHeader) of
178	ok ->
179	    send_part_start(SocketType,Socket,FileDescriptor,Start,End);
180	_ ->
181	    close
182    end.
183
184send_range_response(Path, Info, Start, Stop, FileInfo, LastModified)->
185    case file:open(Path, [raw,binary]) of
186	{ok, FileDescriptor} ->
187	    file:close(FileDescriptor),
188	    Suffix = httpd_util:strip_extension_dot(Path),
189	    MimeType = httpd_util:lookup_mime_default(Info#mod.config_db,
190						      Suffix,"text/plain"),
191	    Size = get_range_size(Start,Stop,FileInfo),
192	    case valid_range(Start,Stop,FileInfo) of
193		{true,StartByte,EndByte,TotByte}->
194		    Head =[{code,206},{content_type, MimeType},
195			   {etag, httpd_util:create_etag(FileInfo)},
196			   {content_range,["bytes=",
197					   integer_to_list(StartByte),"-",
198					   integer_to_list(EndByte),"/",
199					   integer_to_list(TotByte)]},
200			   {content_length, Size} | LastModified],
201		    BodyFunc = fun send_range_body/5,
202		    Arg = [Info#mod.socket_type,
203			  Info#mod.socket, Path, Start, Stop],
204		    {proceed,[{response,{response ,Head,  {BodyFunc,Arg}}}|
205			      Info#mod.data]};
206		{false,Reason} ->
207		    {proceed, [{status, {416, Reason, bad_range_boundaries }}]}
208	    end;
209	{error, _Reason} ->
210	    {proceed,Info#mod.data}
211    end.
212
213
214send_range_body(SocketType,Socket,Path,Start,End) ->
215    case file:open(Path, [raw,binary]) of
216	{ok,FileDescriptor} ->
217	    send_part_start(SocketType,Socket,FileDescriptor,Start,End),
218	    file:close(FileDescriptor);
219	_ ->
220	    close
221    end.
222
223send_part_start(SocketType,Socket,FileDescriptor,Start,End) ->
224    case Start of
225	from_end ->
226	    file:position(FileDescriptor,{eof,End}),
227	    send_body(SocketType,Socket,FileDescriptor);
228	from_start ->
229	    file:position(FileDescriptor,{bof,End}),
230	    send_body(SocketType,Socket,FileDescriptor);
231	Byte when is_integer(Byte) ->
232	    file:position(FileDescriptor,{bof,Start}),
233	    send_part(SocketType,Socket,FileDescriptor,End)
234    end,
235    sent.
236
237
238%%This function could replace send_body by calling it with Start=0 end
239%%=FileSize But i gues it would be stupid when we look at performance
240send_part(SocketType,Socket,FileDescriptor,End)->
241    case file:position(FileDescriptor,{cur,0}) of
242	{ok,NewPos} ->
243	   if
244	       NewPos > End ->
245		   ok;
246	       true ->
247		   Size = get_file_chunk_size(NewPos,End,?FILE_CHUNK_SIZE),
248		   case file:read(FileDescriptor,Size) of
249		       eof ->
250			   ok;
251		       {error, _Reason} ->
252			   ok;
253		       {ok,Binary} ->
254			   case httpd_socket:deliver(SocketType,Socket,
255						     Binary) of
256			       socket_closed ->
257				   socket_close;
258			       _ ->
259				   send_part(SocketType,Socket,
260					     FileDescriptor,End)
261			   end
262		   end
263	   end;
264	_->
265	    ok
266    end.
267
268%% validate that the range is in the limits of the file
269valid_ranges(RangeList, _Path, FileInfo)->
270    lists:mapfoldl(fun({Start,End},Acc)->
271			case Acc of
272			    true ->
273				case valid_range(Start,End,FileInfo) of
274				    {true,StartB,EndB,Size}->
275					{{{Start,End},
276					  {StartB,EndB,Size}},true};
277				    _ ->
278					false
279				end;
280			    _ ->
281				{false,false}
282			end
283		   end,true,RangeList).
284
285
286
287valid_range(from_end,End,FileInfo)->
288    Size=FileInfo#file_info.size,
289    if
290	End < Size ->
291	    {true,(Size+End),Size-1,Size};
292	true ->
293	    false
294    end;
295valid_range(from_start,End,FileInfo)->
296  Size=FileInfo#file_info.size,
297    if
298	End < Size ->
299	    {true,End,Size-1,Size};
300	true ->
301	    false
302    end;
303
304valid_range(Start,End,FileInfo) when Start =< End ->
305    case FileInfo#file_info.size of
306	FileSize when Start< FileSize ->
307	    case FileInfo#file_info.size of
308		Size when End<Size ->
309		    {true,Start,End,FileInfo#file_info.size};
310		Size ->
311		    {true,Start,Size-1,Size}
312	    end;
313	_->
314	    {false,"The size of the range is negative"}
315    end;
316
317valid_range(_Start,_End,_FileInfo)->
318    {false,"Range starts out of file boundaries"}.
319%% Find the modification date of the file
320get_modification_date(Path)->
321    case file:read_file_info(Path) of
322	{ok, FileInfo0} ->
323	    case (catch httpd_util:rfc1123_date(FileInfo0#file_info.mtime)) of
324		Date when is_list(Date) ->
325		    {FileInfo0, [{last_modified, Date}]};
326		_ ->
327		    {FileInfo0, []}
328	    end;
329	_ ->
330	    {#file_info{}, []}
331    end.
332
333%Calculate the size of the chunk to read
334
335get_file_chunk_size(Position, End, DefaultChunkSize)
336  when (Position+DefaultChunkSize) =< End ->
337    DefaultChunkSize;
338get_file_chunk_size(Position, End, _DefaultChunkSize) ->
339    (End-Position) +1.
340
341
342
343%Get the size of the range to send. Remember that
344%A range is from startbyte up to endbyte which means that
345%the nuber of byte in a range is (StartByte-EndByte)+1
346
347get_range_size(from_end, Stop, _FileInfo)->
348    integer_to_list(-1*Stop);
349
350get_range_size(from_start, StartByte, FileInfo) ->
351    integer_to_list((((FileInfo#file_info.size)-StartByte)));
352
353get_range_size(StartByte, EndByte, _FileInfo) ->
354    integer_to_list((EndByte-StartByte)+1).
355
356parse_ranges("\bytes\=" ++ Ranges)->
357    parse_ranges("bytes\=" ++ Ranges);
358parse_ranges("bytes\=" ++ Ranges)->
359    case string:tokens(Ranges,", ") of
360       [Range] ->
361	   parse_range(Range);
362       [Range1|SplittedRanges]->
363	   {multipart,lists:map(fun parse_range/1,[Range1|SplittedRanges])}
364    end;
365%Bad unit
366parse_ranges(Ranges)->
367    io:format("Bad Ranges : ~p",[Ranges]),
368    error.
369%Parse the range  specification from the request to {Start,End}
370%Start=End : Numreric string | []
371
372parse_range(Range)->
373    format_range(split_range(Range,[],[])).
374format_range({[],BytesFromEnd})->
375    {from_end,-1*(list_to_integer(BytesFromEnd))};
376format_range({StartByte,[]})->
377    {from_start,list_to_integer(StartByte)};
378format_range({StartByte,EndByte})->
379    {list_to_integer(StartByte),list_to_integer(EndByte)}.
380%Last case return the splitted range
381split_range([],Current,Other)->
382    {lists:reverse(Other),lists:reverse(Current)};
383
384split_range([$-|Rest],Current,Other)->
385    split_range(Rest,Other,Current);
386
387split_range([N|Rest],Current,End) ->
388    split_range(Rest,[N|Current],End).
389
390send_body(SocketType,Socket,FileDescriptor) ->
391    case file:read(FileDescriptor,?FILE_CHUNK_SIZE) of
392	{ok,Binary} ->
393	    case httpd_socket:deliver(SocketType,Socket,Binary) of
394		socket_closed ->
395		    socket_close;
396		_ ->
397		    send_body(SocketType,Socket,FileDescriptor)
398	    end;
399	eof ->
400	    eof
401    end.
402