Index: container/webapps/docs/config/filter.xml =================================================================== --- container/webapps/docs/config/filter.xml (révision 0) +++ container/webapps/docs/config/filter.xml (révision 1221282) @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<!DOCTYPE document [ + <!ENTITY project SYSTEM "project.xml"> +]> +<document url="filter.html"> + + &project; + + <properties> + <title>Container Provided Filters</title> + </properties> + +<body> + +<section name="Table of Contents"> +<toc/> +</section> + +<section name="Introduction"> + + <p>Tomcat provides a number of <strong>Filters</strong> which may be + configured for use with all web applications using + <code>$CATALINA_BASE/conf/web.xml</code> or may be configured for individual + web applications by configuring them in the application's + <code>WEB-INF/web.xml</code>. Each filter is described below.</p> + + <blockquote><em> + <p>This description uses the variable name $CATALINA_BASE to refer the + base directory against which most relative paths are resolved. If you have + not configured Tomcat for multiple instances by setting a CATALINA_BASE + directory, then $CATALINA_BASE will be set to the value of $CATALINA_HOME, + the directory into which you have installed Tomcat.</p> + </em></blockquote> + +</section> + + +<section name="Failed Request Filter"> + + <subsection name="Introduction"> + + <p>This filter triggers parameters parsing in a request and rejects the + request if some parameters were skipped during parameter parsing because + of parsing errors or request size limitations (such as + <code>maxParameterCount</code> attribute in a + <a href="http.html">Connector</a>). + This filter can be used to ensure that none parameter values submitted by + client are lost.</p> + + <p>Note that parameter parsing may consume the body of an HTTP request, so + caution is needed if the servlet protected by this filter uses + <code>request.getInputStream()</code> or <code>request.getReader()</code> + calls. In general the risk of breaking a web application by adding this + filter is not so high, because parameter parsing does check content type + of the request before consuming the request body.</p> + + <p>The request is rejected with HTTP status code 400 (Bad Request).</p> + + </subsection> + + <subsection name="Filter Class Name"> + + <p>The filter class name for the Failed Request Filter is + <strong><code>org.apache.catalina.filters.FailedRequestFilter</code> + </strong>.</p> + + </subsection> + + <subsection name="Initialisation parameters"> + + <p>The Failed Request Filter does not support any initialization parameters.</p> + + </subsection> + +</section> + + +</body> + + +</document> Modification de propriétés sur container/webapps/docs/config/filter.xml ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: container/webapps/docs/config/project.xml =================================================================== --- container/webapps/docs/config/project.xml (révision 1221281) +++ container/webapps/docs/config/project.xml (révision 1221282) @@ -49,6 +49,7 @@ </menu> <menu name="Nested Components"> + <item name="Filter" href="filter.html"/> <item name="Global Resources" href="globalresources.html"/> <item name="Loader" href="loader.html"/> <item name="Manager" href="manager.html"/> Index: container/webapps/docs/config/ajp.xml =================================================================== --- container/webapps/docs/config/ajp.xml (révision 1221281) +++ container/webapps/docs/config/ajp.xml (révision 1221282) @@ -95,6 +95,14 @@ By default, DNS lookups are enabled.</p> </attribute> + <attribute name="maxParameterCount" required="false"> + <p>The maximum number of parameters (GET plus POST) which will be + automatically parsed by the container. A value of less than 0 means no + limit. If not specified, a default of 10000 is used. Note that + <code>FailedRequestFilter</code> <a href="filter.html">filter</a> can be + used to reject requests that hit the limit.</p> + </attribute> + <attribute name="maxPostSize" required="false"> <p>The maximum size in bytes of the POST which will be handled by the container FORM URL parameter parsing. The feature can be disabled by Index: container/webapps/docs/config/http.xml =================================================================== --- container/webapps/docs/config/http.xml (révision 1221281) +++ container/webapps/docs/config/http.xml (révision 1221282) @@ -98,6 +98,14 @@ By default, DNS lookups are enabled.</p> </attribute> + <attribute name="maxParameterCount" required="false"> + <p>The maximum number of parameters (GET plus POST) which will be + automatically parsed by the container. A value of less than 0 means no + limit. If not specified, a default of 10000 is used. Note that + <code>FailedRequestFilter</code> <a href="filter.html">filter</a> can be + used to reject requests that hit the limit.</p> + </attribute> + <attribute name="maxPostSize" required="false"> <p>The maximum size in bytes of the POST which will be handled by the container FORM URL parameter parsing. The limit can be disabled by Index: container/catalina/src/share/org/apache/catalina/Globals.java =================================================================== --- container/catalina/src/share/org/apache/catalina/Globals.java (révision 1221281) +++ container/catalina/src/share/org/apache/catalina/Globals.java (révision 1221282) @@ -327,6 +327,17 @@ /** + * The request attribute that is set to <code>Boolean.TRUE</code> if some request + * parameters have been ignored during request parameters parsing. It can + * happen, for example, if there is a limit on the total count of parseable + * parameters, or if parameter cannot be decoded, or any other error + * happened during parameter parsing. + */ + public static final String PARAMETER_PARSE_FAILED_ATTR = + "org.apache.catalina.parameter_parse_failed"; + + + /** * The master flag which controls strict servlet specification * compliance. */ Index: container/catalina/src/share/org/apache/catalina/filters/FailedRequestFilter.java =================================================================== --- container/catalina/src/share/org/apache/catalina/filters/FailedRequestFilter.java (révision 0) +++ container/catalina/src/share/org/apache/catalina/filters/FailedRequestFilter.java (révision 1221282) @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.filters; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.apache.catalina.Globals; + +/** + * Filter that will reject requests if there was a failure during parameter + * parsing. This filter can be used to ensure that none parameter values + * submitted by client are lost. + * + * <p> + * Note that it has side effect that it triggers parameter parsing and thus + * consumes the body for POST requests. Parameter parsing does check content + * type of the request, so there should not be problems with addresses that use + * <code>request.getInputStream()</code> and <code>request.getReader()</code>, + * if requests parsed by them do not use standard value for content mime-type. + */ +public class FailedRequestFilter implements Filter { + + public void init(FilterConfig filterConfig) { + // NOOP + } + + public void destroy() { + // NOOP + } + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (!isGoodRequest(request)) { + ((HttpServletResponse) response) + .sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + chain.doFilter(request, response); + } + + private boolean isGoodRequest(ServletRequest request) { + // Trigger parsing of parameters + request.getParameter("none"); + // Detect failure + if (request.getAttribute(Globals.PARAMETER_PARSE_FAILED_ATTR) != null) { + return false; + } + return true; + } + +} Modification de propriétés sur container/catalina/src/share/org/apache/catalina/filters/FailedRequestFilter.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: container/catalina/src/share/org/apache/catalina/connector/mbeans-descriptors.xml =================================================================== --- container/catalina/src/share/org/apache/catalina/connector/mbeans-descriptors.xml (révision 1221281) +++ container/catalina/src/share/org/apache/catalina/connector/mbeans-descriptors.xml (révision 1221282) @@ -109,6 +109,10 @@ description="Maximum number of Keep-Alive requests to honor per connection" type="int"/> + <attribute name="maxParameterCount" + description="The maximum number of parameters (GET plus POST) which will be automatically parsed by the container. 10000 by default. A value of less than 0 means no limit." + type="int"/> + <attribute name="maxPostSize" description="Maximum size in bytes of a POST which will be handled by the servlet API provided features" type="int"/> Index: container/catalina/src/share/org/apache/catalina/connector/Request.java =================================================================== --- container/catalina/src/share/org/apache/catalina/connector/Request.java (révision 1221281) +++ container/catalina/src/share/org/apache/catalina/connector/Request.java (révision 1221282) @@ -864,6 +864,11 @@ return (requestDispatcherPath == null) ? getRequestPathMB().toString() : requestDispatcherPath.toString(); + } else if (name.equals(Globals.PARAMETER_PARSE_FAILED_ATTR)) { + if (coyoteRequest.getParameters().isParseFailed()) { + return Boolean.TRUE; + } + return null; } Object attr=attributes.get(name); @@ -912,6 +917,26 @@ /** * Return the names of all request attributes for this Request, or an * empty <code>Enumeration</code> if there are none. + * Note that the attribute names return will only be those for the attributes set via + * {@link #setAttribute(String, Object)}. Tomcat internal attributes will + * not be included although they are accessible via + * {@link #getAttribute(String)}. The Tomcat internal attributes include: + * <ul> + * <li>{@link Globals.DISPATCHER_TYPE_ATTR}</li> + * <li>{@link Globals.DISPATCHER_REQUEST_PATH_ATTR}</li> + * <li>{@link Globals.CERTIFICATES_ATTR} (SSL connections only)</li> + * <li>{@link Globals.CIPHER_SUITE_ATTR} (SSL connections only)</li> + * <li>{@link Globals.KEY_SIZE_ATTR} (SSL connections only)</li> + * <li>{@link Globals.SSL_SESSION_ID_ATTR} (SSL connections only)</li> + * <li>{@link Globals#PARAMETER_PARSE_FAILED_ATTR}</li> + * </ul> + * The underlying connector may also expose request attributes. These all + * have names starting with "org.apache.tomcat" and include: + * <ul> + * <li>org.apache.tomcat.sendfile.support</li> + * </ul> + * Connector implementations may return some, all or none of these + * attributes and may also support additional attributes. */ public Enumeration getAttributeNames() { if (isSecure()) { @@ -2419,6 +2444,8 @@ parametersParsed = true; Parameters parameters = coyoteRequest.getParameters(); + // Set this every time in case limit has been changed via JMX + parameters.setLimit(getConnector().getMaxParameterCount()); // getCharacterEncoding() may have been overridden to search for // hidden form field containing request encoding @@ -2459,47 +2486,56 @@ if (!("application/x-www-form-urlencoded".equals(contentType))) return; + boolean success = false; int len = getContentLength(); - if (len > 0) { - int maxPostSize = connector.getMaxPostSize(); - if ((maxPostSize > 0) && (len > maxPostSize)) { - context.getLogger().info - (sm.getString("coyoteRequest.postTooLarge")); - throw new IllegalStateException("Post too large"); - } - try { - byte[] formData = null; - if (len < CACHED_POST_LEN) { - if (postData == null) - postData = new byte[CACHED_POST_LEN]; - formData = postData; - } else { - formData = new byte[len]; + try { + if (len > 0) { + int maxPostSize = connector.getMaxPostSize(); + if ((maxPostSize > 0) && (len > maxPostSize)) { + context.getLogger().info + (sm.getString("coyoteRequest.postTooLarge")); + throw new IllegalStateException("Post too large"); } - int actualLen = readPostBody(formData, len); - if (actualLen == len) { + try { + byte[] formData = null; + if (len < CACHED_POST_LEN) { + if (postData == null) + postData = new byte[CACHED_POST_LEN]; + formData = postData; + } else { + formData = new byte[len]; + } + if (readPostBody(formData, len) != len) { + return; + } parameters.processParameters(formData, 0, len); + } catch (Throwable t) { + context.getLogger().warn + (sm.getString("coyoteRequest.parseParameters"), t); + return; } - } catch (Throwable t) { - context.getLogger().warn - (sm.getString("coyoteRequest.parseParameters"), t); - } - } else if ("chunked".equalsIgnoreCase( - coyoteRequest.getHeader("transfer-encoding"))) { - byte[] formData = null; - try { - formData = readChunkedPostBody(); - } catch (IOException e) { - // Client disconnect - if (context.getLogger().isDebugEnabled()) { - context.getLogger().debug( - sm.getString("coyoteRequest.parseParameters"), e); + } else if ("chunked".equalsIgnoreCase( + coyoteRequest.getHeader("transfer-encoding"))) { + byte[] formData = null; + try { + formData = readChunkedPostBody(); + } catch (IOException e) { + // Client disconnect + if (context.getLogger().isDebugEnabled()) { + context.getLogger().debug( + sm.getString("coyoteRequest.parseParameters"), e); + } + return; } - return; + if (formData != null) { + parameters.processParameters(formData, 0, formData.length); + } } - if (formData != null) { - parameters.processParameters(formData, 0, formData.length); + success = true; + } finally { + if (!success) { + parameters.setParseFailed(true); } } Index: container/catalina/src/share/org/apache/catalina/connector/Connector.java =================================================================== --- container/catalina/src/share/org/apache/catalina/connector/Connector.java (révision 1221281) +++ container/catalina/src/share/org/apache/catalina/connector/Connector.java (révision 1221282) @@ -18,7 +18,6 @@ package org.apache.catalina.connector; -import java.lang.reflect.Method; import java.net.URLEncoder; import java.util.HashMap; @@ -187,6 +186,14 @@ /** + * The maximum number of parameters (GET plus POST) which will be + * automatically parsed by the container. 10000 by default. A value of less + * than 0 means no limit. + */ + protected int maxParameterCount = 10000; + + + /** * Maximum size of a POST which will be automatically parsed by the * container. 2MB by default. */ @@ -498,17 +505,37 @@ } - /** - * Return the mapper. - */ - public Mapper getMapper() { + /** + * Return the mapper. + */ + public Mapper getMapper() { + return (mapper); + } - return (mapper); - } + /** + * Return the maximum number of parameters (GET plus POST) that will be + * automatically parsed by the container. A value of less than 0 means no + * limit. + */ + public int getMaxParameterCount() { + return maxParameterCount; + } /** + * Set the maximum number of parameters (GET plus POST) that will be + * automatically parsed by the container. A value of less than 0 means no + * limit. + * + * @param maxParameterCount The new setting + */ + public void setMaxParameterCount(int maxParameterCount) { + this.maxParameterCount = maxParameterCount; + } + + + /** * Return the maximum size of a POST which will be automatically * parsed by the container. */ Index: container/catalina/src/conf/web.xml =================================================================== --- container/catalina/src/conf/web.xml (révision 1221281) +++ container/catalina/src/conf/web.xml (révision 1221282) @@ -404,6 +404,19 @@ <!-- ================== Built In Filter Definitions ===================== --> + <!-- A filter that triggers request parameters parsing and rejects the --> + <!-- request if some parameters were skipped because of parsing errors or --> + <!-- request size limitations. --> +<!-- + <filter> + <filter-name>failedRequestFilter</filter-name> + <filter-class> + org.apache.catalina.filters.FailedRequestFilter + </filter-class> + </filter> +--> + + <!-- NOTE: An SSI Servlet is also available as an alternative SSI --> <!-- implementation. Use either the Servlet or the Filter but NOT both. --> <!-- --> @@ -467,6 +480,14 @@ <!-- ==================== Built In Filter Mappings ====================== --> + <!-- The mapping for the Failed Request Filter --> +<!-- + <filter-mapping> + <filter-name>failedRequestFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> +--> + <!-- The mapping for the SSI Filter --> <!-- <filter-mapping> Index: connectors/coyote/src/java/org/apache/coyote/Request.java =================================================================== --- connectors/coyote/src/java/org/apache/coyote/Request.java (révision 1221281) +++ connectors/coyote/src/java/org/apache/coyote/Request.java (révision 1221282) @@ -73,7 +73,6 @@ parameters.setQuery(queryMB); parameters.setURLDecoder(urlDecoder); - parameters.setHeaders(headers); } Index: connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties (révision 0) +++ connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties (révision 1221282) @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +parameters.bytes=Start processing with input [{0}] +paramerers.copyFail=Failed to create copy of original parameter values for debug logging purposes +parameters.decodeFail.debug=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. +parameters.decodeFail.info=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. Note that the name and value quoted here may be corrupted due to the failed decoding. Use debug level logging to see the original, non-corrupted values. +parameters.invalidChunk=Invalid chunk starting at byte [{0}] and ending at byte [{1}] with a value of [{2}] ignored +parameters.maxCountFail=More than the maximum number of request parameters (GET plus POST) for a single request ([{0}]) were detected. Any parameters beyond this limit have been ignored. To change this limit, set the maxParameterCount attribute on the Connector. +parameters.multipleDecodingFail=Character decoding failed. A total of [{0}] failures were detected but only the first was logged. Enable debug level logging for this logger to log all failures. +parameters.noequal=Parameter starting at position [{0}] and ending at position [{1}] with a value of [{0}] was not followed by an '=' character Modification de propriétés sur connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: connectors/util/java/org/apache/tomcat/util/http/Parameters.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/Parameters.java (révision 1221281) +++ connectors/util/java/org/apache/tomcat/util/http/Parameters.java (révision 1221282) @@ -18,227 +18,146 @@ package org.apache.tomcat.util.http; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; -import java.util.Hashtable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.apache.tomcat.util.buf.B2CConverter; import org.apache.tomcat.util.buf.ByteChunk; -import org.apache.tomcat.util.buf.CharChunk; import org.apache.tomcat.util.buf.MessageBytes; import org.apache.tomcat.util.buf.UDecoder; -import org.apache.tomcat.util.collections.MultiMap; +import org.apache.tomcat.util.res.StringManager; /** * * @author Costin Manolache */ -public final class Parameters extends MultiMap { +public final class Parameters { - - private static org.apache.commons.logging.Log log= - org.apache.commons.logging.LogFactory.getLog(Parameters.class ); - - // Transition: we'll use the same Hashtable( String->String[] ) - // for the beginning. When we are sure all accesses happen through - // this class - we can switch to MultiMap - private Hashtable paramHashStringArray=new Hashtable(); + private static final org.apache.commons.logging.Log log = + org.apache.commons.logging.LogFactory.getLog(Parameters.class); + + protected static final StringManager sm = + StringManager.getManager("org.apache.tomcat.util.http"); + + // HashMap<String,ArrayList<String>> + private final HashMap paramHashValues = new HashMap(); + private boolean didQueryParameters=false; - private boolean didMerge=false; MessageBytes queryMB; - MimeHeaders headers; UDecoder urlDec; MessageBytes decodedQuery=MessageBytes.newInstance(); - - public static final int INITIAL_SIZE=4; - // Garbage-less parameter merging. - // In a sub-request with parameters, the new parameters - // will be stored in child. When a getParameter happens, - // the 2 are merged togheter. The child will be altered - // to contain the merged values - the parent is allways the - // original request. - private Parameters child=null; - private Parameters parent=null; - private Parameters currentChild=null; - String encoding=null; String queryStringEncoding=null; - + + private int limit = -1; + private int parameterCount = 0; + /** - * + * Is set to <code>true</code> if there were failures during parameter + * parsing. */ + private boolean parseFailed = false; + public Parameters() { - super( INITIAL_SIZE ); + // NO-OP } public void setQuery( MessageBytes queryMB ) { this.queryMB=queryMB; } - public void setHeaders( MimeHeaders headers ) { - this.headers=headers; + public void setLimit(int limit) { + this.limit = limit; } + public String getEncoding() { + return encoding; + } + public void setEncoding( String s ) { encoding=s; - if(debug>0) log( "Set encoding to " + s ); + if(log.isDebugEnabled()) { + log.debug( "Set encoding to " + s ); + } } public void setQueryStringEncoding( String s ) { queryStringEncoding=s; - if(debug>0) log( "Set query string encoding to " + s ); + if(log.isDebugEnabled()) { + log.debug( "Set query string encoding to " + s ); + } } + public boolean isParseFailed() { + return parseFailed; + } + + public void setParseFailed(boolean parseFailed) { + this.parseFailed = parseFailed; + } + public void recycle() { - super.recycle(); - paramHashStringArray.clear(); + parameterCount = 0; + paramHashValues.clear(); didQueryParameters=false; - currentChild=null; - didMerge=false; encoding=null; decodedQuery.recycle(); + parseFailed = false; } - - // -------------------- Sub-request support -------------------- - public Parameters getCurrentSet() { - if( currentChild==null ) - return this; - return currentChild; - } + // -------------------- Data access -------------------- + // Access to the current name/values, no side effect ( processing ). + // You must explicitly call handleQueryParameters and the post methods. - /** Create ( or reuse ) a child that will be used during a sub-request. - All future changes ( setting query string, adding parameters ) - will affect the child ( the parent request is never changed ). - Both setters and getters will return the data from the deepest - child, merged with data from parents. - */ - public void push() { - // We maintain a linked list, that will grow to the size of the - // longest include chain. - // The list has 2 points of interest: - // - request.parameters() is the original request and head, - // - request.parameters().currentChild() is the current set. - // The ->child and parent<- links are preserved ( currentChild is not - // the last in the list ) - - // create a new element in the linked list - // note that we reuse the child, if any - pop will not - // set child to null ! - if( currentChild==null ) { - currentChild=new Parameters(); - currentChild.setURLDecoder( urlDec ); - currentChild.parent=this; + public void addParameterValues(String key, String[] newValues) { + if (key == null) { return; } - if( currentChild.child==null ) { - currentChild.child=new Parameters(); - currentChild.setURLDecoder( urlDec ); - currentChild.child.parent=currentChild; - } // it is not null if this object already had a child - // i.e. a deeper include() ( we keep it ) - - // the head will be the new element. - currentChild=currentChild.child; - currentChild.setEncoding( encoding ); - } - - /** Discard the last child. This happens when we return from a - sub-request and the parameters are locally modified. - */ - public void pop() { - if( currentChild==null ) { - throw new RuntimeException( "Attempt to pop without a push" ); - } - currentChild.recycle(); - currentChild=currentChild.parent; - // don't remove the top. - } - - // -------------------- Data access -------------------- - // Access to the current name/values, no side effect ( processing ). - // You must explicitely call handleQueryParameters and the post methods. - - // This is the original data representation ( hash of String->String[]) - - public void addParameterValues( String key, String[] newValues) { - if ( key==null ) return; - String values[]; - if (paramHashStringArray.containsKey(key)) { - String oldValues[] = (String[])paramHashStringArray.get(key); - values = new String[oldValues.length + newValues.length]; - for (int i = 0; i < oldValues.length; i++) { - values[i] = oldValues[i]; - } - for (int i = 0; i < newValues.length; i++) { - values[i+ oldValues.length] = newValues[i]; - } + ArrayList values = (ArrayList) paramHashValues.get(key); + if (values == null) { + values = new ArrayList(newValues.length); + paramHashValues.put(key, values); } else { - values = newValues; + values.ensureCapacity(values.size() + newValues.length); } - - paramHashStringArray.put(key, values); + for (int i = 0; i < newValues.length; i++) { + values.add(newValues[i]); + } } public String[] getParameterValues(String name) { handleQueryParameters(); - // sub-request - if( currentChild!=null ) { - currentChild.merge(); - return (String[])currentChild.paramHashStringArray.get(name); + // no "facade" + ArrayList values = (ArrayList) paramHashValues.get(name); + if (values == null) { + return null; } - - // no "facade" - String values[]=(String[])paramHashStringArray.get(name); - return values; + return (String[]) values.toArray(new String[values.size()]); } public Enumeration getParameterNames() { handleQueryParameters(); - // Slow - the original code - if( currentChild!=null ) { - currentChild.merge(); - return currentChild.paramHashStringArray.keys(); - } - - // merge in child - return paramHashStringArray.keys(); + return Collections.enumeration(paramHashValues.keySet()); } - /** Combine the parameters from parent with our local ones - */ - private void merge() { - // recursive - if( debug > 0 ) { - log("Before merging " + this + " " + parent + " " + didMerge ); - log( paramsAsString()); - } - // Local parameters first - they take precedence as in spec. - handleQueryParameters(); - - // we already merged with the parent - if( didMerge ) return; - - // we are the top level - if( parent==null ) return; - - // Add the parent props to the child ( lower precedence ) - parent.merge(); - Hashtable parentProps=parent.paramHashStringArray; - merge2( paramHashStringArray , parentProps); - didMerge=true; - if(debug > 0 ) - log("After " + paramsAsString()); - } - - // Shortcut. public String getParameter(String name ) { - String[] values = getParameterValues(name); + handleQueryParameters(); + ArrayList values = (ArrayList) paramHashValues.get(name); if (values != null) { - if( values.length==0 ) return ""; - return values[0]; + if(values.size() == 0) { + return ""; + } + return (String) values.get(0); } else { return null; } @@ -254,8 +173,10 @@ if( queryMB==null || queryMB.isNull() ) return; - if( debug > 0 ) - log( "Decoding query " + decodedQuery + " " + queryStringEncoding); + if(log.isDebugEnabled()) { + log.debug("Decoding query " + decodedQuery + " " + + queryStringEncoding); + } try { decodedQuery.duplicate( queryMB ); @@ -266,62 +187,15 @@ processParameters( decodedQuery, queryStringEncoding ); } - // -------------------- - - /** Combine 2 hashtables into a new one. - * ( two will be added to one ). - * Used to combine child parameters ( RequestDispatcher's query ) - * with parent parameters ( original query or parent dispatcher ) - */ - private static void merge2(Hashtable one, Hashtable two ) { - Enumeration e = two.keys(); - while (e.hasMoreElements()) { - String name = (String) e.nextElement(); - String[] oneValue = (String[]) one.get(name); - String[] twoValue = (String[]) two.get(name); - String[] combinedValue; - - if (twoValue == null) { - continue; - } else { - if( oneValue==null ) { - combinedValue = new String[twoValue.length]; - System.arraycopy(twoValue, 0, combinedValue, - 0, twoValue.length); - } else { - combinedValue = new String[oneValue.length + - twoValue.length]; - System.arraycopy(oneValue, 0, combinedValue, 0, - oneValue.length); - System.arraycopy(twoValue, 0, combinedValue, - oneValue.length, twoValue.length); - } - one.put(name, combinedValue); - } - } - } - - // incredibly inefficient data representation for parameters, - // until we test the new one private void addParam( String key, String value ) { if( key==null ) return; - String values[]; - if (paramHashStringArray.containsKey(key)) { - String oldValues[] = (String[])paramHashStringArray. - get(key); - values = new String[oldValues.length + 1]; - for (int i = 0; i < oldValues.length; i++) { - values[i] = oldValues[i]; - } - values[oldValues.length] = value; - } else { - values = new String[1]; - values[0] = value; + ArrayList values = (ArrayList) paramHashValues.get(key); + if (values == null) { + values = new ArrayList(1); + paramHashValues.put(key, values); } - - - paramHashStringArray.put(key, values); + values.add(value); } public void setURLDecoder( UDecoder u ) { @@ -329,289 +203,252 @@ } // -------------------- Parameter parsing -------------------- - - // This code is not used right now - it's the optimized version - // of the above. - // we are called from a single thread - we can do it the hard way // if needed ByteChunk tmpName=new ByteChunk(); ByteChunk tmpValue=new ByteChunk(); - CharChunk tmpNameC=new CharChunk(1024); - CharChunk tmpValueC=new CharChunk(1024); + private ByteChunk origName=new ByteChunk(); + private ByteChunk origValue=new ByteChunk(); + private static final String DEFAULT_ENCODING = "ISO-8859-1"; + private static final Charset DEFAULT_CHARSET = + Charset.forName(DEFAULT_ENCODING); + public void processParameters( byte bytes[], int start, int len ) { - processParameters(bytes, start, len, encoding); + processParameters(bytes, start, len, getCharset(encoding)); } - public void processParameters( byte bytes[], int start, int len, - String enc ) { - int end=start+len; - int pos=start; + private void processParameters(byte bytes[], int start, int len, + Charset charset) { - if( debug>0 ) - log( "Bytes: " + new String( bytes, start, len )); + if(log.isDebugEnabled()) { + try { + log.debug(sm.getString("parameters.bytes", + new String(bytes, start, len, DEFAULT_CHARSET.name()))); + } catch (UnsupportedEncodingException uee) { + // Not possible. All JVMs must support ISO-8859-1 + } + } - do { - boolean noEq=false; - int valStart=-1; - int valEnd=-1; + int decodeFailCount = 0; - int nameStart=pos; - int nameEnd=ByteChunk.indexOf(bytes, nameStart, end, '=' ); - // Workaround for a&b&c encoding - int nameEnd2=ByteChunk.indexOf(bytes, nameStart, end, '&' ); - if( (nameEnd2!=-1 ) && - ( nameEnd==-1 || nameEnd > nameEnd2) ) { - nameEnd=nameEnd2; - noEq=true; - valStart=nameEnd; - valEnd=nameEnd; - if(debug>0) log("no equal " + nameStart + " " + nameEnd + " " + - new String(bytes, nameStart, nameEnd-nameStart) ); + int pos = start; + int end = start + len; + + while(pos < end) { + parameterCount ++; + + if (limit > -1 && parameterCount >= limit) { + parseFailed = true; + log.warn(sm.getString("parameters.maxCountFail", + Integer.toString(limit))); + break; } - if( nameEnd== -1 ) - nameEnd=end; + int nameStart = pos; + int nameEnd = -1; + int valueStart = -1; + int valueEnd = -1; - if( ! noEq ) { - valStart= (nameEnd < end) ? nameEnd+1 : end; - valEnd=ByteChunk.indexOf(bytes, valStart, end, '&'); - if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; + boolean parsingName = true; + boolean decodeName = false; + boolean decodeValue = false; + boolean parameterComplete = false; + + do { + switch(bytes[pos]) { + case '=': + if (parsingName) { + // Name finished. Value starts from next character + nameEnd = pos; + parsingName = false; + valueStart = ++pos; + } else { + // Equals character in value + pos++; + } + break; + case '&': + if (parsingName) { + // Name finished. No value. + nameEnd = pos; + } else { + // Value finished + valueEnd = pos; + } + parameterComplete = true; + pos++; + break; + case '%': + case '+': + // Decoding required + if (parsingName) { + decodeName = true; + } else { + decodeValue = true; + } + pos ++; + break; + default: + pos ++; + break; + } + } while (!parameterComplete && pos < end); + + if (pos == end) { + if (nameEnd == -1) { + nameEnd = pos; + } else if (valueStart > -1 && valueEnd == -1){ + valueEnd = pos; + } } - pos=valEnd+1; + if (log.isDebugEnabled() && valueStart == -1) { + try { + log.debug(sm.getString("parameters.noequal", + Integer.toString(nameStart), + Integer.toString(nameEnd), + new String(bytes, nameStart, nameEnd-nameStart, + DEFAULT_CHARSET.name()))); + } catch (UnsupportedEncodingException uee) { + // Not possible. All JVMs must support ISO-8859-1 + } + } - if( nameEnd<=nameStart ) { - log.warn("Parameters: Invalid chunk ignored."); + if (nameEnd <= nameStart ) { + if (log.isInfoEnabled()) { + if (valueEnd >= nameStart && log.isDebugEnabled()) { + String extract = null; + try { + extract = new String(bytes, nameStart, + valueEnd - nameStart, + DEFAULT_CHARSET.name()); + } catch (UnsupportedEncodingException uee) { + // Not possible. All JVMs must support ISO-8859-1 + } + log.info(sm.getString("parameters.invalidChunk", + Integer.toString(nameStart), + Integer.toString(valueEnd), + extract)); + } else { + log.info(sm.getString("parameters.invalidChunk", + Integer.toString(nameStart), + Integer.toString(nameEnd), + null)); + } + } + parseFailed = true; continue; // invalid chunk - it's better to ignore } - tmpName.setBytes( bytes, nameStart, nameEnd-nameStart ); - tmpValue.setBytes( bytes, valStart, valEnd-valStart ); + + tmpName.setBytes(bytes, nameStart, nameEnd - nameStart); + tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart); + // Take copies as if anything goes wrong originals will be + // corrupted. This means original values can be logged. + // For performance - only done for debug + if (log.isDebugEnabled()) { + try { + origName.append(bytes, nameStart, nameEnd - nameStart); + origValue.append(bytes, valueStart, valueEnd - valueStart); + } catch (IOException ioe) { + // Should never happen... + log.error(sm.getString("paramerers.copyFail"), ioe); + } + } + try { - addParam( urlDecode(tmpName, enc), urlDecode(tmpValue, enc) ); + String name; + String value; + + if (decodeName) { + urlDecode(tmpName); + } + tmpName.setCharset(charset); + name = tmpName.toString(); + + if (decodeValue) { + urlDecode(tmpValue); + } + tmpValue.setCharset(charset); + value = tmpValue.toString(); + + addParam(name, value); } catch (IOException e) { - // Exception during character decoding: skip parameter - log.warn("Parameters: Character decoding failed. " + - "Parameter skipped.", e); + parseFailed = true; + decodeFailCount++; + if (decodeFailCount == 1 || log.isDebugEnabled()) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("parameters.decodeFail.debug", + origName.toString(), origValue.toString()), e); + } else if (log.isInfoEnabled()) { + log.info(sm.getString("parameters.decodeFail.info", + tmpName.toString(), tmpValue.toString()), e); + } + } } tmpName.recycle(); tmpValue.recycle(); + // Only recycle copies if we used them + if (log.isDebugEnabled()) { + origName.recycle(); + origValue.recycle(); + } + } - } while( pos<end ); + if (decodeFailCount > 1 && !log.isDebugEnabled()) { + log.info(sm.getString("parameters.multipleDecodingFail", + Integer.toString(decodeFailCount))); + } } - private String urlDecode(ByteChunk bc, String enc) + private void urlDecode(ByteChunk bc) throws IOException { if( urlDec==null ) { urlDec=new UDecoder(); } urlDec.convert(bc); - String result = null; - if (enc != null) { - bc.setEncoding(enc); - result = bc.toString(); - } else { - CharChunk cc = tmpNameC; - cc.allocate(bc.getLength(), -1); - // Default encoding: fast conversion - byte[] bbuf = bc.getBuffer(); - char[] cbuf = cc.getBuffer(); - int start = bc.getStart(); - for (int i = 0; i < bc.getLength(); i++) { - cbuf[i] = (char) (bbuf[i + start] & 0xff); - } - cc.setChars(cbuf, 0, bc.getLength()); - result = cc.toString(); - cc.recycle(); - } - return result; } - public void processParameters( char chars[], int start, int len ) { - int end=start+len; - int pos=start; - - if( debug>0 ) - log( "Chars: " + new String( chars, start, len )); - do { - boolean noEq=false; - int nameStart=pos; - int valStart=-1; - int valEnd=-1; - - int nameEnd=CharChunk.indexOf(chars, nameStart, end, '=' ); - int nameEnd2=CharChunk.indexOf(chars, nameStart, end, '&' ); - if( (nameEnd2!=-1 ) && - ( nameEnd==-1 || nameEnd > nameEnd2) ) { - nameEnd=nameEnd2; - noEq=true; - valStart=nameEnd; - valEnd=nameEnd; - if(debug>0) log("no equal " + nameStart + " " + nameEnd + " " + - new String(chars, nameStart, nameEnd-nameStart) ); - } - if( nameEnd== -1 ) nameEnd=end; - - if( ! noEq ) { - valStart= (nameEnd < end) ? nameEnd+1 : end; - valEnd=CharChunk.indexOf(chars, valStart, end, '&'); - if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; - } - - pos=valEnd+1; - - if( nameEnd<=nameStart ) { - continue; - // invalid chunk - no name, it's better to ignore - // XXX log it ? - } - - try { - tmpNameC.append( chars, nameStart, nameEnd-nameStart ); - tmpValueC.append( chars, valStart, valEnd-valStart ); - - if( debug > 0 ) - log( tmpNameC + "= " + tmpValueC); - - if( urlDec==null ) { - urlDec=new UDecoder(); - } - - urlDec.convert( tmpNameC ); - urlDec.convert( tmpValueC ); - - if( debug > 0 ) - log( tmpNameC + "= " + tmpValueC); - - addParam( tmpNameC.toString(), tmpValueC.toString() ); - } catch( IOException ex ) { - ex.printStackTrace(); - } - - tmpNameC.recycle(); - tmpValueC.recycle(); - - } while( pos<end ); - } - - public void processParameters( MessageBytes data ) { - processParameters(data, encoding); - } - public void processParameters( MessageBytes data, String encoding ) { if( data==null || data.isNull() || data.getLength() <= 0 ) return; - if( data.getType() == MessageBytes.T_BYTES ) { - ByteChunk bc=data.getByteChunk(); - processParameters( bc.getBytes(), bc.getOffset(), - bc.getLength(), encoding); - } else { - if (data.getType()!= MessageBytes.T_CHARS ) - data.toChars(); - CharChunk cc=data.getCharChunk(); - processParameters( cc.getChars(), cc.getOffset(), - cc.getLength()); + if( data.getType() != MessageBytes.T_BYTES ) { + data.toBytes(); } + ByteChunk bc=data.getByteChunk(); + processParameters( bc.getBytes(), bc.getOffset(), + bc.getLength(), getCharset(encoding)); } - /** Debug purpose - */ - public String paramsAsString() { - StringBuffer sb=new StringBuffer(); - Enumeration en= paramHashStringArray.keys(); - while( en.hasMoreElements() ) { - String k=(String)en.nextElement(); - sb.append( k ).append("="); - String v[]=(String[])paramHashStringArray.get( k ); - for( int i=0; i<v.length; i++ ) - sb.append( v[i] ).append(","); - sb.append("\n"); + private Charset getCharset(String encoding) { + if (encoding == null) { + return DEFAULT_CHARSET; } - return sb.toString(); + try { + return B2CConverter.getCharset(encoding); + } catch (UnsupportedEncodingException e) { + return DEFAULT_CHARSET; + } } - private static int debug=0; - private void log(String s ) { - if (log.isDebugEnabled()) - log.debug("Parameters: " + s ); - } - - // -------------------- Old code, needs rewrite -------------------- - - /** Used by RequestDispatcher + /** + * Debug purpose */ - public void processParameters( String str ) { - int end=str.length(); - int pos=0; - if( debug > 0) - log("String: " + str ); - - do { - boolean noEq=false; - int valStart=-1; - int valEnd=-1; - - int nameStart=pos; - int nameEnd=str.indexOf('=', nameStart ); - int nameEnd2=str.indexOf('&', nameStart ); - if( nameEnd2== -1 ) nameEnd2=end; - if( (nameEnd2!=-1 ) && - ( nameEnd==-1 || nameEnd > nameEnd2) ) { - nameEnd=nameEnd2; - noEq=true; - valStart=nameEnd; - valEnd=nameEnd; - if(debug>0) log("no equal " + nameStart + " " + nameEnd + " " + - str.substring(nameStart, nameEnd)); + public String paramsAsString() { + StringBuffer sb = new StringBuffer(); + Iterator it = paramHashValues.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry e = (Map.Entry) it.next(); + sb.append(e.getKey()).append('='); + ArrayList values = (ArrayList) e.getValue(); + for(int i = 0; i < values.size(); i++) { + sb.append(values.get(i)).append(','); } - - if( nameEnd== -1 ) nameEnd=end; - - if( ! noEq ) { - valStart=nameEnd+1; - valEnd=str.indexOf('&', valStart); - if( valEnd== -1 ) valEnd = (valStart < end) ? end : valStart; - } - - pos=valEnd+1; - - if( nameEnd<=nameStart ) { - continue; - } - if( debug>0) - log( "XXX " + nameStart + " " + nameEnd + " " - + valStart + " " + valEnd ); - - try { - tmpNameC.append(str, nameStart, nameEnd-nameStart ); - tmpValueC.append(str, valStart, valEnd-valStart ); - - if( debug > 0 ) - log( tmpNameC + "= " + tmpValueC); - - if( urlDec==null ) { - urlDec=new UDecoder(); - } - - urlDec.convert( tmpNameC ); - urlDec.convert( tmpValueC ); - - if( debug > 0 ) - log( tmpNameC + "= " + tmpValueC); - - addParam( tmpNameC.toString(), tmpValueC.toString() ); - } catch( IOException ex ) { - ex.printStackTrace(); - } - - tmpNameC.recycle(); - tmpValueC.recycle(); - - } while( pos<end ); + sb.append('\n'); + } + return sb.toString(); } - } Index: connectors/util/java/org/apache/tomcat/util/buf/StringCache.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/buf/StringCache.java (révision 1221281) +++ connectors/util/java/org/apache/tomcat/util/buf/StringCache.java (révision 1221282) @@ -17,6 +17,7 @@ package org.apache.tomcat.util.buf; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -290,7 +291,7 @@ entry.name = new byte[bc.getLength()]; System.arraycopy(bc.getBuffer(), start, entry.name, 0, end - start); // Set encoding - entry.enc = bc.getEncoding(); + entry.charset = bc.getCharset(); // Initialize occurrence count to one count = new int[1]; count[0] = 1; @@ -470,7 +471,7 @@ protected static final String find(ByteChunk name) { int pos = findClosest(name, bcCache, bcCache.length); if ((pos < 0) || (compare(name, bcCache[pos].name) != 0) - || !(name.getEncoding().equals(bcCache[pos].enc))) { + || !(name.getCharset().equals(bcCache[pos].charset))) { return null; } else { return bcCache[pos].value; @@ -622,7 +623,7 @@ public static class ByteEntry { public byte[] name = null; - public String enc = null; + public Charset charset = null; public String value = null; public String toString() { Index: connectors/util/java/org/apache/tomcat/util/buf/UDecoder.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/buf/UDecoder.java (révision 1221281) +++ connectors/util/java/org/apache/tomcat/util/buf/UDecoder.java (révision 1221282) @@ -34,7 +34,30 @@ System.getProperty( "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "false")).booleanValue(); - + + private static class DecodeException extends CharConversionException { + private static final long serialVersionUID = 1L; + public DecodeException(String s) { + super(s); + } + + public synchronized Throwable fillInStackTrace() { + // This class does not provide a stack trace + return this; + } + } + + /** Unexpected end of data. */ + private static final IOException EXCEPTION_EOF = new DecodeException("EOF"); + + /** %xx with not-hex digit */ + private static final IOException EXCEPTION_NOT_HEX_DIGIT = new DecodeException( + "isHexDigit"); + + /** %-encoded slash is forbidden in resource path */ + private static final IOException EXCEPTION_SLASH = new DecodeException( + "noSlash"); + public UDecoder() { } @@ -59,16 +82,18 @@ int idx= ByteChunk.indexOf( buff, start, end, '%' ); int idx2=-1; - if( query ) - idx2= ByteChunk.indexOf( buff, start, end, '+' ); + if( query ) { + idx2= ByteChunk.indexOf( buff, start, (idx >= 0 ? idx : end), '+' ); + } if( idx<0 && idx2<0 ) { return; } - // idx will be the smallest positive inxes ( first % or + ) - if( idx2 >= 0 && idx2 < idx ) idx=idx2; - if( idx < 0 ) idx=idx2; - + // idx will be the smallest positive index ( first % or + ) + if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) { + idx=idx2; + } + boolean noSlash = !(ALLOW_ENCODED_SLASH || query); for( int j=idx; j<end; j++, idx++ ) { @@ -79,17 +104,17 @@ } else { // read next 2 digits if( j+2 >= end ) { - throw new CharConversionException("EOF"); + throw EXCEPTION_EOF; } byte b1= buff[j+1]; byte b2=buff[j+2]; if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) - throw new CharConversionException( "isHexDigit"); + throw EXCEPTION_NOT_HEX_DIGIT; j+=2; int res=x2c( b1, b2 ); if (noSlash && (res == '/')) { - throw new CharConversionException( "noSlash"); + throw EXCEPTION_SLASH; } buff[idx]=(byte)res; } @@ -124,14 +149,17 @@ int idx= CharChunk.indexOf( buff, start, cend, '%' ); int idx2=-1; - if( query ) - idx2= CharChunk.indexOf( buff, start, cend, '+' ); + if( query ) { + idx2= CharChunk.indexOf( buff, start, (idx >= 0 ? idx : cend), '+' ); + } if( idx<0 && idx2<0 ) { return; } - - if( idx2 >= 0 && idx2 < idx ) idx=idx2; - if( idx < 0 ) idx=idx2; + + // idx will be the smallest positive index ( first % or + ) + if( (idx2 >= 0 && idx2 < idx) || idx < 0 ) { + idx=idx2; + } boolean noSlash = !(ALLOW_ENCODED_SLASH || query); for( int j=idx; j<cend; j++, idx++ ) { @@ -143,17 +171,17 @@ // read next 2 digits if( j+2 >= cend ) { // invalid - throw new CharConversionException("EOF"); + throw EXCEPTION_EOF; } char b1= buff[j+1]; char b2=buff[j+2]; if( !isHexDigit( b1 ) || ! isHexDigit(b2 )) - throw new CharConversionException("isHexDigit"); + throw EXCEPTION_NOT_HEX_DIGIT; j+=2; int res=x2c( b1, b2 ); if (noSlash && (res == '/')) { - throw new CharConversionException( "noSlash"); + throw EXCEPTION_SLASH; } buff[idx]=(char)res; } Index: connectors/util/java/org/apache/tomcat/util/buf/MessageBytes.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/buf/MessageBytes.java (révision 1221281) +++ connectors/util/java/org/apache/tomcat/util/buf/MessageBytes.java (révision 1221282) @@ -21,6 +21,7 @@ import java.util.*; import java.io.Serializable; import java.io.IOException; +import java.nio.charset.Charset; /** * This class is used to represent a subarray of bytes in an HTTP message. @@ -140,13 +141,13 @@ * previous conversion is reset. * If no encoding is set, we'll use 8859-1. */ - public void setEncoding( String enc ) { - if( !byteC.isNull() ) { - // if the encoding changes we need to reset the converion results - charC.recycle(); - hasStrValue=false; - } - byteC.setEncoding(enc); + public void setCharset(Charset charset) { + if( !byteC.isNull() ) { + // if the encoding changes we need to reset the conversion results + charC.recycle(); + hasStrValue=false; + } + byteC.setCharset(charset); } /** Index: connectors/util/java/org/apache/tomcat/util/buf/ByteChunk.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/buf/ByteChunk.java (révision 1221281) +++ connectors/util/java/org/apache/tomcat/util/buf/ByteChunk.java (révision 1221282) @@ -19,6 +19,10 @@ import java.io.IOException; import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; /* * In a server it is very important to be able to operate on @@ -94,15 +98,26 @@ as most standards seem to converge, but the servlet API requires 8859_1, and this object is used mostly for servlets. */ - public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1"; - + public static final Charset DEFAULT_CHARSET; + + static { + Charset c = null; + try { + c = B2CConverter.getCharset("ISO-8859-1"); + } catch (UnsupportedEncodingException e) { + // Should never happen since all JVMs must support ISO-8859-1 + } + DEFAULT_CHARSET = c; + } + + // byte[] private byte[] buff; private int start=0; private int end; - private String enc; + private Charset charset; private boolean isSet=false; // XXX @@ -142,7 +157,7 @@ */ public void recycle() { // buff = null; - enc=null; + charset=null; start=0; end=0; isSet=false; @@ -182,13 +197,15 @@ this.optimizedWrite = optimizedWrite; } - public void setEncoding( String enc ) { - this.enc=enc; + public void setCharset(Charset charset) { + this.charset=charset; } - public String getEncoding() { - if (enc == null) - enc=DEFAULT_CHARACTER_ENCODING; - return enc; + + public Charset getCharset() { + if (charset == null) { + charset = DEFAULT_CHARSET; + } + return charset; } /** @@ -491,28 +508,15 @@ } public String toStringInternal() { - String strValue=null; - try { - if( enc==null ) enc=DEFAULT_CHARACTER_ENCODING; - strValue = new String( buff, start, end-start, enc ); - /* - Does not improve the speed too much on most systems, - it's safer to use the "clasical" new String(). - - Most overhead is in creating char[] and copying, - the internal implementation of new String() is very close to - what we do. The decoder is nice for large buffers and if - we don't go to String ( so we can take advantage of reduced GC) - - // Method is commented out, in: - return B2CConverter.decodeString( enc ); - */ - } catch (java.io.UnsupportedEncodingException e) { - // Use the platform encoding in that case; the usage of a bad - // encoding will have been logged elsewhere already - strValue = new String(buff, start, end-start); + if (charset == null) { + charset = DEFAULT_CHARSET; } - return strValue; + // new String(byte[], int, int, Charset) takes a defensive copy of the + // entire byte array. This is expensive if only a small subset of the + // bytes will be used. The code below is from Apache Harmony. + CharBuffer cb; + cb = charset.decode(ByteBuffer.wrap(buff, start, end-start)); + return new String(cb.array(), cb.arrayOffset(), cb.length()); } public int getInt() Index: connectors/util/java/org/apache/tomcat/util/buf/B2CConverter.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/buf/B2CConverter.java (révision 1221281) +++ connectors/util/java/org/apache/tomcat/util/buf/B2CConverter.java (révision 1221282) @@ -22,6 +22,11 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; /** Efficient conversion of bytes to character . * @@ -38,8 +43,40 @@ private static org.apache.commons.logging.Log log= - org.apache.commons.logging.LogFactory.getLog( B2CConverter.class ); + org.apache.commons.logging.LogFactory.getLog( B2CConverter.class ); + + private static final Map encodingToCharsetCache = new HashMap(); + static { + Iterator charsets = Charset.availableCharsets().values().iterator(); + while (charsets.hasNext()) { + Charset charset = (Charset) charsets.next(); + encodingToCharsetCache.put( + charset.name().toLowerCase(Locale.US), charset); + Iterator aliases = charset.aliases().iterator(); + while (aliases.hasNext()) { + String alias = (String) aliases.next(); + encodingToCharsetCache.put( + alias.toLowerCase(Locale.US), charset); + } + } + } + + public static Charset getCharset(String enc) + throws UnsupportedEncodingException { + + // Encoding names should all be ASCII + String lowerCaseEnc = enc.toLowerCase(Locale.US); + + Charset charset = (Charset) encodingToCharsetCache.get(lowerCaseEnc); + + if (charset == null) { + // Pre-population of the cache means this must be invalid + throw new UnsupportedEncodingException(enc); + } + return charset; + } + private IntermediateInputStream iis; private ReadConvertor conv; private String encoding; @@ -68,38 +105,42 @@ char result[]=new char[BUFFER_SIZE]; /** Convert a buffer of bytes into a chars + * @deprecated */ public void convert( ByteChunk bb, CharChunk cb ) throws IOException { + // Set the ByteChunk as input to the Intermediate reader convert(bb, cb, cb.getBuffer().length - cb.getEnd()); } - /** Convert a buffer of bytes into a chars + /** + * Convert a buffer of bytes into a chars. + * + * @param bb Input byte buffer + * @param cb Output char buffer + * @param limit Number of bytes to convert + * @throws IOException */ - public void convert( ByteChunk bb, CharChunk cb, int limit) + public void convert( ByteChunk bb, CharChunk cb, int limit) throws IOException { - // Set the ByteChunk as input to the Intermediate reader iis.setByteChunk( bb ); try { // read from the reader int bbLengthBeforeRead = 0; - while( limit > 0 ) { - int size = limit < BUFFER_SIZE ? limit : BUFFER_SIZE; + while( limit > 0 ) { + int size = limit < BUFFER_SIZE ? limit : BUFFER_SIZE; bbLengthBeforeRead = bb.getLength(); int cnt=conv.read( result, 0, size ); if( cnt <= 0 ) { // End of stream ! - we may be in a bad state if( debug>0) log( "EOF" ); - // reset(); return; } if( debug > 1 ) log("Converted: " + new String( result, 0, cnt )); - - // XXX go directly cb.append( result, 0, cnt ); limit = limit - (bbLengthBeforeRead - bb.getLength()); } @@ -111,12 +152,13 @@ } } + public void reset() throws IOException { // destroy the reader/iis iis=new IntermediateInputStream(); - conv=new ReadConvertor( iis, encoding ); + conv=new ReadConvertor( iis, getCharset(encoding) ); } private final int debug=0; @@ -194,10 +236,9 @@ /** Create a converter. */ - public ReadConvertor( IntermediateInputStream in, String enc ) - throws UnsupportedEncodingException + public ReadConvertor( IntermediateInputStream in, Charset charset ) { - super( in, enc ); + super( in, charset ); } /** Overriden - will do nothing but reset internal state. @@ -237,7 +278,6 @@ */ final class IntermediateInputStream extends InputStream { ByteChunk bc = null; - boolean initialized = false; public IntermediateInputStream() { } @@ -248,27 +288,17 @@ } public final int read(byte cbuf[], int off, int len) throws IOException { - if (!initialized) return -1; - int nread = bc.substract(cbuf, off, len); - return nread; + return bc.substract(cbuf, off, len); } public final int read() throws IOException { - if (!initialized) return -1; return bc.substract(); } - - public int available() throws IOException { - if (!initialized) return 0; - return bc.getLength(); - } - // -------------------- Internal methods -------------------- void setByteChunk( ByteChunk mb ) { - initialized = (mb!=null); bc = mb; } Index: connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties (révision 1224639) +++ connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties (révision 1224640) @@ -14,7 +14,7 @@ # limitations under the License. parameters.bytes=Start processing with input [{0}] -paramerers.copyFail=Failed to create copy of original parameter values for debug logging purposes +parameters.copyFail=Failed to create copy of original parameter values for debug logging purposes parameters.decodeFail.debug=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. parameters.decodeFail.info=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. Note that the name and value quoted here may be corrupted due to the failed decoding. Use debug level logging to see the original, non-corrupted values. parameters.invalidChunk=Invalid chunk starting at byte [{0}] and ending at byte [{1}] with a value of [{2}] ignored Index: connectors/util/java/org/apache/tomcat/util/http/Parameters.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/Parameters.java (révision 1224639) +++ connectors/util/java/org/apache/tomcat/util/http/Parameters.java (révision 1224640) @@ -353,7 +353,7 @@ origValue.append(bytes, valueStart, valueEnd - valueStart); } catch (IOException ioe) { // Should never happen... - log.error(sm.getString("paramerers.copyFail"), ioe); + log.error(sm.getString("parameters.copyFail"), ioe); } } Index: connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties (révision 1228190) +++ connectors/util/java/org/apache/tomcat/util/http/LocalStrings.properties (révision 1228191) @@ -17,6 +17,7 @@ parameters.copyFail=Failed to create copy of original parameter values for debug logging purposes parameters.decodeFail.debug=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. parameters.decodeFail.info=Character decoding failed. Parameter [{0}] with value [{1}] has been ignored. Note that the name and value quoted here may be corrupted due to the failed decoding. Use debug level logging to see the original, non-corrupted values. +parameters.emptyChunk=Empty parameter chunk ignored parameters.invalidChunk=Invalid chunk starting at byte [{0}] and ending at byte [{1}] with a value of [{2}] ignored parameters.maxCountFail=More than the maximum number of request parameters (GET plus POST) for a single request ([{0}]) were detected. Any parameters beyond this limit have been ignored. To change this limit, set the maxParameterCount attribute on the Connector. parameters.multipleDecodingFail=Character decoding failed. A total of [{0}] failures were detected but only the first was logged. Enable debug level logging for this logger to log all failures. Index: connectors/util/java/org/apache/tomcat/util/http/Parameters.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/Parameters.java (révision 1228190) +++ connectors/util/java/org/apache/tomcat/util/http/Parameters.java (révision 1228191) @@ -315,7 +315,17 @@ } if (nameEnd <= nameStart ) { + if (valueStart == -1) { + // && + if (log.isDebugEnabled()) { + log.debug(sm.getString("parameters.emptyChunk")); + } + // Do not flag as error + continue; + } + // &=foo& if (log.isInfoEnabled()) { + if (valueEnd >= nameStart && log.isDebugEnabled()) { String extract = null; try { @@ -342,7 +352,11 @@ } tmpName.setBytes(bytes, nameStart, nameEnd - nameStart); - tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart); + if (valueStart >= 0) { + tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart); + } else { + tmpValue.setBytes(bytes, 0, 0); + } // Take copies as if anything goes wrong originals will be // corrupted. This means original values can be logged. @@ -350,7 +364,11 @@ if (log.isDebugEnabled()) { try { origName.append(bytes, nameStart, nameEnd - nameStart); - origValue.append(bytes, valueStart, valueEnd - valueStart); + if (valueStart >= 0) { + origValue.append(bytes, valueStart, valueEnd - valueStart); + } else { + origValue.append(bytes, 0, 0); + } } catch (IOException ioe) { // Should never happen... log.error(sm.getString("parameters.copyFail"), ioe); @@ -367,11 +385,15 @@ tmpName.setCharset(charset); name = tmpName.toString(); - if (decodeValue) { - urlDecode(tmpValue); + if (valueStart >= 0) { + if (decodeValue) { + urlDecode(tmpValue); + } + tmpValue.setCharset(charset); + value = tmpValue.toString(); + } else { + value = ""; } - tmpValue.setCharset(charset); - value = tmpValue.toString(); addParam(name, value); } catch (IOException e) {