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