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