Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
12 7u83 1
%% @author Emad El-Haraty <emad@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 HTTP Cookie parsing and generating (RFC 2109, RFC 2965).
23
 
24
-module(mochiweb_cookies).
25
-export([parse_cookie/1, cookie/3, cookie/2]).
26
 
27
-define(QUOTE, $\").
28
 
29
-define(IS_WHITESPACE(C),
30
        (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)).
31
 
32
%% RFC 2616 separators (called tspecials in RFC 2068)
33
-define(IS_SEPARATOR(C),
34
        (C < 32 orelse
35
         C =:= $\s orelse C =:= $\t orelse
36
         C =:= $( orelse C =:= $) orelse C =:= $< orelse C =:= $> orelse
37
         C =:= $@ orelse C =:= $, orelse C =:= $; orelse C =:= $: orelse
38
         C =:= $\\ orelse C =:= $\" orelse C =:= $/ orelse
39
         C =:= $[ orelse C =:= $] orelse C =:= $? orelse C =:= $= orelse
40
         C =:= ${ orelse C =:= $})).
41
 
42
%% RFC 6265 cookie value allowed characters
43
%%  cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
44
%%                        ; US-ASCII characters excluding CTLs,
45
%%                        ; whitespace DQUOTE, comma, semicolon,
46
%%                        ; and backslash
47
-define(IS_COOKIE_OCTET(C),
48
        (C =:= 16#21
49
         orelse (C >= 16#23 andalso C =< 16#2B)
50
         orelse (C >= 16#2D andalso C =< 16#3A)
51
         orelse (C >= 16#3C andalso C =< 16#5B)
52
         orelse (C >= 16#5D andalso C =< 16#7E)
53
        )).
54
 
55
%% @type proplist() = [{Key::string(), Value::string()}].
56
%% @type header() = {Name::string(), Value::string()}.
57
%% @type int_seconds() = integer().
58
 
59
%% @spec cookie(Key::string(), Value::string()) -> header()
60
%% @doc Short-hand for <code>cookie(Key, Value, [])</code>.
61
cookie(Key, Value) ->
62
    cookie(Key, Value, []).
63
 
64
%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
65
%% where Option = {max_age, int_seconds()} | {local_time, {date(), time()}}
66
%%                | {domain, string()} | {path, string()}
67
%%                | {secure, true | false} | {http_only, true | false}
68
%%                | {same_site, lax | strict | none}
69
%%
70
%% @doc Generate a Set-Cookie header field tuple.
71
cookie(Key, Value, Options) ->
72
    Cookie = [any_to_list(Key), "=", quote(Value), "; Version=1"],
73
    %% Set-Cookie:
74
    %%    Comment, Domain, Max-Age, Path, Secure, Version
75
    %% Set-Cookie2:
76
    %%    Comment, CommentURL, Discard, Domain, Max-Age, Path, Port, Secure,
77
    %%    Version
78
    ExpiresPart =
79
        case proplists:get_value(max_age, Options) of
80
            undefined ->
81
                "";
82
            RawAge ->
83
                When = case proplists:get_value(local_time, Options) of
84
                           undefined ->
85
                               calendar:local_time();
86
                           LocalTime ->
87
                               LocalTime
88
                       end,
89
                Age = case RawAge < 0 of
90
                          true ->
91
                              0;
92
                          false ->
93
                              RawAge
94
                      end,
95
                ["; Expires=", age_to_cookie_date(Age, When),
96
                 "; Max-Age=", quote(Age)]
97
        end,
98
    SecurePart =
99
        case proplists:get_value(secure, Options) of
100
            true ->
101
                "; Secure";
102
            _ ->
103
                ""
104
        end,
105
    DomainPart =
106
        case proplists:get_value(domain, Options) of
107
            undefined ->
108
                "";
109
            Domain ->
110
                ["; Domain=", quote(Domain)]
111
        end,
112
    PathPart =
113
        case proplists:get_value(path, Options) of
114
            undefined ->
115
                "";
116
            Path ->
117
                ["; Path=", quote(Path)]
118
        end,
119
    HttpOnlyPart =
120
        case proplists:get_value(http_only, Options) of
121
            true ->
122
                "; HttpOnly";
123
            _ ->
124
                ""
125
        end,
126
    SameSitePart =
127
        case proplists:get_value(same_site, Options) of
128
            undefined ->
129
                "";
130
            lax ->
131
                "; SameSite=Lax";
132
            strict ->
133
                "; SameSite=Strict";
134
            none ->
135
                "; SameSite=None"
136
        end,
137
    CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart,
138
        HttpOnlyPart, SameSitePart],
139
    {"Set-Cookie", lists:flatten(CookieParts)}.
140
 
141
 
142
%% Every major browser incorrectly handles quoted strings in a
143
%% different and (worse) incompatible manner.  Instead of wasting time
144
%% writing redundant code for each browser, we restrict cookies to
145
%% only contain characters that browsers handle compatibly.
146
%%
147
%% By replacing the definition of quote with this, we generate
148
%% RFC-compliant cookies:
149
%%
150
%%     quote(V) ->
151
%%         Fun = fun(?QUOTE, Acc) -> [$\\, ?QUOTE | Acc];
152
%%                  (Ch, Acc) -> [Ch | Acc]
153
%%               end,
154
%%         [?QUOTE | lists:foldr(Fun, [?QUOTE], V)].
155
 
156
%% Convert to a string and raise an error if quoting is required.
157
quote(V0) ->
158
    V = any_to_list(V0),
159
    lists:all(fun(Ch) -> Ch =:= $/ orelse not ?IS_SEPARATOR(Ch) end, V)
160
        orelse erlang:error({cookie_quoting_required, V}),
161
    V.
162
 
163
 
164
%% Return a date in the form of: Wdy, DD-Mon-YYYY HH:MM:SS GMT
165
%% See also: rfc2109: 10.1.2
166
rfc2109_cookie_expires_date(LocalTime) ->
167
    {{YYYY,MM,DD},{Hour,Min,Sec}} =
168
        case calendar:local_time_to_universal_time_dst(LocalTime) of
169
            [] ->
170
                {Date, {Hour1, Min1, Sec1}} = LocalTime,
171
                LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}},
172
                case calendar:local_time_to_universal_time_dst(LocalTime2) of
173
                    [Gmt]   -> Gmt;
174
                    [_,Gmt] -> Gmt
175
                end;
176
            [Gmt]   -> Gmt;
177
            [_,Gmt] -> Gmt
178
        end,
179
    DayNumber = calendar:day_of_the_week({YYYY,MM,DD}),
180
    lists:flatten(
181
      io_lib:format("~s, ~2.2.0w-~3.s-~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT",
182
                    [httpd_util:day(DayNumber),DD,httpd_util:month(MM),YYYY,Hour,Min,Sec])).
183
 
184
add_seconds(Secs, LocalTime) ->
185
    Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
186
    calendar:gregorian_seconds_to_datetime(Greg + Secs).
187
 
188
age_to_cookie_date(Age, LocalTime) ->
189
    rfc2109_cookie_expires_date(add_seconds(Age, LocalTime)).
190
 
191
%% @spec parse_cookie(string()) -> [{K::string(), V::string()}]
192
%% @doc Parse the contents of a Cookie header field, ignoring cookie
193
%% attributes, and return a simple property list.
194
parse_cookie("") ->
195
    [];
196
parse_cookie(Cookie) ->
197
    parse_cookie(Cookie, []).
198
 
199
%% Internal API
200
 
201
parse_cookie([], Acc) ->
202
    lists:reverse(Acc);
203
parse_cookie(String, Acc) ->
204
    {{Token, Value}, Rest} = read_pair(String),
205
    Acc1 = case Token of
206
               "" ->
207
                   Acc;
208
               "$" ++ _ ->
209
                   Acc;
210
               _ ->
211
                   [{Token, Value} | Acc]
212
           end,
213
    parse_cookie(Rest, Acc1).
214
 
215
read_pair(String) ->
216
    {Token, Rest} = read_token(skip_whitespace(String)),
217
    {Value, Rest1} = read_value(skip_whitespace(Rest)),
218
    {{Token, Value}, skip_past_separator(Rest1)}.
219
 
220
read_value([$= | Value]) ->
221
    Value1 = skip_whitespace(Value),
222
    case Value1 of
223
        [?QUOTE | _] ->
224
            read_quoted(Value1);
225
        _ ->
226
            read_value_(Value1)
227
    end;
228
read_value(String) ->
229
    {"", String}.
230
 
231
read_value_(String) ->
232
    F = fun (C) -> ?IS_COOKIE_OCTET(C) end,
233
    lists:splitwith(F, String).
234
 
235
read_quoted([?QUOTE | String]) ->
236
    read_quoted(String, []).
237
 
238
read_quoted([], Acc) ->
239
    {lists:reverse(Acc), []};
240
read_quoted([?QUOTE | Rest], Acc) ->
241
    {lists:reverse(Acc), Rest};
242
read_quoted([$\\, Any | Rest], Acc) ->
243
    read_quoted(Rest, [Any | Acc]);
244
read_quoted([C | Rest], Acc) ->
245
    read_quoted(Rest, [C | Acc]).
246
 
247
skip_whitespace(String) ->
248
    F = fun (C) -> ?IS_WHITESPACE(C) end,
249
    lists:dropwhile(F, String).
250
 
251
read_token(String) ->
252
    F = fun (C) -> not ?IS_SEPARATOR(C) end,
253
    lists:splitwith(F, String).
254
 
255
skip_past_separator([]) ->
256
    [];
257
skip_past_separator([$; | Rest]) ->
258
    Rest;
259
skip_past_separator([$, | Rest]) ->
260
    Rest;
261
skip_past_separator([_ | Rest]) ->
262
    skip_past_separator(Rest).
263
 
264
any_to_list(V) when is_list(V) ->
265
    V;
266
any_to_list(V) when is_atom(V) ->
267
    atom_to_list(V);
268
any_to_list(V) when is_binary(V) ->
269
    binary_to_list(V);
270
any_to_list(V) when is_integer(V) ->
271
    integer_to_list(V).
272
 
273
%%
274
%% Tests
275
%%
276
-ifdef(TEST).
277
-include_lib("eunit/include/eunit.hrl").
278
 
279
quote_test() ->
280
    %% ?assertError eunit macro is not compatible with coverage module
281
    try quote(":wq")
282
    catch error:{cookie_quoting_required, ":wq"} -> ok
283
    end,
284
    ?assertEqual(
285
       "foo",
286
       quote(foo)),
287
    ok.
288
 
289
parse_cookie_test() ->
290
    %% RFC example
291
    C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
292
    Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
293
    Shipping=\"FedEx\"; $Path=\"/acme\"",
294
    ?assertEqual(
295
       [{"Customer","WILE_E_COYOTE"},
296
        {"Part_Number","Rocket_Launcher_0001"},
297
        {"Shipping","FedEx"}],
298
       parse_cookie(C1)),
299
    %% Potential edge cases
300
    ?assertEqual(
301
       [{"foo", "x"}],
302
       parse_cookie("foo=\"\\x\"")),
303
    ?assertEqual(
304
       [],
305
       parse_cookie("=")),
306
    ?assertEqual(
307
       [{"foo", ""}, {"bar", ""}],
308
       parse_cookie("  foo ; bar  ")),
309
    ?assertEqual(
310
       [{"foo", ""}, {"bar", ""}],
311
       parse_cookie("foo=;bar=")),
312
    ?assertEqual(
313
       [{"foo", "\";"}, {"bar", ""}],
314
       parse_cookie("foo = \"\\\";\";bar ")),
315
    ?assertEqual(
316
       [{"foo", "\";bar"}],
317
       parse_cookie("foo=\"\\\";bar")),
318
    ?assertEqual(
319
       [],
320
       parse_cookie([])),
321
    ?assertEqual(
322
       [{"foo", "bar"}, {"baz", "wibble"}],
323
       parse_cookie("foo=bar , baz=wibble ")),
324
    ?assertEqual(
325
       [{"foo", "base64=="}, {"bar", "base64="}],
326
       parse_cookie("foo=\"base64==\";bar=\"base64=\"")),
327
    ?assertEqual(
328
       [{"foo", "base64=="}, {"bar", "base64="}],
329
       parse_cookie("foo=base64==;bar=base64=")),
330
    ok.
331
 
332
domain_test() ->
333
    ?assertEqual(
334
       {"Set-Cookie",
335
        "Customer=WILE_E_COYOTE; "
336
        "Version=1; "
337
        "Domain=acme.com; "
338
        "HttpOnly"},
339
       cookie("Customer", "WILE_E_COYOTE",
340
              [{http_only, true}, {domain, "acme.com"}])),
341
    ok.
342
 
343
local_time_test() ->
344
    {"Set-Cookie", S} = cookie("Customer", "WILE_E_COYOTE",
345
                               [{max_age, 111}, {secure, true}]),
346
    ?assertMatch(
347
       ["Customer=WILE_E_COYOTE",
348
        " Version=1",
349
        " Expires=" ++ _,
350
        " Max-Age=111",
351
        " Secure"],
352
       string:tokens(S, ";")),
353
    ok.
354
 
355
cookie_test() ->
356
    C1 = {"Set-Cookie",
357
          "Customer=WILE_E_COYOTE; "
358
          "Version=1; "
359
          "Path=/acme"},
360
    C1 = cookie("Customer", "WILE_E_COYOTE", [{path, "/acme"}]),
361
    C1 = cookie("Customer", "WILE_E_COYOTE",
362
                [{path, "/acme"}, {badoption, "negatory"}]),
363
    C1 = cookie('Customer', 'WILE_E_COYOTE', [{path, '/acme'}]),
364
    C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
365
 
366
    {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey", []),
367
    {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey"),
368
    LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}),
369
    C2 = {"Set-Cookie",
370
          "Customer=WILE_E_COYOTE; "
371
          "Version=1; "
372
          "Expires=Tue, 15-May-2007 13:45:33 GMT; "
373
          "Max-Age=0"},
374
    C2 = cookie("Customer", "WILE_E_COYOTE",
375
                [{max_age, -111}, {local_time, LocalTime}]),
376
    C3 = {"Set-Cookie",
377
          "Customer=WILE_E_COYOTE; "
378
          "Version=1; "
379
          "Expires=Wed, 16-May-2007 13:45:50 GMT; "
380
          "Max-Age=86417"},
381
    C3 = cookie("Customer", "WILE_E_COYOTE",
382
                [{max_age, 86417}, {local_time, LocalTime}]),
383
 
384
    % test various values for SameSite
385
    %
386
    % unset default to nothing
387
    C4 = {"Set-Cookie","i=test123; Version=1"},
388
    C4 = cookie("i", "test123", []),
389
    C5 = {"Set-Cookie","i=test123; Version=1; SameSite=Strict"},
390
    C5 = cookie("i", "test123", [ {same_site, strict}]),
391
    C6 = {"Set-Cookie","i=test123; Version=1; SameSite=Lax"},
392
    C6 = cookie("i", "test123", [ {same_site, lax}]),
393
    C7 = {"Set-Cookie","i=test123; Version=1; SameSite=None"},
394
    C7 = cookie("i", "test123", [ {same_site, none}]),
395
    ok.
396
 
397
-endif.