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 }