001: /*
002: * Copyright 1999-2004 The Apache Software Foundation
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.apache.tomcat.util.http;
018:
019: import java.io.PrintWriter;
020: import java.io.StringWriter;
021: import java.util.StringTokenizer;
022:
023: import org.apache.tomcat.util.buf.ByteChunk;
024: import org.apache.tomcat.util.buf.MessageBytes;
025:
026: /**
027: * A collection of cookies - reusable and tuned for server side performance.
028: * Based on RFC2965 ( and 2109 )
029: *
030: * This class is not synchronized.
031: *
032: * @author Costin Manolache
033: * @author kevin seguin
034: */
035: public final class Cookies { // extends MultiMap {
036:
037: // expected average number of cookies per request
038: public static final int INITIAL_SIZE = 4;
039: ServerCookie scookies[] = new ServerCookie[INITIAL_SIZE];
040: int cookieCount = 0;
041: boolean unprocessed = true;
042:
043: MimeHeaders headers;
044:
045: /**
046: * Construct a new cookie collection, that will extract
047: * the information from headers.
048: *
049: * @param headers Cookies are lazy-evaluated and will extract the
050: * information from the provided headers.
051: */
052: public Cookies(MimeHeaders headers) {
053: this .headers = headers;
054: }
055:
056: /**
057: * Construct a new uninitialized cookie collection.
058: * Use {@link #setHeaders} to initialize.
059: */
060: // [seguin] added so that an empty Cookies object could be
061: // created, have headers set, then recycled.
062: public Cookies() {
063: }
064:
065: /**
066: * Set the headers from which cookies will be pulled.
067: * This has the side effect of recycling the object.
068: *
069: * @param headers Cookies are lazy-evaluated and will extract the
070: * information from the provided headers.
071: */
072: // [seguin] added so that an empty Cookies object could be
073: // created, have headers set, then recycled.
074: public void setHeaders(MimeHeaders headers) {
075: recycle();
076: this .headers = headers;
077: }
078:
079: /**
080: * Recycle.
081: */
082: public void recycle() {
083: for (int i = 0; i < cookieCount; i++) {
084: if (scookies[i] != null)
085: scookies[i].recycle();
086: }
087: cookieCount = 0;
088: unprocessed = true;
089: }
090:
091: /**
092: * EXPENSIVE!!! only for debugging.
093: */
094: public String toString() {
095: StringWriter sw = new StringWriter();
096: PrintWriter pw = new PrintWriter(sw);
097: pw.println("=== Cookies ===");
098: int count = getCookieCount();
099: for (int i = 0; i < count; ++i) {
100: pw.println(getCookie(i).toString());
101: }
102: return sw.toString();
103: }
104:
105: // -------------------- Indexed access --------------------
106:
107: public ServerCookie getCookie(int idx) {
108: if (unprocessed) {
109: getCookieCount(); // will also update the cookies
110: }
111: return scookies[idx];
112: }
113:
114: public int getCookieCount() {
115: if (unprocessed) {
116: unprocessed = false;
117: processCookies(headers);
118: }
119: return cookieCount;
120: }
121:
122: // -------------------- Adding cookies --------------------
123:
124: /** Register a new, unitialized cookie. Cookies are recycled, and
125: * most of the time an existing ServerCookie object is returned.
126: * The caller can set the name/value and attributes for the cookie
127: */
128: public ServerCookie addCookie() {
129: if (cookieCount >= scookies.length) {
130: ServerCookie scookiesTmp[] = new ServerCookie[2 * cookieCount];
131: System.arraycopy(scookies, 0, scookiesTmp, 0, cookieCount);
132: scookies = scookiesTmp;
133: }
134:
135: ServerCookie c = scookies[cookieCount];
136: if (c == null) {
137: c = new ServerCookie();
138: scookies[cookieCount] = c;
139: }
140: cookieCount++;
141: return c;
142: }
143:
144: // code from CookieTools
145:
146: /** Add all Cookie found in the headers of a request.
147: */
148: public void processCookies(MimeHeaders headers) {
149: if (headers == null)
150: return;// nothing to process
151: // process each "cookie" header
152: int pos = 0;
153: while (pos >= 0) {
154: // Cookie2: version ? not needed
155: pos = headers.findHeader("Cookie", pos);
156: // no more cookie headers headers
157: if (pos < 0)
158: break;
159:
160: MessageBytes cookieValue = headers.getValue(pos);
161: if (cookieValue == null || cookieValue.isNull())
162: continue;
163:
164: // Uncomment to test the new parsing code
165: if (cookieValue.getType() == MessageBytes.T_BYTES) {
166: if (dbg > 0)
167: log("Parsing b[]: " + cookieValue.toString());
168: ByteChunk bc = cookieValue.getByteChunk();
169: processCookieHeader(bc.getBytes(), bc.getOffset(), bc
170: .getLength());
171: } else {
172: if (dbg > 0)
173: log("Parsing S: " + cookieValue.toString());
174: processCookieHeader(cookieValue.toString());
175: }
176: pos++;// search from the next position
177: }
178: }
179:
180: /** Process a byte[] header - allowing fast processing of the
181: * raw data
182: */
183: void processCookieHeader(byte bytes[], int off, int len) {
184: if (len <= 0 || bytes == null)
185: return;
186: int end = off + len;
187: int pos = off;
188:
189: int version = 0; //sticky
190: ServerCookie sc = null;
191:
192: while (pos < end) {
193: byte cc;
194: // [ skip_spaces name skip_spaces "=" skip_spaces value EXTRA ; ] *
195: if (dbg > 0)
196: log("Start: " + pos + " " + end);
197:
198: pos = skipSpaces(bytes, pos, end);
199: if (pos >= end)
200: return; // only spaces
201: int startName = pos;
202: if (dbg > 0)
203: log("SN: " + pos);
204:
205: // Version should be the first token
206: boolean isSpecial = false;
207: if (bytes[pos] == '$') {
208: pos++;
209: isSpecial = true;
210: }
211:
212: pos = findDelim1(bytes, startName, end); // " =;,"
213: int endName = pos;
214: // current = "=" or " " or DELIM
215: pos = skipSpaces(bytes, endName, end);
216: if (dbg > 0)
217: log("DELIM: " + endName + " " + (char) bytes[pos]);
218:
219: if (pos >= end) {
220: // it's a name-only cookie ( valid in RFC2109 )
221: if (!isSpecial) {
222: sc = addCookie();
223: sc.getName().setBytes(bytes, startName,
224: endName - startName);
225: sc.getValue().setString("");
226: sc.setVersion(version);
227: if (dbg > 0)
228: log("Name only, end: " + startName + " "
229: + endName);
230: }
231: return;
232: }
233:
234: cc = bytes[pos];
235: pos++;
236: if (cc == ';' || cc == ',') {
237: if (!isSpecial && startName != endName) {
238: sc = addCookie();
239: sc.getName().setBytes(bytes, startName,
240: endName - startName);
241: sc.getValue().setString("");
242: sc.setVersion(version);
243: if (dbg > 0)
244: log("Name only: " + startName + " " + endName);
245: }
246: continue;
247: }
248:
249: // we should have "=" ( tested all other alternatives )
250: int startValue = skipSpaces(bytes, pos, end);
251: int endValue = startValue;
252:
253: // quote is valid only in version=1 cookies
254: cc = bytes[pos];
255: if ((version == 1 || isSpecial)
256: && (cc == '\'' || cc == '"')) {
257: startValue++;
258: endValue = indexOf(bytes, startValue, end, cc);
259: pos = endValue + 1; // to skip to next cookie
260: } else {
261: endValue = findDelim2(bytes, startValue, end);
262: pos = endValue + 1;
263: }
264:
265: // if not $Version, etc
266: if (!isSpecial) {
267: sc = addCookie();
268: sc.getName().setBytes(bytes, startName,
269: endName - startName);
270: sc.getValue().setBytes(bytes, startValue,
271: endValue - startValue);
272: sc.setVersion(version);
273: if (dbg > 0)
274: log("New: " + sc.getName() + "X=X" + sc.getValue());
275: continue;
276: }
277:
278: // special - Path, Version, Domain, Port
279: if (dbg > 0)
280: log("Special: " + startName + " " + endName);
281: // XXX TODO
282: if (equals("$Version", bytes, startName, endName)) {
283: if (dbg > 0)
284: log("Found version ");
285: if (bytes[startValue] == '1'
286: && endValue == startValue + 1) {
287: version = 1;
288: if (dbg > 0)
289: log("Found version=1");
290: }
291: continue;
292: }
293: if (sc == null) {
294: // Path, etc without a previous cookie
295: continue;
296: }
297: if (equals("$Path", bytes, startName, endName)) {
298: sc.getPath().setBytes(bytes, startValue,
299: endValue - startValue);
300: }
301: if (equals("$Domain", bytes, startName, endName)) {
302: sc.getDomain().setBytes(bytes, startValue,
303: endValue - startValue);
304: }
305: if (equals("$Port", bytes, startName, endName)) {
306: // sc.getPort().setBytes( bytes, startValue, endValue-startValue );
307: }
308: }
309: }
310:
311: // -------------------- Utils --------------------
312: public static int skipSpaces(byte bytes[], int off, int end) {
313: while (off < end) {
314: byte b = bytes[off];
315: if (b != ' ')
316: return off;
317: off++;
318: }
319: return off;
320: }
321:
322: public static int findDelim1(byte bytes[], int off, int end) {
323: while (off < end) {
324: byte b = bytes[off];
325: if (b == ' ' || b == '=' || b == ';' || b == ',')
326: return off;
327: off++;
328: }
329: return off;
330: }
331:
332: public static int findDelim2(byte bytes[], int off, int end) {
333: while (off < end) {
334: byte b = bytes[off];
335: if (b == ';' || b == ',')
336: return off;
337: off++;
338: }
339: return off;
340: }
341:
342: public static int indexOf(byte bytes[], int off, int end, byte qq) {
343: while (off < end) {
344: byte b = bytes[off];
345: if (b == qq)
346: return off;
347: off++;
348: }
349: return off;
350: }
351:
352: public static int indexOf(byte bytes[], int off, int end, char qq) {
353: while (off < end) {
354: byte b = bytes[off];
355: if (b == qq)
356: return off;
357: off++;
358: }
359: return off;
360: }
361:
362: // XXX will be refactored soon!
363: public static boolean equals(String s, byte b[], int start, int end) {
364: int blen = end - start;
365: if (b == null || blen != s.length()) {
366: return false;
367: }
368: int boff = start;
369: for (int i = 0; i < blen; i++) {
370: if (b[boff++] != s.charAt(i)) {
371: return false;
372: }
373: }
374: return true;
375: }
376:
377: // ---------------------------------------------------------
378: // -------------------- DEPRECATED, OLD --------------------
379:
380: private void processCookieHeader(String cookieString) {
381: if (dbg > 0)
382: log("Parsing cookie header " + cookieString);
383: // normal cookie, with a string value.
384: // This is the original code, un-optimized - it shouldn't
385: // happen in normal case
386:
387: StringTokenizer tok = new StringTokenizer(cookieString, ";",
388: false);
389: while (tok.hasMoreTokens()) {
390: String token = tok.nextToken();
391: int i = token.indexOf("=");
392: if (i > -1) {
393:
394: // XXX
395: // the trims here are a *hack* -- this should
396: // be more properly fixed to be spec compliant
397:
398: String name = token.substring(0, i).trim();
399: String value = token.substring(i + 1, token.length())
400: .trim();
401: // RFC 2109 and bug
402: value = stripQuote(value);
403: ServerCookie cookie = addCookie();
404:
405: cookie.getName().setString(name);
406: cookie.getValue().setString(value);
407: if (dbg > 0)
408: log("Add cookie " + name + "=" + value);
409: } else {
410: // we have a bad cookie.... just let it go
411: }
412: }
413: }
414:
415: /**
416: *
417: * Strips quotes from the start and end of the cookie string
418: * This conforms to RFC 2109
419: *
420: * @param value a <code>String</code> specifying the cookie
421: * value (possibly quoted).
422: *
423: * @see #setValue
424: *
425: */
426: private static String stripQuote(String value) {
427: // log("Strip quote from " + value );
428: if (((value.startsWith("\"")) && (value.endsWith("\"")))
429: || ((value.startsWith("'") && (value.endsWith("'"))))) {
430: try {
431: return value.substring(1, value.length() - 1);
432: } catch (Exception ex) {
433: }
434: }
435: return value;
436: }
437:
438: // log
439: static final int dbg = 0;
440:
441: public void log(String s) {
442: System.out.println("Cookies: " + s);
443: }
444:
445: /*
446: public static void main( String args[] ) {
447: test("foo=bar; a=b");
448: test("foo=bar;a=b");
449: test("foo=bar;a=b;");
450: test("foo=bar;a=b; ");
451: test("foo=bar;a=b; ;");
452: test("foo=;a=b; ;");
453: test("foo;a=b; ;");
454: // v1
455: test("$Version=1; foo=bar;a=b");
456: test("$Version=\"1\"; foo='bar'; $Path=/path; $Domain=\"localhost\"");
457: test("$Version=1;foo=bar;a=b; ; ");
458: test("$Version=1;foo=;a=b; ; ");
459: test("$Version=1;foo= ;a=b; ; ");
460: test("$Version=1;foo;a=b; ; ");
461: test("$Version=1;foo=\"bar\";a=b; ; ");
462: test("$Version=1;foo=\"bar\";$Path=/examples;a=b; ; ");
463: test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b");
464: test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b;$Domain=yahoo.com");
465: // rfc2965
466: test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
467:
468: // wrong
469: test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
470: }
471:
472: public static void test( String s ) {
473: System.out.println("Processing " + s );
474: Cookies cs=new Cookies(null);
475: cs.processCookieHeader( s.getBytes(), 0, s.length());
476: for( int i=0; i< cs.getCookieCount() ; i++ ) {
477: System.out.println("Cookie: " + cs.getCookie( i ));
478: }
479:
480: }
481: */
482:
483: }
|