00001 /******************************************************************************* 00002 00003 @file HttpCookies.d 00004 00005 Copyright (c) 2004 Kris Bell 00006 00007 This software is provided 'as-is', without any express or implied 00008 warranty. In no event will the authors be held liable for damages 00009 of any kind arising from the use of this software. 00010 00011 Permission is hereby granted to anyone to use this software for any 00012 purpose, including commercial applications, and to alter it and/or 00013 redistribute it freely, subject to the following restrictions: 00014 00015 1. The origin of this software must not be misrepresented; you must 00016 not claim that you wrote the original software. If you use this 00017 software in a product, an acknowledgment within documentation of 00018 said product would be appreciated but is not required. 00019 00020 2. Altered source versions must be plainly marked as such, and must 00021 not be misrepresented as being the original software. 00022 00023 3. This notice may not be removed or altered from any distribution 00024 of the source. 00025 00026 4. Derivative works are permitted, but they must carry this notice 00027 in full and credit the original source. 00028 00029 00030 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 00031 00032 00033 @version Initial version, April 2004 00034 @author Kris 00035 00036 00037 *******************************************************************************/ 00038 00039 module mango.http.server.HttpCookies; 00040 00041 version (Ares) 00042 private import std.c.ctype; 00043 else 00044 private import std.ctype; 00045 00046 private import mango.utils.Text; 00047 00048 private import mango.format.Int, 00049 mango.format.Long; 00050 00051 private import mango.io.Token, 00052 mango.io.Buffer, 00053 mango.io.Tokenizer; 00054 00055 private import mango.io.model.IWriter, 00056 mango.io.model.IBuffer, 00057 mango.io.model.IConduit; 00058 00059 private import mango.http.HttpWriter; 00060 00061 private import mango.http.server.HttpHeaders; 00062 00063 /******************************************************************************* 00064 00065 Defines the Cookie class, and the means for reading & writing them. 00066 Cookie implementation conforms with RFC 2109, but supports parsing 00067 of server-side cookies only. Client-side cookies are supported in 00068 terms of output, but response parsing is not yet implemented ... 00069 00070 See over <A HREF="http://www.faqs.org/rfcs/rfc2109.html">here</A> 00071 for the RFC document. 00072 00073 *******************************************************************************/ 00074 00075 class Cookie : IWritable 00076 { 00077 private char[] name, 00078 path, 00079 value, 00080 domain, 00081 comment; 00082 private uint vrsn=1; // 'version' is a reserved word 00083 private long maxAge; 00084 private bool secure; 00085 00086 /*********************************************************************** 00087 00088 Construct an empty client-side cookie. You add these 00089 to an output request using HttpClient.addCookie(), or 00090 the equivalent. 00091 00092 ***********************************************************************/ 00093 00094 this () 00095 { 00096 } 00097 00098 /*********************************************************************** 00099 00100 Construct a cookie with the provided attributes. You add 00101 these to an output request using HttpClient.addCookie(), 00102 or the equivalent. 00103 00104 ***********************************************************************/ 00105 00106 this (char[] name, char[] value) 00107 { 00108 setName (name); 00109 setValue (value); 00110 } 00111 00112 /*********************************************************************** 00113 00114 Set the name of this cookie 00115 00116 ***********************************************************************/ 00117 00118 void setName (char[] name) 00119 { 00120 this.name = name; 00121 } 00122 00123 /*********************************************************************** 00124 00125 Set the value of this cookie 00126 00127 ***********************************************************************/ 00128 00129 void setValue (char[] value) 00130 { 00131 this.value = value; 00132 } 00133 00134 /*********************************************************************** 00135 00136 Set the version of this cookie 00137 00138 ***********************************************************************/ 00139 00140 void setVersion (uint vrsn) 00141 { 00142 this.vrsn = vrsn; 00143 } 00144 00145 /*********************************************************************** 00146 00147 Set the path of this cookie 00148 00149 ***********************************************************************/ 00150 00151 void setPath (char[] path) 00152 { 00153 this.path = path; 00154 } 00155 00156 /*********************************************************************** 00157 00158 Set the domain of this cookie 00159 00160 ***********************************************************************/ 00161 00162 void setDomain (char[] domain) 00163 { 00164 this.domain = domain; 00165 } 00166 00167 /*********************************************************************** 00168 00169 Set the comment associated with this cookie 00170 00171 ***********************************************************************/ 00172 00173 void setComment (char[] comment) 00174 { 00175 this.comment = comment; 00176 } 00177 00178 /*********************************************************************** 00179 00180 Set the maximum duration of this cookie 00181 00182 ***********************************************************************/ 00183 00184 void setMaxAge (long maxAge) 00185 { 00186 this.maxAge = maxAge; 00187 } 00188 00189 /*********************************************************************** 00190 00191 Indicate wether this cookie should be considered secure or not 00192 00193 ***********************************************************************/ 00194 00195 void setSecure (bool secure) 00196 { 00197 this.secure = secure; 00198 } 00199 00200 /*********************************************************************** 00201 00202 Output the cookie as a text stream, via the provided IWriter. 00203 00204 ***********************************************************************/ 00205 00206 void write (IWriter writer) 00207 { 00208 writer.put (name); 00209 00210 if (value.length) 00211 writer.put ('=').put(value); 00212 00213 if (path.length) 00214 writer.put (";Path=").put(path); 00215 00216 if (domain.length) 00217 writer.put (";Domain=").put(domain); 00218 00219 if (vrsn) 00220 { 00221 char[32] tmp; 00222 00223 writer.put (";Version=").put(Int.format(tmp, vrsn)); 00224 00225 if (comment.length) 00226 writer.put (";Comment=\"").put(comment).put('"'); 00227 00228 if (secure) 00229 writer.put (";Secure"); 00230 00231 if (maxAge >= 0) 00232 writer.put (";Max-Age=").put(Long.format(tmp, maxAge)); 00233 } 00234 } 00235 00236 /*********************************************************************** 00237 00238 Reset this cookie 00239 00240 ***********************************************************************/ 00241 00242 void clear () 00243 { 00244 maxAge = 0; 00245 vrsn = 1; 00246 secure = false; 00247 name = path = domain = comment = null; 00248 } 00249 } 00250 00251 00252 00253 /******************************************************************************* 00254 00255 Implements a stack of cookies. Each cookie is pushed onto the 00256 stack by a parser, which takes its input from HttpHeaders. The 00257 stack can be populated for both client and server side cookies. 00258 00259 *******************************************************************************/ 00260 00261 class CookieStack 00262 { 00263 private int depth; 00264 private Cookie[] cookies; 00265 00266 /********************************************************************** 00267 00268 Construct a cookie stack with the specified initial extent. 00269 The stack will grow as necessary over time. 00270 00271 **********************************************************************/ 00272 00273 this (int size) 00274 { 00275 cookies = new Cookie[0]; 00276 resize (cookies, size); 00277 } 00278 00279 /********************************************************************** 00280 00281 Pop the stack all the way to zero 00282 00283 **********************************************************************/ 00284 00285 final void reset () 00286 { 00287 depth = 0; 00288 } 00289 00290 /********************************************************************** 00291 00292 Return a fresh cookie from the stack 00293 00294 **********************************************************************/ 00295 00296 final Cookie push () 00297 { 00298 if (depth == cookies.length) 00299 resize (cookies, depth * 2); 00300 return cookies [depth++]; 00301 } 00302 00303 /********************************************************************** 00304 00305 Resize the stack such that is has more room. 00306 00307 **********************************************************************/ 00308 00309 private final static void resize (inout Cookie[] cookies, int size) 00310 { 00311 int i = cookies.length; 00312 00313 for (cookies.length=size; i < cookies.length; ++i) 00314 cookies[i] = new Cookie(); 00315 } 00316 00317 /********************************************************************** 00318 00319 Iterate over all cookies in stack 00320 00321 **********************************************************************/ 00322 00323 int opApply (int delegate(inout Cookie) dg) 00324 { 00325 int result = 0; 00326 00327 for (int i=0; i < depth; ++i) 00328 if ((result = dg (cookies[i])) != 0) 00329 break; 00330 return result; 00331 } 00332 } 00333 00334 00335 00336 /******************************************************************************* 00337 00338 This is the support point for server-side cookies. It wraps a 00339 CookieStack together with a set of HttpHeaders, along with the 00340 appropriate cookie parser. One would do something very similar 00341 for client side cookie parsing also. 00342 00343 *******************************************************************************/ 00344 00345 class HttpCookies : IWritable 00346 { 00347 private bool parsed; 00348 private Cookie cookie; 00349 private IBuffer buffer; 00350 private CookieStack stack; 00351 private HttpHeaders headers; 00352 00353 private static CookieParser parser; 00354 00355 /********************************************************************** 00356 00357 Setup the parser for server-side cookies 00358 00359 **********************************************************************/ 00360 00361 static this () 00362 { 00363 parser = new CookieParser (); 00364 } 00365 00366 /********************************************************************** 00367 00368 Construct cookie wrapper with the provided headers. 00369 00370 **********************************************************************/ 00371 00372 this (HttpHeaders headers) 00373 { 00374 this.headers = headers; 00375 00376 // setup an empty buffer for later use 00377 buffer = new Buffer; 00378 00379 // create a stack for parsed cookies 00380 stack = new CookieStack (10); 00381 } 00382 00383 /********************************************************************** 00384 00385 Output each of the cookies parsed to the provided IWriter. 00386 00387 **********************************************************************/ 00388 00389 void write (IWriter writer) 00390 { 00391 foreach (Cookie cookie; parse()) 00392 writer.put (cookie).cr(); 00393 } 00394 00395 /********************************************************************** 00396 00397 Reset these cookies for another parse 00398 00399 **********************************************************************/ 00400 00401 void reset () 00402 { 00403 stack.reset(); 00404 parsed = false; 00405 } 00406 00407 /********************************************************************** 00408 00409 Parse all cookies from our HttpHeaders, pushing each onto 00410 the CookieStack as we go. 00411 00412 **********************************************************************/ 00413 00414 CookieStack parse () 00415 { 00416 if (! parsed) 00417 { 00418 parsed = true; 00419 00420 foreach (HeaderElement header; headers) 00421 if (header.name.value == HttpHeader.Cookie.value) 00422 { 00423 buffer.setValidContent (header.value); 00424 parser.next (buffer, stack); 00425 } 00426 } 00427 return stack; 00428 } 00429 } 00430 00431 00432 00433 /******************************************************************************* 00434 00435 Handles a set of output cookies by writing them into the list of 00436 output headers. 00437 00438 *******************************************************************************/ 00439 00440 class HttpMutableCookies 00441 { 00442 private HttpWriter writer; 00443 private HttpMutableHeaders headers; 00444 00445 /********************************************************************** 00446 00447 Construct an output cookie wrapper upon the provided 00448 output headers. Each cookie added is converted to an 00449 addition to those headers. 00450 00451 **********************************************************************/ 00452 00453 this (HttpMutableHeaders headers) 00454 { 00455 this.headers = headers; 00456 writer = new HttpWriter (headers.getOutputBuffer()); 00457 } 00458 00459 /********************************************************************** 00460 00461 Add a cookie to our output headers. 00462 00463 **********************************************************************/ 00464 00465 void add (Cookie cookie) 00466 { 00467 // nested function to actually perform the output 00468 void writeCookie (IBuffer buf) 00469 { 00470 cookie.write (writer); 00471 } 00472 00473 // add the cookie header via our callback 00474 headers.add (HttpHeader.SetCookie, &writeCookie); 00475 } 00476 } 00477 00478 00479 00480 /******************************************************************************* 00481 00482 Server-side cookie parser. See RFC 2109 for details. 00483 00484 *******************************************************************************/ 00485 00486 class CookieParser : Scanner 00487 { 00488 private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote}; 00489 00490 /*********************************************************************** 00491 00492 Locate the next token from the provided buffer, and map a 00493 buffer reference into token. Returns true if a token was 00494 located, false otherwise. 00495 00496 Note that the buffer content is not duplicated. Instead, a 00497 slice of the buffer is referenced by the token. You can use 00498 Token.clone() or Token.toString().dup() to copy content per 00499 your application needs. 00500 00501 Note also that there may still be one token left in a buffer 00502 that was not terminated correctly (as in eof conditions). In 00503 such cases, tokens are mapped onto remaining content and the 00504 buffer will have no more readable content. 00505 00506 ***********************************************************************/ 00507 00508 bool next (IBuffer buffer, CookieStack stack) 00509 { 00510 /*************************************************************** 00511 00512 callback from Scanner.next(). We scan for name-value 00513 pairs, populating Cookie instances along the way. 00514 00515 ***************************************************************/ 00516 00517 int scan (char[] content) 00518 { 00519 char c; 00520 int mark, 00521 vrsn; 00522 char[] name, 00523 token; 00524 Cookie cookie; 00525 State state = State.Begin; 00526 00527 /******************************************************* 00528 00529 Found a value; set that also 00530 00531 *******************************************************/ 00532 00533 void setValue (int i) 00534 { 00535 token = content [mark..i]; 00536 //printf ("::name '%.*s'\n", name); 00537 //printf ("::value '%.*s'\n", token); 00538 00539 if (name[0] != '$') 00540 { 00541 cookie = stack.push(); 00542 cookie.setName (name); 00543 cookie.setValue (token); 00544 cookie.setVersion (vrsn); 00545 } 00546 else 00547 switch (Text.tolower (name)) 00548 { 00549 case "$path": 00550 if (cookie) 00551 cookie.setPath (token); 00552 break; 00553 00554 case "$domain": 00555 if (cookie) 00556 cookie.setDomain (token); 00557 break; 00558 00559 case "$version": 00560 vrsn = Int.parse (token); 00561 break; 00562 00563 default: 00564 break; 00565 } 00566 state = State.Begin; 00567 } 00568 00569 /******************************************************* 00570 00571 Scan content looking for cookie fields 00572 00573 *******************************************************/ 00574 00575 for (int i; i < content.length; ++i) 00576 { 00577 c = content [i]; 00578 switch (state) 00579 { 00580 // look for an lValue 00581 case State.Begin: 00582 mark = i; 00583 if (isalpha (c) || c == '$') 00584 state = State.LValue; 00585 continue; 00586 00587 // scan until we have all lValue chars 00588 case State.LValue: 00589 if (! isalnum (c)) 00590 { 00591 state = State.Equals; 00592 name = content [mark..i]; 00593 --i; 00594 } 00595 continue; 00596 00597 // should now have either a '=', ';', or ',' 00598 case State.Equals: 00599 if (c == '=') 00600 state = State.RValue; 00601 else 00602 if (c == ',' || c == ';') 00603 // get next NVPair 00604 state = State.Begin; 00605 continue; 00606 00607 // look for a quoted token, or a plain one 00608 case State.RValue: 00609 mark = i; 00610 if (c == '\'') 00611 state = State.SQuote; 00612 else 00613 if (c == '"') 00614 state = State.DQuote; 00615 else 00616 if (isalpha (c)) 00617 state = State.Token; 00618 continue; 00619 00620 // scan for all plain token chars 00621 case State.Token: 00622 if (! isalnum (c)) 00623 { 00624 setValue (i); 00625 --i; 00626 } 00627 continue; 00628 00629 // scan until the next ' 00630 case State.SQuote: 00631 if (c == '\'') 00632 ++mark, setValue (i); 00633 continue; 00634 00635 // scan until the next " 00636 case State.DQuote: 00637 if (c == '"') 00638 ++mark, setValue (i); 00639 continue; 00640 00641 default: 00642 continue; 00643 } 00644 } 00645 00646 // we ran out of content; patch partial cookie values 00647 if (state == State.Token) 00648 setValue (content.length); 00649 00650 // go home 00651 return IConduit.Eof; 00652 } 00653 00654 return super.next (buffer, &scan); 00655 } 00656 } 00657 00658