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