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