Permalink
Browse files

Merge pull request #88 from doubleyou/handling-combined-header

Handling combined header
  • Loading branch information...
2 parents e282266 + 2ba8d24 commit 20f2f00af0c722853e42a98c6cabc67e1d14d5dc @doubleyou doubleyou committed Nov 6, 2012
Showing with 130 additions and 6 deletions.
  1. +122 −1 src/mochiweb_headers.erl
  2. +1 −1 src/mochiweb_multipart.erl
  3. +7 −4 src/mochiweb_request.erl
@@ -6,7 +6,7 @@
-module(mochiweb_headers).
-author('bob@mochimedia.com').
-export([empty/0, from_list/1, insert/3, enter/3, get_value/2, lookup/2]).
--export([delete_any/2, get_primary_value/2]).
+-export([delete_any/2, get_primary_value/2, get_combined_value/2]).
-export([default/3, enter_from_list/2, default_from_list/2]).
-export([to_list/1, make/1]).
-export([from_binary/1]).
@@ -112,6 +112,34 @@ get_primary_value(K, T) ->
lists:takewhile(fun (C) -> C =/= $; end, V)
end.
+%% @spec get_combined_value(key(), headers()) -> string() | undefined
+%% @doc Return the value from the given header using a case insensitive search.
+%% If the value of the header is a comma-separated list where holds values
+%% are all identical, the identical value will be returned.
+%% undefined will be returned for keys that are not present or the
+%% values in the list are not the same.
+%%
+%% NOTE: The process isn't designed for a general purpose. If you need
+%% to access all values in the combined header, please refer to
+%% '''tokenize_header_value/1'''.
+%%
+%% Section 4.2 of the RFC 2616 (HTTP 1.1) describes multiple message-header
+%% fields with the same field-name may be present in a message if and only
+%% if the entire field-value for that header field is defined as a
+%% comma-separated list [i.e., #(values)].
+get_combined_value(K, T) ->
+ case get_value(K, T) of
+ undefined ->
+ undefined;
+ V ->
+ case sets:to_list(sets:from_list(tokenize_header_value(V))) of
+ [Val] ->
+ Val;
+ _ ->
+ undefined
+ end
+ end.
+
%% @spec lookup(key(), headers()) -> {value, {key(), string()}} | none
%% @doc Return the case preserved key and value for the given header using
%% a case insensitive search. none will be returned for keys that are
@@ -164,6 +192,49 @@ delete_any(K, T) ->
%% Internal API
+tokenize_header_value(undefined) ->
+ undefined;
+tokenize_header_value(V) ->
+ reversed_tokens(trim_and_reverse(V, false), [], []).
+
+trim_and_reverse([S | Rest], Reversed) when S=:=$ ; S=:=$\n; S=:=$\t ->
+ trim_and_reverse(Rest, Reversed);
+trim_and_reverse(V, false) ->
+ trim_and_reverse(lists:reverse(V), true);
+trim_and_reverse(V, true) ->
+ V.
+
+reversed_tokens([], [], Acc) ->
+ Acc;
+reversed_tokens([], Token, Acc) ->
+ [Token | Acc];
+reversed_tokens("\"" ++ Rest, [], Acc) ->
+ case extract_quoted_string(Rest, []) of
+ {String, NewRest} ->
+ reversed_tokens(NewRest, [], [String | Acc]);
+ undefined ->
+ undefined
+ end;
+reversed_tokens("\"" ++ _Rest, _Token, _Acc) ->
+ undefined;
+reversed_tokens([C | Rest], [], Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, ->
+ reversed_tokens(Rest, [], Acc);
+reversed_tokens([C | Rest], Token, Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, ->
+ reversed_tokens(Rest, [], [Token | Acc]);
+reversed_tokens([C | Rest], Token, Acc) ->
+ reversed_tokens(Rest, [C | Token], Acc);
+reversed_tokens(_, _, _) ->
+ undefeined.
+
+extract_quoted_string([], _Acc) ->
+ undefined;
+extract_quoted_string("\"\\" ++ Rest, Acc) ->
+ extract_quoted_string(Rest, "\"" ++ Acc);
+extract_quoted_string("\"" ++ Rest, Acc) ->
+ {Acc, Rest};
+extract_quoted_string([C | Rest], Acc) ->
+ extract_quoted_string(Rest, [C | Acc]).
+
expand({array, L}) ->
mochiweb_util:join(lists:reverse(L), ", ");
expand(V) ->
@@ -237,6 +308,37 @@ get_primary_value_test() ->
get_primary_value(<<"baz">>, H)),
ok.
+get_combined_value_test() ->
+ H = make([{hdr, foo}, {baz, <<"wibble,taco">>}, {content_length, "123, 123"},
+ {test, " 123, 123, 123 , 123,123 "},
+ {test2, "456, 123, 123 , 123"},
+ {test3, "123"}, {test4, " 123, "}]),
+ ?assertEqual(
+ "foo",
+ get_combined_value(hdr, H)),
+ ?assertEqual(
+ undefined,
+ get_combined_value(bar, H)),
+ ?assertEqual(
+ undefined,
+ get_combined_value(<<"baz">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"content_length">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"test">>, H)),
+ ?assertEqual(
+ undefined,
+ get_combined_value(<<"test2">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"test3">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"test4">>, H)),
+ ok.
+
set_cookie_test() ->
H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]),
?assertEqual(
@@ -296,4 +398,23 @@ headers_test() ->
[] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])),
ok.
+tokenize_header_value_test() ->
+ ?assertEqual(["a quote in a \"quote\"."],
+ tokenize_header_value("\"a quote in a \\\"quote\\\".\"")),
+ ?assertEqual(["abc"], tokenize_header_value("abc")),
+ ?assertEqual(["abc", "def"], tokenize_header_value("abc def")),
+ ?assertEqual(["abc", "def"], tokenize_header_value("abc , def")),
+ ?assertEqual(["abc", "def"], tokenize_header_value(",abc ,, def,,")),
+ ?assertEqual(["abc def"], tokenize_header_value("\"abc def\" ")),
+ ?assertEqual(["abc, def"], tokenize_header_value("\"abc, def\"")),
+ ?assertEqual(["\\a\\$"], tokenize_header_value("\"\\a\\$\"")),
+ ?assertEqual(["abc def", "foo, bar", "12345", ""],
+ tokenize_header_value("\"abc def\" \"foo, bar\" , 12345, \"\"")),
+ ?assertEqual(undefined,
+ tokenize_header_value(undefined)),
+ ?assertEqual(undefined,
+ tokenize_header_value("umatched quote\"")),
+ ?assertEqual(undefined,
+ tokenize_header_value("\"unmatched quote")).
+
-endif.
@@ -128,7 +128,7 @@ default_file_handler_1(Filename, ContentType, Acc) ->
parse_multipart_request(Req, Callback) ->
%% TODO: Support chunked?
- Length = list_to_integer(Req:get_header_value("content-length")),
+ Length = list_to_integer(Req:get_combined_header_value("content-length")),
Boundary = iolist_to_binary(
get_boundary(Req:get_header_value("content-type"))),
Prefix = <<"\r\n--", Boundary/binary>>,
@@ -11,7 +11,7 @@
-define(QUIP, "Any of you quaids got a smint?").
--export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]).
+-export([get_header_value/1, get_primary_header_value/1, get_combined_header_value/1, get/1, dump/0]).
-export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]).
-export([start_response/1, start_response_length/1, start_raw_response/1]).
-export([respond/1, ok/1]).
@@ -52,6 +52,9 @@ get_header_value(K) ->
get_primary_header_value(K) ->
mochiweb_headers:get_primary_value(K, Headers).
+get_combined_header_value(K) ->
+ mochiweb_headers:get_combined_value(K, Headers).
+
%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
%% @spec get(field()) -> term()
@@ -167,7 +170,7 @@ recv(Length, Timeout) ->
body_length() ->
case get_header_value("transfer-encoding") of
undefined ->
- case get_header_value("content-length") of
+ case get_combined_header_value("content-length") of
undefined ->
undefined;
Length ->
@@ -389,8 +392,8 @@ should_close() ->
andalso get_header_value("connection") =/= "Keep-Alive")
%% unread data left on the socket, can't safely continue
orelse (DidNotRecv
- andalso get_header_value("content-length") =/= undefined
- andalso list_to_integer(get_header_value("content-length")) > 0)
+ andalso get_combined_header_value("content-length") =/= undefined
+ andalso list_to_integer(get_combined_header_value("content-length")) > 0)
orelse (DidNotRecv
andalso get_header_value("transfer-encoding") =:= "chunked").

0 comments on commit 20f2f00

Please sign in to comment.