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