1 module hunt.http.Cookie;
2 
3 import hunt.collection.List;
4 import hunt.Exceptions;
5 import hunt.text.Common;
6 import hunt.util.StringBuilder;
7 import hunt.text.StringUtils;
8 import hunt.util.DateTime;
9 
10 import std.array;
11 import std.container.array;
12 import std.conv;
13 import std.datetime;
14 import std.string;
15 
16 import core.stdc.stdio;
17 import core.stdc.time;
18 
19 alias HttpCookie = Cookie;
20 
21 /**
22  * An HttpCookie object represents an HTTP cookie, which carries state
23  * information between server and user agent. Cookie is widely adopted
24  * to create stateful sessions.
25  *
26  * <p> There are 3 HTTP cookie specifications:
27  * <blockquote>
28  *   Netscape draft<br>
29  *   RFC 2109 - <a href="http://www.ietf.org/rfc/rfc2109.txt">
30  * <i>http://www.ietf.org/rfc/rfc2109.txt</i></a><br>
31  *   RFC 2965 - <a href="http://www.ietf.org/rfc/rfc2965.txt">
32  * <i>http://www.ietf.org/rfc/rfc2965.txt</i></a>
33  * </blockquote>
34  *
35  * <p> HttpCookie class can accept all these 3 forms of syntax.
36  *
37  * @author Edward Wang
38  */
39 class Cookie {
40 
41     // Since the positive and zero max-age have their meanings,
42     // this value serves as a hint as 'not specify max-age'
43     private enum MAX_AGE_UNSPECIFIED = -1;
44     //
45     // The value of the cookie itself.
46     //
47 
48     private string name; // NAME= ... "$Name" style is reserved
49     private string value; // value of NAME
50 
51     //
52     // Attributes encoded in the header's cookie fields.
53     //
54 
55     private string comment; // ;Comment=VALUE ... describes cookie's use
56     // ;Discard ... implied by maxAge < 0
57     private string domain; // ;Domain=VALUE ... domain that sees cookie
58     private int maxAge = MAX_AGE_UNSPECIFIED; // ;Max-Age=VALUE ... cookies auto-expire
59     private string path; // ;Path=VALUE ... URLs that see the cookie
60     private bool secure; // ;Secure ... e.g. use SSL
61     private int _version = 1; // ;Version=1 ... means RFC 2109++ style
62     private bool _isHttpOnly = false;
63 
64     // Hold the creation time (in seconds) of the http cookie for later
65     // expiration calculation
66     private long whenCreated;
67 
68     // date formats used by Netscape's cookie draft
69     // as well as formats seen on various sites
70     private enum string[] COOKIE_DATE_FORMATS = [
71         "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'",
72         "EEE',' dd MMM yyyy HH:mm:ss 'GMT'",
73         "EEE MMM dd yyyy HH:mm:ss 'GMT'Z",
74         "EEE',' dd-MMM-yy HH:mm:ss 'GMT'",
75         "EEE',' dd MMM yy HH:mm:ss 'GMT'",
76         "EEE MMM dd yy HH:mm:ss 'GMT'Z"
77     ];	
78 
79     this() {
80 
81     }
82 
83     /**
84      * Constructs a cookie with the specified name and value.
85      *
86      * <p>
87      * The name must conform to RFC 2109. However, vendors may provide a
88      * configuration option that allows cookie names conforming to the original
89      * Netscape Cookie Specification to be accepted.
90      *
91      * <p>
92      * The name of a cookie cannot be changed once the cookie has been created.
93      *
94      * <p>
95      * The value can be anything the server chooses to send. Its value is
96      * probably of interest only to the server. The cookie's value can be
97      * changed after creation with the <code>setValue</code> method.
98      *
99      * <p>
100      * By default, cookies are created according to the Netscape cookie
101      * specification. The version can be changed with the
102      * <code>setVersion</code> method.
103      *
104      * @param name
105      *            the name of the cookie
106      *
107      * @param value
108      *            the value of the cookie
109      *
110      * @throws IllegalArgumentException
111      *             if the cookie name is null or empty or contains any illegal
112      *             characters (for example, a comma, space, or semicolon) or
113      *             matches a token reserved for use by the cookie protocol
114      *
115      * @see #setValue
116      * @see #setVersion
117      */
118     this(string name, string value, int expires=-1, 
119         string path = "/", string domain = null, 
120         bool secure = false, bool httpOnly = true) {
121         if (name.empty) {
122             throw new IllegalArgumentException("the cookie name is empty");
123         }
124 
125         this.name = name;
126         this.value = value;
127         this.maxAge = expires;
128         this.path = path;
129         this.secure = secure;
130         this.domain = domain;
131         this._isHttpOnly = httpOnly;
132         this.whenCreated = hunt.util.DateTime.DateTime.currentTimeMillis();
133     }
134 
135     /**
136      * Specifies a comment that describes a cookie's purpose. The comment is
137      * useful if the browser presents the cookie to the user. Comments are not
138      * supported by Netscape Version 0 cookies.
139      *
140      * @param purpose
141      *            a <code>string</code> specifying the comment to display to the
142      *            user
143      *
144      * @see #getComment
145      */
146     void setComment(string purpose) {
147         comment = purpose;
148     }
149 
150     /**
151      * Returns the comment describing the purpose of this cookie, or
152      * <code>null</code> if the cookie has no comment.
153      *
154      * @return the comment of the cookie, or <code>null</code> if unspecified
155      *
156      * @see #setComment
157      */
158     string getComment() {
159         return comment;
160     }
161 
162     /**
163      *
164      * Specifies the domain within which this cookie should be presented.
165      *
166      * <p>
167      * The form of the domain name is specified by RFC 2109. A domain name
168      * begins with a dot (<code>.foo.com</code>) and means that the cookie is
169      * visible to servers in a specified Domain Name System (DNS) zone (for
170      * example, <code>www.foo.com</code>, but not <code>a.b.foo.com</code>). By
171      * default, cookies are only returned to the server that sent them.
172      *
173      * @param domain
174      *            the domain name within which this cookie is visible; form is
175      *            according to RFC 2109
176      *
177      * @see #getDomain
178      */
179     void setDomain(string domain) {
180         this.domain = domain.toLower(); // IE allegedly needs
181                                                             // this
182     }
183 
184     /**
185      * Gets the domain name of this Cookie.
186      *
187      * <p>
188      * Domain names are formatted according to RFC 2109.
189      *
190      * @return the domain name of this Cookie
191      *
192      * @see #setDomain
193      */
194     string getDomain() nothrow {
195         return domain;
196     }
197 
198     /**
199      * Sets the maximum age in seconds for this Cookie.
200      *
201      * <p>
202      * A positive value indicates that the cookie will expire after that many
203      * seconds have passed. Note that the value is the <i>maximum</i> age when
204      * the cookie will expire, not the cookie's current age.
205      *
206      * <p>
207      * A negative value means that the cookie is not stored persistently and
208      * will be deleted when the Web browser exits. A zero value causes the
209      * cookie to be deleted.
210      *
211      * @param expiry
212      *            an integer specifying the maximum age of the cookie in
213      *            seconds; if negative, means the cookie is not stored; if zero,
214      *            deletes the cookie
215      *
216      * @see #getMaxAge
217      */
218     void setMaxAge(int expiry) {
219         maxAge = expiry;
220     }
221 
222     /**
223      * Gets the maximum age in seconds of this Cookie.
224      *
225      * <p>
226      * By default, <code>-1</code> is returned, which indicates that the cookie
227      * will persist until browser shutdown.
228      *
229      * @return an integer specifying the maximum age of the cookie in seconds;
230      *         if negative, means the cookie persists until browser shutdown
231      *
232      * @see #setMaxAge
233      */
234     int getMaxAge() {
235         return maxAge;
236     }
237 
238     /**
239      * Specifies a path for the cookie to which the client should return the
240      * cookie.
241      *
242      * <p>
243      * The cookie is visible to all the pages in the directory you specify, and
244      * all the pages in that directory's subdirectories. A cookie's path, for
245      * example, <i>/catalog</i>, which makes the cookie visible to all
246      * directories on the server under <i>/catalog</i>.
247      *
248      * <p>
249      * Consult RFC 2109 (available on the Internet) for more information on
250      * setting path names for cookies.
251      *
252      *
253      * @param uri
254      *            a <code>string</code> specifying a path
255      *
256      * @see #getPath
257      */
258     void setPath(string uri) {
259         path = uri;
260     }
261 
262     /**
263      * Returns the path on the server to which the browser returns this cookie.
264      * The cookie is visible to all subpaths on the server.
265      *
266      * @return a <code>string</code> specifying a path , for example,
267      *         <i>/catalog</i>
268      *
269      * @see #setPath
270      */
271     string getPath() nothrow {
272         return path;
273     }
274 
275     /**
276      * Indicates to the browser whether the cookie should only be sent using a
277      * secure protocol, such as HTTPS or SSL.
278      *
279      * <p>
280      * The default value is <code>false</code>.
281      *
282      * @param flag
283      *            if <code>true</code>, sends the cookie from the browser to the
284      *            server only when using a secure protocol; if
285      *            <code>false</code>, sent on any protocol
286      *
287      * @see #getSecure
288      */
289     void setSecure(bool flag) {
290         secure = flag;
291     }
292 
293     /**
294      * Returns <code>true</code> if the browser is sending cookies only over a
295      * secure protocol, or <code>false</code> if the browser can send cookies
296      * using any protocol.
297      *
298      * @return <code>true</code> if the browser uses a secure protocol,
299      *         <code>false</code> otherwise
300      *
301      * @see #setSecure
302      */
303     bool getSecure() {
304         return secure;
305     }
306 
307     void setName(string name) {
308         this.name = name;
309     }
310 
311     /**
312      * Returns the name of the cookie. The name cannot be changed after
313      * creation.
314      *
315      * @return the name of the cookie
316      */
317     string getName() nothrow {
318         return name;
319     }
320 
321     /**
322      * Assigns a new value to this Cookie.
323      * 
324      * <p>
325      * If you use a binary value, you may want to use BASE64 encoding.
326      *
327      * <p>
328      * With Version 0 cookies, values should not contain white space, brackets,
329      * parentheses, equals signs, commas, double quotes, slashes, question
330      * marks, at signs, colons, and semicolons. Empty values may not behave the
331      * same way on all browsers.
332      *
333      * @param newValue
334      *            the new value of the cookie
335      *
336      * @see #getValue
337      */
338     void setValue(string newValue) {
339         value = newValue;
340     }
341 
342     /**
343      * Gets the current value of this Cookie.
344      *
345      * @return the current value of this Cookie
346      *
347      * @see #setValue
348      */
349     string getValue() {
350         return value;
351     }
352 
353     /**
354      * Returns the version of the protocol this cookie complies with. Version 1
355      * complies with RFC 2109, and version 0 complies with the original cookie
356      * specification drafted by Netscape. Cookies provided by a browser use and
357      * identify the browser's cookie version.
358      * 
359      * @return 0 if the cookie complies with the original Netscape
360      *         specification; 1 if the cookie complies with RFC 2109
361      *
362      * @see #setVersion
363      */
364     int getVersion() {
365         return _version;
366     }
367 
368     /**
369      * Sets the version of the cookie protocol that this Cookie complies with.
370      *
371      * <p>
372      * Version 0 complies with the original Netscape cookie specification.
373      * Version 1 complies with RFC 2109.
374      *
375      * <p>
376      * Since RFC 2109 is still somewhat new, consider version 1 as experimental;
377      * do not use it yet on production sites.
378      *
379      * @param v
380      *            0 if the cookie should comply with the original Netscape
381      *            specification; 1 if the cookie should comply with RFC 2109
382      *
383      * @see #getVersion
384      */
385     void setVersion(int v) {
386         _version = v;
387     }
388 
389     /**
390      * Overrides the standard <code>java.lang.Object.clone</code> method to
391      * return a copy of this Cookie.
392      */
393     // Object clone() {
394     // 	try {
395     // 		return super.clone();
396     // 	} catch (NotSupportedException e) {
397     // 		throw new RuntimeException(e.getMessage());
398     // 	}
399     // }
400 
401     /**
402      * Marks or unmarks this Cookie as <i>HttpOnly</i>.
403      *
404      * <p>
405      * If <tt>isHttpOnly</tt> is set to <tt>true</tt>, this cookie is marked as
406      * <i>HttpOnly</i>, by adding the <tt>HttpOnly</tt> attribute to it.
407      *
408      * <p>
409      * <i>HttpOnly</i> cookies are not supposed to be exposed to client-side
410      * scripting code, and may therefore help mitigate certain kinds of
411      * cross-site scripting attacks.
412      *
413      * @param isHttpOnly
414      *            true if this cookie is to be marked as <i>HttpOnly</i>, false
415      *            otherwise
416      *
417      */
418     void setHttpOnly(bool isHttpOnly) {
419         this._isHttpOnly = isHttpOnly;
420     }
421 
422     /**
423      * Checks whether this Cookie has been marked as <i>HttpOnly</i>.
424      *
425      * @return true if this Cookie has been marked as <i>HttpOnly</i>, false
426      *         otherwise
427      *
428      */
429     bool isHttpOnly() {
430         return _isHttpOnly;
431     }
432 
433     /**
434      * Reports whether this HTTP cookie has expired or not.
435      *
436      * @return  {@code true} to indicate this HTTP cookie has expired;
437      *          otherwise, {@code false}
438      */
439     bool isExpired() {
440         // if not specify max-age, this cookie should be
441         // discarded when user agent is to be closed, but
442         // it is not expired.
443         if (maxAge < 0) return false;
444         if (maxAge == 0) return true;
445 
446         long deltaSecond = (hunt.util.DateTime.DateTime.currentTimeMillis() - whenCreated) / 1000;
447         return deltaSecond > maxAge;
448     }
449 
450     override
451     string toString() {
452         return "Cookie [name=" ~ name ~ ", value=" ~ value ~ ", comment=" ~ comment ~ 
453             ", domain=" ~ domain ~ ", maxAge=" ~ maxAge.to!string ~ ", path=" ~ path ~ ", secure=" ~ 
454             to!string(secure) ~  ", version=" ~ to!string(_version) ~ ", isHttpOnly=" ~ 
455             to!string(_isHttpOnly) ~ "]";
456     }
457 
458 }
459 
460 /* ----------------------------- CookieGenerator ---------------------------- */
461 
462 string generateCookies(Cookie[] cookies) {
463     if (cookies is null) {
464         throw new IllegalArgumentException("the cookie list is null");
465     }
466 
467     if (cookies.length == 1) {
468         return generateCookie(cookies[0]);
469     } else if (cookies.length > 1) {
470         StringBuilder sb = new StringBuilder();
471 
472         sb.append(generateCookie(cookies[0]));
473         for (size_t i = 1; i < cookies.length; i++) {
474             sb.append(';').append(generateCookie(cookies[i]));
475         }
476 
477         return sb.toString();
478     } else {
479         throw new IllegalArgumentException("the cookie list size is 0");
480     }
481 }
482 
483 string generateCookies(List!Cookie cookies) {
484     if (cookies is null) {
485         throw new IllegalArgumentException("the cookie list is null");
486     }
487 
488     if (cookies.size() == 1) {
489         return generateCookie(cookies.get(0));
490     } else if (cookies.size() > 1) {
491         StringBuilder sb = new StringBuilder();
492 
493         sb.append(generateCookie(cookies.get(0)));
494         for (int i = 1; i < cookies.size(); i++) {
495             sb.append(';').append(generateCookie(cookies.get(i)));
496         }
497 
498         return sb.toString();
499     } else {
500         throw new IllegalArgumentException("the cookie list size is 0");
501     }
502 }
503 
504 string generateCookie(Cookie cookie) {
505     if (cookie is null) {
506         throw new IllegalArgumentException("the cookie is null");
507     } else {
508         return cookie.getName() ~ "=" ~ cookie.getValue();
509     }
510 }
511 
512 string generateSetCookie(Cookie cookie) {
513     if (cookie is null) {
514         throw new IllegalArgumentException("the cookie is null");
515     } else {
516         StringBuilder sb = new StringBuilder();
517 
518         sb.append(cookie.getName()).append('=').append(cookie.getValue());
519 
520         if (!empty(cookie.getComment())) {
521             sb.append(";Comment=").append(cookie.getComment());
522         }
523 
524         if (!empty(cookie.getDomain())) {
525             sb.append(";Domain=").append(cookie.getDomain());
526         }
527         if (cookie.getMaxAge() >= 0) {
528             sb.append(";Max-Age=").append(cookie.getMaxAge());
529         }
530 
531         string path = empty(cookie.getPath()) ? "/" : cookie.getPath();
532         sb.append(";Path=").append(path);
533 
534         if (cookie.getSecure()) {
535             sb.append(";Secure");
536         }
537 
538         sb.append(";Version=").append(cookie.getVersion());
539 
540         return sb.toString();
541     }
542 }
543 
544 // string generateServletSetCookie(javax.servlet.http.Cookie cookie) {
545 // 	if (cookie == null) {
546 // 		throw new IllegalArgumentException("the cookie is null");
547 // 	} else {
548 // 		StringBuilder sb = new StringBuilder();
549 
550 // 		sb.append(cookie.getName()).append('=').append(cookie.getValue());
551 
552 // 		if (VerifyUtils.isNotEmpty(cookie.getComment())) {
553 // 			sb.append(";Comment=").append(cookie.getComment());
554 // 		}
555 
556 // 		if (VerifyUtils.isNotEmpty(cookie.getDomain())) {
557 // 			sb.append(";Domain=").append(cookie.getDomain());
558 // 		}
559 // 		if (cookie.getMaxAge() >= 0) {
560 // 			sb.append(";Max-Age=").append(cookie.getMaxAge());
561 // 		}
562 
563 // 		string path = VerifyUtils.isEmpty(cookie.getPath()) ? "/" : cookie.getPath();
564 // 		sb.append(";Path=").append(path);
565 
566 // 		if (cookie.getSecure()) {
567 // 			sb.append(";Secure");
568 // 		}
569 
570 // 		sb.append(";Version=").append(cookie.getVersion());
571 
572 // 		return sb.toString();
573 // 	}
574 // }
575 
576 /* ------------------------------ CookieParser ------------------------------ */
577 
578 alias CookieParsingHandler = void delegate(string name, string value);
579 
580 void parseCookies(string cookieStr, CookieParsingHandler callback) {
581     if (empty(cookieStr)) {
582         throw new IllegalArgumentException("the cookie string is empty");
583     } else {
584         string[] cookieKeyValues = StringUtils.split(cookieStr, ";");
585         foreach (string cookieKeyValue ; cookieKeyValues) {
586             string[] kv = StringUtils.split(cookieKeyValue, "=", 2);
587             if (kv != null) {
588                 if (kv.length == 2) {
589                     callback(kv[0].strip(), kv[1].strip());
590                 } else if (kv.length == 1) {
591                     callback(kv[0].strip(), "");
592                 } else {
593                     throw new IllegalStateException("the cookie string format error");
594                 }
595             } else {
596                 throw new IllegalStateException("the cookie string format error");
597             }
598         }
599     }
600 }
601 
602 Cookie parseSetCookie(string cookieStr) {
603     Cookie cookie = new Cookie();
604 
605     parseCookies(cookieStr, (name, value) {
606         
607         if("expires".equalsIgnoreCase(name)) {
608             // "Mon, 20-Apr-2020 12:25:04 GMT"
609             char[3] week;
610             char[3] month;
611             tm t;
612             int r = sscanf(value.ptr, "%3s, %2d-%3s-%4d %2d:%2d:%2d GMT", week.ptr,
613                     &t.tm_mday, month.ptr, &t.tm_year, &t.tm_hour, &t.tm_min, &t.tm_sec);
614             if(r == 7) {
615                 t.tm_mon = monthByName(cast(string) month) + 1;
616                 t.tm_wday = weekDayByName(cast(string) week);
617                 
618                 SysTime dt = SysTime(std.datetime.DateTime(t.tm_year, t.tm_mon, 
619                     t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec), UTC());
620                 SysTime now = Clock.currTime();
621                 Duration dur = dt - Clock.currTime();
622                 long sec = dur.total!"seconds"();
623                 cookie.setMaxAge(cast(int)sec);
624             } else {
625                 // FIXME: Needing refactor or cleanup -@zhangxueping at 2020-04-21T09:38:27+08:00
626                 // 				
627                 // int sec;
628                 // r = sscanf(str.ptr, "%d", &sec);
629                 // if(r == 1) {
630                 // 	SysTime now = Clock.currTime();
631                 // 	cookie.setMaxAge(sec);
632                 // }
633             }
634             
635         } else if("HttpOnly".equalsIgnoreCase(name)) {
636             cookie.setHttpOnly(true);
637         } else if ("Comment".equalsIgnoreCase(name)) {
638             cookie.setComment(value);
639         } else if ("Domain".equalsIgnoreCase(name)) {
640             cookie.setDomain(value);
641         } else if ("Max-Age".equalsIgnoreCase(name)) {
642             cookie.setMaxAge(to!int(value));
643         } else if ("Path".equalsIgnoreCase(name)) {
644             cookie.setPath(value);
645         } else if ("Secure".equalsIgnoreCase(name)) {
646             cookie.setSecure(true);
647         } else if ("Version".equalsIgnoreCase(name)) {
648             cookie.setVersion(to!int(value));
649         } else {
650             cookie.setName(name);
651             cookie.setValue(value);
652         }
653 
654     });
655     return cookie;
656 }
657 
658 Cookie[] parseCookie(string cookieStr) {
659     Array!(Cookie) list;
660     parseCookies(cookieStr, (name, value) { list.insertBack(new Cookie(name, value)); });
661     return list.array();
662 }
663 
664 // List<javax.servlet.http.Cookie> parserServletCookie(string cookieStr) {
665 //     List<javax.servlet.http.Cookie> list = new ArrayList<>();
666 //     parseCookies(cookieStr, (name, value) -> list.add(new javax.servlet.http.Cookie(name, value)));
667 //     return list;
668 // }