zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

cow_http.erl (14134B)


      1 %% Copyright (c) 2013-2018, Loïc Hoguin <essen@ninenines.eu>
      2 %%
      3 %% Permission to use, copy, modify, and/or distribute this software for any
      4 %% purpose with or without fee is hereby granted, provided that the above
      5 %% copyright notice and this permission notice appear in all copies.
      6 %%
      7 %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      8 %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
      9 %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     10 %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     11 %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     12 %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     13 %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     14 
     15 -module(cow_http).
     16 
     17 -export([parse_request_line/1]).
     18 -export([parse_status_line/1]).
     19 -export([status_to_integer/1]).
     20 -export([parse_headers/1]).
     21 
     22 -export([parse_fullpath/1]).
     23 -export([parse_version/1]).
     24 
     25 -export([request/4]).
     26 -export([response/3]).
     27 -export([headers/1]).
     28 -export([version/1]).
     29 
     30 -type version() :: 'HTTP/1.0' | 'HTTP/1.1'.
     31 -export_type([version/0]).
     32 
     33 -type status() :: 100..999.
     34 -export_type([status/0]).
     35 
     36 -type headers() :: [{binary(), iodata()}].
     37 -export_type([headers/0]).
     38 
     39 -include("cow_inline.hrl").
     40 
     41 %% @doc Parse the request line.
     42 
     43 -spec parse_request_line(binary()) -> {binary(), binary(), version(), binary()}.
     44 parse_request_line(Data) ->
     45 	{Pos, _} = binary:match(Data, <<"\r">>),
     46 	<<RequestLine:Pos/binary, "\r\n", Rest/bits>> = Data,
     47 	[Method, Target, Version0] = binary:split(RequestLine, <<$\s>>, [trim_all, global]),
     48 	Version = case Version0 of
     49 		<<"HTTP/1.1">> -> 'HTTP/1.1';
     50 		<<"HTTP/1.0">> -> 'HTTP/1.0'
     51 	end,
     52 	{Method, Target, Version, Rest}.
     53 
     54 -ifdef(TEST).
     55 parse_request_line_test_() ->
     56 	Tests = [
     57 		{<<"GET /path HTTP/1.0\r\nRest">>,
     58 			{<<"GET">>, <<"/path">>, 'HTTP/1.0', <<"Rest">>}},
     59 		{<<"GET /path HTTP/1.1\r\nRest">>,
     60 			{<<"GET">>, <<"/path">>, 'HTTP/1.1', <<"Rest">>}},
     61 		{<<"CONNECT proxy.example.org:1080 HTTP/1.1\r\nRest">>,
     62 			{<<"CONNECT">>, <<"proxy.example.org:1080">>, 'HTTP/1.1', <<"Rest">>}}
     63 	],
     64 	[{V, fun() -> R = parse_request_line(V) end}
     65 		|| {V, R} <- Tests].
     66 
     67 parse_request_line_error_test_() ->
     68 	Tests = [
     69 		<<>>,
     70 		<<"GET">>,
     71 		<<"GET /path\r\n">>,
     72 		<<"GET /path HTTP/1.1">>,
     73 		<<"GET /path HTTP/1.1\r">>,
     74 		<<"GET /path HTTP/1.1\n">>,
     75 		<<"GET /path HTTP/0.9\r\n">>,
     76 		<<"content-type: text/plain\r\n">>,
     77 		<<0:80, "\r\n">>
     78 	],
     79 	[{V, fun() -> {'EXIT', _} = (catch parse_request_line(V)) end}
     80 		|| V <- Tests].
     81 
     82 horse_parse_request_line_get_path() ->
     83 	horse:repeat(200000,
     84 		parse_request_line(<<"GET /path HTTP/1.1\r\n">>)
     85 	).
     86 -endif.
     87 
     88 %% @doc Parse the status line.
     89 
     90 -spec parse_status_line(binary()) -> {version(), status(), binary(), binary()}.
     91 parse_status_line(<< "HTTP/1.1 200 OK\r\n", Rest/bits >>) ->
     92 	{'HTTP/1.1', 200, <<"OK">>, Rest};
     93 parse_status_line(<< "HTTP/1.1 404 Not Found\r\n", Rest/bits >>) ->
     94 	{'HTTP/1.1', 404, <<"Not Found">>, Rest};
     95 parse_status_line(<< "HTTP/1.1 500 Internal Server Error\r\n", Rest/bits >>) ->
     96 	{'HTTP/1.1', 500, <<"Internal Server Error">>, Rest};
     97 parse_status_line(<< "HTTP/1.1 ", Status/bits >>) ->
     98 	parse_status_line(Status, 'HTTP/1.1');
     99 parse_status_line(<< "HTTP/1.0 ", Status/bits >>) ->
    100 	parse_status_line(Status, 'HTTP/1.0').
    101 
    102 parse_status_line(<<H, T, U, " ", Rest/bits>>, Version) ->
    103 	Status = status_to_integer(H, T, U),
    104 	{Pos, _} = binary:match(Rest, <<"\r">>),
    105 	<< StatusStr:Pos/binary, "\r\n", Rest2/bits >> = Rest,
    106 	{Version, Status, StatusStr, Rest2}.
    107 
    108 -spec status_to_integer(status() | binary()) -> status().
    109 status_to_integer(Status) when is_integer(Status) ->
    110 	Status;
    111 status_to_integer(Status) ->
    112 	case Status of
    113 		<<H, T, U>> ->
    114 			status_to_integer(H, T, U);
    115 		<<H, T, U, " ", _/bits>> ->
    116 			status_to_integer(H, T, U)
    117 	end.
    118 
    119 status_to_integer(H, T, U)
    120 		when $0 =< H, H =< $9, $0 =< T, T =< $9, $0 =< U, U =< $9 ->
    121 	(H - $0) * 100 + (T - $0) * 10 + (U - $0).
    122 
    123 -ifdef(TEST).
    124 parse_status_line_test_() ->
    125 	Tests = [
    126 		{<<"HTTP/1.1 200 OK\r\nRest">>,
    127 			{'HTTP/1.1', 200, <<"OK">>, <<"Rest">>}},
    128 		{<<"HTTP/1.0 404 Not Found\r\nRest">>,
    129 			{'HTTP/1.0', 404, <<"Not Found">>, <<"Rest">>}},
    130 		{<<"HTTP/1.1 500 Something very funny here\r\nRest">>,
    131 			{'HTTP/1.1', 500, <<"Something very funny here">>, <<"Rest">>}},
    132 		{<<"HTTP/1.1 200 \r\nRest">>,
    133 			{'HTTP/1.1', 200, <<>>, <<"Rest">>}}
    134 	],
    135 	[{V, fun() -> R = parse_status_line(V) end}
    136 		|| {V, R} <- Tests].
    137 
    138 parse_status_line_error_test_() ->
    139 	Tests = [
    140 		<<>>,
    141 		<<"HTTP/1.1">>,
    142 		<<"HTTP/1.1 200\r\n">>,
    143 		<<"HTTP/1.1 200 OK">>,
    144 		<<"HTTP/1.1 200 OK\r">>,
    145 		<<"HTTP/1.1 200 OK\n">>,
    146 		<<"HTTP/0.9 200 OK\r\n">>,
    147 		<<"HTTP/1.1 42 Answer\r\n">>,
    148 		<<"HTTP/1.1 999999999 More than OK\r\n">>,
    149 		<<"content-type: text/plain\r\n">>,
    150 		<<0:80, "\r\n">>
    151 	],
    152 	[{V, fun() -> {'EXIT', _} = (catch parse_status_line(V)) end}
    153 		|| V <- Tests].
    154 
    155 horse_parse_status_line_200() ->
    156 	horse:repeat(200000,
    157 		parse_status_line(<<"HTTP/1.1 200 OK\r\n">>)
    158 	).
    159 
    160 horse_parse_status_line_404() ->
    161 	horse:repeat(200000,
    162 		parse_status_line(<<"HTTP/1.1 404 Not Found\r\n">>)
    163 	).
    164 
    165 horse_parse_status_line_500() ->
    166 	horse:repeat(200000,
    167 		parse_status_line(<<"HTTP/1.1 500 Internal Server Error\r\n">>)
    168 	).
    169 
    170 horse_parse_status_line_other() ->
    171 	horse:repeat(200000,
    172 		parse_status_line(<<"HTTP/1.1 416 Requested range not satisfiable\r\n">>)
    173 	).
    174 -endif.
    175 
    176 %% @doc Parse the list of headers.
    177 
    178 -spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}.
    179 parse_headers(Data) ->
    180 	parse_header(Data, []).
    181 
    182 parse_header(<< $\r, $\n, Rest/bits >>, Acc) ->
    183 	{lists:reverse(Acc), Rest};
    184 parse_header(Data, Acc) ->
    185 	parse_hd_name(Data, Acc, <<>>).
    186 
    187 parse_hd_name(<< C, Rest/bits >>, Acc, SoFar) ->
    188 	case C of
    189 		$: -> parse_hd_before_value(Rest, Acc, SoFar);
    190 		$\s -> parse_hd_name_ws(Rest, Acc, SoFar);
    191 		$\t -> parse_hd_name_ws(Rest, Acc, SoFar);
    192 		_ -> ?LOWER(parse_hd_name, Rest, Acc, SoFar)
    193 	end.
    194 
    195 parse_hd_name_ws(<< C, Rest/bits >>, Acc, Name) ->
    196 	case C of
    197 		$: -> parse_hd_before_value(Rest, Acc, Name);
    198 		$\s -> parse_hd_name_ws(Rest, Acc, Name);
    199 		$\t -> parse_hd_name_ws(Rest, Acc, Name)
    200 	end.
    201 
    202 parse_hd_before_value(<< $\s, Rest/bits >>, Acc, Name) ->
    203 	parse_hd_before_value(Rest, Acc, Name);
    204 parse_hd_before_value(<< $\t, Rest/bits >>, Acc, Name) ->
    205 	parse_hd_before_value(Rest, Acc, Name);
    206 parse_hd_before_value(Data, Acc, Name) ->
    207 	parse_hd_value(Data, Acc, Name, <<>>).
    208 
    209 parse_hd_value(<< $\r, Rest/bits >>, Acc, Name, SoFar) ->
    210 	case Rest of
    211 		<< $\n, C, Rest2/bits >> when C =:= $\s; C =:= $\t ->
    212 			parse_hd_value(Rest2, Acc, Name, << SoFar/binary, C >>);
    213 		<< $\n, Rest2/bits >> ->
    214 			Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1),
    215 			parse_header(Rest2, [{Name, Value}|Acc])
    216 	end;
    217 parse_hd_value(<< C, Rest/bits >>, Acc, Name, SoFar) ->
    218 	parse_hd_value(Rest, Acc, Name, << SoFar/binary, C >>).
    219 
    220 %% This function has been copied from cowboy_http.
    221 clean_value_ws_end(_, -1) ->
    222 	<<>>;
    223 clean_value_ws_end(Value, N) ->
    224 	case binary:at(Value, N) of
    225 		$\s -> clean_value_ws_end(Value, N - 1);
    226 		$\t -> clean_value_ws_end(Value, N - 1);
    227 		_ ->
    228 			S = N + 1,
    229 			<< Value2:S/binary, _/bits >> = Value,
    230 			Value2
    231 	end.
    232 
    233 -ifdef(TEST).
    234 parse_headers_test_() ->
    235 	Tests = [
    236 		{<<"\r\nRest">>,
    237 			{[], <<"Rest">>}},
    238 		{<<"Server: Erlang/R17  \r\n\r\n">>,
    239 			{[{<<"server">>, <<"Erlang/R17">>}], <<>>}},
    240 		{<<"Server: Erlang/R17\r\n"
    241 			"Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n"
    242 			"Multiline-Header: why hello!\r\n"
    243 				" I didn't see you all the way over there!\r\n"
    244 			"Content-Length: 12\r\n"
    245 			"Content-Type: text/plain\r\n"
    246 			"\r\nRest">>,
    247 			{[{<<"server">>, <<"Erlang/R17">>},
    248 				{<<"date">>, <<"Sun, 23 Feb 2014 09:30:39 GMT">>},
    249 				{<<"multiline-header">>,
    250 					<<"why hello! I didn't see you all the way over there!">>},
    251 				{<<"content-length">>, <<"12">>},
    252 				{<<"content-type">>, <<"text/plain">>}],
    253 				<<"Rest">>}}
    254 	],
    255 	[{V, fun() -> R = parse_headers(V) end}
    256 		|| {V, R} <- Tests].
    257 
    258 parse_headers_error_test_() ->
    259 	Tests = [
    260 		<<>>,
    261 		<<"\r">>,
    262 		<<"Malformed\r\n\r\n">>,
    263 		<<"content-type: text/plain\r\nMalformed\r\n\r\n">>,
    264 		<<"HTTP/1.1 200 OK\r\n\r\n">>,
    265 		<<0:80, "\r\n\r\n">>,
    266 		<<"content-type: text/plain\r\ncontent-length: 12\r\n">>
    267 	],
    268 	[{V, fun() -> {'EXIT', _} = (catch parse_headers(V)) end}
    269 		|| V <- Tests].
    270 
    271 horse_parse_headers() ->
    272 	horse:repeat(50000,
    273 		parse_headers(<<"Server: Erlang/R17\r\n"
    274 			"Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n"
    275 			"Multiline-Header: why hello!\r\n"
    276 				" I didn't see you all the way over there!\r\n"
    277 			"Content-Length: 12\r\n"
    278 			"Content-Type: text/plain\r\n"
    279 			"\r\nRest">>)
    280 	).
    281 -endif.
    282 
    283 %% @doc Extract path and query string from a binary,
    284 %% removing any fragment component.
    285 
    286 -spec parse_fullpath(binary()) -> {binary(), binary()}.
    287 parse_fullpath(Fullpath) ->
    288 	parse_fullpath(Fullpath, <<>>).
    289 
    290 parse_fullpath(<<>>, Path) -> {Path, <<>>};
    291 parse_fullpath(<< $#, _/bits >>, Path) -> {Path, <<>>};
    292 parse_fullpath(<< $?, Qs/bits >>, Path) -> parse_fullpath_query(Qs, Path, <<>>);
    293 parse_fullpath(<< C, Rest/bits >>, SoFar) -> parse_fullpath(Rest, << SoFar/binary, C >>).
    294 
    295 parse_fullpath_query(<<>>, Path, Query) -> {Path, Query};
    296 parse_fullpath_query(<< $#, _/bits >>, Path, Query) -> {Path, Query};
    297 parse_fullpath_query(<< C, Rest/bits >>, Path, SoFar) ->
    298 	parse_fullpath_query(Rest, Path, << SoFar/binary, C >>).
    299 
    300 -ifdef(TEST).
    301 parse_fullpath_test() ->
    302 	{<<"*">>, <<>>} = parse_fullpath(<<"*">>),
    303 	{<<"/">>, <<>>} = parse_fullpath(<<"/">>),
    304 	{<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource#fragment">>),
    305 	{<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource">>),
    306 	{<<"/">>, <<>>} = parse_fullpath(<<"/?">>),
    307 	{<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy#fragment">>),
    308 	{<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy">>),
    309 	{<<"/path/to/resource">>, <<"q=cowboy">>}
    310 		= parse_fullpath(<<"/path/to/resource?q=cowboy">>),
    311 	ok.
    312 -endif.
    313 
    314 %% @doc Convert an HTTP version to atom.
    315 
    316 -spec parse_version(binary()) -> version().
    317 parse_version(<<"HTTP/1.1">>) -> 'HTTP/1.1';
    318 parse_version(<<"HTTP/1.0">>) -> 'HTTP/1.0'.
    319 
    320 -ifdef(TEST).
    321 parse_version_test() ->
    322 	'HTTP/1.1' = parse_version(<<"HTTP/1.1">>),
    323 	'HTTP/1.0' = parse_version(<<"HTTP/1.0">>),
    324 	{'EXIT', _} = (catch parse_version(<<"HTTP/1.2">>)),
    325 	ok.
    326 -endif.
    327 
    328 %% @doc Return formatted request-line and headers.
    329 %% @todo Add tests when the corresponding reverse functions are added.
    330 
    331 -spec request(binary(), iodata(), version(), headers()) -> iodata().
    332 request(Method, Path, Version, Headers) ->
    333 	[Method, <<" ">>, Path, <<" ">>, version(Version), <<"\r\n">>,
    334 		[[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers],
    335 		<<"\r\n">>].
    336 
    337 -spec response(status() | binary(), version(), headers()) -> iodata().
    338 response(Status, Version, Headers) ->
    339 	[version(Version), <<" ">>, status(Status), <<"\r\n">>,
    340 		headers(Headers), <<"\r\n">>].
    341 
    342 -spec headers(headers()) -> iodata().
    343 headers(Headers) ->
    344 	[[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers].
    345 
    346 %% @doc Return the version as a binary.
    347 
    348 -spec version(version()) -> binary().
    349 version('HTTP/1.1') -> <<"HTTP/1.1">>;
    350 version('HTTP/1.0') -> <<"HTTP/1.0">>.
    351 
    352 -ifdef(TEST).
    353 version_test() ->
    354 	<<"HTTP/1.1">> = version('HTTP/1.1'),
    355 	<<"HTTP/1.0">> = version('HTTP/1.0'),
    356 	{'EXIT', _} = (catch version('HTTP/1.2')),
    357 	ok.
    358 -endif.
    359 
    360 %% @doc Return the status code and string as binary.
    361 
    362 -spec status(status() | binary()) -> binary().
    363 status(100) -> <<"100 Continue">>;
    364 status(101) -> <<"101 Switching Protocols">>;
    365 status(102) -> <<"102 Processing">>;
    366 status(103) -> <<"103 Early Hints">>;
    367 status(200) -> <<"200 OK">>;
    368 status(201) -> <<"201 Created">>;
    369 status(202) -> <<"202 Accepted">>;
    370 status(203) -> <<"203 Non-Authoritative Information">>;
    371 status(204) -> <<"204 No Content">>;
    372 status(205) -> <<"205 Reset Content">>;
    373 status(206) -> <<"206 Partial Content">>;
    374 status(207) -> <<"207 Multi-Status">>;
    375 status(208) -> <<"208 Already Reported">>;
    376 status(226) -> <<"226 IM Used">>;
    377 status(300) -> <<"300 Multiple Choices">>;
    378 status(301) -> <<"301 Moved Permanently">>;
    379 status(302) -> <<"302 Found">>;
    380 status(303) -> <<"303 See Other">>;
    381 status(304) -> <<"304 Not Modified">>;
    382 status(305) -> <<"305 Use Proxy">>;
    383 status(306) -> <<"306 Switch Proxy">>;
    384 status(307) -> <<"307 Temporary Redirect">>;
    385 status(308) -> <<"308 Permanent Redirect">>;
    386 status(400) -> <<"400 Bad Request">>;
    387 status(401) -> <<"401 Unauthorized">>;
    388 status(402) -> <<"402 Payment Required">>;
    389 status(403) -> <<"403 Forbidden">>;
    390 status(404) -> <<"404 Not Found">>;
    391 status(405) -> <<"405 Method Not Allowed">>;
    392 status(406) -> <<"406 Not Acceptable">>;
    393 status(407) -> <<"407 Proxy Authentication Required">>;
    394 status(408) -> <<"408 Request Timeout">>;
    395 status(409) -> <<"409 Conflict">>;
    396 status(410) -> <<"410 Gone">>;
    397 status(411) -> <<"411 Length Required">>;
    398 status(412) -> <<"412 Precondition Failed">>;
    399 status(413) -> <<"413 Request Entity Too Large">>;
    400 status(414) -> <<"414 Request-URI Too Long">>;
    401 status(415) -> <<"415 Unsupported Media Type">>;
    402 status(416) -> <<"416 Requested Range Not Satisfiable">>;
    403 status(417) -> <<"417 Expectation Failed">>;
    404 status(418) -> <<"418 I'm a teapot">>;
    405 status(421) -> <<"421 Misdirected Request">>;
    406 status(422) -> <<"422 Unprocessable Entity">>;
    407 status(423) -> <<"423 Locked">>;
    408 status(424) -> <<"424 Failed Dependency">>;
    409 status(425) -> <<"425 Unordered Collection">>;
    410 status(426) -> <<"426 Upgrade Required">>;
    411 status(428) -> <<"428 Precondition Required">>;
    412 status(429) -> <<"429 Too Many Requests">>;
    413 status(431) -> <<"431 Request Header Fields Too Large">>;
    414 status(451) -> <<"451 Unavailable For Legal Reasons">>;
    415 status(500) -> <<"500 Internal Server Error">>;
    416 status(501) -> <<"501 Not Implemented">>;
    417 status(502) -> <<"502 Bad Gateway">>;
    418 status(503) -> <<"503 Service Unavailable">>;
    419 status(504) -> <<"504 Gateway Timeout">>;
    420 status(505) -> <<"505 HTTP Version Not Supported">>;
    421 status(506) -> <<"506 Variant Also Negotiates">>;
    422 status(507) -> <<"507 Insufficient Storage">>;
    423 status(508) -> <<"508 Loop Detected">>;
    424 status(510) -> <<"510 Not Extended">>;
    425 status(511) -> <<"511 Network Authentication Required">>;
    426 status(B) when is_binary(B) -> B.