%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2005-2016. 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(httpd_1_1).
-include_lib("kernel/include/file.hrl").
-export([host/4, chunked/4, expect/4, range/4, if_test/5, trace/4,
head/4, mod_cgi_chunked_encoding_test/5, mod_esi_chunk_timeout/4]).
%% -define(all_keys_lower_case,true).
-ifndef(all_keys_lower_case).
-define(CONTENT_LENGTH, "Content-Length: ").
-define(CONTENT_RANGE, "Content-Range: ").
-define(CONTENT_TYPE, "Content-Type: ").
-else.
-define(CONTENT_LENGTH, "content-length:").
-define(CONTENT_RANGE, "content-range:").
-define(CONTENT_TYPE, "content-type:").
-endif.
%%-------------------------------------------------------------------------
%% Test cases
%%-------------------------------------------------------------------------
host(Type, Port, Host, Node) ->
%% No host must generate an error
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\n\r\n",
[{statuscode, 400}]),
%% If it is a fully qualified URL no host is needed
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET HTTP://"++ Host ++ ":" ++
integer_to_list(Port)++
"/ HTTP/1.1\r\n\r\n",
[{statuscode, 200}]),
%% If both look at the url.
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET HTTP://"++ Host ++ ":"++
integer_to_list(Port) ++
"/ HTTP/1.1\r\nHost:"++ Host ++
"\r\n\r\n",[{statuscode, 200}]),
%% Allow the request if its a Host field
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:"++
Host ++ "\r\n\r\n",
[{statuscode, 200}]),
ok.
chunked(Type, Port, Host, Node)->
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\n"
"Host:"++ Host ++"\r\n"
"Transfer-Encoding:chunked\r\n"
"\r\n"
"A\r\n"
"1234567890\r\n"
"4\r\n"
"HEJ!\r\n"
"0\r\n\r\n",
[{statuscode, 200}]),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\n"
"Host:"++ Host ++"\r\n"
"Transfer-Encoding:chunked\r\n"
"Trailer:Content-Type\r\n"
"\r\n"
"A\r\n"
"1234567890\r\n"
"4\r\n"
"HEJ!\r\n"
"0\r\n"
"Content-Type:text/plain\r\n\r\n",
[{statuscode, 200}]),
ok.
expect(Type, Port, Host, Node)->
Request="GET / HTTP/1.1\r\nHost:" ++ Host ++
"\r\nContent-Length:22\r\nExpect:100-continue\r\n\r\n",
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
Request,
[{statuscode, 100}]).
range(Type, Port, Host, Node)->
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET /range.txt HTTP/1.1\r\nHost:"
++ Host
++ "\r\nRange:bytes=110-120\r\n\r\n",
[{statuscode,416}]),
%%The simples of all range request a range
Request1="GET /range.txt HTTP/1.1\r\nHost:" ++ Host ++
"\r\nRange:bytes=0-9\r\n\r\n",
{ok, Socket1} = inets_test_lib:connect_byte(Type, Host, Port),
inets_test_lib:send(Type, Socket1,Request1),
ok = validateRangeRequest(Socket1,[],"1234567890",$2,$0,$6),
inets_test_lib:close(Type,Socket1),
%% Request the end of the file
Request2 =
"GET /range.txt HTTP/1.1\r\nHost:" ++ Host ++
"\r\nRange:bytes=90-\r\n\r\n",
{ok, Socket2} = inets_test_lib:connect_byte(Type, Host, Port),
inets_test_lib:send(Type, Socket2, Request2),
ok = validateRangeRequest(Socket2,[],"1234567890",$2,$0,$6),
inets_test_lib:close(Type,Socket2),
%% The last byte in the file
Request3 =
"GET /range.txt HTTP/1.1\r\nHost:"++
Host ++ "\r\nRange:bytes=-1\r\n\r\n",
{ok, Socket3} = inets_test_lib:connect_byte(Type, Host, Port),
inets_test_lib:send(Type, Socket3,Request3),
ok = validateRangeRequest(Socket3,[],"0",$2,$0,$6),
inets_test_lib:close(Type, Socket3),
%%Multi Range
Request4 = "GET /range.txt HTTP/1.1\r\nHost:" ++ Host ++
"\r\nRange:bytes=0-0,2-2,-1\r\n\r\n",
{ok, Socket4} = inets_test_lib:connect_byte(Type, Host, Port),
inets_test_lib:send(Type, Socket4, Request4),
ok = validateRangeRequest(Socket4,[],"130",$2,$0,$6),
inets_test_lib:close(Type, Socket4).
if_test(Type, Port, Host, Node, DocRoot)->
{ok, FileInfo} =
file:read_file_info(filename:join([DocRoot,"index.html"])),
CreatedSec =
calendar:datetime_to_gregorian_seconds(FileInfo#file_info.mtime),
Mod = httpd_util:rfc1123_date(calendar:gregorian_seconds_to_datetime(
CreatedSec-1)),
%% Test that we get the data when the file is modified
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:" ++ Host ++
"\r\nIf-Modified-Since:" ++
Mod ++ "\r\n\r\n",
[{statuscode, 200}]),
Mod1 = httpd_util:rfc1123_date(calendar:gregorian_seconds_to_datetime(
CreatedSec+100)),
ok = httpd_test_lib:verify_request(Type,Host,Port,Node,
"GET / HTTP/1.1\r\nHost:"
++ Host ++"\r\nIf-Modified-Since:"
++ Mod1 ++"\r\n\r\n",
[{statuscode, 304}]),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:" ++ Host ++
"\r\nIf-Modified-Since:" ++
"AAA[...]AAAA" ++ "\r\n\r\n",
[{statuscode, 400}]),
Mod2 = httpd_util:rfc1123_date(calendar:gregorian_seconds_to_datetime(
CreatedSec+1)),
%% Control that the If-Unmodified-Header lmits the response
ok = httpd_test_lib:verify_request(Type,Host,Port,Node,
"GET / HTTP/1.1\r\nHost:"
++ Host ++
"\r\nIf-Unmodified-Since:" ++ Mod2
++ "\r\n\r\n",
[{statuscode, 200}]),
Mod3 = httpd_util:rfc1123_date(calendar:gregorian_seconds_to_datetime(
CreatedSec-1)),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:"
++ Host ++
"\r\nIf-Unmodified-Since:"++ Mod3
++"\r\n\r\n",
[{statuscode, 412}]),
%% Control that we get the body when the etag match
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:" ++ Host
++"\r\n"++
"If-Match:"++
httpd_util:create_etag(FileInfo)++
"\r\n\r\n",
[{statuscode, 200}]),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:" ++
Host ++ "\r\n"++
"If-Match:NotEtag\r\n\r\n",
[{statuscode, 412}]),
%% Control the response when the if-none-match header is there
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:"
++ Host ++"\r\n"++
"If-None-Match:NoTaag," ++
httpd_util:create_etag(FileInfo) ++
"\r\n\r\n",
[{statuscode, 304}]),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET / HTTP/1.1\r\nHost:"
++ Host ++ "\r\n"++
"If-None-Match:NotEtag,"
"NeihterEtag\r\n\r\n",
[{statuscode,200}]),
ok.
trace(Type, Port, Host, Node)->
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"TRACE / HTTP/1.1\r\n" ++
"Host:" ++ Host ++ "\r\n" ++
"Max-Forwards:2\r\n\r\n",
[{statuscode, 200}]).
head(Type, Port, Host, Node)->
%% mod_include
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"HEAD /fsize.shtml HTTP/1.0\r\n\r\n",
[{statuscode, 200},
{version, "HTTP/1.0"}]),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"HEAD /fsize.shtml HTTP/1.1\r\nhost:" ++
Host ++ "\r\n\r\n", [{statuscode, 200}]),
%% mod_esi
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"HEAD /cgi-bin/erl/httpd_example/newformat"
" HTTP/1.0\r\n\r\n", [{statuscode, 200},
{version, "HTTP/1.0"}]),
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"HEAD /cgi-bin/erl/httpd_example/newformat "
"HTTP/1.1\r\nhost:" ++ Host ++ "\r\n\r\n",
[{statuscode, 200}]),
%% mod_cgi
Script =
case test_server:os_type() of
{win32, _} ->
"printenv.bat";
_ ->
"printenv.sh"
end,
ok = httpd_test_lib:verify_request(Type,Host,Port,Node,"HEAD /cgi-bin/"
++ Script ++ " HTTP/1.0\r\n\r\n",
[{statuscode, 200},
{version, "HTTP/1.0"}]),
ok = httpd_test_lib:verify_request(Type,Host,Port,Node, "HEAD /cgi-bin/"
++ Script ++ " HTTP/1.1\r\nhost:" ++
Host ++ "\r\n\r\n",
[{statuscode, 200}]).
mod_cgi_chunked_encoding_test(_, _, _, _, [])->
ok;
mod_cgi_chunked_encoding_test(Type, Port, Host, Node, [Request| Rest])->
ok = httpd_test_lib:verify_request(Type, Host, Port, Node, Request,
[{statuscode, 200}]),
mod_cgi_chunked_encoding_test(Type, Port, Host, Node, Rest).
mod_esi_chunk_timeout(Type, Port, Host, Node) ->
ok = httpd_test_lib:verify_request(Type, Host, Port, Node,
"GET /cgi-bin/erl/httpd_example/chunk_timeout?input=20000 HTTP/1.1\r\n"
"Host:"++ Host ++"\r\n"
"\r\n",
[{statuscode, 200},
{header, "warning"}]).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
validateRangeRequest(Socket,Response,ValidBody,C,O,DE)->
receive
{_,Socket,Data} ->
case string:str(Data,"\r\n") of
0->
validateRangeRequest(Socket,
Response ++ Data,
ValidBody, C, O, DE);
_N ->
case Response ++ Data of
[$H,$T,$T,$P,$/,$1,$.,$1,$ ,C,O,DE | _Rest]->
case [C,O,DE] of
"206" ->
validateRangeRequest1(Socket,
Response ++ Data,
ValidBody);
_ ->
bad_code
end;
_->
error
end
end;
_Error ->
error
end.
validateRangeRequest1(Socket, Response, ValidBody) ->
case end_of_header(Response) of
false ->
receive
{_,Socket,Data} ->
validateRangeRequest1(Socket, Response ++ Data,
ValidBody);
_->
error
end;
{true, Head1, Body, _Size} ->
%% In this case size will be 0 if it is a multipart so
%% don't use it.
validateRangeRequest2(Socket, Head1, Body, ValidBody,
getRangeSize(Head1))
end.
validateRangeRequest2(Socket, Head, Body, ValidBody, {multiPart,Boundary})->
case endReached(Body,Boundary) of
true ->
validateMultiPartRangeRequest(Body, ValidBody, Boundary);
false->
receive
{_, Socket, Data} ->
validateRangeRequest2(Socket, Head, Body ++ Data,
ValidBody, {multiPart, Boundary});
{_, Socket} ->
error;
_ ->
error
end
end;
validateRangeRequest2(Socket, Head, Body, ValidBody, BodySize)
when is_integer(BodySize) ->
case length(Body) of
Size when Size =:= BodySize ->
case Body of
ValidBody ->
ok;
Body ->
error
end;
Size when Size < BodySize ->
receive
{_, Socket, Data} ->
validateRangeRequest2(Socket, Head,
Body ++ Data, ValidBody, BodySize);
_ ->
error
end;
_ ->
error
end.
validateMultiPartRangeRequest(Body, ValidBody, Boundary)->
case re:split(Body,"--"++Boundary++"--", [{return, list}]) of
%%Last is the epilogue and must be ignored
[First | _Last]->
%%First is now the actuall http request body.
case re:split(First, "--" ++ Boundary, [{return, list}]) of
%%Parts is now a list of ranges and the heads for each range
%%Gues we try to split out the body
Parts->
case lists:flatten(lists:map(fun splitRange/1,Parts)) of
ValidBody->
ok;
ParsedBody->
error = ParsedBody
end
end;
_ ->
error
end.
splitRange(Part)->
case re:split(Part, "\r\n\r\n", [{return, list}]) of
[_, Body] ->
string:substr(Body, 1, length(Body) - 2);
_ ->
[]
end.
endReached(Body, Boundary)->
EndBound = "--" ++ Boundary ++ "--",
case string:str(Body, EndBound) of
0 ->
false;
_ ->
true
end.
getRangeSize(Head)->
case controlMimeType(Head) of
{multiPart, BoundaryString}->
{multiPart, BoundaryString};
_X1 ->
case re:run(Head, ?CONTENT_RANGE "bytes=.*\r\n", [{capture, first}]) of
{match, [{Start, Lenght}]} ->
%% Get the range data remove the fieldname and the
%% end of line.
RangeInfo = string:substr(Head, Start + 1 + 20,
Lenght - (20 +2)),
rangeSize(string:strip(RangeInfo));
_X2 ->
error
end
end.
%%RangeInfo is NNN1-NNN2/NNN3
%%NNN1=RangeStartByte
%%NNN2=RangeEndByte
%%NNN3=total amount of bytes in file
rangeSize([$=|RangeInfo]) ->
rangeSize(RangeInfo);
rangeSize(RangeInfo) ->
StartByte = lists:takewhile(fun(X)->
num(X, true)
end, RangeInfo),
RangeInfo2 = string:substr(RangeInfo, length(StartByte) + 2),
EndByte = lists:takewhile(fun(X)->
num(X,true)
end, RangeInfo2),
case list_to_integer(EndByte) - list_to_integer(StartByte) of
Val when is_number(Val) ->
%%Add one since it is startByte to endbyte ie 0-0 is 1
%%byte 0-99 is 100 bytes
Val + 1;
_Val ->
error
end.
num(CharVal, RetVal) when (CharVal >= 48) andalso (CharVal =< 57) ->
RetVal;
num(_CharVal, true) ->
false;
num(_CharVal, false) ->
true.
controlMimeType(Head)->
case re:run(Head,?CONTENT_TYPE "multipart/byteranges.*\r\n", [{capture, first}]) of
{match, [{Start,Length}]}->
FieldNameLen = length(?CONTENT_TYPE "multipart/byteranges"),
case clearBoundary(string:substr(Head, Start + 1 + FieldNameLen,
Length - (FieldNameLen+2))) of
error ->
error;
BoundaryStr ->
{multiPart,BoundaryStr}
end;
nomatch->
0;
_ ->
error
end.
clearBoundary(Boundary)->
case re:run(Boundary, "boundary=.*\$", [{capture, first}]) of
{match, [{Start1, Length1}]}->
BoundLen = length("boundary="),
string:substr(Boundary, Start1 + 1 + BoundLen, Length1 - BoundLen);
_ ->
error
end.
end_of_header(HeaderPart) ->
case httpd_util:split(HeaderPart,"\r\n\r\n",2) of
{ok, [Head, Body]} ->
{true, Head, Body, get_body_size(Head)};
_Pos ->
false
end.
get_body_size(Head) ->
case re:run(Head,?CONTENT_LENGTH ".*\r\n", [{capture, first}]) of
{match, [{Start, Length}]} ->
%% 15 is length of Content-Length,
%% 17 Is length of Content-Length and \r\
S = list_to_integer(
string:strip(string:substr(Head, Start +1 + 15, Length-17))),
S;
_->
0
end.