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