1 /*******************************************************************************
2 
3         Copyright:
4             Copyright (c) 2004 Kris Bell.
5             Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH.
6             All rights reserved.
7 
8         License:
9             Tango Dual License: 3-Clause BSD License / Academic Free License v3.0.
10             See LICENSE_TANGO.txt for details.
11 
12         Version: Initial release: April 2004
13 
14         Authors: Kris
15 
16 *******************************************************************************/
17 
18 module ocean.net.http.HttpCookies;
19 
20 import ocean.meta.types.Qualifiers;
21 
22 import core.stdc.ctype;
23 
24 import ocean.io.device.Array;
25 
26 import ocean.io.model.IConduit;
27 
28 import ocean.io.stream.Iterator;
29 
30 import ocean.net.http.HttpHeaders;
31 
32 import  Integer = ocean.text.convert.Integer_tango;
33 
34 /*******************************************************************************
35 
36         Defines the Cookie class, and the means for reading & writing them.
37         Cookie implementation conforms with RFC 2109, but supports parsing
38         of server-side cookies only. Client-side cookies are supported in
39         terms of output, but response parsing is not yet implemented ...
40 
41         See over <A HREF="http://www.faqs.org/rfcs/rfc2109.html">here</A>
42         for the RFC document.
43 
44 *******************************************************************************/
45 
46 class Cookie //: IWritable
47 {
48         char[]          name,
49                         path,
50                         value,
51                         domain,
52                         comment;
53         uint            vrsn=1;              // 'version' is a reserved word
54         bool            secure=false;
55         long            maxAge=long.min;
56 
57         /***********************************************************************
58 
59                 Construct an empty client-side cookie. You add these
60                 to an output request using HttpClient.addCookie(), or
61                 the equivalent.
62 
63         ***********************************************************************/
64 
65         this () {}
66 
67         /***********************************************************************
68 
69                 Construct a cookie with the provided attributes. You add
70                 these to an output request using HttpClient.addCookie(),
71                 or the equivalent.
72 
73         ***********************************************************************/
74 
75         this (char[] name, char[] value)
76         {
77                 setName (name);
78                 setValue (value);
79         }
80 
81         /***********************************************************************
82 
83                 Set the name of this cookie
84 
85         ***********************************************************************/
86 
87         Cookie setName (char[] name)
88         {
89                 this.name = name;
90                 return this;
91         }
92 
93         /***********************************************************************
94 
95                 Set the value of this cookie
96 
97         ***********************************************************************/
98 
99         Cookie setValue (char[] value)
100         {
101                 this.value = value;
102                 return this;
103         }
104 
105         /***********************************************************************
106 
107                 Set the version of this cookie
108 
109         ***********************************************************************/
110 
111         Cookie setVersion (uint vrsn)
112         {
113                 this.vrsn = vrsn;
114                 return this;
115         }
116 
117         /***********************************************************************
118 
119                 Set the path of this cookie
120 
121         ***********************************************************************/
122 
123         Cookie setPath (char[] path)
124         {
125                 this.path = path;
126                 return this;
127         }
128 
129         /***********************************************************************
130 
131                 Set the domain of this cookie
132 
133         ***********************************************************************/
134 
135         Cookie setDomain (char[] domain)
136         {
137                 this.domain = domain;
138                 return this;
139         }
140 
141         /***********************************************************************
142 
143                 Set the comment associated with this cookie
144 
145         ***********************************************************************/
146 
147         Cookie setComment (char[] comment)
148         {
149                 this.comment = comment;
150                 return this;
151         }
152 
153         /***********************************************************************
154 
155                 Set the maximum duration of this cookie
156 
157         ***********************************************************************/
158 
159         Cookie setMaxAge (long maxAge)
160         {
161                 this.maxAge = maxAge;
162                 return this;
163         }
164 
165         /***********************************************************************
166 
167                 Indicate whether this cookie should be considered secure or not
168 
169         ***********************************************************************/
170 
171         Cookie setSecure (bool secure)
172         {
173                 this.secure = secure;
174                 return this;
175         }
176 /+
177         /***********************************************************************
178 
179                 Output the cookie as a text stream, via the provided IWriter
180 
181         ***********************************************************************/
182 
183         void write (IWriter writer)
184         {
185                 produce (&writer.buffer.consume);
186         }
187 +/
188         /***********************************************************************
189 
190                 Output the cookie as a text stream, via the provided consumer
191 
192         ***********************************************************************/
193 
194         void produce (scope size_t delegate(const(void)[]) consume)
195         {
196                 consume (name);
197 
198                 if (value.length)
199                     consume ("="), consume (value);
200 
201                 if (path.length)
202                     consume (";Path="), consume (path);
203 
204                 if (domain.length)
205                     consume (";Domain="), consume (domain);
206 
207                 if (vrsn)
208                    {
209                    char[16] tmp = void;
210 
211                    consume (";Version=");
212                    consume (Integer.format (tmp, vrsn));
213 
214                    if (comment.length)
215                        consume (";Comment=\""), consume(comment), consume("\"");
216 
217                    if (secure)
218                        consume (";Secure");
219 
220                    if (maxAge != maxAge.min)
221                        consume (";Max-Age="c), consume (Integer.format (tmp, maxAge));
222                    }
223         }
224 
225         /***********************************************************************
226 
227                 Reset this cookie
228 
229         ***********************************************************************/
230 
231         Cookie clear ()
232         {
233                 vrsn = 1;
234                 secure = false;
235                 maxAge = maxAge.min;
236                 name = path = domain = comment = null;
237                 return this;
238         }
239 }
240 
241 
242 
243 /*******************************************************************************
244 
245         Implements a stack of cookies. Each cookie is pushed onto the
246         stack by a parser, which takes its input from HttpHeaders. The
247         stack can be populated for both client and server side cookies.
248 
249 *******************************************************************************/
250 
251 class CookieStack
252 {
253         private int             depth;
254         private Cookie[]        cookies;
255 
256         /**********************************************************************
257 
258                 Construct a cookie stack with the specified initial extent.
259                 The stack will grow as necessary over time.
260 
261         **********************************************************************/
262 
263         this (int size)
264         {
265                 cookies = new Cookie[0];
266                 resize (cookies, size);
267         }
268 
269         /**********************************************************************
270 
271                 Pop the stack all the way to zero
272 
273         **********************************************************************/
274 
275         final void reset ()
276         {
277                 depth = 0;
278         }
279 
280         /**********************************************************************
281 
282                 Return a fresh cookie from the stack
283 
284         **********************************************************************/
285 
286         final Cookie push ()
287         {
288                 if (depth == cookies.length)
289                     resize (cookies, depth * 2);
290                 return cookies [depth++];
291         }
292 
293         /**********************************************************************
294 
295                 Resize the stack such that it has more room.
296 
297         **********************************************************************/
298 
299         private final static void resize (ref Cookie[] cookies, int size)
300         {
301                 auto i = cookies.length;
302 
303                 for (cookies.length=size; i < cookies.length; ++i)
304                      cookies[i] = new Cookie();
305         }
306 
307         /**********************************************************************
308 
309                 Iterate over all cookies in stack
310 
311         **********************************************************************/
312 
313         int opApply (scope int delegate(ref Cookie) dg)
314         {
315                 int result = 0;
316 
317                 for (int i=0; i < depth; ++i)
318                      if ((result = dg (cookies[i])) != 0)
319                           break;
320                 return result;
321         }
322 }
323 
324 
325 
326 /*******************************************************************************
327 
328         This is the support point for server-side cookies. It wraps a
329         CookieStack together with a set of HttpHeaders, along with the
330         appropriate cookie parser. One would do something very similar
331         for client side cookie parsing also.
332 
333 *******************************************************************************/
334 
335 class HttpCookiesView //: IWritable
336 {
337         private bool                    parsed;
338         private CookieStack             stack;
339         private CookieParser            parser;
340         private HttpHeadersView         headers;
341 
342         /**********************************************************************
343 
344                 Construct cookie wrapper with the provided headers.
345 
346         **********************************************************************/
347 
348         this (HttpHeadersView headers)
349         {
350                 this.headers = headers;
351 
352                 // create a stack for parsed cookies
353                 stack = new CookieStack (10);
354 
355                 // create a parser
356                 parser = new CookieParser (stack);
357         }
358 /+
359         /**********************************************************************
360 
361                 Output each of the cookies parsed to the provided IWriter.
362 
363         **********************************************************************/
364 
365         void write (IWriter writer)
366         {
367                 produce (&writer.buffer.consume, HttpConst.Eol);
368         }
369 +/
370         /**********************************************************************
371 
372                 Output the token list to the provided consumer
373 
374         **********************************************************************/
375 
376         void produce (scope size_t delegate(const(void)[]) consume, istring eol = HttpConst.Eol)
377         {
378                 foreach (cookie; parse)
379                          cookie.produce (consume), consume (eol);
380         }
381 
382         /**********************************************************************
383 
384                 Reset these cookies for another parse
385 
386         **********************************************************************/
387 
388         void reset ()
389         {
390                 stack.reset;
391                 parsed = false;
392         }
393 
394         /**********************************************************************
395 
396                 Parse all cookies from our HttpHeaders, pushing each onto
397                 the CookieStack as we go.
398 
399         **********************************************************************/
400 
401         CookieStack parse ()
402         {
403                 if (! parsed)
404                    {
405                    parsed = true;
406 
407                    foreach (HeaderElement header; headers)
408                             if (header.name.value == HttpHeader.Cookie.value)
409                                 parser.parse (header.value.dup);
410                    }
411                 return stack;
412         }
413 }
414 
415 
416 
417 /*******************************************************************************
418 
419         Handles a set of output cookies by writing them into the list of
420         output headers.
421 
422 *******************************************************************************/
423 
424 class HttpCookies
425 {
426         private HttpHeaderName  name;
427         private HttpHeaders     headers;
428 
429         /**********************************************************************
430 
431                 Construct an output cookie wrapper upon the provided
432                 output headers. Each cookie added is converted to an
433                 addition to those headers.
434 
435         **********************************************************************/
436 
437         this (HttpHeaders headers, HttpHeaderName name = HttpHeader.SetCookie)
438         {
439                 this.headers = headers;
440                 this.name = name;
441         }
442 
443         /**********************************************************************
444 
445                 Add a cookie to our output headers.
446 
447         **********************************************************************/
448 
449         void add (Cookie cookie)
450         {
451                 // add the cookie header via our callback
452                 headers.add (name, (OutputBuffer buf){cookie.produce (&buf.write);});
453         }
454 }
455 
456 
457 
458 /*******************************************************************************
459 
460         Server-side cookie parser. See RFC 2109 for details.
461 
462 *******************************************************************************/
463 
464 class CookieParser : Iterator
465 {
466         private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote};
467 
468         private CookieStack       stack;
469         private Array             array;
470         private static bool[128]  charMap;
471 
472         /***********************************************************************
473 
474                 populate a map of token separators
475 
476         ***********************************************************************/
477 
478         static this ()
479         {
480                 charMap['('] = true;
481                 charMap[')'] = true;
482                 charMap['<'] = true;
483                 charMap['>'] = true;
484                 charMap['@'] = true;
485                 charMap[','] = true;
486                 charMap[';'] = true;
487                 charMap[':'] = true;
488                 charMap['\\'] = true;
489                 charMap['"'] = true;
490                 charMap['/'] = true;
491                 charMap['['] = true;
492                 charMap[']'] = true;
493                 charMap['?'] = true;
494                 charMap['='] = true;
495                 charMap['{'] = true;
496                 charMap['}'] = true;
497         }
498 
499         /***********************************************************************
500 
501         ***********************************************************************/
502 
503         this (CookieStack stack)
504         {
505                 super();
506                 this.stack = stack;
507                 array = new Array(0);
508         }
509 
510         /***********************************************************************
511 
512                 Callback for iterator.next(). We scan for name-value
513                 pairs, populating Cookie instances along the way.
514 
515         ***********************************************************************/
516 
517         protected override size_t scan (const(void)[] data)
518         {
519                 char    c;
520                 int     mark,
521                         vrsn;
522                 char[]  name,
523                         token;
524                 Cookie  cookie;
525 
526                 State   state = State.Begin;
527                 char[]  content = cast(char[]) data;
528 
529                 /***************************************************************
530 
531                         Found a value; set that also
532 
533                 ***************************************************************/
534 
535                 void setValue (int i)
536                 {
537                         token = content [mark..i];
538                         //Print ("::name '%.*s'\n", name);
539                         //Print ("::value '%.*s'\n", token);
540 
541                         if (name[0] != '$')
542                            {
543                            cookie = stack.push;
544                            cookie.setName (name);
545                            cookie.setValue (token);
546                            cookie.setVersion (vrsn);
547                            }
548                         else
549                            switch (toLower (name))
550                                   {
551                                   case "$path":
552                                         if (cookie)
553                                             cookie.setPath (token);
554                                         break;
555 
556                                   case "$domain":
557                                         if (cookie)
558                                             cookie.setDomain (token);
559                                         break;
560 
561                                   case "$version":
562                                         vrsn = cast(int) Integer.parse (token);
563                                         break;
564 
565                                   default:
566                                        break;
567                                   }
568                         state = State.Begin;
569                 }
570 
571                 /***************************************************************
572 
573                         Scan content looking for cookie fields
574 
575                 ***************************************************************/
576 
577                 for (int i; i < content.length; ++i)
578                     {
579                     c = content [i];
580                     switch (state)
581                            {
582                            // look for an lValue
583                            case State.Begin:
584                                 mark = i;
585                                 if (isToken(c))
586                                     state = State.LValue;
587                                 continue;
588 
589                            // scan until we have all lValue chars
590                            case State.LValue:
591                                 if (! isToken(c))
592                                    {
593                                    state = State.Equals;
594                                    name = content [mark..i];
595                                    --i;
596                                    }
597                                 continue;
598 
599                            // should now have either a '=', ';', or ','
600                            case State.Equals:
601                                 if (c is '=')
602                                     state = State.RValue;
603                                 else
604                                    if (c is ',' || c is ';')
605                                        // get next NVPair
606                                        state = State.Begin;
607                                 continue;
608 
609                            // look for a quoted token, or a plain one
610                            case State.RValue:
611                                 mark = i;
612                                 if (c is '\'')
613                                     state = State.SQuote;
614                                 else
615                                    if (c is '"')
616                                        state = State.DQuote;
617                                    else
618                                       if (isToken(c))
619                                           state = State.Token;
620                                 continue;
621 
622                            // scan for all plain token chars
623                            case State.Token:
624                                 if (! isToken(c))
625                                    {
626                                    setValue (i);
627                                    --i;
628                                    }
629                                 continue;
630 
631                            // scan until the next '
632                            case State.SQuote:
633                                 if (c is '\'')
634                                     ++mark, setValue (i);
635                                 continue;
636 
637                            // scan until the next "
638                            case State.DQuote:
639                                 if (c is '"')
640                                     ++mark, setValue (i);
641                                 continue;
642 
643                            default:
644                                 continue;
645                            }
646                     }
647 
648                 // we ran out of content; patch partial cookie values
649                 if (state is State.Token)
650                     setValue (cast(int) content.length);
651 
652                 // go home
653                 return IConduit.Eof;
654         }
655 
656         /***********************************************************************
657 
658                 Locate the next token from the provided buffer, and map a
659                 buffer reference into token. Returns true if a token was
660                 located, false otherwise.
661 
662                 Note that the buffer content is not duplicated. Instead, a
663                 slice of the buffer is referenced by the token. You can use
664                 Token.clone() or Token.toString().dup() to copy content per
665                 your application needs.
666 
667                 Note also that there may still be one token left in a buffer
668                 that was not terminated correctly (as in eof conditions). In
669                 such cases, tokens are mapped onto remaining content and the
670                 buffer will have no more readable content.
671 
672         ***********************************************************************/
673 
674         bool parse (char[] header)
675         {
676                 super.set (array.assign (header));
677                 return next.ptr > null;
678         }
679 
680         /**********************************************************************
681 
682                 in-place conversion to lowercase
683 
684         **********************************************************************/
685 
686         final static char[] toLower (ref char[] src)
687         {
688                 foreach (size_t i, char c; src)
689                          if (c >= 'A' && c <= 'Z')
690                              src[i] = cast(char)(c + ('a' - 'A'));
691                 return src;
692         }
693 
694         /***********************************************************************
695 
696                 Is 'c' a valid token character?
697 
698         ***********************************************************************/
699 
700         private static bool isToken (char c)
701         {
702                 return (c > 32 && c < 127 && !charMap[c]);
703         }
704 }