1 /*******************************************************************************
2 
3         copyright:      Copyright (c) 2004 Kris Bell. All rights reserved
4 
5         license:        BSD style: $(LICENSE)
6 
7         version:        Initial release: April 2004      
8         
9         author:         Kris
10 
11 *******************************************************************************/
12 
13 module tango.net.http.HttpCookies;
14 
15 private import  tango.stdc.ctype;
16 
17 private import  tango.io.device.Array;
18 
19 private import  tango.io.model.IConduit;
20 
21 private import  tango.io.stream.Iterator;
22 
23 private import  tango.net.http.HttpHeaders;
24 
25 private import  Integer = tango.text.convert.Integer;
26 
27 /*******************************************************************************
28 
29         Defines the Cookie class, and the means for reading & writing them.
30         Cookie implementation conforms with RFC 2109, but supports parsing 
31         of server-side cookies only. Client-side cookies are supported in
32         terms of output, but response parsing is not yet implemented ...
33 
34         See over <A HREF="http://www.faqs.org/rfcs/rfc2109.html">here</A>
35         for the RFC document.        
36 
37 *******************************************************************************/
38 
39 class Cookie //: IWritable
40 {
41         const(char)[]   name,
42                         path,
43                         value,
44                         domain,
45                         comment;
46         uint            vrsn=1;              // 'version' is a reserved word
47         bool            secure=false;
48         long            maxAge=long.min;
49 
50         /***********************************************************************
51                 
52                 Construct an empty client-side cookie. You add these
53                 to an output request using HttpClient.addCookie(), or
54                 the equivalent.
55 
56         ***********************************************************************/
57 
58         this () {}
59 
60         /***********************************************************************
61         
62                 Construct a cookie with the provided attributes. You add 
63                 these to an output request using HttpClient.addCookie(), 
64                 or the equivalent.
65 
66         ***********************************************************************/
67 
68         this (const(char)[] name, const(char)[] value)
69         {
70                 setName (name);
71                 setValue (value);
72         }
73 
74         /***********************************************************************
75         
76                 Set the name of this cookie
77 
78         ***********************************************************************/
79 
80         Cookie setName (const(char)[] name)
81         {
82                 this.name = name;
83                 return this;
84         }
85 
86         /***********************************************************************
87         
88                 Set the value of this cookie
89 
90         ***********************************************************************/
91 
92         Cookie setValue (const(char)[] value)
93         {
94                 this.value = value;
95                 return this;
96         }
97 
98         /***********************************************************************
99                 
100                 Set the version of this cookie
101 
102         ***********************************************************************/
103 
104         Cookie setVersion (uint vrsn)
105         {
106                 this.vrsn = vrsn;
107                 return this;
108         }
109 
110         /***********************************************************************
111         
112                 Set the path of this cookie
113 
114         ***********************************************************************/
115 
116         Cookie setPath (const(char)[] path)
117         {
118                 this.path = path;
119                 return this;
120         }
121 
122         /***********************************************************************
123         
124                 Set the domain of this cookie
125 
126         ***********************************************************************/
127 
128         Cookie setDomain (const(char)[] domain)
129         {
130                 this.domain = domain;
131                 return this;
132         }
133 
134         /***********************************************************************
135         
136                 Set the comment associated with this cookie
137 
138         ***********************************************************************/
139 
140         Cookie setComment (const(char)[] comment)
141         {
142                 this.comment = comment;
143                 return this;
144         }
145 
146         /***********************************************************************
147         
148                 Set the maximum duration of this cookie
149 
150         ***********************************************************************/
151 
152         Cookie setMaxAge (long maxAge)
153         {
154                 this.maxAge = maxAge;
155                 return this;
156         }
157 
158         /***********************************************************************
159         
160                 Indicate whether this cookie should be considered secure or not
161 
162         ***********************************************************************/
163 
164         Cookie setSecure (bool secure)
165         {
166                 this.secure = secure;
167                 return this;
168         }
169 /+
170         /***********************************************************************
171         
172                 Output the cookie as a text stream, via the provided IWriter
173 
174         ***********************************************************************/
175 
176         void write (IWriter writer)
177         {
178                 produce (&writer.buffer.consume);
179         }
180 +/
181         /***********************************************************************
182         
183                 Output the cookie as a text stream, via the provided consumer
184 
185         ***********************************************************************/
186 
187         void produce (scope size_t delegate(const(void)[]) consume)
188         {
189                 consume (name);
190 
191                 if (value.length)
192                     consume ("="), consume (value);
193 
194                 if (path.length)
195                     consume (";Path="), consume (path);
196 
197                 if (domain.length)
198                     consume (";Domain="), consume (domain);
199 
200                 if (vrsn)
201                    {
202                    char[16] tmp = void;
203 
204                    consume (";Version=");
205                    consume (Integer.format (tmp, vrsn));
206 
207                    if (comment.length)
208                        consume (";Comment=\""), consume(comment), consume("\"");
209 
210                    if (secure)
211                        consume (";Secure");
212 
213                    if (maxAge != maxAge.min)
214                        consume (";Max-Age="c), consume (Integer.format (tmp, maxAge));
215                    }
216         }
217 
218         /***********************************************************************
219         
220                 Reset this cookie
221 
222         ***********************************************************************/
223 
224         Cookie clear ()
225         {
226                 vrsn = 1;
227                 secure = false;
228                 maxAge = maxAge.min;
229                 name = path = domain = comment = null;
230                 return this;
231         }
232 }
233 
234 
235 
236 /*******************************************************************************
237 
238         Implements a stack of cookies. Each cookie is pushed onto the
239         stack by a parser, which takes its input from HttpHeaders. The
240         stack can be populated for both client and server side cookies.
241 
242 *******************************************************************************/
243 
244 class CookieStack
245 {
246         private int             depth;
247         private Cookie[]        cookies;
248 
249         /**********************************************************************
250 
251                 Construct a cookie stack with the specified initial extent.
252                 The stack will grow as necessary over time.
253 
254         **********************************************************************/
255 
256         this (int size)
257         {
258                 cookies = new Cookie[0];
259                 resize (cookies, size);
260         }
261 
262         /**********************************************************************
263 
264                 Pop the stack all the way to zero
265 
266         **********************************************************************/
267 
268         final void reset ()
269         {
270                 depth = 0;
271         }
272 
273         /**********************************************************************
274 
275                 Return a fresh cookie from the stack
276 
277         **********************************************************************/
278 
279         final Cookie push ()
280         {
281                 if (depth == cookies.length)
282                     resize (cookies, depth * 2);
283                 return cookies [depth++];
284         }
285         
286         /**********************************************************************
287 
288                 Resize the stack such that it has more room.
289 
290         **********************************************************************/
291 
292         private final static void resize (ref Cookie[] cookies, size_t size)
293         {
294                 size_t i = cookies.length;
295                 
296                 for (cookies.length=size; i < cookies.length; ++i)
297                      cookies[i] = new Cookie();
298         }
299 
300         /**********************************************************************
301 
302                 Iterate over all cookies in stack
303 
304         **********************************************************************/
305 
306         int opApply (scope int delegate(ref Cookie) dg)
307         {
308                 int result = 0;
309 
310                 for (int i=0; i < depth; ++i)
311                      if ((result = dg (cookies[i])) != 0)
312                           break;
313                 return result;
314         }
315 }
316 
317 
318 
319 /*******************************************************************************
320 
321         This is the support point for server-side cookies. It wraps a
322         CookieStack together with a set of HttpHeaders, along with the
323         appropriate cookie parser. One would do something very similar
324         for client side cookie parsing also.
325 
326 *******************************************************************************/
327 
328 class HttpCookiesView //: IWritable
329 {
330         private bool                    parsed;
331         private CookieStack             stack;
332         private CookieParser            parser;
333         private HttpHeadersView         headers;
334 
335         /**********************************************************************
336 
337                 Construct cookie wrapper with the provided headers.
338 
339         **********************************************************************/
340 
341         this (HttpHeadersView headers)
342         {
343                 this.headers = headers;
344 
345                 // create a stack for parsed cookies
346                 stack = new CookieStack (10);
347 
348                 // create a parser
349                 parser = new CookieParser (stack);
350         }
351 /+
352         /**********************************************************************
353 
354                 Output each of the cookies parsed to the provided IWriter.
355 
356         **********************************************************************/
357 
358         void write (IWriter writer)
359         {
360                 produce (&writer.buffer.consume, HttpConst.Eol);
361         }
362 +/
363         /**********************************************************************
364 
365                 Output the token list to the provided consumer
366 
367         **********************************************************************/
368 
369         void produce (scope size_t delegate(const(void)[]) consume, const(char)[] eol = HttpConst.Eol)
370         {
371                 foreach (cookie; parse())
372                          cookie.produce (consume), consume (eol);
373         }
374 
375         /**********************************************************************
376 
377                 Reset these cookies for another parse
378 
379         **********************************************************************/
380 
381         void reset ()
382         {
383                 stack.reset();
384                 parsed = false;
385         }
386 
387         /**********************************************************************
388 
389                 Parse all cookies from our HttpHeaders, pushing each onto
390                 the CookieStack as we go.
391 
392         **********************************************************************/
393 
394         CookieStack parse ()
395         {
396                 if (! parsed)
397                    {
398                    parsed = true;
399 
400                    foreach (HeaderElement header; headers)
401                             if (header.name.value == HttpHeader.Cookie.value)
402                                 parser.parse (header.value);
403                    }
404                 return stack;
405         }
406 }
407 
408 
409 
410 /*******************************************************************************
411 
412         Handles a set of output cookies by writing them into the list of
413         output headers.
414 
415 *******************************************************************************/
416 
417 class HttpCookies
418 {
419         private HttpHeaderName  name;
420         private HttpHeaders     headers;
421 
422         /**********************************************************************
423 
424                 Construct an output cookie wrapper upon the provided 
425                 output headers. Each cookie added is converted to an
426                 addition to those headers.
427 
428         **********************************************************************/
429 
430         this (HttpHeaders headers, const(HttpHeaderName) name = HttpHeader.SetCookie)
431         {
432                 this.headers = headers;
433                 this.name = name;
434         }
435 
436         /**********************************************************************
437 
438                 Add a cookie to our output headers.
439 
440         **********************************************************************/
441 
442         void add (Cookie cookie)
443         {
444                 // add the cookie header via our callback
445                 headers.add (name, (OutputBuffer buf){cookie.produce (&buf.write);});        
446         }
447 }
448 
449 
450 
451 /*******************************************************************************
452 
453         Server-side cookie parser. See RFC 2109 for details.
454 
455 *******************************************************************************/
456 
457 class CookieParser : Iterator!(char)
458 {
459         private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote};
460 
461         private CookieStack       stack;
462         private Array             array;
463         private static __gshared bool[128]  charMap;
464 
465         /***********************************************************************
466 
467                 populate a map of token separators
468 
469         ***********************************************************************/
470 
471         shared static this ()
472         {
473                 charMap['('] = true;
474                 charMap[')'] = true;
475                 charMap['<'] = true;
476                 charMap['>'] = true;
477                 charMap['@'] = true;
478                 charMap[','] = true;
479                 charMap[';'] = true;
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         }
491         
492         /***********************************************************************
493 
494         ***********************************************************************/
495 
496         this (CookieStack stack)
497         {
498                 super();
499                 this.stack = stack;
500                 array = new Array(0);
501         }
502 
503         /***********************************************************************
504 
505                 Callback for iterator.next(). We scan for name-value
506                 pairs, populating Cookie instances along the way.
507 
508         ***********************************************************************/
509 
510         protected override size_t scan (const(void)[] data)
511         {      
512                 char           c;
513                 int            mark,
514                                vrsn;
515                 const(char)[]  name,
516                                token;
517                 Cookie         cookie;
518 
519                 State          state = State.Begin;
520                 const(char)[]  content = cast(const(char)[]) data;
521 
522                 /***************************************************************
523 
524                         Found a value; set that also
525 
526                 ***************************************************************/
527 
528                 void setValue (size_t i)
529                 {   
530                         token = content [mark..i];
531                         //Print ("::name '%.*s'\n", name);
532                         //Print ("::value '%.*s'\n", token);
533 
534                         if (name[0] != '$')
535                            {
536                            cookie = stack.push();
537                            cookie.setName (name);
538                            cookie.setValue (token);
539                            cookie.setVersion (vrsn);
540                            }
541                         else
542                            {
543                            if(name.length < 9)
544                               {
545                               char[8] temp;
546                               temp[0..name.length] = name[];
547                               switch (toLower (temp[0..name.length]))
548                                      {
549                                      case "$path":
550                                            if (cookie)
551                                                cookie.setPath (token); 
552                                            break;
553 
554                                      case "$domain":
555                                            if (cookie)
556                                                cookie.setDomain (token); 
557                                            break;
558 
559                                      case "$version":
560                                            vrsn = cast(int) Integer.parse (token); 
561                                            break;
562 
563                                      default:
564                                           break;
565                                      }
566                                }
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 (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 (const(char)[] header)
675         {
676                 super.set (array.assign (cast(void[]) header));
677                 return next.ptr > null;
678         }
679 
680         /**********************************************************************
681 
682                 in-place conversion to lowercase 
683 
684         **********************************************************************/
685 
686         final static char[] toLower (char[] src)
687         {
688                 foreach (int 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 }
705    
706