view 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 source

/*******************************************************************************

        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;
        }
}