123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707 |
|
/*******************************************************************************
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.stdc.ctype;
private import tango.io.device.Array;
private import tango.io.model.IConduit;
private import tango.io.stream.Iterator;
private import tango.net.http.HttpHeaders;
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
{
const(char)[] name,
path,
value,
domain,
comment;
uint vrsn=1; // 'version' is a reserved word
bool secure=false;
long maxAge=long.min;
/***********************************************************************
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 (const(char)[] name, const(char)[] value)
{
setName (name);
setValue (value);
}
/***********************************************************************
Set the name of this cookie
***********************************************************************/
Cookie setName (const(char)[] name)
{
this.name = name;
return this;
}
/***********************************************************************
Set the value of this cookie
***********************************************************************/
Cookie setValue (const(char)[] value)
{
this.value = value;
return this;
}
/***********************************************************************
Set the version of this cookie
***********************************************************************/
Cookie setVersion (uint vrsn)
{
this.vrsn = vrsn;
return this;
}
/***********************************************************************
Set the path of this cookie
***********************************************************************/
Cookie setPath (const(char)[] path)
{
this.path = path;
return this;
}
/***********************************************************************
Set the domain of this cookie
***********************************************************************/
Cookie setDomain (const(char)[] domain)
{
this.domain = domain;
return this;
}
/***********************************************************************
Set the comment associated with this cookie
***********************************************************************/
Cookie setComment (const(char)[] comment)
{
this.comment = comment;
return this;
}
/***********************************************************************
Set the maximum duration of this cookie
***********************************************************************/
Cookie setMaxAge (long maxAge)
{
this.maxAge = maxAge;
return this;
}
/***********************************************************************
Indicate whether this cookie should be considered secure or not
***********************************************************************/
Cookie setSecure (bool secure)
{
this.secure = secure;
return this;
}
/+
/***********************************************************************
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 (scope size_t delegate(const(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 != maxAge.min)
consume (";Max-Age="c), consume (Integer.format (tmp, maxAge));
}
}
/***********************************************************************
Reset this cookie
***********************************************************************/
Cookie clear ()
{
vrsn = 1;
secure = false;
maxAge = maxAge.min;
name = path = domain = comment = null;
return this;
}
}
/*******************************************************************************
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 (ref Cookie[] cookies, size_t size)
{
size_t i = cookies.length;
for (cookies.length=size; i < cookies.length; ++i)
cookies[i] = new Cookie();
}
/**********************************************************************
Iterate over all cookies in stack
**********************************************************************/
int opApply (scope int delegate(ref 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 (scope size_t delegate(const(void)[]) consume, const(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 HttpHeaderName name;
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, const(HttpHeaderName) name = HttpHeader.SetCookie)
{
this.headers = headers;
this.name = name;
}
/**********************************************************************
Add a cookie to our output headers.
**********************************************************************/
void add (Cookie cookie)
{
// add the cookie header via our callback
headers.add (name, (OutputBuffer buf){cookie.produce (&buf.write);});
}
}
/*******************************************************************************
Server-side cookie parser. See RFC 2109 for details.
*******************************************************************************/
class CookieParser : Iterator!(char)
{
private enum State {Begin, LValue, Equals, RValue, Token, SQuote, DQuote};
private CookieStack stack;
private Array array;
private static __gshared bool[128] charMap;
/***********************************************************************
populate a map of token separators
***********************************************************************/
shared static this ()
{
charMap['('] = true;
charMap[')'] = true;
charMap['<'] = true;
charMap['>'] = true;
charMap['@'] = true;
charMap[','] = true;
charMap[';'] = true;
charMap[':'] = true;
charMap['\\'] = true;
charMap['"'] = true;
charMap['/'] = true;
charMap['['] = true;
charMap[']'] = true;
charMap['?'] = true;
charMap['='] = true;
charMap['{'] = true;
charMap['}'] = true;
}
/***********************************************************************
***********************************************************************/
this (CookieStack stack)
{
super();
this.stack = stack;
array = new Array(0);
}
/***********************************************************************
Callback for iterator.next(). We scan for name-value
pairs, populating Cookie instances along the way.
***********************************************************************/
protected override size_t scan (const(void)[] data)
{
char c;
int mark,
vrsn;
const(char)[] name,
token;
Cookie cookie;
State state = State.Begin;
const(char)[] content = cast(const(char)[]) data;
/***************************************************************
Found a value; set that also
***************************************************************/
void setValue (size_t 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
{
if(name.length < 9)
{
char[8] temp;
temp[0..name.length] = name[];
switch (toLower (temp[0..name.length]))
{
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 (isToken(c))
state = State.LValue;
continue;
// scan until we have all lValue chars
case State.LValue:
if (! isToken(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 (isToken(c))
state = State.Token;
continue;
// scan for all plain token chars
case State.Token:
if (! isToken(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 (const(char)[] header)
{
super.set (array.assign (cast(void[]) header));
return next.ptr > null;
}
/**********************************************************************
in-place conversion to lowercase
**********************************************************************/
final static char[] toLower (ref char[] src)
{
foreach (int i, char c; src)
if (c >= 'A' && c <= 'Z')
src[i] = cast(char)(c + ('a' - 'A'));
return src;
}
/***********************************************************************
Is 'c' a valid token character?
***********************************************************************/
private static bool isToken (char c)
{
return (c > 32 && c < 127 && !charMap[c]);
}
}
|