1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.tomcat.util.http;
19
20 import java.io.PrintWriter;
21 import java.io.StringWriter;
22 import java.util.StringTokenizer;
23
24 import org.apache.tomcat.util.buf.ByteChunk;
25 import org.apache.tomcat.util.buf.MessageBytes;
26
27 /**
28 * A collection of cookies - reusable and tuned for server side performance.
29 * Based on RFC2965 ( and 2109 )
30 *
31 * This class is not synchronized.
32 *
33 * @author Costin Manolache
34 * @author kevin seguin
35 */
36 public final class Cookies { // extends MultiMap {
37
38 private static org.apache.juli.logging.Log log=
39 org.apache.juli.logging.LogFactory.getLog(Cookies.class );
40
41 // expected average number of cookies per request
42 public static final int INITIAL_SIZE=4;
43 ServerCookie scookies[]=new ServerCookie[INITIAL_SIZE];
44 int cookieCount=0;
45 boolean unprocessed=true;
46
47 MimeHeaders headers;
48
49 /*
50 List of Separator Characters (see isSeparator())
51 Excluding the '/' char violates the RFC, but
52 it looks like a lot of people put '/'
53 in unquoted values: '/': ; //47
54 '\t':9 ' ':32 '\"':34 '\'':39 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60
55 '=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125
56 */
57 public static final char SEPARATORS[] = { '\t', ' ', '\"', '\'', '(', ')', ',',
58 ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' };
59
60 protected static final boolean separators[] = new boolean[128];
61 static {
62 for (int i = 0; i < 128; i++) {
63 separators[i] = false;
64 }
65 for (int i = 0; i < SEPARATORS.length; i++) {
66 separators[SEPARATORS[i]] = true;
67 }
68 }
69
70 /**
71 * Construct a new cookie collection, that will extract
72 * the information from headers.
73 *
74 * @param headers Cookies are lazy-evaluated and will extract the
75 * information from the provided headers.
76 */
77 public Cookies(MimeHeaders headers) {
78 this.headers=headers;
79 }
80
81 /**
82 * Construct a new uninitialized cookie collection.
83 * Use {@link #setHeaders} to initialize.
84 */
85 // [seguin] added so that an empty Cookies object could be
86 // created, have headers set, then recycled.
87 public Cookies() {
88 }
89
90 /**
91 * Set the headers from which cookies will be pulled.
92 * This has the side effect of recycling the object.
93 *
94 * @param headers Cookies are lazy-evaluated and will extract the
95 * information from the provided headers.
96 */
97 // [seguin] added so that an empty Cookies object could be
98 // created, have headers set, then recycled.
99 public void setHeaders(MimeHeaders headers) {
100 recycle();
101 this.headers=headers;
102 }
103
104 /**
105 * Recycle.
106 */
107 public void recycle() {
108 for( int i=0; i< cookieCount; i++ ) {
109 if( scookies[i]!=null )
110 scookies[i].recycle();
111 }
112 cookieCount=0;
113 unprocessed=true;
114 }
115
116 /**
117 * EXPENSIVE!!! only for debugging.
118 */
119 public String toString() {
120 StringWriter sw = new StringWriter();
121 PrintWriter pw = new PrintWriter(sw);
122 pw.println("=== Cookies ===");
123 int count = getCookieCount();
124 for (int i = 0; i < count; ++i) {
125 pw.println(getCookie(i).toString());
126 }
127 return sw.toString();
128 }
129
130 // -------------------- Indexed access --------------------
131
132 public ServerCookie getCookie( int idx ) {
133 if( unprocessed ) {
134 getCookieCount(); // will also update the cookies
135 }
136 return scookies[idx];
137 }
138
139 public int getCookieCount() {
140 if( unprocessed ) {
141 unprocessed=false;
142 processCookies(headers);
143 }
144 return cookieCount;
145 }
146
147 // -------------------- Adding cookies --------------------
148
149 /** Register a new, unitialized cookie. Cookies are recycled, and
150 * most of the time an existing ServerCookie object is returned.
151 * The caller can set the name/value and attributes for the cookie
152 */
153 public ServerCookie addCookie() {
154 if( cookieCount >= scookies.length ) {
155 ServerCookie scookiesTmp[]=new ServerCookie[2*cookieCount];
156 System.arraycopy( scookies, 0, scookiesTmp, 0, cookieCount);
157 scookies=scookiesTmp;
158 }
159
160 ServerCookie c = scookies[cookieCount];
161 if( c==null ) {
162 c= new ServerCookie();
163 scookies[cookieCount]=c;
164 }
165 cookieCount++;
166 return c;
167 }
168
169
170 // code from CookieTools
171
172 /** Add all Cookie found in the headers of a request.
173 */
174 public void processCookies( MimeHeaders headers ) {
175 if( headers==null )
176 return;// nothing to process
177 // process each "cookie" header
178 int pos=0;
179 while( pos>=0 ) {
180 // Cookie2: version ? not needed
181 pos=headers.findHeader( "Cookie", pos );
182 // no more cookie headers headers
183 if( pos<0 ) break;
184
185 MessageBytes cookieValue=headers.getValue( pos );
186 if( cookieValue==null || cookieValue.isNull() ) {
187 pos++;
188 continue;
189 }
190
191 // Uncomment to test the new parsing code
192 if( cookieValue.getType() == MessageBytes.T_BYTES ) {
193 if( dbg>0 ) log( "Parsing b[]: " + cookieValue.toString());
194 ByteChunk bc=cookieValue.getByteChunk();
195 processCookieHeader( bc.getBytes(),
196 bc.getOffset(),
197 bc.getLength());
198 } else {
199 if( dbg>0 ) log( "Parsing S: " + cookieValue.toString());
200 processCookieHeader( cookieValue.toString() );
201 }
202 pos++;// search from the next position
203 }
204 }
205
206 // XXX will be refactored soon!
207 public static boolean equals( String s, byte b[], int start, int end) {
208 int blen = end-start;
209 if (b == null || blen != s.length()) {
210 return false;
211 }
212 int boff = start;
213 for (int i = 0; i < blen; i++) {
214 if (b[boff++] != s.charAt(i)) {
215 return false;
216 }
217 }
218 return true;
219 }
220
221
222 // ---------------------------------------------------------
223 // -------------------- DEPRECATED, OLD --------------------
224
225 private void processCookieHeader( String cookieString )
226 {
227 if( dbg>0 ) log( "Parsing cookie header " + cookieString );
228 // normal cookie, with a string value.
229 // This is the original code, un-optimized - it shouldn't
230 // happen in normal case
231
232 StringTokenizer tok = new StringTokenizer(cookieString,
233 ";", false);
234 while (tok.hasMoreTokens()) {
235 String token = tok.nextToken();
236 int i = token.indexOf("=");
237 if (i > -1) {
238
239 // XXX
240 // the trims here are a *hack* -- this should
241 // be more properly fixed to be spec compliant
242
243 String name = token.substring(0, i).trim();
244 String value = token.substring(i+1, token.length()).trim();
245 // RFC 2109 and bug
246 value=stripQuote( value );
247 ServerCookie cookie = addCookie();
248
249 cookie.getName().setString(name);
250 cookie.getValue().setString(value);
251 if( dbg > 0 ) log( "Add cookie " + name + "=" + value);
252 } else {
253 // we have a bad cookie.... just let it go
254 }
255 }
256 }
257
258 /**
259 *
260 * Strips quotes from the start and end of the cookie string
261 * This conforms to RFC 2965
262 *
263 * @param value a <code>String</code> specifying the cookie
264 * value (possibly quoted).
265 *
266 * @see #setValue
267 *
268 */
269 private static String stripQuote( String value )
270 {
271 // log("Strip quote from " + value );
272 if (value.startsWith("\"") && value.endsWith("\"")) {
273 try {
274 return value.substring(1,value.length()-1);
275 } catch (Exception ex) {
276 }
277 }
278 return value;
279 }
280
281
282 // log
283 static final int dbg=0;
284 public void log(String s ) {
285 if (log.isDebugEnabled())
286 log.debug("Cookies: " + s);
287 }
288
289
290 /**
291 * Returns true if the byte is a separator character as
292 * defined in RFC2619. Since this is called often, this
293 * function should be organized with the most probable
294 * outcomes first.
295 * JVK
296 */
297 public static final boolean isSeparator(final byte c) {
298 if (c > 0 && c < 126)
299 return separators[c];
300 else
301 return false;
302 }
303
304 /**
305 * Returns true if the byte is a whitespace character as
306 * defined in RFC2619
307 * JVK
308 */
309 public static final boolean isWhiteSpace(final byte c) {
310 // This switch statement is slightly slower
311 // for my vm than the if statement.
312 // Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-164)
313 /*
314 switch (c) {
315 case ' ':;
316 case '\t':;
317 case '\n':;
318 case '\r':;
319 case '\f':;
320 return true;
321 default:;
322 return false;
323 }
324 */
325 if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f')
326 return true;
327 else
328 return false;
329 }
330
331 /**
332 * Parses a cookie header after the initial "Cookie:"
333 * [WS][$]token[WS]=[WS](token|QV)[;|,]
334 * RFC 2965
335 * JVK
336 */
337 public final void processCookieHeader(byte bytes[], int off, int len){
338 if( len<=0 || bytes==null ) return;
339 int end=off+len;
340 int pos=off;
341 int nameStart=0;
342 int nameEnd=0;
343 int valueStart=0;
344 int valueEnd=0;
345 int version = 0;
346 ServerCookie sc=null;
347 boolean isSpecial;
348 boolean isQuoted;
349
350 while (pos < end) {
351 isSpecial = false;
352 isQuoted = false;
353
354 // Skip whitespace and non-token characters (separators)
355 while (pos < end &&
356 (isSeparator(bytes[pos]) || isWhiteSpace(bytes[pos])))
357 {pos++; }
358
359 if (pos >= end)
360 return;
361
362 // Detect Special cookies
363 if (bytes[pos] == '$') {
364 isSpecial = true;
365 pos++;
366 }
367
368 // Get the cookie name. This must be a token
369 valueEnd = valueStart = nameStart = pos;
370 pos = nameEnd = getTokenEndPosition(bytes,pos,end);
371
372 // Skip whitespace
373 while (pos < end && isWhiteSpace(bytes[pos])) {pos++; };
374
375
376 // Check for an '=' -- This could also be a name-only
377 // cookie at the end of the cookie header, so if we
378 // are past the end of the header, but we have a name
379 // skip to the name-only part.
380 if (pos < end && bytes[pos] == '=') {
381
382 // Skip whitespace
383 do {
384 pos++;
385 } while (pos < end && isWhiteSpace(bytes[pos]));
386
387 if (pos >= end)
388 return;
389
390 // Determine what type of value this is, quoted value,
391 // token, name-only with an '=', or other (bad)
392 switch (bytes[pos]) {
393 case '"':; // Quoted Value
394 isQuoted = true;
395 valueStart=pos + 1; // strip "
396 // getQuotedValue returns the position before
397 // at the last qoute. This must be dealt with
398 // when the bytes are copied into the cookie
399 valueEnd=getQuotedValueEndPosition(bytes,
400 valueStart, end);
401 // We need pos to advance
402 pos = valueEnd;
403 // Handles cases where the quoted value is
404 // unterminated and at the end of the header,
405 // e.g. [myname="value]
406 if (pos >= end)
407 return;
408 break;
409 case ';':
410 case ',':
411 // Name-only cookie with an '=' after the name token
412 // This may not be RFC compliant
413 valueStart = valueEnd = -1;
414 // The position is OK (On a delimiter)
415 break;
416 default:;
417 if (!isSeparator(bytes[pos])) {
418 // Token
419 valueStart=pos;
420 // getToken returns the position at the delimeter
421 // or other non-token character
422 valueEnd=getTokenEndPosition(bytes, valueStart, end);
423 // We need pos to advance
424 pos = valueEnd;
425 } else {
426 // INVALID COOKIE, advance to next delimiter
427 // The starting character of the cookie value was
428 // not valid.
429 log("Invalid cookie. Value not a token or quoted value");
430 while (pos < end && bytes[pos] != ';' &&
431 bytes[pos] != ',')
432 {pos++; };
433 pos++;
434 // Make sure no special avpairs can be attributed to
435 // the previous cookie by setting the current cookie
436 // to null
437 sc = null;
438 continue;
439 }
440 }
441 } else {
442 // Name only cookie
443 valueStart = valueEnd = -1;
444 pos = nameEnd;
445
446 }
447
448 // We should have an avpair or name-only cookie at this
449 // point. Perform some basic checks to make sure we are
450 // in a good state.
451
452 // Skip whitespace
453 while (pos < end && isWhiteSpace(bytes[pos])) {pos++; };
454
455
456 // Make sure that after the cookie we have a separator. This
457 // is only important if this is not the last cookie pair
458 while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') {
459 pos++;
460 }
461
462 pos++;
463
464 /*
465 if (nameEnd <= nameStart || valueEnd < valueStart ) {
466 // Something is wrong, but this may be a case
467 // of having two ';' characters in a row.
468 // log("Cookie name/value does not conform to RFC 2965");
469 // Advance to next delimiter (ignoring everything else)
470 while (pos < end && bytes[pos] != ';' && bytes[pos] != ',')
471 { pos++; };
472 pos++;
473 // Make sure no special cookies can be attributed to
474 // the previous cookie by setting the current cookie
475 // to null
476 sc = null;
477 continue;
478 }
479 */
480
481 // All checks passed. Add the cookie, start with the
482 // special avpairs first
483 if (isSpecial) {
484 isSpecial = false;
485 // $Version must be the first avpair in the cookie header
486 // (sc must be null)
487 if (equals( "Version", bytes, nameStart, nameEnd) &&
488 sc == null) {
489 // Set version
490 if( bytes[valueStart] =='1' && valueEnd == (valueStart+1)) {
491 version=1;
492 } else {
493 // unknown version (Versioning is not very strict)
494 }
495 continue;
496 }
497
498 // We need an active cookie for Path/Port/etc.
499 if (sc == null) {
500 continue;
501 }
502
503 // Domain is more common, so it goes first
504 if (equals( "Domain", bytes, nameStart, nameEnd)) {
505 sc.getDomain().setBytes( bytes,
506 valueStart,
507 valueEnd-valueStart);
508 continue;
509 }
510
511 if (equals( "Path", bytes, nameStart, nameEnd)) {
512 sc.getPath().setBytes( bytes,
513 valueStart,
514 valueEnd-valueStart);
515 continue;
516 }
517
518
519 if (equals( "Port", bytes, nameStart, nameEnd)) {
520 // sc.getPort is not currently implemented.
521 // sc.getPort().setBytes( bytes,
522 // valueStart,
523 // valueEnd-valueStart );
524 continue;
525 }
526
527 // Unknown cookie, complain
528 log("Unknown Special Cookie");
529
530 } else { // Normal Cookie
531 sc = addCookie();
532 sc.setVersion( version );
533 sc.getName().setBytes( bytes, nameStart,
534 nameEnd-nameStart);
535
536 if (valueStart != -1) { // Normal AVPair
537 sc.getValue().setBytes( bytes, valueStart,
538 valueEnd-valueStart);
539 if (isQuoted) {
540 // We know this is a byte value so this is safe
541 ServerCookie.unescapeDoubleQuotes(
542 sc.getValue().getByteChunk());
543 }
544 } else {
545 // Name Only
546 sc.getValue().setString("");
547 }
548 continue;
549 }
550 }
551 }
552
553 /**
554 * Given the starting position of a token, this gets the end of the
555 * token, with no separator characters in between.
556 * JVK
557 */
558 public static final int getTokenEndPosition(byte bytes[], int off, int end){
559 int pos = off;
560 while (pos < end && !isSeparator(bytes[pos])) {pos++; };
561
562 if (pos > end)
563 return end;
564 return pos;
565 }
566
567 /**
568 * Given a starting position after an initial quote chracter, this gets
569 * the position of the end quote. This escapes anything after a '\' char
570 * JVK RFC 2616
571 */
572 public static final int getQuotedValueEndPosition(byte bytes[], int off, int end){
573 int pos = off;
574 while (pos < end) {
575 if (bytes[pos] == '"') {
576 return pos;
577 } else if (bytes[pos] == '\\' && pos < (end - 1)) {
578 pos+=2;
579 } else {
580 pos++;
581 }
582 }
583 // Error, we have reached the end of the header w/o a end quote
584 return end;
585 }
586
587 }