cowboy_http_req.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. %% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
  2. %% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
  3. %%
  4. %% Permission to use, copy, modify, and/or distribute this software for any
  5. %% purpose with or without fee is hereby granted, provided that the above
  6. %% copyright notice and this permission notice appear in all copies.
  7. %%
  8. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. %% @doc HTTP request manipulation API.
  16. %%
  17. %% Almost all functions in this module return a new <em>Req</em> variable.
  18. %% It should always be used instead of the one used in your function call
  19. %% because it keeps the state of the request. It also allows Cowboy to do
  20. %% some lazy evaluation and cache results where possible.
  21. -module(cowboy_http_req).
  22. -export([
  23. method/1, version/1, peer/1,
  24. host/1, host_info/1, raw_host/1, port/1,
  25. path/1, path_info/1, raw_path/1,
  26. qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
  27. binding/2, binding/3, bindings/1,
  28. header/2, header/3, headers/1,
  29. cookie/2, cookie/3, cookies/1
  30. ]). %% Request API.
  31. -export([
  32. body/1, body/2, body_qs/1
  33. ]). %% Request Body API.
  34. -export([
  35. reply/4, chunked_reply/3, chunk/2
  36. ]). %% Response API.
  37. -export([
  38. compact/1
  39. ]). %% Misc API.
  40. -include("include/http.hrl").
  41. -include_lib("eunit/include/eunit.hrl").
  42. %% Request API.
  43. %% @doc Return the HTTP method of the request.
  44. -spec method(#http_req{}) -> {http_method(), #http_req{}}.
  45. method(Req) ->
  46. {Req#http_req.method, Req}.
  47. %% @doc Return the HTTP version used for the request.
  48. -spec version(#http_req{}) -> {http_version(), #http_req{}}.
  49. version(Req) ->
  50. {Req#http_req.version, Req}.
  51. %% @doc Return the peer address and port number of the remote host.
  52. -spec peer(#http_req{}) -> {{inet:ip_address(), inet:ip_port()}, #http_req{}}.
  53. peer(Req=#http_req{socket=Socket, transport=Transport, peer=undefined}) ->
  54. {ok, Peer} = Transport:peername(Socket),
  55. {Peer, Req#http_req{peer=Peer}};
  56. peer(Req) ->
  57. {Req#http_req.peer, Req}.
  58. %% @doc Return the tokens for the hostname requested.
  59. -spec host(#http_req{}) -> {cowboy_dispatcher:path_tokens(), #http_req{}}.
  60. host(Req) ->
  61. {Req#http_req.host, Req}.
  62. %% @doc Return the extra host information obtained from partially matching
  63. %% the hostname using <em>'...'</em>.
  64. -spec host_info(#http_req{})
  65. -> {cowboy_dispatcher:path_tokens() | undefined, #http_req{}}.
  66. host_info(Req) ->
  67. {Req#http_req.host_info, Req}.
  68. %% @doc Return the raw host directly taken from the request.
  69. -spec raw_host(#http_req{}) -> {binary(), #http_req{}}.
  70. raw_host(Req) ->
  71. {Req#http_req.raw_host, Req}.
  72. %% @doc Return the port used for this request.
  73. -spec port(#http_req{}) -> {inet:ip_port(), #http_req{}}.
  74. port(Req) ->
  75. {Req#http_req.port, Req}.
  76. %% @doc Return the tokens for the path requested.
  77. -spec path(#http_req{}) -> {cowboy_dispatcher:path_tokens(), #http_req{}}.
  78. path(Req) ->
  79. {Req#http_req.path, Req}.
  80. %% @doc Return the extra path information obtained from partially matching
  81. %% the patch using <em>'...'</em>.
  82. -spec path_info(#http_req{})
  83. -> {cowboy_dispatcher:path_tokens() | undefined, #http_req{}}.
  84. path_info(Req) ->
  85. {Req#http_req.path_info, Req}.
  86. %% @doc Return the raw path directly taken from the request.
  87. -spec raw_path(#http_req{}) -> {binary(), #http_req{}}.
  88. raw_path(Req) ->
  89. {Req#http_req.raw_path, Req}.
  90. %% @equiv qs_val(Name, Req, undefined)
  91. -spec qs_val(binary(), #http_req{})
  92. -> {binary() | true | undefined, #http_req{}}.
  93. qs_val(Name, Req) when is_binary(Name) ->
  94. qs_val(Name, Req, undefined).
  95. %% @doc Return the query string value for the given key, or a default if
  96. %% missing.
  97. -spec qs_val(binary(), #http_req{}, Default)
  98. -> {binary() | true | Default, #http_req{}} when Default::any().
  99. qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default)
  100. when is_binary(Name) ->
  101. QsVals = parse_qs(RawQs),
  102. qs_val(Name, Req#http_req{qs_vals=QsVals}, Default);
  103. qs_val(Name, Req, Default) ->
  104. case lists:keyfind(Name, 1, Req#http_req.qs_vals) of
  105. {Name, Value} -> {Value, Req};
  106. false -> {Default, Req}
  107. end.
  108. %% @doc Return the full list of query string values.
  109. -spec qs_vals(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
  110. qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
  111. QsVals = parse_qs(RawQs),
  112. qs_vals(Req#http_req{qs_vals=QsVals});
  113. qs_vals(Req=#http_req{qs_vals=QsVals}) ->
  114. {QsVals, Req}.
  115. %% @doc Return the raw query string directly taken from the request.
  116. -spec raw_qs(#http_req{}) -> {binary(), #http_req{}}.
  117. raw_qs(Req) ->
  118. {Req#http_req.raw_qs, Req}.
  119. %% @equiv binding(Name, Req, undefined)
  120. -spec binding(atom(), #http_req{}) -> {binary() | undefined, #http_req{}}.
  121. binding(Name, Req) when is_atom(Name) ->
  122. binding(Name, Req, undefined).
  123. %% @doc Return the binding value for the given key obtained when matching
  124. %% the host and path against the dispatch list, or a default if missing.
  125. -spec binding(atom(), #http_req{}, Default)
  126. -> {binary() | Default, #http_req{}} when Default::any().
  127. binding(Name, Req, Default) when is_atom(Name) ->
  128. case lists:keyfind(Name, 1, Req#http_req.bindings) of
  129. {Name, Value} -> {Value, Req};
  130. false -> {Default, Req}
  131. end.
  132. %% @doc Return the full list of binding values.
  133. -spec bindings(#http_req{}) -> {list({atom(), binary()}), #http_req{}}.
  134. bindings(Req) ->
  135. {Req#http_req.bindings, Req}.
  136. %% @equiv header(Name, Req, undefined)
  137. -spec header(atom() | binary(), #http_req{})
  138. -> {binary() | undefined, #http_req{}}.
  139. header(Name, Req) when is_atom(Name) orelse is_binary(Name) ->
  140. header(Name, Req, undefined).
  141. %% @doc Return the header value for the given key, or a default if missing.
  142. -spec header(atom() | binary(), #http_req{}, Default)
  143. -> {binary() | Default, #http_req{}} when Default::any().
  144. header(Name, Req, Default) when is_atom(Name) orelse is_binary(Name) ->
  145. case lists:keyfind(Name, 1, Req#http_req.headers) of
  146. {Name, Value} -> {Value, Req};
  147. false -> {Default, Req}
  148. end.
  149. %% @doc Return the full list of headers.
  150. -spec headers(#http_req{}) -> {http_headers(), #http_req{}}.
  151. headers(Req) ->
  152. {Req#http_req.headers, Req}.
  153. %% @equiv cookie(Name, Req, undefined)
  154. -spec cookie(binary(), #http_req{})
  155. -> {binary() | true | undefined, #http_req{}}.
  156. cookie(Name, Req) when is_binary(Name) ->
  157. cookie(Name, Req, undefined).
  158. %% @doc Return the cookie value for the given key, or a default if
  159. %% missing.
  160. -spec cookie(binary(), #http_req{}, Default)
  161. -> {binary() | true | Default, #http_req{}} when Default::any().
  162. cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) ->
  163. case header('Cookie', Req) of
  164. {undefined, Req2} ->
  165. {Default, Req2#http_req{cookies=[]}};
  166. {RawCookie, Req2} ->
  167. Cookies = cowboy_cookies:parse_cookie(RawCookie),
  168. cookie(Name, Req2#http_req{cookies=Cookies}, Default)
  169. end;
  170. cookie(Name, Req, Default) ->
  171. case lists:keyfind(Name, 1, Req#http_req.cookies) of
  172. {Name, Value} -> {Value, Req};
  173. false -> {Default, Req}
  174. end.
  175. %% @doc Return the full list of cookie values.
  176. -spec cookies(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
  177. cookies(Req=#http_req{cookies=undefined}) ->
  178. case header('Cookie', Req) of
  179. {undefined, Req2} ->
  180. {[], Req2#http_req{cookies=[]}};
  181. {RawCookie, Req2} ->
  182. Cookies = cowboy_cookies:parse_cookie(RawCookie),
  183. cookies(Req2#http_req{cookies=Cookies})
  184. end;
  185. cookies(Req=#http_req{cookies=Cookies}) ->
  186. {Cookies, Req}.
  187. %% Request Body API.
  188. %% @doc Return the full body sent with the request, or <em>{error, badarg}</em>
  189. %% if no <em>Content-Length</em> is available.
  190. %% @todo We probably want to allow a max length.
  191. -spec body(#http_req{}) -> {ok, binary(), #http_req{}} | {error, atom()}.
  192. body(Req) ->
  193. {Length, Req2} = cowboy_http_req:header('Content-Length', Req),
  194. case Length of
  195. undefined -> {error, badarg};
  196. _Any ->
  197. Length2 = list_to_integer(binary_to_list(Length)),
  198. body(Length2, Req2)
  199. end.
  200. %% @doc Return <em>Length</em> bytes of the request body.
  201. %%
  202. %% You probably shouldn't be calling this function directly, as it expects the
  203. %% <em>Length</em> argument to be the full size of the body, and will consider
  204. %% the body to be fully read from the socket.
  205. %% @todo We probably want to configure the timeout.
  206. -spec body(non_neg_integer(), #http_req{})
  207. -> {ok, binary(), #http_req{}} | {error, atom()}.
  208. body(Length, Req=#http_req{body_state=waiting, buffer=Buffer})
  209. when Length =:= byte_size(Buffer) ->
  210. {ok, Buffer, Req#http_req{body_state=done, buffer= <<>>}};
  211. body(Length, Req=#http_req{socket=Socket, transport=Transport,
  212. body_state=waiting, buffer=Buffer})
  213. when is_integer(Length) andalso Length > byte_size(Buffer) ->
  214. case Transport:recv(Socket, Length - byte_size(Buffer), 5000) of
  215. {ok, Body} -> {ok, << Buffer/binary, Body/binary >>,
  216. Req#http_req{body_state=done, buffer= <<>>}};
  217. {error, Reason} -> {error, Reason}
  218. end.
  219. %% @doc Return the full body sent with the reqest, parsed as an
  220. %% application/x-www-form-urlencoded string. Essentially a POST query string.
  221. -spec body_qs(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
  222. body_qs(Req) ->
  223. {ok, Body, Req2} = body(Req),
  224. {parse_qs(Body), Req2}.
  225. %% Response API.
  226. %% @doc Send a reply to the client.
  227. -spec reply(http_status(), http_headers(), iodata(), #http_req{})
  228. -> {ok, #http_req{}}.
  229. reply(Code, Headers, Body, Req=#http_req{socket=Socket,
  230. transport=Transport, connection=Connection,
  231. method=Method, resp_state=waiting}) ->
  232. Head = response_head(Code, Headers, [
  233. {<<"Connection">>, atom_to_connection(Connection)},
  234. {<<"Content-Length">>,
  235. list_to_binary(integer_to_list(iolist_size(Body)))},
  236. {<<"Date">>, cowboy_clock:rfc1123()},
  237. {<<"Server">>, <<"Cowboy">>}
  238. ]),
  239. case Method of
  240. 'HEAD' -> Transport:send(Socket, Head);
  241. _ -> Transport:send(Socket, [Head, Body])
  242. end,
  243. {ok, Req#http_req{resp_state=done}}.
  244. %% @doc Initiate the sending of a chunked reply to the client.
  245. %% @see cowboy_http_req:chunk/2
  246. -spec chunked_reply(http_status(), http_headers(), #http_req{})
  247. -> {ok, #http_req{}}.
  248. chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport,
  249. method='HEAD', resp_state=waiting}) ->
  250. Head = response_head(Code, Headers, [
  251. {<<"Date">>, cowboy_clock:rfc1123()},
  252. {<<"Server">>, <<"Cowboy">>}
  253. ]),
  254. Transport:send(Socket, Head),
  255. {ok, Req#http_req{resp_state=done}};
  256. chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport,
  257. resp_state=waiting}) ->
  258. Head = response_head(Code, Headers, [
  259. {<<"Connection">>, <<"close">>},
  260. {<<"Transfer-Encoding">>, <<"chunked">>},
  261. {<<"Date">>, cowboy_clock:rfc1123()},
  262. {<<"Server">>, <<"Cowboy">>}
  263. ]),
  264. Transport:send(Socket, Head),
  265. {ok, Req#http_req{resp_state=chunks}}.
  266. %% @doc Send a chunk of data.
  267. %%
  268. %% A chunked reply must have been initiated before calling this function.
  269. -spec chunk(iodata(), #http_req{}) -> ok.
  270. chunk(_Data, #http_req{socket=_Socket, transport=_Transport, method='HEAD'}) ->
  271. ok;
  272. chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) ->
  273. Transport:send(Socket, [integer_to_list(iolist_size(Data), 16),
  274. <<"\r\n">>, Data, <<"\r\n">>]).
  275. %% Misc API.
  276. %% @doc Compact the request data by removing all non-system information.
  277. %%
  278. %% This essentially removes the host, path, query string, bindings and headers.
  279. %% Use it when you really need to save up memory, for example when having
  280. %% many concurrent long-running connections.
  281. -spec compact(#http_req{}) -> #http_req{}.
  282. compact(Req) ->
  283. Req#http_req{host=undefined, host_info=undefined, path=undefined,
  284. path_info=undefined, qs_vals=undefined, raw_qs=undefined,
  285. bindings=undefined, headers=[]}.
  286. %% Internal.
  287. -spec parse_qs(binary()) -> list({binary(), binary() | true}).
  288. parse_qs(<<>>) ->
  289. [];
  290. parse_qs(Qs) ->
  291. Tokens = binary:split(Qs, <<"&">>, [global, trim]),
  292. [case binary:split(Token, <<"=">>) of
  293. [Token] -> {quoted:from_url(Token), true};
  294. [Name, Value] -> {quoted:from_url(Name), quoted:from_url(Value)}
  295. end || Token <- Tokens].
  296. -spec response_head(http_status(), http_headers(), http_headers()) -> iolist().
  297. response_head(Code, Headers, DefaultHeaders) ->
  298. StatusLine = <<"HTTP/1.1 ", (status(Code))/binary, "\r\n">>,
  299. Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers],
  300. Headers3 = lists:keysort(1, Headers2),
  301. Headers4 = lists:ukeymerge(1, Headers3, DefaultHeaders),
  302. Headers5 = [<< Key/binary, ": ", Value/binary, "\r\n" >>
  303. || {Key, Value} <- Headers4],
  304. [StatusLine, Headers5, <<"\r\n">>].
  305. -spec atom_to_connection(keepalive) -> <<_:80>>;
  306. (close) -> <<_:40>>.
  307. atom_to_connection(keepalive) ->
  308. <<"keep-alive">>;
  309. atom_to_connection(close) ->
  310. <<"close">>.
  311. -spec status(http_status()) -> binary().
  312. status(100) -> <<"100 Continue">>;
  313. status(101) -> <<"101 Switching Protocols">>;
  314. status(102) -> <<"102 Processing">>;
  315. status(200) -> <<"200 OK">>;
  316. status(201) -> <<"201 Created">>;
  317. status(202) -> <<"202 Accepted">>;
  318. status(203) -> <<"203 Non-Authoritative Information">>;
  319. status(204) -> <<"204 No Content">>;
  320. status(205) -> <<"205 Reset Content">>;
  321. status(206) -> <<"206 Partial Content">>;
  322. status(207) -> <<"207 Multi-Status">>;
  323. status(226) -> <<"226 IM Used">>;
  324. status(300) -> <<"300 Multiple Choices">>;
  325. status(301) -> <<"301 Moved Permanently">>;
  326. status(302) -> <<"302 Found">>;
  327. status(303) -> <<"303 See Other">>;
  328. status(304) -> <<"304 Not Modified">>;
  329. status(305) -> <<"305 Use Proxy">>;
  330. status(306) -> <<"306 Switch Proxy">>;
  331. status(307) -> <<"307 Temporary Redirect">>;
  332. status(400) -> <<"400 Bad Request">>;
  333. status(401) -> <<"401 Unauthorized">>;
  334. status(402) -> <<"402 Payment Required">>;
  335. status(403) -> <<"403 Forbidden">>;
  336. status(404) -> <<"404 Not Found">>;
  337. status(405) -> <<"405 Method Not Allowed">>;
  338. status(406) -> <<"406 Not Acceptable">>;
  339. status(407) -> <<"407 Proxy Authentication Required">>;
  340. status(408) -> <<"408 Request Timeout">>;
  341. status(409) -> <<"409 Conflict">>;
  342. status(410) -> <<"410 Gone">>;
  343. status(411) -> <<"411 Length Required">>;
  344. status(412) -> <<"412 Precondition Failed">>;
  345. status(413) -> <<"413 Request Entity Too Large">>;
  346. status(414) -> <<"414 Request-URI Too Long">>;
  347. status(415) -> <<"415 Unsupported Media Type">>;
  348. status(416) -> <<"416 Requested Range Not Satisfiable">>;
  349. status(417) -> <<"417 Expectation Failed">>;
  350. status(418) -> <<"418 I'm a teapot">>;
  351. status(422) -> <<"422 Unprocessable Entity">>;
  352. status(423) -> <<"423 Locked">>;
  353. status(424) -> <<"424 Failed Dependency">>;
  354. status(425) -> <<"425 Unordered Collection">>;
  355. status(426) -> <<"426 Upgrade Required">>;
  356. status(500) -> <<"500 Internal Server Error">>;
  357. status(501) -> <<"501 Not Implemented">>;
  358. status(502) -> <<"502 Bad Gateway">>;
  359. status(503) -> <<"503 Service Unavailable">>;
  360. status(504) -> <<"504 Gateway Timeout">>;
  361. status(505) -> <<"505 HTTP Version Not Supported">>;
  362. status(506) -> <<"506 Variant Also Negotiates">>;
  363. status(507) -> <<"507 Insufficient Storage">>;
  364. status(510) -> <<"510 Not Extended">>;
  365. status(B) when is_binary(B) -> B.
  366. -spec header_to_binary(http_header()) -> binary().
  367. header_to_binary('Cache-Control') -> <<"Cache-Control">>;
  368. header_to_binary('Connection') -> <<"Connection">>;
  369. header_to_binary('Date') -> <<"Date">>;
  370. header_to_binary('Pragma') -> <<"Pragma">>;
  371. header_to_binary('Transfer-Encoding') -> <<"Transfer-Encoding">>;
  372. header_to_binary('Upgrade') -> <<"Upgrade">>;
  373. header_to_binary('Via') -> <<"Via">>;
  374. header_to_binary('Accept') -> <<"Accept">>;
  375. header_to_binary('Accept-Charset') -> <<"Accept-Charset">>;
  376. header_to_binary('Accept-Encoding') -> <<"Accept-Encoding">>;
  377. header_to_binary('Accept-Language') -> <<"Accept-Language">>;
  378. header_to_binary('Authorization') -> <<"Authorization">>;
  379. header_to_binary('From') -> <<"From">>;
  380. header_to_binary('Host') -> <<"Host">>;
  381. header_to_binary('If-Modified-Since') -> <<"If-Modified-Since">>;
  382. header_to_binary('If-Match') -> <<"If-Match">>;
  383. header_to_binary('If-None-Match') -> <<"If-None-Match">>;
  384. header_to_binary('If-Range') -> <<"If-Range">>;
  385. header_to_binary('If-Unmodified-Since') -> <<"If-Unmodified-Since">>;
  386. header_to_binary('Max-Forwards') -> <<"Max-Forwards">>;
  387. header_to_binary('Proxy-Authorization') -> <<"Proxy-Authorization">>;
  388. header_to_binary('Range') -> <<"Range">>;
  389. header_to_binary('Referer') -> <<"Referer">>;
  390. header_to_binary('User-Agent') -> <<"User-Agent">>;
  391. header_to_binary('Age') -> <<"Age">>;
  392. header_to_binary('Location') -> <<"Location">>;
  393. header_to_binary('Proxy-Authenticate') -> <<"Proxy-Authenticate">>;
  394. header_to_binary('Public') -> <<"Public">>;
  395. header_to_binary('Retry-After') -> <<"Retry-After">>;
  396. header_to_binary('Server') -> <<"Server">>;
  397. header_to_binary('Vary') -> <<"Vary">>;
  398. header_to_binary('Warning') -> <<"Warning">>;
  399. header_to_binary('Www-Authenticate') -> <<"Www-Authenticate">>;
  400. header_to_binary('Allow') -> <<"Allow">>;
  401. header_to_binary('Content-Base') -> <<"Content-Base">>;
  402. header_to_binary('Content-Encoding') -> <<"Content-Encoding">>;
  403. header_to_binary('Content-Language') -> <<"Content-Language">>;
  404. header_to_binary('Content-Length') -> <<"Content-Length">>;
  405. header_to_binary('Content-Location') -> <<"Content-Location">>;
  406. header_to_binary('Content-Md5') -> <<"Content-Md5">>;
  407. header_to_binary('Content-Range') -> <<"Content-Range">>;
  408. header_to_binary('Content-Type') -> <<"Content-Type">>;
  409. header_to_binary('Etag') -> <<"Etag">>;
  410. header_to_binary('Expires') -> <<"Expires">>;
  411. header_to_binary('Last-Modified') -> <<"Last-Modified">>;
  412. header_to_binary('Accept-Ranges') -> <<"Accept-Ranges">>;
  413. header_to_binary('Set-Cookie') -> <<"Set-Cookie">>;
  414. header_to_binary('Set-Cookie2') -> <<"Set-Cookie2">>;
  415. header_to_binary('X-Forwarded-For') -> <<"X-Forwarded-For">>;
  416. header_to_binary('Cookie') -> <<"Cookie">>;
  417. header_to_binary('Keep-Alive') -> <<"Keep-Alive">>;
  418. header_to_binary('Proxy-Connection') -> <<"Proxy-Connection">>;
  419. header_to_binary(B) when is_binary(B) -> B.
  420. %% Tests.
  421. -ifdef(TEST).
  422. parse_qs_test_() ->
  423. %% {Qs, Result}
  424. Tests = [
  425. {<<"">>, []},
  426. {<<"a=b">>, [{<<"a">>, <<"b">>}]},
  427. {<<"aaa=bbb">>, [{<<"aaa">>, <<"bbb">>}]},
  428. {<<"a&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
  429. {<<"a=b&c&d=e">>, [{<<"a">>, <<"b">>},
  430. {<<"c">>, true}, {<<"d">>, <<"e">>}]},
  431. {<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]},
  432. {<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]}
  433. ],
  434. [{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
  435. -endif.