Browse Source

Add 'If-Modified-Since' and 'If-Unmodified-Since' to parse_header/2

Implementing the full HTTP-date type (RFC1123, RFC850, asctime).
Loïc Hoguin 13 years ago
parent
commit
aba1ea4636
2 changed files with 267 additions and 1 deletions
  1. 261 1
      src/cowboy_http.erl
  2. 6 0
      src/cowboy_http_req.erl

+ 261 - 1
src/cowboy_http.erl

@@ -18,6 +18,7 @@
 %% Parsing.
 -export([list/2, nonempty_list/2,
 	media_range/2, conneg/2, digits/1,
+	http_date/1, rfc1123_date/1, rfc850_date/1, asctime_date/1,
 	token/2, token_ci/2, quoted_string/2]).
 
 %% Interpretation.
@@ -194,11 +195,239 @@ conneg(Data, Fun) ->
 					end)
 		end).
 
-%% Parse a quality parameter string (for example q=0.500).
+%% @doc Parse a quality parameter string (for example q=0.500).
 -spec qparam(binary(), fun()) -> any().
 qparam(<< Q, $=, Data/bits >>, Fun) when Q =:= $q; Q =:= $Q ->
 	qvalue(Data, Fun).
 
+%% @doc Parse an HTTP date (RFC1123, RFC850 or asctime date).
+%% @end
+%%
+%% While this may not be the most efficient date parsing we can do,
+%% it should work fine for our purposes because all HTTP dates should
+%% be sent as RFC1123 dates in HTTP/1.1.
+-spec http_date(binary()) -> any().
+http_date(Data) ->
+	case rfc1123_date(Data) of
+		{error, badarg} ->
+			case rfc850_date(Data) of
+				{error, badarg} ->
+					case asctime_date(Data) of
+						{error, badarg} ->
+							{error, badarg};
+						HTTPDate ->
+							HTTPDate
+					end;
+				HTTPDate ->
+					HTTPDate
+			end;
+		HTTPDate ->
+			HTTPDate
+	end.
+
+%% @doc Parse an RFC1123 date.
+-spec rfc1123_date(binary()) -> any().
+rfc1123_date(Data) ->
+	wkday(Data,
+		fun (<< ", ", Rest/bits >>, _WkDay) ->
+				date1(Rest,
+					fun (<< " ", Rest2/bits >>, Date) ->
+							time(Rest2,
+								fun (<< " GMT", Rest3/bits >>, Time) ->
+										http_date_ret(Rest3, {Date, Time});
+									(_Any, _Time) ->
+										{error, badarg}
+								end);
+						(_Any, _Date) ->
+							{error, badarg}
+					end);
+			(_Any, _WkDay) ->
+				{error, badarg}
+		end).
+
+%% @doc Parse an RFC850 date.
+-spec rfc850_date(binary()) -> any().
+%% From the RFC:
+%% HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date
+%% which appears to be more than 50 years in the future is in fact
+%% in the past (this helps solve the "year 2000" problem).
+rfc850_date(Data) ->
+	weekday(Data,
+		fun (<< ", ", Rest/bits >>, _WeekDay) ->
+				date2(Rest,
+					fun (<< " ", Rest2/bits >>, Date) ->
+							time(Rest2,
+								fun (<< " GMT", Rest3/bits >>, Time) ->
+										http_date_ret(Rest3, {Date, Time});
+									(_Any, _Time) ->
+										{error, badarg}
+								end);
+						(_Any, _Date) ->
+							{error, badarg}
+					end);
+			(_Any, _WeekDay) ->
+				{error, badarg}
+		end).
+
+%% @doc Parse an asctime date.
+-spec asctime_date(binary()) -> any().
+asctime_date(Data) ->
+	wkday(Data,
+		fun (<< " ", Rest/bits >>, _WkDay) ->
+				date3(Rest,
+					fun (<< " ", Rest2/bits >>, PartialDate) ->
+							time(Rest2,
+								fun (<< " ", Rest3/bits >>, Time) ->
+										asctime_year(Rest3,
+											PartialDate, Time);
+									(_Any, _Time) ->
+										{error, badarg}
+								end);
+						(_Any, _PartialDate) ->
+							{error, badarg}
+					end);
+			(_Any, _WkDay) ->
+				{error, badarg1}
+		end).
+
+-spec asctime_year(binary(), tuple(), tuple()) -> any().
+asctime_year(<< Y1, Y2, Y3, Y4, Rest/bits >>, {Month, Day}, Time)
+		when Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
+			 Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
+	Year = (Y1 - $0) * 1000 + (Y2 - $0) * 100 + (Y3 - $0) * 10 + (Y4 - $0),
+	http_date_ret(Rest, {{Year, Month, Day}, Time}).
+
+-spec http_date_ret(binary(), tuple()) -> any().
+http_date_ret(Data, DateTime = {Date, _Time}) ->
+	whitespace(Data,
+		fun (<<>>) ->
+				case calendar:valid_date(Date) of
+					true -> DateTime;
+					false -> {error, badarg}
+				end;
+			(_Any) ->
+				{error, badarg}
+		end).
+
+%% We never use it, pretty much just checks the wkday is right.
+-spec wkday(binary(), fun()) -> any().
+wkday(<< WkDay:3/binary, Rest/bits >>, Fun)
+		when WkDay =:= <<"Mon">>; WkDay =:= "Tue"; WkDay =:= "Wed";
+			 WkDay =:= <<"Thu">>; WkDay =:= "Fri"; WkDay =:= "Sat";
+			 WkDay =:= <<"Sun">> ->
+	Fun(Rest, WkDay);
+wkday(_Any, _Fun) ->
+	{error, badarg}.
+
+%% We never use it, pretty much just checks the weekday is right.
+-spec weekday(binary(), fun()) -> any().
+weekday(<< "Monday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Monday">>);
+weekday(<< "Tuesday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Tuesday">>);
+weekday(<< "Wednesday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Wednesday">>);
+weekday(<< "Thursday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Thursday">>);
+weekday(<< "Friday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Friday">>);
+weekday(<< "Saturday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Saturday">>);
+weekday(<< "Sunday", Rest/binary >>, Fun) ->
+	Fun(Rest, <<"Sunday">>);
+weekday(_Any, _Fun) ->
+	{error, badarg}.
+
+-spec date1(binary(), fun()) -> any().
+date1(<< D1, D2, " ", M:3/binary, " ", Y1, Y2, Y3, Y4, Rest/bits >>, Fun)
+		when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
+			 Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
+			 Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
+	case month(M) of
+		{error, badarg} ->
+			{error, badarg};
+		Month ->
+			Fun(Rest, {
+				(Y1 - $0) * 1000 + (Y2 - $0) * 100 + (Y3 - $0) * 10 + (Y4 - $0),
+				Month,
+				(D1 - $0) * 10 + (D2 - $0)
+			})
+	end;
+date1(_Data, _Fun) ->
+	{error, badarg}.
+
+-spec date2(binary(), fun()) -> any().
+date2(<< D1, D2, "-", M:3/binary, "-", Y1, Y2, Rest/bits >>, Fun)
+		when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
+			 Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9 ->
+	case month(M) of
+		{error, badarg} ->
+			{error, badarg};
+		Month ->
+			Year = (Y1 - $0) * 10 + (Y2 - $0),
+			Year2 = case Year > 50 of
+				true -> Year + 1900;
+				false -> Year + 2000
+			end,
+			Fun(Rest, {
+				Year2,
+				Month,
+				(D1 - $0) * 10 + (D2 - $0)
+			})
+	end;
+date2(_Data, _Fun) ->
+	{error, badarg}.
+
+-spec date3(binary(), fun()) -> any().
+date3(<< M:3/binary, " ", D1, D2, Rest/bits >>, Fun)
+		when (D1 >= $0 andalso D1 =< $3) orelse D1 =:= $\s,
+			 D2 >= $0, D2 =< $9 ->
+	case month(M) of
+		{error, badarg} ->
+			{error, badarg};
+		Month ->
+			Day = case D1 of
+				$\s -> D2 - $0;
+				D1 -> (D1 - $0) * 10 + (D2 - $0)
+			end,
+			Fun(Rest, {Month, Day})
+	end;
+date3(_Data, _Fun) ->
+	{error, badarg}.
+
+-spec month(<< _:24 >>) -> 1..12 | {error, badarg}.
+month(<<"Jan">>) -> 1;
+month(<<"Feb">>) -> 2;
+month(<<"Mar">>) -> 3;
+month(<<"Apr">>) -> 4;
+month(<<"May">>) -> 5;
+month(<<"Jun">>) -> 6;
+month(<<"Jul">>) -> 7;
+month(<<"Aug">>) -> 8;
+month(<<"Sep">>) -> 9;
+month(<<"Oct">>) -> 10;
+month(<<"Nov">>) -> 11;
+month(<<"Dec">>) -> 12;
+month(_Any) -> {error, badarg}.
+
+-spec time(binary(), fun()) -> any().
+time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/bits >>, Fun)
+		when H1 >= $0, H1 =< $2, H2 >= $0, H2 =< $9,
+			 M1 >= $0, M1 =< $5, M2 >= $0, M2 =< $9,
+			 S1 >= $0, S1 =< $5, S2 >= $0, S2 =< $9 ->
+	Hour = (H1 - $0) * 10 + (H2 - $0),
+	case Hour < 24 of
+		true ->
+			Time = {
+				Hour,
+				(M1 - $0) * 10 + (M2 - $0),
+				(S1 - $0) * 10 + (S2 - $0)
+			},
+			Fun(Rest, Time);
+		false ->
+			{error, badarg}
+	end.
+
 %% @doc Skip whitespace.
 -spec whitespace(binary(), fun()) -> any().
 whitespace(<< C, Rest/bits >>, Fun)
@@ -300,6 +529,7 @@ qvalue(<< C, Rest/bits >>, Fun, Q, M) when ?IS_DIGIT(C) ->
 qvalue(Data, Fun, Q, _M) ->
 	Fun(Data, Q).
 
+
 %% Interpretation.
 
 %% @doc Walk through a tokens list and return whether
@@ -385,6 +615,36 @@ media_range_list_test_() ->
 	],
 	[{V, fun() -> R = list(V, fun media_range/2) end} || {V, R} <- Tests].
 
+http_date_test_() ->
+	%% {Tokens, Result}
+	Tests = [
+		{<<"Sun, 06 Nov 1994 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}},
+		{<<"Sunday, 06-Nov-94 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}},
+		{<<"Sun Nov  6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}}
+	],
+	[{V, fun() -> R = http_date(V) end} || {V, R} <- Tests].
+
+rfc1123_date_test_() ->
+	%% {Tokens, Result}
+	Tests = [
+		{<<"Sun, 06 Nov 1994 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}}
+	],
+	[{V, fun() -> R = rfc1123_date(V) end} || {V, R} <- Tests].
+
+rfc850_date_test_() ->
+	%% {Tokens, Result}
+	Tests = [
+		{<<"Sunday, 06-Nov-94 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}}
+	],
+	[{V, fun() -> R = rfc850_date(V) end} || {V, R} <- Tests].
+
+asctime_date_test_() ->
+	%% {Tokens, Result}
+	Tests = [
+		{<<"Sun Nov  6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}}
+	],
+	[{V, fun() -> R = asctime_date(V) end} || {V, R} <- Tests].
+
 connection_to_atom_test_() ->
 	%% {Tokens, Result}
 	Tests = [

+ 6 - 0
src/cowboy_http_req.erl

@@ -236,6 +236,12 @@ parse_header(Name, Req, Default) when Name =:= 'Content-Length' ->
 		fun (Value) ->
 			cowboy_http:digits(Value)
 		end);
+parse_header(Name, Req, Default)
+		when Name =:= 'If-Modified-Since'; Name =:= 'If-Unmodified-Since' ->
+	parse_header(Name, Req, Default,
+		fun (Value) ->
+			cowboy_http:http_date(Value)
+		end);
 parse_header(Name, Req, Default) ->
 	{Value, Req2} = header(Name, Req, Default),
 	{undefined, Value, Req2}.