PageRenderTime 68ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/mochiweb_headers.erl

http://github.com/basho/mochiweb
Erlang | 438 lines | 302 code | 38 blank | 98 comment | 0 complexity | a206affe8b911da4d1b26cd7f54fd091 MD5 | raw file
Possible License(s): MIT
  1. %% @author Bob Ippolito <bob@mochimedia.com>
  2. %% @copyright 2007 Mochi Media, Inc.
  3. %%
  4. %% Permission is hereby granted, free of charge, to any person obtaining a
  5. %% copy of this software and associated documentation files (the "Software"),
  6. %% to deal in the Software without restriction, including without limitation
  7. %% the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. %% and/or sell copies of the Software, and to permit persons to whom the
  9. %% Software is furnished to do so, subject to the following conditions:
  10. %%
  11. %% The above copyright notice and this permission notice shall be included in
  12. %% all copies or substantial portions of the Software.
  13. %%
  14. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  17. %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  20. %% DEALINGS IN THE SOFTWARE.
  21. %% @doc Case preserving (but case insensitive) HTTP Header dictionary.
  22. -module(mochiweb_headers).
  23. -author('bob@mochimedia.com').
  24. -export([empty/0, from_list/1, insert/3, enter/3, get_value/2, lookup/2]).
  25. -export([delete_any/2, get_primary_value/2, get_combined_value/2]).
  26. -export([default/3, enter_from_list/2, default_from_list/2]).
  27. -export([to_list/1, make/1]).
  28. -export([from_binary/1]).
  29. %% @type headers().
  30. %% @type key() = atom() | binary() | string().
  31. %% @type value() = atom() | binary() | string() | integer().
  32. %% @spec empty() -> headers()
  33. %% @doc Create an empty headers structure.
  34. empty() ->
  35. gb_trees:empty().
  36. %% @spec make(headers() | [{key(), value()}]) -> headers()
  37. %% @doc Construct a headers() from the given list.
  38. make(L) when is_list(L) ->
  39. from_list(L);
  40. %% assume a non-list is already mochiweb_headers.
  41. make(T) ->
  42. T.
  43. %% @spec from_binary(iolist()) -> headers()
  44. %% @doc Transforms a raw HTTP header into a mochiweb headers structure.
  45. %%
  46. %% The given raw HTTP header can be one of the following:
  47. %%
  48. %% 1) A string or a binary representing a full HTTP header ending with
  49. %% double CRLF.
  50. %% Examples:
  51. %% ```
  52. %% "Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n"
  53. %% <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>'''
  54. %%
  55. %% 2) A list of binaries or strings where each element represents a raw
  56. %% HTTP header line ending with a single CRLF.
  57. %% Examples:
  58. %% ```
  59. %% [<<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">>]
  60. %% ["Content-Length: 47\r\n", "Content-Type: text/plain\r\n"]
  61. %% ["Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">>]'''
  62. %%
  63. from_binary(RawHttpHeader) when is_binary(RawHttpHeader) ->
  64. from_binary(RawHttpHeader, []);
  65. from_binary(RawHttpHeaderList) ->
  66. from_binary(list_to_binary([RawHttpHeaderList, "\r\n"])).
  67. from_binary(RawHttpHeader, Acc) ->
  68. case erlang:decode_packet(httph, RawHttpHeader, []) of
  69. {ok, {http_header, _, H, _, V}, Rest} ->
  70. from_binary(Rest, [{H, V} | Acc]);
  71. _ ->
  72. make(Acc)
  73. end.
  74. %% @spec from_list([{key(), value()}]) -> headers()
  75. %% @doc Construct a headers() from the given list.
  76. from_list(List) ->
  77. lists:foldl(fun ({K, V}, T) -> insert(K, V, T) end, empty(), List).
  78. %% @spec enter_from_list([{key(), value()}], headers()) -> headers()
  79. %% @doc Insert pairs into the headers, replace any values for existing keys.
  80. enter_from_list(List, T) ->
  81. lists:foldl(fun ({K, V}, T1) -> enter(K, V, T1) end, T, List).
  82. %% @spec default_from_list([{key(), value()}], headers()) -> headers()
  83. %% @doc Insert pairs into the headers for keys that do not already exist.
  84. default_from_list(List, T) ->
  85. lists:foldl(fun ({K, V}, T1) -> default(K, V, T1) end, T, List).
  86. %% @spec to_list(headers()) -> [{key(), string()}]
  87. %% @doc Return the contents of the headers. The keys will be the exact key
  88. %% that was first inserted (e.g. may be an atom or binary, case is
  89. %% preserved).
  90. to_list(T) ->
  91. F = fun ({K, {array, L}}, Acc) ->
  92. L1 = lists:reverse(L),
  93. lists:foldl(fun (V, Acc1) -> [{K, V} | Acc1] end, Acc, L1);
  94. (Pair, Acc) ->
  95. [Pair | Acc]
  96. end,
  97. lists:reverse(lists:foldl(F, [], gb_trees:values(T))).
  98. %% @spec get_value(key(), headers()) -> string() | undefined
  99. %% @doc Return the value of the given header using a case insensitive search.
  100. %% undefined will be returned for keys that are not present.
  101. get_value(K, T) ->
  102. case lookup(K, T) of
  103. {value, {_, V}} ->
  104. expand(V);
  105. none ->
  106. undefined
  107. end.
  108. %% @spec get_primary_value(key(), headers()) -> string() | undefined
  109. %% @doc Return the value of the given header up to the first semicolon using
  110. %% a case insensitive search. undefined will be returned for keys
  111. %% that are not present.
  112. get_primary_value(K, T) ->
  113. case get_value(K, T) of
  114. undefined ->
  115. undefined;
  116. V ->
  117. lists:takewhile(fun (C) -> C =/= $; end, V)
  118. end.
  119. %% @spec get_combined_value(key(), headers()) -> string() | undefined
  120. %% @doc Return the value from the given header using a case insensitive search.
  121. %% If the value of the header is a comma-separated list where holds values
  122. %% are all identical, the identical value will be returned.
  123. %% undefined will be returned for keys that are not present or the
  124. %% values in the list are not the same.
  125. %%
  126. %% NOTE: The process isn't designed for a general purpose. If you need
  127. %% to access all values in the combined header, please refer to
  128. %% '''tokenize_header_value/1'''.
  129. %%
  130. %% Section 4.2 of the RFC 2616 (HTTP 1.1) describes multiple message-header
  131. %% fields with the same field-name may be present in a message if and only
  132. %% if the entire field-value for that header field is defined as a
  133. %% comma-separated list [i.e., #(values)].
  134. get_combined_value(K, T) ->
  135. case get_value(K, T) of
  136. undefined ->
  137. undefined;
  138. V ->
  139. case sets:to_list(sets:from_list(tokenize_header_value(V))) of
  140. [Val] ->
  141. Val;
  142. _ ->
  143. undefined
  144. end
  145. end.
  146. %% @spec lookup(key(), headers()) -> {value, {key(), string()}} | none
  147. %% @doc Return the case preserved key and value for the given header using
  148. %% a case insensitive search. none will be returned for keys that are
  149. %% not present.
  150. lookup(K, T) ->
  151. case gb_trees:lookup(normalize(K), T) of
  152. {value, {K0, V}} ->
  153. {value, {K0, expand(V)}};
  154. none ->
  155. none
  156. end.
  157. %% @spec default(key(), value(), headers()) -> headers()
  158. %% @doc Insert the pair into the headers if it does not already exist.
  159. default(K, V, T) ->
  160. K1 = normalize(K),
  161. V1 = any_to_list(V),
  162. try gb_trees:insert(K1, {K, V1}, T)
  163. catch
  164. error:{key_exists, _} ->
  165. T
  166. end.
  167. %% @spec enter(key(), value(), headers()) -> headers()
  168. %% @doc Insert the pair into the headers, replacing any pre-existing key.
  169. enter(K, V, T) ->
  170. K1 = normalize(K),
  171. V1 = any_to_list(V),
  172. gb_trees:enter(K1, {K, V1}, T).
  173. %% @spec insert(key(), value(), headers()) -> headers()
  174. %% @doc Insert the pair into the headers, merging with any pre-existing key.
  175. %% A merge is done with Value = V0 ++ ", " ++ V1.
  176. insert(K, V, T) ->
  177. K1 = normalize(K),
  178. V1 = any_to_list(V),
  179. try gb_trees:insert(K1, {K, V1}, T)
  180. catch
  181. error:{key_exists, _} ->
  182. {K0, V0} = gb_trees:get(K1, T),
  183. V2 = merge(K1, V1, V0),
  184. gb_trees:update(K1, {K0, V2}, T)
  185. end.
  186. %% @spec delete_any(key(), headers()) -> headers()
  187. %% @doc Delete the header corresponding to key if it is present.
  188. delete_any(K, T) ->
  189. K1 = normalize(K),
  190. gb_trees:delete_any(K1, T).
  191. %% Internal API
  192. tokenize_header_value(undefined) ->
  193. undefined;
  194. tokenize_header_value(V) ->
  195. reversed_tokens(trim_and_reverse(V, false), [], []).
  196. trim_and_reverse([S | Rest], Reversed) when S=:=$ ; S=:=$\n; S=:=$\t ->
  197. trim_and_reverse(Rest, Reversed);
  198. trim_and_reverse(V, false) ->
  199. trim_and_reverse(lists:reverse(V), true);
  200. trim_and_reverse(V, true) ->
  201. V.
  202. reversed_tokens([], [], Acc) ->
  203. Acc;
  204. reversed_tokens([], Token, Acc) ->
  205. [Token | Acc];
  206. reversed_tokens("\"" ++ Rest, [], Acc) ->
  207. case extract_quoted_string(Rest, []) of
  208. {String, NewRest} ->
  209. reversed_tokens(NewRest, [], [String | Acc]);
  210. undefined ->
  211. undefined
  212. end;
  213. reversed_tokens("\"" ++ _Rest, _Token, _Acc) ->
  214. undefined;
  215. reversed_tokens([C | Rest], [], Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, ->
  216. reversed_tokens(Rest, [], Acc);
  217. reversed_tokens([C | Rest], Token, Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, ->
  218. reversed_tokens(Rest, [], [Token | Acc]);
  219. reversed_tokens([C | Rest], Token, Acc) ->
  220. reversed_tokens(Rest, [C | Token], Acc);
  221. reversed_tokens(_, _, _) ->
  222. undefeined.
  223. extract_quoted_string([], _Acc) ->
  224. undefined;
  225. extract_quoted_string("\"\\" ++ Rest, Acc) ->
  226. extract_quoted_string(Rest, "\"" ++ Acc);
  227. extract_quoted_string("\"" ++ Rest, Acc) ->
  228. {Acc, Rest};
  229. extract_quoted_string([C | Rest], Acc) ->
  230. extract_quoted_string(Rest, [C | Acc]).
  231. expand({array, L}) ->
  232. mochiweb_util:join(lists:reverse(L), ", ");
  233. expand(V) ->
  234. V.
  235. merge("set-cookie", V1, {array, L}) ->
  236. {array, [V1 | L]};
  237. merge("set-cookie", V1, V0) ->
  238. {array, [V1, V0]};
  239. merge(_, V1, V0) ->
  240. V0 ++ ", " ++ V1.
  241. normalize(K) when is_list(K) ->
  242. string:to_lower(K);
  243. normalize(K) when is_atom(K) ->
  244. normalize(atom_to_list(K));
  245. normalize(K) when is_binary(K) ->
  246. normalize(binary_to_list(K)).
  247. any_to_list(V) when is_list(V) ->
  248. V;
  249. any_to_list(V) when is_atom(V) ->
  250. atom_to_list(V);
  251. any_to_list(V) when is_binary(V) ->
  252. binary_to_list(V);
  253. any_to_list(V) when is_integer(V) ->
  254. integer_to_list(V).
  255. %%
  256. %% Tests.
  257. %%
  258. -ifdef(TEST).
  259. -include_lib("eunit/include/eunit.hrl").
  260. make_test() ->
  261. Identity = make([{hdr, foo}]),
  262. ?assertEqual(
  263. Identity,
  264. make(Identity)).
  265. enter_from_list_test() ->
  266. H = make([{hdr, foo}]),
  267. ?assertEqual(
  268. [{baz, "wibble"}, {hdr, "foo"}],
  269. to_list(enter_from_list([{baz, wibble}], H))),
  270. ?assertEqual(
  271. [{hdr, "bar"}],
  272. to_list(enter_from_list([{hdr, bar}], H))),
  273. ok.
  274. default_from_list_test() ->
  275. H = make([{hdr, foo}]),
  276. ?assertEqual(
  277. [{baz, "wibble"}, {hdr, "foo"}],
  278. to_list(default_from_list([{baz, wibble}], H))),
  279. ?assertEqual(
  280. [{hdr, "foo"}],
  281. to_list(default_from_list([{hdr, bar}], H))),
  282. ok.
  283. get_primary_value_test() ->
  284. H = make([{hdr, foo}, {baz, <<"wibble;taco">>}]),
  285. ?assertEqual(
  286. "foo",
  287. get_primary_value(hdr, H)),
  288. ?assertEqual(
  289. undefined,
  290. get_primary_value(bar, H)),
  291. ?assertEqual(
  292. "wibble",
  293. get_primary_value(<<"baz">>, H)),
  294. ok.
  295. get_combined_value_test() ->
  296. H = make([{hdr, foo}, {baz, <<"wibble,taco">>}, {content_length, "123, 123"},
  297. {test, " 123, 123, 123 , 123,123 "},
  298. {test2, "456, 123, 123 , 123"},
  299. {test3, "123"}, {test4, " 123, "}]),
  300. ?assertEqual(
  301. "foo",
  302. get_combined_value(hdr, H)),
  303. ?assertEqual(
  304. undefined,
  305. get_combined_value(bar, H)),
  306. ?assertEqual(
  307. undefined,
  308. get_combined_value(<<"baz">>, H)),
  309. ?assertEqual(
  310. "123",
  311. get_combined_value(<<"content_length">>, H)),
  312. ?assertEqual(
  313. "123",
  314. get_combined_value(<<"test">>, H)),
  315. ?assertEqual(
  316. undefined,
  317. get_combined_value(<<"test2">>, H)),
  318. ?assertEqual(
  319. "123",
  320. get_combined_value(<<"test3">>, H)),
  321. ?assertEqual(
  322. "123",
  323. get_combined_value(<<"test4">>, H)),
  324. ok.
  325. set_cookie_test() ->
  326. H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]),
  327. ?assertEqual(
  328. [{"set-cookie", "foo"}, {"set-cookie", "bar"}, {"set-cookie", "baz"}],
  329. to_list(H)),
  330. ok.
  331. headers_test() ->
  332. H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]),
  333. [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H),
  334. H1 = ?MODULE:insert(taco, grande, H),
  335. [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1),
  336. H2 = ?MODULE:make([{"Set-Cookie", "foo"}]),
  337. [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2),
  338. H3 = ?MODULE:insert("Set-Cookie", "bar", H2),
  339. [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3),
  340. "foo, bar" = ?MODULE:get_value("set-cookie", H3),
  341. {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3),
  342. undefined = ?MODULE:get_value("shibby", H3),
  343. none = ?MODULE:lookup("shibby", H3),
  344. H4 = ?MODULE:insert("content-type",
  345. "application/x-www-form-urlencoded; charset=utf8",
  346. H3),
  347. "application/x-www-form-urlencoded" = ?MODULE:get_primary_value(
  348. "content-type", H4),
  349. H4 = ?MODULE:delete_any("nonexistent-header", H4),
  350. H3 = ?MODULE:delete_any("content-type", H4),
  351. HB = <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>,
  352. H_HB = ?MODULE:from_binary(HB),
  353. H_HB = ?MODULE:from_binary(binary_to_list(HB)),
  354. "47" = ?MODULE:get_value("Content-Length", H_HB),
  355. "text/plain" = ?MODULE:get_value("Content-Type", H_HB),
  356. L_H_HB = ?MODULE:to_list(H_HB),
  357. 2 = length(L_H_HB),
  358. true = lists:member({'Content-Length', "47"}, L_H_HB),
  359. true = lists:member({'Content-Type', "text/plain"}, L_H_HB),
  360. HL = [ <<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">> ],
  361. HL2 = [ "Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">> ],
  362. HL3 = [ <<"Content-Length: 47\r\n">>, "Content-Type: text/plain\r\n" ],
  363. H_HL = ?MODULE:from_binary(HL),
  364. H_HL = ?MODULE:from_binary(HL2),
  365. H_HL = ?MODULE:from_binary(HL3),
  366. "47" = ?MODULE:get_value("Content-Length", H_HL),
  367. "text/plain" = ?MODULE:get_value("Content-Type", H_HL),
  368. L_H_HL = ?MODULE:to_list(H_HL),
  369. 2 = length(L_H_HL),
  370. true = lists:member({'Content-Length', "47"}, L_H_HL),
  371. true = lists:member({'Content-Type', "text/plain"}, L_H_HL),
  372. [] = ?MODULE:to_list(?MODULE:from_binary(<<>>)),
  373. [] = ?MODULE:to_list(?MODULE:from_binary(<<"">>)),
  374. [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n">>)),
  375. [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n\r\n">>)),
  376. [] = ?MODULE:to_list(?MODULE:from_binary("")),
  377. [] = ?MODULE:to_list(?MODULE:from_binary([<<>>])),
  378. [] = ?MODULE:to_list(?MODULE:from_binary([<<"">>])),
  379. [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n">>])),
  380. [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])),
  381. ok.
  382. tokenize_header_value_test() ->
  383. ?assertEqual(["a quote in a \"quote\"."],
  384. tokenize_header_value("\"a quote in a \\\"quote\\\".\"")),
  385. ?assertEqual(["abc"], tokenize_header_value("abc")),
  386. ?assertEqual(["abc", "def"], tokenize_header_value("abc def")),
  387. ?assertEqual(["abc", "def"], tokenize_header_value("abc , def")),
  388. ?assertEqual(["abc", "def"], tokenize_header_value(",abc ,, def,,")),
  389. ?assertEqual(["abc def"], tokenize_header_value("\"abc def\" ")),
  390. ?assertEqual(["abc, def"], tokenize_header_value("\"abc, def\"")),
  391. ?assertEqual(["\\a\\$"], tokenize_header_value("\"\\a\\$\"")),
  392. ?assertEqual(["abc def", "foo, bar", "12345", ""],
  393. tokenize_header_value("\"abc def\" \"foo, bar\" , 12345, \"\"")),
  394. ?assertEqual(undefined,
  395. tokenize_header_value(undefined)),
  396. ?assertEqual(undefined,
  397. tokenize_header_value("umatched quote\"")),
  398. ?assertEqual(undefined,
  399. tokenize_header_value("\"unmatched quote")).
  400. -endif.