Mercurial > projects > ldc
diff tango/tango/net/http/HttpCookies.d @ 132:1700239cab2e trunk
[svn r136] MAJOR UNSTABLE UPDATE!!!
Initial commit after moving to Tango instead of Phobos.
Lots of bugfixes...
This build is not suitable for most things.
author | lindquist |
---|---|
date | Fri, 11 Jan 2008 17:57:40 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tango/tango/net/http/HttpCookies.d Fri Jan 11 17:57:40 2008 +0100 @@ -0,0 +1,649 @@ +/******************************************************************************* + + copyright: Copyright (c) 2004 Kris Bell. All rights reserved + + license: BSD style: $(LICENSE) + + version: Initial release: April 2004 + + author: Kris + +*******************************************************************************/ + +module tango.net.http.HttpCookies; + +private import tango.io.Buffer; + +private import tango.stdc.ctype; + +private import tango.net.http.HttpHeaders; + +private import tango.io.protocol.model.IWriter; + +private import tango.text.stream.StreamIterator; + +private import Integer = tango.text.convert.Integer; + +/******************************************************************************* + + Defines the Cookie class, and the means for reading & writing them. + Cookie implementation conforms with RFC 2109, but supports parsing + of server-side cookies only. Client-side cookies are supported in + terms of output, but response parsing is not yet implemented ... + + See over <A HREF="http://www.faqs.org/rfcs/rfc2109.html">here</A> + for the RFC document. + +*******************************************************************************/ + +class Cookie : IWritable +{ + private char[] name, + path, + value, + domain, + comment; + private uint vrsn=1; // 'version' is a reserved word + private long maxAge; + private bool secure; + + /*********************************************************************** + + Construct an empty client-side cookie. You add these + to an output request using HttpClient.addCookie(), or + the equivalent. + + ***********************************************************************/ + + this () {} + + /*********************************************************************** + + Construct a cookie with the provided attributes. You add + these to an output request using HttpClient.addCookie(), + or the equivalent. + + ***********************************************************************/ + + this (char[] name, char[] value) + { + setName (name); + setValue (value); + } + + /*********************************************************************** + + Set the name of this cookie + + ***********************************************************************/ + + void setName (char[] name) + { + this.name = name; + } + + /*********************************************************************** + + Set the value of this cookie + + ***********************************************************************/ + + void setValue (char[] value) + { + this.value = value; + } + + /*********************************************************************** + + Set the version of this cookie + + ***********************************************************************/ + + void setVersion (uint vrsn) + { + this.vrsn = vrsn; + } + + /*********************************************************************** + + Set the path of this cookie + + ***********************************************************************/ + + void setPath (char[] path) + { + this.path = path; + } + + /*********************************************************************** + + Set the domain of this cookie + + ***********************************************************************/ + + void setDomain (char[] domain) + { + this.domain = domain; + } + + /*********************************************************************** + + Set the comment associated with this cookie + + ***********************************************************************/ + + void setComment (char[] comment) + { + this.comment = comment; + } + + /*********************************************************************** + + Set the maximum duration of this cookie + + ***********************************************************************/ + + void setMaxAge (long maxAge) + { + this.maxAge = maxAge; + } + + /*********************************************************************** + + Indicate wether this cookie should be considered secure or not + + ***********************************************************************/ + + void setSecure (bool secure) + { + this.secure = secure; + } + + /*********************************************************************** + + Output the cookie as a text stream, via the provided IWriter + + ***********************************************************************/ + + void write (IWriter writer) + { + produce (&writer.buffer.consume); + } + + /*********************************************************************** + + Output the cookie as a text stream, via the provided consumer + + ***********************************************************************/ + + void produce (void delegate(void[]) consume) + { + consume (name); + + if (value.length) + consume ("="), consume (value); + + if (path.length) + consume (";Path="), consume (path); + + if (domain.length) + consume (";Domain="), consume (domain); + + if (vrsn) + { + char[16] tmp = void; + + consume (";Version="); + consume (Integer.format (tmp, vrsn)); + + if (comment.length) + consume (";Comment=\""), consume(comment), consume("\""); + + if (secure) + consume (";Secure"); + + if (maxAge >= 0) + consume (";Max-Age="c), consume (Integer.format (tmp, maxAge)); + } + } + + /*********************************************************************** + + Reset this cookie + + ***********************************************************************/ + + void clear () + { + vrsn = 1; + maxAge = 0; + secure = false; + name = path = domain = comment = null; + } +} + + + +/******************************************************************************* + + Implements a stack of cookies. Each cookie is pushed onto the + stack by a parser, which takes its input from HttpHeaders. The + stack can be populated for both client and server side cookies. + +*******************************************************************************/ + +class CookieStack +{ + private int depth; + private Cookie[] cookies; + + /********************************************************************** + + Construct a cookie stack with the specified initial extent. + The stack will grow as necessary over time. + + **********************************************************************/ + + this (int size) + { + cookies = new Cookie[0]; + resize (cookies, size); + } + + /********************************************************************** + + Pop the stack all the way to zero + + **********************************************************************/ + + final void reset () + { + depth = 0; + } + + /********************************************************************** + + Return a fresh cookie from the stack + + **********************************************************************/ + + final Cookie push () + { + if (depth == cookies.length) + resize (cookies, depth * 2); + return cookies [depth++]; + } + + /********************************************************************** + + Resize the stack such that it has more room. + + **********************************************************************/ + + private final static void resize (inout Cookie[] cookies, int size) + { + int i = cookies.length; + + for (cookies.length=size; i < cookies.length; ++i) + cookies[i] = new Cookie(); + } + + /********************************************************************** + + Iterate over all cookies in stack + + **********************************************************************/ + + int opApply (int delegate(inout Cookie) dg) + { + int result = 0; + + for (int i=0; i < depth; ++i) + if ((result = dg (cookies[i])) != 0) + break; + return result; + } +} + + + +/******************************************************************************* + + This is the support point for server-side cookies. It wraps a + CookieStack together with a set of HttpHeaders, along with the + appropriate cookie parser. One would do something very similar + for client side cookie parsing also. + +*******************************************************************************/ + +class HttpCookiesView : IWritable +{ + private bool parsed; + private CookieStack stack; + private CookieParser parser; + private HttpHeadersView headers; + + /********************************************************************** + + Construct cookie wrapper with the provided headers. + + **********************************************************************/ + + this (HttpHeadersView headers) + { + this.headers = headers; + + // create a stack for parsed cookies + stack = new CookieStack (10); + + // create a parser + parser = new CookieParser (stack); + } + + /********************************************************************** + + Output each of the cookies parsed to the provided IWriter. + + **********************************************************************/ + + void write (IWriter writer) + { + produce (&writer.buffer.consume, HttpConst.Eol); + } + + /********************************************************************** + + Output the token list to the provided consumer + + **********************************************************************/ + + void produce (void delegate (void[]) consume, char[] eol = HttpConst.Eol) + { + foreach (cookie; parse) + cookie.produce (consume), consume (eol); + } + + /********************************************************************** + + Reset these cookies for another parse + + **********************************************************************/ + + void reset () + { + stack.reset; + parsed = false; + } + + /********************************************************************** + + Parse all cookies from our HttpHeaders, pushing each onto + the CookieStack as we go. + + **********************************************************************/ + + CookieStack parse () + { + if (! parsed) + { + parsed = true; + + foreach (HeaderElement header; headers) + if (header.name.value == HttpHeader.Cookie.value) + parser.parse (header.value); + } + return stack; + } +} + + + +/******************************************************************************* + + Handles a set of output cookies by writing them into the list of + output headers. + +*******************************************************************************/ + +class HttpCookies +{ + private HttpHeaders headers; + + /********************************************************************** + + Construct an output cookie wrapper upon the provided + output headers. Each cookie added is converted to an + addition to those headers. + + **********************************************************************/ + + this (HttpHeaders headers) + { + this.headers = headers; + } + + /********************************************************************** + + Add a cookie to our output headers. + + **********************************************************************/ + + void add (Cookie cookie) + { + // add the cookie header via our callback + headers.add (HttpHeader.SetCookie, (IBuffer buf){cookie.produce (&buf.consume);}); + } +} + + + +/******************************************************************************* + + Server-side cookie parser. See RFC 2109 for details. + +*******************************************************************************/ + +class CookieParser : StreamIterator!(char) +{ + private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote}; + + private CookieStack stack; + private Buffer buffer; + + /*********************************************************************** + + ***********************************************************************/ + + this (CookieStack stack) + { + super(); + this.stack = stack; + buffer = new Buffer; + } + + /*********************************************************************** + + Callback for iterator.next(). We scan for name-value + pairs, populating Cookie instances along the way. + + ***********************************************************************/ + + protected uint scan (void[] data) + { + char c; + int mark, + vrsn; + char[] name, + token; + Cookie cookie; + + State state = State.Begin; + char[] content = cast(char[]) data; + + /*************************************************************** + + Found a value; set that also + + ***************************************************************/ + + void setValue (int i) + { + token = content [mark..i]; + //Print ("::name '%.*s'\n", name); + //Print ("::value '%.*s'\n", token); + + if (name[0] != '$') + { + cookie = stack.push(); + cookie.setName (name); + cookie.setValue (token); + cookie.setVersion (vrsn); + } + else + switch (toLower (name)) + { + case "$path": + if (cookie) + cookie.setPath (token); + break; + + case "$domain": + if (cookie) + cookie.setDomain (token); + break; + + case "$version": + vrsn = cast(int) Integer.parse (token); + break; + + default: + break; + } + state = State.Begin; + } + + /*************************************************************** + + Scan content looking for cookie fields + + ***************************************************************/ + + for (int i; i < content.length; ++i) + { + c = content [i]; + switch (state) + { + // look for an lValue + case State.Begin: + mark = i; + if (isalpha (c) || c is '$') + state = State.LValue; + continue; + + // scan until we have all lValue chars + case State.LValue: + if (! isalnum (c)) + { + state = State.Equals; + name = content [mark..i]; + --i; + } + continue; + + // should now have either a '=', ';', or ',' + case State.Equals: + if (c is '=') + state = State.RValue; + else + if (c is ',' || c is ';') + // get next NVPair + state = State.Begin; + continue; + + // look for a quoted token, or a plain one + case State.RValue: + mark = i; + if (c is '\'') + state = State.SQuote; + else + if (c is '"') + state = State.DQuote; + else + if (isalpha (c)) + state = State.Token; + continue; + + // scan for all plain token chars + case State.Token: + if (! isalnum (c)) + { + setValue (i); + --i; + } + continue; + + // scan until the next ' + case State.SQuote: + if (c is '\'') + ++mark, setValue (i); + continue; + + // scan until the next " + case State.DQuote: + if (c is '"') + ++mark, setValue (i); + continue; + + default: + continue; + } + } + + // we ran out of content; patch partial cookie values + if (state is State.Token) + setValue (content.length); + + // go home + return IConduit.Eof; + } + + /*********************************************************************** + + Locate the next token from the provided buffer, and map a + buffer reference into token. Returns true if a token was + located, false otherwise. + + Note that the buffer content is not duplicated. Instead, a + slice of the buffer is referenced by the token. You can use + Token.clone() or Token.toString().dup() to copy content per + your application needs. + + Note also that there may still be one token left in a buffer + that was not terminated correctly (as in eof conditions). In + such cases, tokens are mapped onto remaining content and the + buffer will have no more readable content. + + ***********************************************************************/ + + bool parse (char[] header) + { + super.set (buffer.setContent (header)); + return next.ptr > null; + } + + /********************************************************************** + + in-place conversion to lowercase + + **********************************************************************/ + + final static char[] toLower (inout char[] src) + { + foreach (int i, char c; src) + if (c >= 'A' && c <= 'Z') + src[i] = c + ('a' - 'A'); + return src; + } +} + +