blob: 6d7667b017d0a52a1151ba717fdb811bb6ff7c7d [file] [log] [blame]
/*
* Copyright 1999-2004 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tomcat.util.http;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.StringTokenizer;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
/**
* A collection of cookies - reusable and tuned for server side performance.
* Based on RFC2965 ( and 2109 )
*
* This class is not synchronized.
*
* @author Costin Manolache
* @author kevin seguin
*/
public final class Cookies { // extends MultiMap {
// expected average number of cookies per request
public static final int INITIAL_SIZE=4;
ServerCookie scookies[]=new ServerCookie[INITIAL_SIZE];
int cookieCount=0;
boolean unprocessed=true;
MimeHeaders headers;
/**
* Construct a new cookie collection, that will extract
* the information from headers.
*
* @param headers Cookies are lazy-evaluated and will extract the
* information from the provided headers.
*/
public Cookies(MimeHeaders headers) {
this.headers=headers;
}
/**
* Construct a new uninitialized cookie collection.
* Use {@link #setHeaders} to initialize.
*/
// [seguin] added so that an empty Cookies object could be
// created, have headers set, then recycled.
public Cookies() {
}
/**
* Set the headers from which cookies will be pulled.
* This has the side effect of recycling the object.
*
* @param headers Cookies are lazy-evaluated and will extract the
* information from the provided headers.
*/
// [seguin] added so that an empty Cookies object could be
// created, have headers set, then recycled.
public void setHeaders(MimeHeaders headers) {
recycle();
this.headers=headers;
}
/**
* Recycle.
*/
public void recycle() {
for( int i=0; i< cookieCount; i++ ) {
if( scookies[i]!=null )
scookies[i].recycle();
}
cookieCount=0;
unprocessed=true;
}
/**
* EXPENSIVE!!! only for debugging.
*/
public String toString() {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println("=== Cookies ===");
int count = getCookieCount();
for (int i = 0; i < count; ++i) {
pw.println(getCookie(i).toString());
}
return sw.toString();
}
// -------------------- Indexed access --------------------
public ServerCookie getCookie( int idx ) {
if( unprocessed ) {
getCookieCount(); // will also update the cookies
}
return scookies[idx];
}
public int getCookieCount() {
if( unprocessed ) {
unprocessed=false;
processCookies(headers);
}
return cookieCount;
}
// -------------------- Adding cookies --------------------
/** Register a new, unitialized cookie. Cookies are recycled, and
* most of the time an existing ServerCookie object is returned.
* The caller can set the name/value and attributes for the cookie
*/
public ServerCookie addCookie() {
if( cookieCount >= scookies.length ) {
ServerCookie scookiesTmp[]=new ServerCookie[2*cookieCount];
System.arraycopy( scookies, 0, scookiesTmp, 0, cookieCount);
scookies=scookiesTmp;
}
ServerCookie c = scookies[cookieCount];
if( c==null ) {
c= new ServerCookie();
scookies[cookieCount]=c;
}
cookieCount++;
return c;
}
// code from CookieTools
/** Add all Cookie found in the headers of a request.
*/
public void processCookies( MimeHeaders headers ) {
if( headers==null )
return;// nothing to process
// process each "cookie" header
int pos=0;
while( pos>=0 ) {
// Cookie2: version ? not needed
pos=headers.findHeader( "Cookie", pos );
// no more cookie headers headers
if( pos<0 ) break;
MessageBytes cookieValue=headers.getValue( pos );
if( cookieValue==null || cookieValue.isNull() ) {
pos++;
continue;
}
// Uncomment to test the new parsing code
if( cookieValue.getType() == MessageBytes.T_BYTES ) {
if( dbg>0 ) log( "Parsing b[]: " + cookieValue.toString());
ByteChunk bc=cookieValue.getByteChunk();
processCookieHeader( bc.getBytes(),
bc.getOffset(),
bc.getLength());
} else {
if( dbg>0 ) log( "Parsing S: " + cookieValue.toString());
processCookieHeader( cookieValue.toString() );
}
pos++;// search from the next position
}
}
/** Process a byte[] header - allowing fast processing of the
* raw data
*/
void processCookieHeader( byte bytes[], int off, int len )
{
if( len<=0 || bytes==null ) return;
int end=off+len;
int pos=off;
int version=0; //sticky
ServerCookie sc=null;
while( pos<end ) {
byte cc;
// [ skip_spaces name skip_spaces "=" skip_spaces value EXTRA ; ] *
if( dbg>0 ) log( "Start: " + pos + " " + end );
pos=skipSpaces(bytes, pos, end);
if( pos>=end )
return; // only spaces
int startName=pos;
if( dbg>0 ) log( "SN: " + pos );
// Version should be the first token
boolean isSpecial=false;
if(bytes[pos]=='$') { pos++; isSpecial=true; }
pos= findDelim1( bytes, startName, end); // " =;,"
int endName=pos;
// current = "=" or " " or DELIM
pos= skipSpaces( bytes, endName, end );
if( dbg>0 ) log( "DELIM: " + endName + " " + (char)bytes[pos]);
if(pos >= end ) {
// it's a name-only cookie ( valid in RFC2109 )
if( ! isSpecial ) {
sc=addCookie();
sc.getName().setBytes( bytes, startName,
endName-startName );
sc.getValue().setString("");
sc.setVersion( version );
if( dbg>0 ) log( "Name only, end: " + startName + " " +
endName);
}
return;
}
cc=bytes[pos];
pos++;
if( cc==';' || cc==',' || pos>=end ) {
if( ! isSpecial && startName!= endName ) {
sc=addCookie();
sc.getName().setBytes( bytes, startName,
endName-startName );
sc.getValue().setString("");
sc.setVersion( version );
if( dbg>0 ) log( "Name only: " + startName + " " + endName);
}
continue;
}
// we should have "=" ( tested all other alternatives )
int startValue=skipSpaces( bytes, pos, end);
int endValue=startValue;
// quote is valid only in version=1 cookies
cc=bytes[pos];
if( ( version == 1 || isSpecial ) && ( cc== '\'' || cc=='"' ) ) {
startValue++;
endValue=indexOf( bytes, startValue, end, cc );
pos=endValue+1; // to skip to next cookie
} else {
endValue=findDelim2( bytes, startValue, end );
pos=endValue+1;
}
// if not $Version, etc
if( ! isSpecial ) {
sc=addCookie();
sc.getName().setBytes( bytes, startName, endName-startName );
sc.getValue().setBytes( bytes, startValue, endValue-startValue);
sc.setVersion( version );
if( dbg>0 ) {
log( "New: " + sc.getName() + "X=X" + sc.getValue());
}
continue;
}
// special - Path, Version, Domain, Port
if( dbg>0 ) log( "Special: " + startName + " " + endName);
// XXX TODO
if( equals( "$Version", bytes, startName, endName ) ) {
if(dbg>0 ) log( "Found version " );
if( bytes[startValue]=='1' && endValue==startValue+1 ) {
version=1;
if(dbg>0 ) log( "Found version=1" );
}
continue;
}
if( sc==null ) {
// Path, etc without a previous cookie
continue;
}
if( equals( "$Path", bytes, startName, endName ) ) {
sc.getPath().setBytes( bytes,
startValue,
endValue-startValue );
}
if( equals( "$Domain", bytes, startName, endName ) ) {
sc.getDomain().setBytes( bytes,
startValue,
endValue-startValue );
}
if( equals( "$Port", bytes, startName, endName ) ) {
// sc.getPort().setBytes( bytes,
// startValue,
// endValue-startValue );
}
}
}
// -------------------- Utils --------------------
public static int skipSpaces( byte bytes[], int off, int end ) {
while( off < end ) {
byte b=bytes[off];
if( b!= ' ' ) return off;
off ++;
}
return off;
}
public static int findDelim1( byte bytes[], int off, int end )
{
while( off < end ) {
byte b=bytes[off];
if( b==' ' || b=='=' || b==';' || b==',' )
return off;
off++;
}
return off;
}
public static int findDelim2( byte bytes[], int off, int end )
{
while( off < end ) {
byte b=bytes[off];
if( b==';' || b==',' )
return off;
off++;
}
return off;
}
public static int indexOf( byte bytes[], int off, int end, byte qq )
{
while( off < end ) {
byte b=bytes[off];
if( b==qq )
return off;
off++;
}
return off;
}
public static int indexOf( byte bytes[], int off, int end, char qq )
{
while( off < end ) {
byte b=bytes[off];
if( b==qq )
return off;
off++;
}
return off;
}
// XXX will be refactored soon!
public static boolean equals( String s, byte b[], int start, int end) {
int blen = end-start;
if (b == null || blen != s.length()) {
return false;
}
int boff = start;
for (int i = 0; i < blen; i++) {
if (b[boff++] != s.charAt(i)) {
return false;
}
}
return true;
}
// ---------------------------------------------------------
// -------------------- DEPRECATED, OLD --------------------
private void processCookieHeader( String cookieString )
{
if( dbg>0 ) log( "Parsing cookie header " + cookieString );
// normal cookie, with a string value.
// This is the original code, un-optimized - it shouldn't
// happen in normal case
StringTokenizer tok = new StringTokenizer(cookieString,
";", false);
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
int i = token.indexOf("=");
if (i > -1) {
// XXX
// the trims here are a *hack* -- this should
// be more properly fixed to be spec compliant
String name = token.substring(0, i).trim();
String value = token.substring(i+1, token.length()).trim();
// RFC 2109 and bug
value=stripQuote( value );
ServerCookie cookie = addCookie();
cookie.getName().setString(name);
cookie.getValue().setString(value);
if( dbg > 0 ) log( "Add cookie " + name + "=" + value);
} else {
// we have a bad cookie.... just let it go
}
}
}
/**
*
* Strips quotes from the start and end of the cookie string
* This conforms to RFC 2109
*
* @param value a <code>String</code> specifying the cookie
* value (possibly quoted).
*
* @see #setValue
*
*/
private static String stripQuote( String value )
{
// log("Strip quote from " + value );
if (((value.startsWith("\"")) && (value.endsWith("\""))) ||
((value.startsWith("'") && (value.endsWith("'"))))) {
try {
return value.substring(1,value.length()-1);
} catch (Exception ex) {
}
}
return value;
}
// log
static final int dbg=0;
public void log(String s ) {
System.out.println("Cookies: " + s);
}
/*
public static void main( String args[] ) {
test("foo=bar; a=b");
test("foo=bar;a=b");
test("foo=bar;a=b;");
test("foo=bar;a=b; ");
test("foo=bar;a=b; ;");
test("foo=;a=b; ;");
test("foo;a=b; ;");
// v1
test("$Version=1; foo=bar;a=b");
test("$Version=\"1\"; foo='bar'; $Path=/path; $Domain=\"localhost\"");
test("$Version=1;foo=bar;a=b; ; ");
test("$Version=1;foo=;a=b; ; ");
test("$Version=1;foo= ;a=b; ; ");
test("$Version=1;foo;a=b; ; ");
test("$Version=1;foo=\"bar\";a=b; ; ");
test("$Version=1;foo=\"bar\";$Path=/examples;a=b; ; ");
test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b");
test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b;$Domain=yahoo.com");
// rfc2965
test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
// wrong
test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
}
public static void test( String s ) {
System.out.println("Processing " + s );
Cookies cs=new Cookies(null);
cs.processCookieHeader( s.getBytes(), 0, s.length());
for( int i=0; i< cs.getCookieCount() ; i++ ) {
System.out.println("Cookie: " + cs.getCookie( i ));
}
}
*/
}