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.IOException; 21 import java.io.UnsupportedEncodingException; 22 import java.util.Enumeration; 23 import java.util.Hashtable; 24 25 import org.apache.tomcat.util.buf.ByteChunk; 26 import org.apache.tomcat.util.buf.CharChunk; 27 import org.apache.tomcat.util.buf.MessageBytes; 28 import org.apache.tomcat.util.buf.UDecoder; 29 import org.apache.tomcat.util.collections.MultiMap; 30 31 /** 32 * 33 * @author Costin Manolache 34 */ 35 public final class Parameters extends MultiMap { 36 37 38 private static org.apache.juli.logging.Log log= 39 org.apache.juli.logging.LogFactory.getLog(Parameters.class ); 40 41 // Transition: we'll use the same Hashtable( String->String[] ) 42 // for the beginning. When we are sure all accesses happen through 43 // this class - we can switch to MultiMap 44 private Hashtable<String,String[]> paramHashStringArray = 45 new Hashtable<String,String[]>(); 46 private boolean didQueryParameters=false; 47 private boolean didMerge=false; 48 49 MessageBytes queryMB; 50 MimeHeaders headers; 51 52 UDecoder urlDec; 53 MessageBytes decodedQuery=MessageBytes.newInstance(); 54 55 public static final int INITIAL_SIZE=4; 56 57 // Garbage-less parameter merging. 58 // In a sub-request with parameters, the new parameters 59 // will be stored in child. When a getParameter happens, 60 // the 2 are merged togheter. The child will be altered 61 // to contain the merged values - the parent is allways the 62 // original request. 63 private Parameters child=null; 64 private Parameters parent=null; 65 private Parameters currentChild=null; 66 67 String encoding=null; 68 String queryStringEncoding=null; 69 70 /** 71 * 72 */ 73 public Parameters() { 74 super( INITIAL_SIZE ); 75 } 76 77 public void setQuery( MessageBytes queryMB ) { 78 this.queryMB=queryMB; 79 } 80 81 public void setHeaders( MimeHeaders headers ) { 82 this.headers=headers; 83 } 84 85 public void setEncoding( String s ) { 86 encoding=s; 87 if(debug>0) log( "Set encoding to " + s ); 88 } 89 90 public void setQueryStringEncoding( String s ) { 91 queryStringEncoding=s; 92 if(debug>0) log( "Set query string encoding to " + s ); 93 } 94 95 public void recycle() { 96 super.recycle(); 97 paramHashStringArray.clear(); 98 didQueryParameters=false; 99 currentChild=null; 100 didMerge=false; 101 encoding=null; 102 decodedQuery.recycle(); 103 } 104 105 // -------------------- Sub-request support -------------------- 106 107 public Parameters getCurrentSet() { 108 if( currentChild==null ) 109 return this; 110 return currentChild; 111 } 112 113 /** Create ( or reuse ) a child that will be used during a sub-request. 114 All future changes ( setting query string, adding parameters ) 115 will affect the child ( the parent request is never changed ). 116 Both setters and getters will return the data from the deepest 117 child, merged with data from parents. 118 */ 119 public void push() { 120 // We maintain a linked list, that will grow to the size of the 121 // longest include chain. 122 // The list has 2 points of interest: 123 // - request.parameters() is the original request and head, 124 // - request.parameters().currentChild() is the current set. 125 // The ->child and parent<- links are preserved ( currentChild is not 126 // the last in the list ) 127 128 // create a new element in the linked list 129 // note that we reuse the child, if any - pop will not 130 // set child to null ! 131 if( currentChild==null ) { 132 currentChild=new Parameters(); 133 currentChild.setURLDecoder( urlDec ); 134 currentChild.parent=this; 135 return; 136 } 137 if( currentChild.child==null ) { 138 currentChild.child=new Parameters(); 139 currentChild.setURLDecoder( urlDec ); 140 currentChild.child.parent=currentChild; 141 } // it is not null if this object already had a child 142 // i.e. a deeper include() ( we keep it ) 143 144 // the head will be the new element. 145 currentChild=currentChild.child; 146 currentChild.setEncoding( encoding ); 147 } 148 149 /** Discard the last child. This happens when we return from a 150 sub-request and the parameters are locally modified. 151 */ 152 public void pop() { 153 if( currentChild==null ) { 154 throw new RuntimeException( "Attempt to pop without a push" ); 155 } 156 currentChild.recycle(); 157 currentChild=currentChild.parent; 158 // don't remove the top. 159 } 160 161 // -------------------- Data access -------------------- 162 // Access to the current name/values, no side effect ( processing ). 163 // You must explicitely call handleQueryParameters and the post methods. 164 165 // This is the original data representation ( hash of String->String[]) 166 167 public void addParameterValues( String key, String[] newValues) { 168 if ( key==null ) return; 169 String values[]; 170 if (paramHashStringArray.containsKey(key)) { 171 String oldValues[] = (String[])paramHashStringArray.get(key); 172 values = new String[oldValues.length + newValues.length]; 173 for (int i = 0; i < oldValues.length; i++) { 174 values[i] = oldValues[i]; 175 } 176 for (int i = 0; i < newValues.length; i++) { 177 values[i+ oldValues.length] = newValues[i]; 178 } 179 } else { 180 values = newValues; 181 } 182 183 paramHashStringArray.put(key, values); 184 } 185 186 public String[] getParameterValues(String name) { 187 handleQueryParameters(); 188 // sub-request 189 if( currentChild!=null ) { 190 currentChild.merge(); 191 return (String[])currentChild.paramHashStringArray.get(name); 192 } 193 194 // no "facade" 195 String values[]=(String[])paramHashStringArray.get(name); 196 return values; 197 } 198 199 public Enumeration getParameterNames() { 200 handleQueryParameters(); 201 // Slow - the original code 202 if( currentChild!=null ) { 203 currentChild.merge(); 204 return currentChild.paramHashStringArray.keys(); 205 } 206 207 // merge in child 208 return paramHashStringArray.keys(); 209 } 210 211 /** Combine the parameters from parent with our local ones 212 */ 213 private void merge() { 214 // recursive 215 if( debug > 0 ) { 216 log("Before merging " + this + " " + parent + " " + didMerge ); 217 log( paramsAsString()); 218 } 219 // Local parameters first - they take precedence as in spec. 220 handleQueryParameters(); 221 222 // we already merged with the parent 223 if( didMerge ) return; 224 225 // we are the top level 226 if( parent==null ) return; 227 228 // Add the parent props to the child ( lower precedence ) 229 parent.merge(); 230 Hashtable<String,String[]> parentProps=parent.paramHashStringArray; 231 merge2( paramHashStringArray , parentProps); 232 didMerge=true; 233 if(debug > 0 ) 234 log("After " + paramsAsString()); 235 } 236 237 238 // Shortcut. 239 public String getParameter(String name ) { 240 String[] values = getParameterValues(name); 241 if (values != null) { 242 if( values.length==0 ) return ""; 243 return values[0]; 244 } else { 245 return null; 246 } 247 } 248 // -------------------- Processing -------------------- 249 /** Process the query string into parameters 250 */ 251 public void handleQueryParameters() { 252 if( didQueryParameters ) return; 253 254 didQueryParameters=true; 255 256 if( queryMB==null || queryMB.isNull() ) 257 return; 258 259 if( debug > 0 ) 260 log( "Decoding query " + decodedQuery + " " + queryStringEncoding); 261 262 try { 263 decodedQuery.duplicate( queryMB ); 264 } catch (IOException e) { 265 // Can't happen, as decodedQuery can't overflow 266 e.printStackTrace(); 267 } 268 processParameters( decodedQuery, queryStringEncoding ); 269 } 270 271 // -------------------- 272 273 /** Combine 2 hashtables into a new one. 274 * ( two will be added to one ). 275 * Used to combine child parameters ( RequestDispatcher's query ) 276 * with parent parameters ( original query or parent dispatcher ) 277 */ 278 private static void merge2(Hashtable<String,String[]> one, 279 Hashtable<String,String[]> two ) { 280 Enumeration e = two.keys(); 281 282 while (e.hasMoreElements()) { 283 String name = (String) e.nextElement(); 284 String[] oneValue = one.get(name); 285 String[] twoValue = two.get(name); 286 String[] combinedValue; 287 288 if (twoValue == null) { 289 continue; 290 } else { 291 if( oneValue==null ) { 292 combinedValue = new String[twoValue.length]; 293 System.arraycopy(twoValue, 0, combinedValue, 294 0, twoValue.length); 295 } else { 296 combinedValue = new String[oneValue.length + 297 twoValue.length]; 298 System.arraycopy(oneValue, 0, combinedValue, 0, 299 oneValue.length); 300 System.arraycopy(twoValue, 0, combinedValue, 301 oneValue.length, twoValue.length); 302 } 303 one.put(name, combinedValue); 304 } 305 } 306 } 307 308 // incredibly inefficient data representation for parameters, 309 // until we test the new one 310 private void addParam( String key, String value ) { 311 if( key==null ) return; 312 String values[]; 313 if (paramHashStringArray.containsKey(key)) { 314 String oldValues[] = (String[])paramHashStringArray. 315 get(key); 316 values = new String[oldValues.length + 1]; 317 for (int i = 0; i < oldValues.length; i++) { 318 values[i] = oldValues[i]; 319 } 320 values[oldValues.length] = value; 321 } else { 322 values = new String[1]; 323 values[0] = value; 324 } 325 326 327 paramHashStringArray.put(key, values); 328 } 329 330 public void setURLDecoder( UDecoder u ) { 331 urlDec=u; 332 } 333 334 // -------------------- Parameter parsing -------------------- 335 336 // This code is not used right now - it's the optimized version 337 // of the above. 338 339 // we are called from a single thread - we can do it the hard way 340 // if needed 341 ByteChunk tmpName=new ByteChunk(); 342 ByteChunk tmpValue=new ByteChunk(); 343 private ByteChunk origName=new ByteChunk(); 344 private ByteChunk origValue=new ByteChunk(); 345 CharChunk tmpNameC=new CharChunk(1024); 346 CharChunk tmpValueC=new CharChunk(1024); 347 private static final String DEFAULT_ENCODING = "ISO-8859-1"; 348 349 public void processParameters( byte bytes[], int start, int len ) { 350 processParameters(bytes, start, len, encoding); 351 } 352 353 public void processParameters( byte bytes[], int start, int len, 354 String enc ) { 355 int end=start+len; 356 int pos=start; 357 358 if(log.isDebugEnabled()) { 359 try { 360 log.debug("Bytes: " + 361 new String(bytes, start, len, DEFAULT_ENCODING)); 362 } catch (UnsupportedEncodingException e) { 363 // Should never happen... 364 log.error("Unable to convert bytes", e); 365 } 366 } 367 368 do { 369 boolean noEq=false; 370 int valStart=-1; 371 int valEnd=-1; 372 373 int nameStart=pos; 374 int nameEnd=ByteChunk.indexOf(bytes, nameStart, end, '=' ); 375 // Workaround for a&b&c encoding 376 int nameEnd2=ByteChunk.indexOf(bytes, nameStart, end, '&' ); 377 if( (nameEnd2!=-1 ) && 378 ( nameEnd==-1 || nameEnd > nameEnd2) ) { 379 nameEnd=nameEnd2; 380 noEq=true; 381 valStart=nameEnd; 382 valEnd=nameEnd; 383 if(log.isDebugEnabled()) { 384 try { 385 log.debug("no equal " + nameStart + " " + nameEnd + " " + 386 new String(bytes, nameStart, nameEnd-nameStart, 387 DEFAULT_ENCODING) ); 388 } catch (UnsupportedEncodingException e) { 389 // Should never happen... 390 log.error("Unable to convert bytes", e); 391 } 392 } 393 } 394 if( nameEnd== -1 ) 395 nameEnd=end; 396 397 if( ! noEq ) { 398 valStart= (nameEnd < end) ? nameEnd+1 : end; 399 valEnd=ByteChunk.indexOf(bytes, valStart, end, '&'); 400 if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; 401 } 402 403 pos=valEnd+1; 404 405 if( nameEnd<=nameStart ) { 406 StringBuilder msg = new StringBuilder("Parameters: Invalid chunk "); 407 // No name eg ...&=xx&... will trigger this 408 if (valEnd >= nameStart) { 409 msg.append('\''); 410 try { 411 msg.append(new String(bytes, nameStart, 412 valEnd - nameStart, DEFAULT_ENCODING)); 413 } catch (UnsupportedEncodingException e) { 414 // Should never happen... 415 log.error("Unable to convert bytes", e); 416 } 417 msg.append("' "); 418 } 419 msg.append("ignored."); 420 log.warn(msg); 421 continue; 422 // invalid chunk - it's better to ignore 423 } 424 tmpName.setBytes( bytes, nameStart, nameEnd-nameStart ); 425 tmpValue.setBytes( bytes, valStart, valEnd-valStart ); 426 427 // Take copies as if anything goes wrong originals will be 428 // corrupted. This means original values can be logged. 429 // For performance - only done for debug 430 if (log.isDebugEnabled()) { 431 try { 432 origName.append(bytes, nameStart, nameEnd-nameStart); 433 origValue.append(bytes, valStart, valEnd-valStart); 434 } catch (IOException ioe) { 435 // Should never happen... 436 log.error("Error copying parameters", ioe); 437 } 438 } 439 440 try { 441 addParam( urlDecode(tmpName, enc), urlDecode(tmpValue, enc) ); 442 } catch (IOException e) { 443 StringBuilder msg = 444 new StringBuilder("Parameters: Character decoding failed."); 445 msg.append(" Parameter '"); 446 if (log.isDebugEnabled()) { 447 msg.append(origName.toString()); 448 msg.append("' with value '"); 449 msg.append(origValue.toString()); 450 msg.append("' has been ignored."); 451 log.debug(msg, e); 452 } else { 453 msg.append(tmpName.toString()); 454 msg.append("' with value '"); 455 msg.append(tmpValue.toString()); 456 msg.append("' has been ignored. Note that the name and "); 457 msg.append("value quoted here may corrupted due to the "); 458 msg.append("failed decoding. Use debug level logging to "); 459 msg.append("see the original, non-corrupted values."); 460 log.warn(msg); 461 } 462 } 463 464 tmpName.recycle(); 465 tmpValue.recycle(); 466 // Only recycle copies if we used them 467 if (log.isDebugEnabled()) { 468 origName.recycle(); 469 origValue.recycle(); 470 } 471 } while( pos<end ); 472 } 473 474 private String urlDecode(ByteChunk bc, String enc) 475 throws IOException { 476 if( urlDec==null ) { 477 urlDec=new UDecoder(); 478 } 479 urlDec.convert(bc); 480 String result = null; 481 if (enc != null) { 482 bc.setEncoding(enc); 483 result = bc.toString(); 484 } else { 485 CharChunk cc = tmpNameC; 486 int length = bc.getLength(); 487 cc.allocate(length, -1); 488 // Default encoding: fast conversion 489 byte[] bbuf = bc.getBuffer(); 490 char[] cbuf = cc.getBuffer(); 491 int start = bc.getStart(); 492 for (int i = 0; i < length; i++) { 493 cbuf[i] = (char) (bbuf[i + start] & 0xff); 494 } 495 cc.setChars(cbuf, 0, length); 496 result = cc.toString(); 497 cc.recycle(); 498 } 499 return result; 500 } 501 502 public void processParameters( char chars[], int start, int len ) { 503 int end=start+len; 504 int pos=start; 505 506 if( debug>0 ) 507 log( "Chars: " + new String( chars, start, len )); 508 do { 509 boolean noEq=false; 510 int nameStart=pos; 511 int valStart=-1; 512 int valEnd=-1; 513 514 int nameEnd=CharChunk.indexOf(chars, nameStart, end, '=' ); 515 int nameEnd2=CharChunk.indexOf(chars, nameStart, end, '&' ); 516 if( (nameEnd2!=-1 ) && 517 ( nameEnd==-1 || nameEnd > nameEnd2) ) { 518 nameEnd=nameEnd2; 519 noEq=true; 520 valStart=nameEnd; 521 valEnd=nameEnd; 522 if( debug>0) log("no equal " + nameStart + " " + nameEnd + " " + new String(chars, nameStart, nameEnd-nameStart) ); 523 } 524 if( nameEnd== -1 ) nameEnd=end; 525 526 if( ! noEq ) { 527 valStart= (nameEnd < end) ? nameEnd+1 : end; 528 valEnd=CharChunk.indexOf(chars, valStart, end, '&'); 529 if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; 530 } 531 532 pos=valEnd+1; 533 534 if( nameEnd<=nameStart ) { 535 continue; 536 // invalid chunk - no name, it's better to ignore 537 // XXX log it ? 538 } 539 540 try { 541 tmpNameC.append( chars, nameStart, nameEnd-nameStart ); 542 tmpValueC.append( chars, valStart, valEnd-valStart ); 543 544 if( debug > 0 ) 545 log( tmpNameC + "= " + tmpValueC); 546 547 if( urlDec==null ) { 548 urlDec=new UDecoder(); 549 } 550 551 urlDec.convert( tmpNameC ); 552 urlDec.convert( tmpValueC ); 553 554 if( debug > 0 ) 555 log( tmpNameC + "= " + tmpValueC); 556 557 addParam( tmpNameC.toString(), tmpValueC.toString() ); 558 } catch( IOException ex ) { 559 ex.printStackTrace(); 560 } 561 562 tmpNameC.recycle(); 563 tmpValueC.recycle(); 564 565 } while( pos<end ); 566 } 567 568 public void processParameters( MessageBytes data ) { 569 processParameters(data, encoding); 570 } 571 572 public void processParameters( MessageBytes data, String encoding ) { 573 if( data==null || data.isNull() || data.getLength() <= 0 ) return; 574 575 if( data.getType() == MessageBytes.T_BYTES ) { 576 ByteChunk bc=data.getByteChunk(); 577 processParameters( bc.getBytes(), bc.getOffset(), 578 bc.getLength(), encoding); 579 } else { 580 if (data.getType()!= MessageBytes.T_CHARS ) 581 data.toChars(); 582 CharChunk cc=data.getCharChunk(); 583 processParameters( cc.getChars(), cc.getOffset(), 584 cc.getLength()); 585 } 586 } 587 588 /** Debug purpose 589 */ 590 public String paramsAsString() { 591 StringBuffer sb=new StringBuffer(); 592 Enumeration en= paramHashStringArray.keys(); 593 while( en.hasMoreElements() ) { 594 String k=(String)en.nextElement(); 595 sb.append( k ).append("="); 596 String v[]=(String[])paramHashStringArray.get( k ); 597 for( int i=0; i<v.length; i++ ) 598 sb.append( v[i] ).append(","); 599 sb.append("\n"); 600 } 601 return sb.toString(); 602 } 603 604 private static int debug=0; 605 private void log(String s ) { 606 if (log.isDebugEnabled()) 607 log.debug("Parameters: " + s ); 608 } 609 610 // -------------------- Old code, needs rewrite -------------------- 611 612 /** Used by RequestDispatcher 613 */ 614 public void processParameters( String str ) { 615 int end=str.length(); 616 int pos=0; 617 if( debug > 0) 618 log("String: " + str ); 619 620 do { 621 boolean noEq=false; 622 int valStart=-1; 623 int valEnd=-1; 624 625 int nameStart=pos; 626 int nameEnd=str.indexOf('=', nameStart ); 627 int nameEnd2=str.indexOf('&', nameStart ); 628 if( nameEnd2== -1 ) nameEnd2=end; 629 if( (nameEnd2!=-1 ) && 630 ( nameEnd==-1 || nameEnd > nameEnd2) ) { 631 nameEnd=nameEnd2; 632 noEq=true; 633 valStart=nameEnd; 634 valEnd=nameEnd; 635 if( debug>0) log("no equal " + nameStart + " " + nameEnd + " " + str.substring(nameStart, nameEnd) ); 636 } 637 638 if( nameEnd== -1 ) nameEnd=end; 639 640 if( ! noEq ) { 641 valStart=nameEnd+1; 642 valEnd=str.indexOf('&', valStart); 643 if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; 644 } 645 646 pos=valEnd+1; 647 648 if( nameEnd<=nameStart ) { 649 continue; 650 } 651 if( debug>0) 652 log( "XXX " + nameStart + " " + nameEnd + " " 653 + valStart + " " + valEnd ); 654 655 try { 656 tmpNameC.append(str, nameStart, nameEnd-nameStart ); 657 tmpValueC.append(str, valStart, valEnd-valStart ); 658 659 if( debug > 0 ) 660 log( tmpNameC + "= " + tmpValueC); 661 662 if( urlDec==null ) { 663 urlDec=new UDecoder(); 664 } 665 666 urlDec.convert( tmpNameC ); 667 urlDec.convert( tmpValueC ); 668 669 if( debug > 0 ) 670 log( tmpNameC + "= " + tmpValueC); 671 672 addParam( tmpNameC.toString(), tmpValueC.toString() ); 673 } catch( IOException ex ) { 674 ex.printStackTrace(); 675 } 676 677 tmpNameC.recycle(); 678 tmpValueC.recycle(); 679 680 } while( pos<end ); 681 } 682 683 684 }