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.