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