You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tomee.apache.org by rz...@apache.org on 2023/04/18 11:40:17 UTC

[tomee] branch tomee-9.x-cve-patches created (now 1f23bdf62e)

This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a change to branch tomee-9.x-cve-patches
in repository https://gitbox.apache.org/repos/asf/tomee.git


      at 1f23bdf62e Patches Tomcat 10.0.27 for CVE-2023-28708 by applying the changeset from https://github.com/apache/tomcat/commit/f509bbf31fc00abe3d9f25ebfabca5e05173da5b

This branch includes the following new commits:

     new 5ba5876488 Before Patch for Commons File Upload
     new d92b6f8ae3 Patch Tomcat 10.0.27 for CVE-2023-24998 by applying the changeset from
     new e7931c96f8 Before applying porting patch for BZ 66471 (JSessionId secure attribute missing with RemoteIpFilter and X-Forwarded-Proto set to https)
     new 1f23bdf62e Patches Tomcat 10.0.27 for CVE-2023-28708 by applying the changeset from https://github.com/apache/tomcat/commit/f509bbf31fc00abe3d9f25ebfabca5e05173da5b

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[tomee] 02/04: Patch Tomcat 10.0.27 for CVE-2023-24998 by applying the changeset from

Posted by rz...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch tomee-9.x-cve-patches
in repository https://gitbox.apache.org/repos/asf/tomee.git

commit d92b6f8ae38dbafc28e719322e2f6689dddfc66b
Author: Richard Zowalla <ri...@hs-heilbronn.de>
AuthorDate: Tue Apr 18 13:32:45 2023 +0200

    Patch Tomcat 10.0.27 for CVE-2023-24998 by applying the changeset from
    
    https://github.com/apache/tomcat/commit/8a2285f13affa961cc65595aad999db5efae45ce
---
 .../org/apache/catalina/connector/Request.java     | 22 +++++++---
 .../org/apache/tomcat/util/http/Parameters.java    |  9 +++-
 .../util/http/fileupload/FileUploadBase.java       | 39 ++++++++++++++---
 .../impl/FileCountLimitExceededException.java      | 50 ++++++++++++++++++++++
 4 files changed, 106 insertions(+), 14 deletions(-)

diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
index d867cdd554..55e7e677fa 100644
--- a/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
@@ -1569,7 +1569,7 @@ public class Request implements HttpServletRequest {
      * @param oldValue Old attribute value
      */
     private void notifyAttributeAssigned(String name, Object value,
-            Object oldValue) {
+                                         Object oldValue) {
         Context context = getContext();
         if (context == null) {
             return;
@@ -1671,7 +1671,7 @@ public class Request implements HttpServletRequest {
     @Override
     public ServletContext getServletContext() {
         return getContext().getServletContext();
-     }
+    }
 
     @Override
     public AsyncContext startAsync() {
@@ -1680,7 +1680,7 @@ public class Request implements HttpServletRequest {
 
     @Override
     public AsyncContext startAsync(ServletRequest request,
-            ServletResponse response) {
+                                   ServletResponse response) {
         if (!isAsyncSupported()) {
             IllegalStateException ise =
                     new IllegalStateException(sm.getString("request.asyncNotSupported"));
@@ -2041,7 +2041,7 @@ public class Request implements HttpServletRequest {
                 handler = (T) instanceManager.newInstance(httpUpgradeHandlerClass);
             }
         } catch (ReflectiveOperationException | NamingException | IllegalArgumentException |
-                SecurityException e) {
+                 SecurityException e) {
             throw new ServletException(e);
         }
         UpgradeToken upgradeToken = new UpgradeToken(handler, getContext(), instanceManager,
@@ -2704,7 +2704,7 @@ public class Request implements HttpServletRequest {
         Session session = this.getSessionInternal(false);
         if (session == null) {
             throw new IllegalStateException(
-                sm.getString("coyoteRequest.changeSessionId"));
+                    sm.getString("coyoteRequest.changeSessionId"));
         }
 
         Manager manager = this.getContext().getManager();
@@ -2840,8 +2840,9 @@ public class Request implements HttpServletRequest {
             }
         }
 
+        int maxParameterCount = getConnector().getMaxParameterCount();
         Parameters parameters = coyoteRequest.getParameters();
-        parameters.setLimit(getConnector().getMaxParameterCount());
+        parameters.setLimit(maxParameterCount);
 
         boolean success = false;
         try {
@@ -2893,6 +2894,13 @@ public class Request implements HttpServletRequest {
             upload.setFileItemFactory(factory);
             upload.setFileSizeMax(mce.getMaxFileSize());
             upload.setSizeMax(mce.getMaxRequestSize());
+            if (maxParameterCount > -1) {
+                // There is a limit. The limit for parts needs to be reduced by
+                // the number of parameters we have already parsed.
+                // Must be under the limit else parsing parameters would have
+                // triggered an exception.
+                upload.setFileCountMax(maxParameterCount - parameters.size());
+            }
 
             parts = new ArrayList<>();
             try {
@@ -3544,7 +3552,7 @@ public class Request implements HttpServletRequest {
                     public Object get(Request request, String name) {
                         return Boolean.valueOf(
                                 request.getConnector().getProtocolHandler(
-                                        ).isSendfileSupported() && request.getCoyoteRequest().getSendfile());
+                                ).isSendfileSupported() && request.getCoyoteRequest().getSendfile());
                     }
                     @Override
                     public void set(Request request, String name, Object value) {
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java
index ce765374e7..bd86844b97 100644
--- a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java
@@ -47,7 +47,7 @@ public final class Parameters {
     private static final UserDataHelper maxParamCountLog = new UserDataHelper(log);
 
     private static final StringManager sm =
-        StringManager.getManager("org.apache.tomcat.util.http");
+            StringManager.getManager("org.apache.tomcat.util.http");
 
     private final Map<String,ArrayList<String>> paramHashValues =
             new LinkedHashMap<>();
@@ -125,6 +125,11 @@ public final class Parameters {
     }
 
 
+    public int size() {
+        return parameterCount;
+    }
+
+
     public void recycle() {
         parameterCount = 0;
         paramHashValues.clear();
@@ -471,7 +476,7 @@ public final class Parameters {
     }
 
     private void urlDecode(ByteChunk bc)
-        throws IOException {
+            throws IOException {
         if( urlDec==null ) {
             urlDec=new UDecoder();
         }
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
index 7d678c24d8..ce903d489c 100644
--- a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
@@ -25,6 +25,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 
+import org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException;
 import org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl;
 import org.apache.tomcat.util.http.fileupload.impl.FileUploadIOException;
 import org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException;
@@ -128,6 +129,12 @@ public abstract class FileUploadBase {
      */
     private long fileSizeMax = -1;
 
+    /**
+     * The maximum permitted number of files that may be uploaded in a single
+     * request. A value of -1 indicates no maximum.
+     */
+    private long fileCountMax = -1;
+
     /**
      * The content encoding to use when reading part headers.
      */
@@ -204,6 +211,24 @@ public abstract class FileUploadBase {
         this.fileSizeMax = fileSizeMax;
     }
 
+    /**
+     * Returns the maximum number of files allowed in a single request.
+     *
+     * @return The maximum number of files allowed in a single request.
+     */
+    public long getFileCountMax() {
+        return fileCountMax;
+    }
+
+    /**
+     * Sets the maximum number of files allowed per request/
+     *
+     * @param fileCountMax The new limit. {@code -1} means no limit.
+     */
+    public void setFileCountMax(long fileCountMax) {
+        this.fileCountMax = fileCountMax;
+    }
+
     /**
      * Retrieves the character encoding used when reading the headers of an
      * individual part. When not specified, or {@code null}, the request
@@ -247,7 +272,7 @@ public abstract class FileUploadBase {
      *   storing the uploaded content.
      */
     public FileItemIterator getItemIterator(final RequestContext ctx)
-    throws FileUploadException, IOException {
+            throws FileUploadException, IOException {
         try {
             return new FileItemIteratorImpl(this, ctx);
         } catch (final FileUploadIOException e) {
@@ -278,11 +303,15 @@ public abstract class FileUploadBase {
                     "No FileItemFactory has been set.");
             final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
             while (iter.hasNext()) {
+                if (items.size() == fileCountMax) {
+                    // The next item will exceed the limit.
+                    throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax());
+                }
                 final FileItemStream item = iter.next();
                 // Don't use getName() here to prevent an InvalidFileNameException.
                 final String fileName = item.getName();
                 final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
-                                                   item.isFormField(), fileName);
+                        item.isFormField(), fileName);
                 items.add(fileItem);
                 try {
                     Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
@@ -290,7 +319,7 @@ public abstract class FileUploadBase {
                     throw (FileUploadException) e.getCause();
                 } catch (final IOException e) {
                     throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
-                                                           MULTIPART_FORM_DATA, e.getMessage()), e);
+                            MULTIPART_FORM_DATA, e.getMessage()), e);
                 }
                 final FileItemHeaders fih = item.getHeaders();
                 fileItem.setHeaders(fih);
@@ -510,7 +539,7 @@ public abstract class FileUploadBase {
             final int offset = headerPart.indexOf('\r', index);
             if (offset == -1  ||  offset + 1 >= headerPart.length()) {
                 throw new IllegalStateException(
-                    "Expected headers to be terminated by an empty line.");
+                        "Expected headers to be terminated by an empty line.");
             }
             if (headerPart.charAt(offset + 1) == '\n') {
                 return offset;
@@ -532,7 +561,7 @@ public abstract class FileUploadBase {
         }
         final String headerName = header.substring(0, colonOffset).trim();
         final String headerValue =
-            header.substring(colonOffset + 1).trim();
+                header.substring(colonOffset + 1).trim();
         headers.addHeader(headerName, headerValue);
     }
 
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java
new file mode 100644
index 0000000000..958f681276
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/impl/FileCountLimitExceededException.java
@@ -0,0 +1,50 @@
+/*
+ * 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.tomcat.util.http.fileupload.impl;
+
+import org.apache.tomcat.util.http.fileupload.FileUploadException;
+
+/**
+ * This exception is thrown if a request contains more files than the specified
+ * limit.
+ */
+public class FileCountLimitExceededException extends FileUploadException {
+
+    private static final long serialVersionUID = 2408766352570556046L;
+
+    private final long limit;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param message The detail message
+     * @param limit The limit that was exceeded
+     */
+    public FileCountLimitExceededException(final String message, final long limit) {
+        super(message);
+        this.limit = limit;
+    }
+
+    /**
+     * Retrieves the limit that was exceeded.
+     *
+     * @return The limit that was exceeded by the request
+     */
+    public long getLimit() {
+        return limit;
+    }
+}


[tomee] 03/04: Before applying porting patch for BZ 66471 (JSessionId secure attribute missing with RemoteIpFilter and X-Forwarded-Proto set to https)

Posted by rz...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch tomee-9.x-cve-patches
in repository https://gitbox.apache.org/repos/asf/tomee.git

commit e7931c96f8edd501e340412d41b0c224dc8e4e6e
Author: Richard Zowalla <ri...@hs-heilbronn.de>
AuthorDate: Tue Apr 18 13:33:36 2023 +0200

    Before applying porting patch for BZ 66471 (JSessionId secure attribute missing with RemoteIpFilter and X-Forwarded-Proto set to https)
---
 .../patch/java/org/apache/catalina/Globals.java    |  291 +++++
 .../apache/catalina/filters/RemoteIpFilter.java    | 1339 ++++++++++++++++++++
 2 files changed, 1630 insertions(+)

diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java
new file mode 100644
index 0000000000..916dd38e1c
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java
@@ -0,0 +1,291 @@
+/*
+ * 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;
+
+/**
+ * Global constants that are applicable to multiple packages within Catalina.
+ *
+ * @author Craig R. McClanahan
+ */
+public final class Globals {
+
+    // ------------------------------------------------- Request attribute names
+
+    public static final String ASYNC_SUPPORTED_ATTR = "org.apache.catalina.ASYNC_SUPPORTED";
+
+
+    public static final String GSS_CREDENTIAL_ATTR = "org.apache.catalina.realm.GSS_CREDENTIAL";
+
+
+    /**
+     * Request dispatcher state.
+     */
+    public static final String DISPATCHER_TYPE_ATTR = "org.apache.catalina.core.DISPATCHER_TYPE";
+
+
+    /**
+     * Request dispatcher path.
+     */
+    public static final String DISPATCHER_REQUEST_PATH_ATTR = "org.apache.catalina.core.DISPATCHER_REQUEST_PATH";
+
+
+    /**
+     * The request attribute under which we store the servlet name on a
+     * named dispatcher request.
+     */
+    public static final String NAMED_DISPATCHER_ATTR = "org.apache.catalina.NAMED";
+
+
+    /**
+     * The request attribute used to expose the current connection ID associated
+     * with the request, if any. Used with multiplexing protocols such as
+     * HTTTP/2.
+     */
+    public static final String CONNECTION_ID = "org.apache.coyote.connectionID";
+
+
+    /**
+     * The request attribute used to expose the current stream ID associated
+     * with the request, if any. Used with multiplexing protocols such as
+     * HTTTP/2.
+     */
+    public static final String STREAM_ID = "org.apache.coyote.streamID";
+
+
+    /**
+     * The request attribute that is set to {@code Boolean.TRUE} 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 reason that the parameter parsing failed.
+     */
+    public static final String PARAMETER_PARSE_FAILED_REASON_ATTR = "org.apache.catalina.parameter_parse_failed_reason";
+
+
+    /**
+     * The request attribute set by the RemoteIpFilter, RemoteIpValve (and may
+     * be set by other similar components) that identifies for the connector the
+     * remote IP address claimed to be associated with this request when a
+     * request is received via one or more proxies. It is typically provided via
+     * the X-Forwarded-For HTTP header.
+     *
+     * Duplicated here for neater code in the catalina packages.
+     */
+    public static final String REMOTE_ADDR_ATTRIBUTE = org.apache.coyote.Constants.REMOTE_ADDR_ATTRIBUTE;
+
+
+    /**
+     * The request attribute that is set to the value of {@code Boolean.TRUE}
+     * by the RemoteIpFilter, RemoteIpValve (and other similar components) that identifies
+     * a request which been forwarded via one or more proxies.
+     */
+    public static final String REQUEST_FORWARDED_ATTRIBUTE = "org.apache.tomcat.request.forwarded";
+
+
+    /**
+     * The request attribute that is set to the value of {@code Boolean.TRUE}
+     * if connector processing this request supports use of sendfile.
+     *
+     * Duplicated here for neater code in the catalina packages.
+     */
+    public static final String SENDFILE_SUPPORTED_ATTR = org.apache.coyote.Constants.SENDFILE_SUPPORTED_ATTR;
+
+
+    /**
+     * The request attribute that can be used by a servlet to pass
+     * to the connector the name of the file that is to be served
+     * by sendfile. The value should be {@code java.lang.String}
+     * that is {@code File.getCanonicalPath()} of the file to be served.
+     *
+     * Duplicated here for neater code in the catalina packages.
+     */
+    public static final String SENDFILE_FILENAME_ATTR = org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR;
+
+
+    /**
+     * The request attribute that can be used by a servlet to pass
+     * to the connector the start offset of the part of a file
+     * that is to be served by sendfile. The value should be
+     * {@code java.lang.Long}. To serve complete file
+     * the value should be {@code Long.valueOf(0)}.
+     *
+     * Duplicated here for neater code in the catalina packages.
+     */
+    public static final String SENDFILE_FILE_START_ATTR = org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR;
+
+
+    /**
+     * The request attribute that can be used by a servlet to pass
+     * to the connector the end offset (not including) of the part
+     * of a file that is to be served by sendfile. The value should be
+     * {@code java.lang.Long}. To serve complete file
+     * the value should be equal to the length of the file.
+     *
+     * Duplicated here for neater code in the catalina packages.
+     */
+    public static final String SENDFILE_FILE_END_ATTR = org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR;
+
+
+    /**
+     * The request attribute under which we store the array of X509Certificate
+     * objects representing the certificate chain presented by our client,
+     * if any.
+     */
+    public static final String CERTIFICATES_ATTR = "jakarta.servlet.request.X509Certificate";
+
+
+    /**
+     * The request attribute under which we store the name of the cipher suite
+     * being used on an SSL connection (as an object of type
+     * java.lang.String).
+     */
+    public static final String CIPHER_SUITE_ATTR = "jakarta.servlet.request.cipher_suite";
+
+
+    /**
+     * The request attribute under which we store the key size being used for
+     * this SSL connection (as an object of type java.lang.Integer).
+     */
+    public static final String KEY_SIZE_ATTR = "jakarta.servlet.request.key_size";
+
+
+    /**
+     * The request attribute under which we store the session id being used
+     * for this SSL connection (as an object of type java.lang.String).
+     */
+    public static final String SSL_SESSION_ID_ATTR = "jakarta.servlet.request.ssl_session_id";
+
+
+    /**
+     * The request attribute key for the session manager.
+     * This one is a Tomcat extension to the Servlet spec.
+     */
+    public static final String SSL_SESSION_MGR_ATTR = "jakarta.servlet.request.ssl_session_mgr";
+
+
+    // ------------------------------------------------- Session attribute names
+
+    /**
+     * The subject under which the AccessControlContext is running.
+     */
+    public static final String SUBJECT_ATTR = "javax.security.auth.subject";
+
+
+    // ------------------------------------------ ServletContext attribute names
+
+    /**
+     * The servlet context attribute under which we store the alternate
+     * deployment descriptor for this web application
+     */
+    public static final String ALT_DD_ATTR = "org.apache.catalina.deploy.alt_dd";
+
+
+    /**
+     * The servlet context attribute under which we store the class path
+     * for our application class loader (as an object of type String),
+     * delimited with the appropriate path delimiter for this platform.
+     */
+    public static final String CLASS_PATH_ATTR =  "org.apache.catalina.jsp_classpath";
+
+
+    /**
+     * Name of the ServletContext attribute under which we store the context
+     * Realm's CredentialHandler (if both the Realm and the CredentialHandler
+     * exist).
+     */
+    public static final String CREDENTIAL_HANDLER = "org.apache.catalina.CredentialHandler";
+
+
+    /**
+     * The WebResourceRoot which is associated with the context. This can be
+     * used to manipulate static files.
+     */
+    public static final String RESOURCES_ATTR = "org.apache.catalina.resources";
+
+
+    /**
+     * Name of the ServletContext attribute under which we store the web
+     * application version string (the text that appears after ## when parallel
+     * deployment is used).
+     */
+    public static final String WEBAPP_VERSION = "org.apache.catalina.webappVersion";
+
+
+    // --------------------------- ServletContext initialisation parameter names
+
+    /**
+     * Name of the ServletContext init-param that determines if the JSP engine
+     * should validate *.tld files when parsing them.
+     * <p>
+     * This must be kept in sync with org.apache.jasper.Constants
+     */
+    public static final String JASPER_XML_VALIDATION_TLD_INIT_PARAM = "org.apache.jasper.XML_VALIDATE_TLD";
+
+
+    /**
+     * Name of the ServletContext init-param that determines if the JSP engine
+     * will block external entities from being used in *.tld, *.jspx, *.tagx and
+     * tagplugin.xml files.
+     * <p>
+     * This must be kept in sync with org.apache.jasper.Constants
+     */
+    public static final String JASPER_XML_BLOCK_EXTERNAL_INIT_PARAM = "org.apache.jasper.XML_BLOCK_EXTERNAL";
+
+
+    // --------------------------------------------------- System property names
+
+    /**
+     * Name of the system property containing
+     * the tomcat product installation path
+     */
+    public static final String CATALINA_HOME_PROP = org.apache.catalina.startup.Constants.CATALINA_HOME_PROP;
+
+
+    /**
+     * Name of the system property containing
+     * the tomcat instance installation path
+     */
+    public static final String CATALINA_BASE_PROP = org.apache.catalina.startup.Constants.CATALINA_BASE_PROP;
+
+
+    // -------------------------------------------------------- Global constants
+
+    /**
+     * The flag which controls strict servlet specification compliance. Setting
+     * this flag to {@code true} will change the defaults for other settings.
+     */
+    public static final boolean STRICT_SERVLET_COMPLIANCE =
+            Boolean.parseBoolean(System.getProperty("org.apache.catalina.STRICT_SERVLET_COMPLIANCE", "false"));
+
+
+    /**
+     * Has security been turned on?
+     */
+    public static final boolean IS_SECURITY_ENABLED = (System.getSecurityManager() != null);
+
+
+    /**
+     * Default domain for MBeans if none can be determined
+     */
+    public static final String DEFAULT_MBEAN_DOMAIN = "Catalina";
+}
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java
new file mode 100644
index 0000000000..75b5404dc9
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java
@@ -0,0 +1,1339 @@
+/*
+ *  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 java.io.ObjectInputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.GenericFilter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletRequestWrapper;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.PushBuilder;
+
+import org.apache.catalina.AccessLog;
+import org.apache.catalina.Globals;
+import org.apache.catalina.connector.RequestFacade;
+import org.apache.catalina.util.RequestUtil;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.buf.StringUtils;
+import org.apache.tomcat.util.http.FastHttpDateFormat;
+import org.apache.tomcat.util.http.parser.Host;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * <p>
+ * Servlet filter to integrate "X-Forwarded-For" and "X-Forwarded-Proto" HTTP headers.
+ * </p>
+ * <p>
+ * Most of the design of this Servlet Filter is a port of <a
+ * href="https://httpd.apache.org/docs/trunk/mod/mod_remoteip.html">mod_remoteip</a>, this servlet filter replaces the apparent client remote
+ * IP address and hostname for the request with the IP address list presented by a proxy or a load balancer via a request headers (e.g.
+ * "X-Forwarded-For").
+ * </p>
+ * <p>
+ * Another feature of this servlet filter is to replace the apparent scheme (http/https) and server port with the scheme presented by a
+ * proxy or a load balancer via a request header (e.g. "X-Forwarded-Proto").
+ * </p>
+ * <p>
+ * This servlet filter proceeds as follows:
+ * </p>
+ * <p>
+ * If the incoming <code>request.getRemoteAddr()</code> matches the servlet
+ * filter's list of internal or trusted proxies:
+ * </p>
+ * <ul>
+ * <li>Loop on the comma delimited list of IPs and hostnames passed by the preceding load balancer or proxy in the given request's Http
+ * header named <code>$remoteIpHeader</code> (default value <code>x-forwarded-for</code>). Values are processed in right-to-left order.</li>
+ * <li>For each ip/host of the list:
+ * <ul>
+ * <li>if it matches the internal proxies list, the ip/host is swallowed</li>
+ * <li>if it matches the trusted proxies list, the ip/host is added to the created proxies header</li>
+ * <li>otherwise, the ip/host is declared to be the remote ip and looping is stopped.</li>
+ * </ul>
+ * </li>
+ * <li>If the request http header named <code>$protocolHeader</code> (e.g. <code>x-forwarded-proto</code>) consists only of forwards that match
+ * <code>protocolHeaderHttpsValue</code> configuration parameter (default <code>https</code>) then <code>request.isSecure = true</code>,
+ * <code>request.scheme = https</code> and <code>request.serverPort = 443</code>. Note that 443 can be overwritten with the
+ * <code>$httpsServerPort</code> configuration parameter.</li>
+ * <li>Mark the request with the attribute {@link Globals#REQUEST_FORWARDED_ATTRIBUTE} and value {@code Boolean.TRUE} to indicate
+ * that this request has been forwarded by one or more proxies.</li>
+ * </ul>
+ * <table border="1">
+ * <caption>Configuration parameters</caption>
+ * <tr>
+ * <th>XForwardedFilter property</th>
+ * <th>Description</th>
+ * <th>Equivalent mod_remoteip directive</th>
+ * <th>Format</th>
+ * <th>Default Value</th>
+ * </tr>
+ * <tr>
+ * <td>remoteIpHeader</td>
+ * <td>Name of the Http Header read by this servlet filter that holds the list of traversed IP addresses starting from the requesting client
+ * </td>
+ * <td>RemoteIPHeader</td>
+ * <td>Compliant http header name</td>
+ * <td>x-forwarded-for</td>
+ * </tr>
+ * <tr>
+ * <td>internalProxies</td>
+ * <td>Regular expression that matches the IP addresses of internal proxies.
+ * If they appear in the <code>remoteIpHeader</code> value, they will be
+ * trusted and will not appear
+ * in the <code>proxiesHeader</code> value</td>
+ * <td>RemoteIPInternalProxy</td>
+ * <td>Regular expression (in the syntax supported by
+ * {@link java.util.regex.Pattern java.util.regex})</td>
+ * <td>10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|
+ *     169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|
+ *     172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}|
+ *     172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}|
+ *     0:0:0:0:0:0:0:1|::1
+ *     <br>
+ * By default, 10/8, 192.168/16, 169.254/16, 127/8, 172.16/12, and 0:0:0:0:0:0:0:1 are allowed.</td>
+ * </tr>
+ * <tr>
+ * <td>proxiesHeader</td>
+ * <td>Name of the http header created by this servlet filter to hold the list of proxies that have been processed in the incoming
+ * <code>remoteIpHeader</code></td>
+ * <td>RemoteIPProxiesHeader</td>
+ * <td>Compliant http header name</td>
+ * <td>x-forwarded-by</td>
+ * </tr>
+ * <tr>
+ * <td>trustedProxies</td>
+ * <td>Regular expression that matches the IP addresses of trusted proxies.
+ * If they appear in the <code>remoteIpHeader</code> value, they will be
+ * trusted and will appear in the <code>proxiesHeader</code> value</td>
+ * <td>RemoteIPTrustedProxy</td>
+ * <td>Regular expression (in the syntax supported by
+ * {@link java.util.regex.Pattern java.util.regex})</td>
+ * <td>&nbsp;</td>
+ * </tr>
+ * <tr>
+ * <td>protocolHeader</td>
+ * <td>Name of the http header read by this servlet filter that holds the flag that this request</td>
+ * <td>N/A</td>
+ * <td>Compliant http header name like <code>X-Forwarded-Proto</code>, <code>X-Forwarded-Ssl</code> or <code>Front-End-Https</code></td>
+ * <td><code>null</code></td>
+ * </tr>
+ * <tr>
+ * <td>protocolHeaderHttpsValue</td>
+ * <td>Value of the <code>protocolHeader</code> to indicate that it is an Https request</td>
+ * <td>N/A</td>
+ * <td>String like <code>https</code> or <code>ON</code></td>
+ * <td><code>https</code></td>
+ * </tr>
+ * <tr>
+ * <td>httpServerPort</td>
+ * <td>Value returned by {@link ServletRequest#getServerPort()} when the <code>protocolHeader</code> indicates <code>http</code> protocol</td>
+ * <td>N/A</td>
+ * <td>integer</td>
+ * <td>80</td>
+ * </tr>
+ * <tr>
+ * <td>httpsServerPort</td>
+ * <td>Value returned by {@link ServletRequest#getServerPort()} when the <code>protocolHeader</code> indicates <code>https</code> protocol</td>
+ * <td>N/A</td>
+ * <td>integer</td>
+ * <td>443</td>
+ * </tr>
+ * <tr>
+ * <td>enableLookups</td>
+ * <td>Should a DNS lookup be performed to provide a host name when calling {@link ServletRequest#getRemoteHost()}</td>
+ * <td>N/A</td>
+ * <td>boolean</td>
+ * <td>false</td>
+ * </tr>
+ * </table>
+ * <p>
+ * <strong>Regular expression vs. IP address blocks:</strong> <code>mod_remoteip</code> allows to use address blocks (e.g.
+ * <code>192.168/16</code>) to configure <code>RemoteIPInternalProxy</code> and <code>RemoteIPTrustedProxy</code> ; as the JVM doesn't have a
+ * library similar to <a
+ * href="https://apr.apache.org/docs/apr/1.3/group__apr__network__io.html#gb74d21b8898b7c40bf7fd07ad3eb993d">apr_ipsubnet_test</a>, we rely on
+ * regular expressions.
+ * </p>
+ * <hr>
+ * <p>
+ * <strong>Sample with internal proxies</strong>
+ * </p>
+ * <p>
+ * XForwardedFilter configuration:
+ * </p>
+ * <code>
+ * &lt;filter&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;filter-class&gt;org.apache.catalina.filters.RemoteIpFilter&lt;/filter-class&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;internalProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;192\.168\.0\.10|192\.168\.0\.11&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-for&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpProxiesHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-by&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;protocolHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-proto&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ * &lt;/filter&gt;
+ *
+ * &lt;filter-mapping&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *    &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
+ * &lt;/filter-mapping&gt;</code>
+ * <table border="1">
+ * <caption>Request Values</caption>
+ * <tr>
+ * <th>property</th>
+ * <th>Value Before RemoteIpFilter</th>
+ * <th>Value After RemoteIpFilter</th>
+ * </tr>
+ * <tr>
+ * <td>request.remoteAddr</td>
+ * <td>192.168.0.10</td>
+ * <td>140.211.11.130</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-for']</td>
+ * <td>140.211.11.130, 192.168.0.10</td>
+ * <td>null</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-by']</td>
+ * <td>null</td>
+ * <td>null</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-proto']</td>
+ * <td>https</td>
+ * <td>https</td>
+ * </tr>
+ * <tr>
+ * <td>request.scheme</td>
+ * <td>http</td>
+ * <td>https</td>
+ * </tr>
+ * <tr>
+ * <td>request.secure</td>
+ * <td>false</td>
+ * <td>true</td>
+ * </tr>
+ * <tr>
+ * <td>request.serverPort</td>
+ * <td>80</td>
+ * <td>443</td>
+ * </tr>
+ * </table>
+ * Note : <code>x-forwarded-by</code> header is null because only internal proxies as been traversed by the request.
+ * <code>x-forwarded-by</code> is null because all the proxies are trusted or internal.
+ * <hr>
+ * <p>
+ * <strong>Sample with trusted proxies</strong>
+ * </p>
+ * <p>
+ * RemoteIpFilter configuration:
+ * </p>
+ * <code>
+ * &lt;filter&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;filter-class&gt;org.apache.catalina.filters.RemoteIpFilter&lt;/filter-class&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;internalProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;192\.168\.0\.10|192\.168\.0\.11&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-for&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpProxiesHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-by&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;trustedProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;proxy1|proxy2&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ * &lt;/filter&gt;
+ *
+ * &lt;filter-mapping&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *    &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
+ * &lt;/filter-mapping&gt;</code>
+ * <table border="1">
+ * <caption>Request Values</caption>
+ * <tr>
+ * <th>property</th>
+ * <th>Value Before RemoteIpFilter</th>
+ * <th>Value After RemoteIpFilter</th>
+ * </tr>
+ * <tr>
+ * <td>request.remoteAddr</td>
+ * <td>192.168.0.10</td>
+ * <td>140.211.11.130</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-for']</td>
+ * <td>140.211.11.130, proxy1, proxy2</td>
+ * <td>null</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-by']</td>
+ * <td>null</td>
+ * <td>proxy1, proxy2</td>
+ * </tr>
+ * </table>
+ * <p>
+ * Note : <code>proxy1</code> and <code>proxy2</code> are both trusted proxies that come in <code>x-forwarded-for</code> header, they both
+ * are migrated in <code>x-forwarded-by</code> header. <code>x-forwarded-by</code> is null because all the proxies are trusted or internal.
+ * </p>
+ * <hr>
+ * <p>
+ * <strong>Sample with internal and trusted proxies</strong>
+ * </p>
+ * <p>
+ * RemoteIpFilter configuration:
+ * </p>
+ * <code>
+ * &lt;filter&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;filter-class&gt;org.apache.catalina.filters.RemoteIpFilter&lt;/filter-class&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;internalProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;192\.168\.0\.10|192\.168\.0\.11&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-for&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpProxiesHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-by&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;trustedProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;proxy1|proxy2&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ * &lt;/filter&gt;
+ *
+ * &lt;filter-mapping&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *    &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
+ * &lt;/filter-mapping&gt;</code>
+ * <table border="1">
+ * <caption>Request Values</caption>
+ * <tr>
+ * <th>property</th>
+ * <th>Value Before RemoteIpFilter</th>
+ * <th>Value After RemoteIpFilter</th>
+ * </tr>
+ * <tr>
+ * <td>request.remoteAddr</td>
+ * <td>192.168.0.10</td>
+ * <td>140.211.11.130</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-for']</td>
+ * <td>140.211.11.130, proxy1, proxy2, 192.168.0.10</td>
+ * <td>null</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-by']</td>
+ * <td>null</td>
+ * <td>proxy1, proxy2</td>
+ * </tr>
+ * </table>
+ * <p>
+ * Note : <code>proxy1</code> and <code>proxy2</code> are both trusted proxies that come in <code>x-forwarded-for</code> header, they both
+ * are migrated in <code>x-forwarded-by</code> header. As <code>192.168.0.10</code> is an internal proxy, it does not appear in
+ * <code>x-forwarded-by</code>. <code>x-forwarded-by</code> is null because all the proxies are trusted or internal.
+ * </p>
+ * <hr>
+ * <p>
+ * <strong>Sample with an untrusted proxy</strong>
+ * </p>
+ * <p>
+ * RemoteIpFilter configuration:
+ * </p>
+ * <code>
+ * &lt;filter&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;filter-class&gt;org.apache.catalina.filters.RemoteIpFilter&lt;/filter-class&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;internalProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;192\.168\.0\.10|192\.168\.0\.11&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-for&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;remoteIpProxiesHeader&lt;/param-name&gt;
+ *       &lt;param-value&gt;x-forwarded-by&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ *    &lt;init-param&gt;
+ *       &lt;param-name&gt;trustedProxies&lt;/param-name&gt;
+ *       &lt;param-value&gt;proxy1|proxy2&lt;/param-value&gt;
+ *    &lt;/init-param&gt;
+ * &lt;/filter&gt;
+ *
+ * &lt;filter-mapping&gt;
+ *    &lt;filter-name&gt;RemoteIpFilter&lt;/filter-name&gt;
+ *    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *    &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
+ * &lt;/filter-mapping&gt;</code>
+ * <table border="1">
+ * <caption>Request Values</caption>
+ * <tr>
+ * <th>property</th>
+ * <th>Value Before RemoteIpFilter</th>
+ * <th>Value After RemoteIpFilter</th>
+ * </tr>
+ * <tr>
+ * <td>request.remoteAddr</td>
+ * <td>192.168.0.10</td>
+ * <td>untrusted-proxy</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-for']</td>
+ * <td>140.211.11.130, untrusted-proxy, proxy1</td>
+ * <td>140.211.11.130</td>
+ * </tr>
+ * <tr>
+ * <td>request.header['x-forwarded-by']</td>
+ * <td>null</td>
+ * <td>proxy1</td>
+ * </tr>
+ * </table>
+ * <p>
+ * Note : <code>x-forwarded-by</code> holds the trusted proxy <code>proxy1</code>. <code>x-forwarded-by</code> holds
+ * <code>140.211.11.130</code> because <code>untrusted-proxy</code> is not trusted and thus, we cannot trust that
+ * <code>untrusted-proxy</code> is the actual remote ip. <code>request.remoteAddr</code> is <code>untrusted-proxy</code> that is an IP
+ * verified by <code>proxy1</code>.
+ * </p>
+ * <hr>
+ */
+public class RemoteIpFilter extends GenericFilter {
+
+    private static final long serialVersionUID = 1L;
+
+    public static class XForwardedRequest extends HttpServletRequestWrapper {
+
+        protected final Map<String, List<String>> headers;
+
+        protected String localName;
+
+        protected int localPort;
+
+        protected String remoteAddr;
+
+        protected String remoteHost;
+
+        protected String scheme;
+
+        protected boolean secure;
+
+        protected String serverName;
+
+        protected int serverPort;
+
+        public XForwardedRequest(HttpServletRequest request) {
+            super(request);
+            this.localName = request.getLocalName();
+            this.localPort = request.getLocalPort();
+            this.remoteAddr = request.getRemoteAddr();
+            this.remoteHost = request.getRemoteHost();
+            this.scheme = request.getScheme();
+            this.secure = request.isSecure();
+            this.serverName = request.getServerName();
+            this.serverPort = request.getServerPort();
+
+            headers = new HashMap<>();
+            for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) {
+                String header = headerNames.nextElement();
+                headers.put(header, Collections.list(request.getHeaders(header)));
+            }
+        }
+
+        @Override
+        public long getDateHeader(String name) {
+            String value = getHeader(name);
+            if (value == null) {
+                return -1;
+            }
+            long date = FastHttpDateFormat.parseDate(value);
+            if (date == -1) {
+                throw new IllegalArgumentException(value);
+            }
+            return date;
+        }
+
+        @Override
+        public String getHeader(String name) {
+            Map.Entry<String, List<String>> header = getHeaderEntry(name);
+            if (header == null || header.getValue() == null || header.getValue().isEmpty()) {
+                return null;
+            }
+            return header.getValue().get(0);
+        }
+
+        protected Map.Entry<String, List<String>> getHeaderEntry(String name) {
+            for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+                if (entry.getKey().equalsIgnoreCase(name)) {
+                    return entry;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public Enumeration<String> getHeaderNames() {
+            return Collections.enumeration(headers.keySet());
+        }
+
+        @Override
+        public Enumeration<String> getHeaders(String name) {
+            Map.Entry<String, List<String>> header = getHeaderEntry(name);
+            if (header == null || header.getValue() == null) {
+                return Collections.enumeration(Collections.<String>emptyList());
+            }
+            return Collections.enumeration(header.getValue());
+        }
+
+        @Override
+        public int getIntHeader(String name) {
+            String value = getHeader(name);
+            if (value == null) {
+                return -1;
+            }
+            return Integer.parseInt(value);
+        }
+
+        @Override
+        public String getLocalName() {
+            return localName;
+        }
+
+        @Override
+        public int getLocalPort() {
+            return localPort;
+        }
+
+        @Override
+        public String getRemoteAddr() {
+            return this.remoteAddr;
+        }
+
+        @Override
+        public String getRemoteHost() {
+            return this.remoteHost;
+        }
+
+        @Override
+        public String getScheme() {
+            return scheme;
+        }
+
+        @Override
+        public String getServerName() {
+            return serverName;
+        }
+
+        @Override
+        public int getServerPort() {
+            return serverPort;
+        }
+
+        @Override
+        public boolean isSecure() {
+            return secure;
+        }
+
+        public void removeHeader(String name) {
+            Map.Entry<String, List<String>> header = getHeaderEntry(name);
+            if (header != null) {
+                headers.remove(header.getKey());
+            }
+        }
+
+        public void setHeader(String name, String value) {
+            List<String> values = Collections.singletonList(value);
+            Map.Entry<String, List<String>> header = getHeaderEntry(name);
+            if (header == null) {
+                headers.put(name, values);
+            } else {
+                header.setValue(values);
+            }
+
+        }
+
+        public void setLocalName(String localName) {
+            this.localName = localName;
+        }
+
+        public void setLocalPort(int localPort) {
+            this.localPort = localPort;
+        }
+
+        public void setRemoteAddr(String remoteAddr) {
+            this.remoteAddr = remoteAddr;
+        }
+
+        public void setRemoteHost(String remoteHost) {
+            this.remoteHost = remoteHost;
+        }
+
+        public void setScheme(String scheme) {
+            this.scheme = scheme;
+        }
+
+        public void setSecure(boolean secure) {
+            this.secure = secure;
+        }
+
+        public void setServerName(String serverName) {
+            this.serverName = serverName;
+        }
+
+        public void setServerPort(int serverPort) {
+            this.serverPort = serverPort;
+        }
+
+        @Override
+        public StringBuffer getRequestURL() {
+            return RequestUtil.getRequestURL(this);
+        }
+
+        @Override
+        public PushBuilder newPushBuilder() {
+            ServletRequest current = getRequest();
+            while (current instanceof ServletRequestWrapper) {
+                current = ((ServletRequestWrapper) current).getRequest();
+            }
+            if (current instanceof RequestFacade) {
+                return ((RequestFacade) current).newPushBuilder(this);
+            } else {
+                return null;
+            }
+        }
+    }
+
+
+    /**
+     * {@link Pattern} for a comma delimited string that support whitespace characters
+     */
+    private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*");
+
+    protected static final String HTTP_SERVER_PORT_PARAMETER = "httpServerPort";
+
+    protected static final String HTTPS_SERVER_PORT_PARAMETER = "httpsServerPort";
+
+    protected static final String INTERNAL_PROXIES_PARAMETER = "internalProxies";
+
+    // Log must be non-static as loggers are created per class-loader and this
+    // Filter may be used in multiple class loaders
+    private transient Log log = LogFactory.getLog(RemoteIpFilter.class);
+    protected static final StringManager sm = StringManager.getManager(RemoteIpFilter.class);
+
+    protected static final String PROTOCOL_HEADER_PARAMETER = "protocolHeader";
+
+    protected static final String PROTOCOL_HEADER_HTTPS_VALUE_PARAMETER = "protocolHeaderHttpsValue";
+
+    protected static final String HOST_HEADER_PARAMETER = "hostHeader";
+
+    protected static final String PORT_HEADER_PARAMETER = "portHeader";
+
+    protected static final String CHANGE_LOCAL_NAME_PARAMETER = "changeLocalName";
+
+    protected static final String CHANGE_LOCAL_PORT_PARAMETER = "changeLocalPort";
+
+    protected static final String PROXIES_HEADER_PARAMETER = "proxiesHeader";
+
+    protected static final String REMOTE_IP_HEADER_PARAMETER = "remoteIpHeader";
+
+    protected static final String TRUSTED_PROXIES_PARAMETER = "trustedProxies";
+
+    protected static final String ENABLE_LOOKUPS_PARAMETER = "enableLookups";
+
+    /**
+     * Convert a given comma delimited list of regular expressions into an array of String
+     *
+     * @param commaDelimitedStrings The string to split
+     * @return array of patterns (non <code>null</code>)
+     */
+    protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) {
+        return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0] : commaSeparatedValuesPattern
+                .split(commaDelimitedStrings);
+    }
+
+    /**
+     * Convert a list of strings in a comma delimited string.
+     *
+     * @param stringList List of strings
+     * @return concatenated string
+     *
+     * @deprecated Unused. Will be removed in Tomcat 11 onwards
+     */
+    @Deprecated
+    protected static String listToCommaDelimitedString(List<String> stringList) {
+        if (stringList == null) {
+            return "";
+        }
+        StringBuilder result = new StringBuilder();
+        for (Iterator<String> it = stringList.iterator(); it.hasNext();) {
+            Object element = it.next();
+            if (element != null) {
+                result.append(element);
+                if (it.hasNext()) {
+                    result.append(", ");
+                }
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * @see #setHttpServerPort(int)
+     */
+    private int httpServerPort = 80;
+
+    /**
+     * @see #setHttpsServerPort(int)
+     */
+    private int httpsServerPort = 443;
+
+    /**
+     * @see #setInternalProxies(String)
+     */
+    private Pattern internalProxies = Pattern.compile(
+            "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|" +
+                    "0:0:0:0:0:0:0:1|::1");
+
+    /**
+     * @see #setProtocolHeader(String)
+     */
+    private String protocolHeader = "X-Forwarded-Proto";
+
+    private String protocolHeaderHttpsValue = "https";
+
+    private String hostHeader = null;
+
+    private boolean changeLocalName = false;
+
+    private String portHeader = null;
+
+    private boolean changeLocalPort = false;
+
+    /**
+     * @see #setProxiesHeader(String)
+     */
+    private String proxiesHeader = "X-Forwarded-By";
+
+    /**
+     * @see #setRemoteIpHeader(String)
+     */
+    private String remoteIpHeader = "X-Forwarded-For";
+
+    /**
+     * @see #setRequestAttributesEnabled(boolean)
+     */
+    private boolean requestAttributesEnabled = true;
+
+    /**
+     * @see #setTrustedProxies(String)
+     */
+    private Pattern trustedProxies = null;
+
+    private boolean enableLookups;
+
+    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+
+        boolean isInternal = internalProxies != null &&
+                internalProxies.matcher(request.getRemoteAddr()).matches();
+
+        if (isInternal || (trustedProxies != null &&
+                trustedProxies.matcher(request.getRemoteAddr()).matches())) {
+            String remoteIp = null;
+            Deque<String> proxiesHeaderValue = new ArrayDeque<>();
+            StringBuilder concatRemoteIpHeaderValue = new StringBuilder();
+
+            for (Enumeration<String> e = request.getHeaders(remoteIpHeader); e.hasMoreElements();) {
+                if (concatRemoteIpHeaderValue.length() > 0) {
+                    concatRemoteIpHeaderValue.append(", ");
+                }
+
+                concatRemoteIpHeaderValue.append(e.nextElement());
+            }
+
+            String[] remoteIpHeaderValue = commaDelimitedListToStringArray(concatRemoteIpHeaderValue.toString());
+            int idx;
+            if (!isInternal) {
+                proxiesHeaderValue.addFirst(request.getRemoteAddr());
+            }
+            // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain
+            for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) {
+                String currentRemoteIp = remoteIpHeaderValue[idx];
+                remoteIp = currentRemoteIp;
+                if (internalProxies !=null && internalProxies.matcher(currentRemoteIp).matches()) {
+                    // do nothing, internalProxies IPs are not appended to the
+                } else if (trustedProxies != null &&
+                        trustedProxies.matcher(currentRemoteIp).matches()) {
+                    proxiesHeaderValue.addFirst(currentRemoteIp);
+                } else {
+                    idx--; // decrement idx because break statement doesn't do it
+                    break;
+                }
+            }
+            // continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader
+            LinkedList<String> newRemoteIpHeaderValue = new LinkedList<>();
+            for (; idx >= 0; idx--) {
+                String currentRemoteIp = remoteIpHeaderValue[idx];
+                newRemoteIpHeaderValue.addFirst(currentRemoteIp);
+            }
+
+            XForwardedRequest xRequest = new XForwardedRequest(request);
+            if (remoteIp != null) {
+
+                xRequest.setRemoteAddr(remoteIp);
+                if (getEnableLookups()) {
+                    // This isn't a lazy lookup but that would be a little more
+                    // invasive - mainly in XForwardedRequest - and if
+                    // enableLookups is true is seems reasonable that the
+                    // hostname will be required so look it up here.
+                    try {
+                        InetAddress inetAddress = InetAddress.getByName(remoteIp);
+                        // We know we need a DNS look up so use getCanonicalHostName()
+                        xRequest.setRemoteHost(inetAddress.getCanonicalHostName());
+                    } catch (UnknownHostException e) {
+                        log.debug(sm.getString("remoteIpFilter.invalidRemoteAddress", remoteIp), e);
+                        xRequest.setRemoteHost(remoteIp);
+                    }
+                } else {
+                    xRequest.setRemoteHost(remoteIp);
+                }
+
+                if (proxiesHeaderValue.size() == 0) {
+                    xRequest.removeHeader(proxiesHeader);
+                } else {
+                    String commaDelimitedListOfProxies = StringUtils.join(proxiesHeaderValue);
+                    xRequest.setHeader(proxiesHeader, commaDelimitedListOfProxies);
+                }
+                if (newRemoteIpHeaderValue.size() == 0) {
+                    xRequest.removeHeader(remoteIpHeader);
+                } else {
+                    String commaDelimitedRemoteIpHeaderValue = StringUtils.join(newRemoteIpHeaderValue);
+                    xRequest.setHeader(remoteIpHeader, commaDelimitedRemoteIpHeaderValue);
+                }
+            }
+
+            if (protocolHeader != null) {
+                String protocolHeaderValue = request.getHeader(protocolHeader);
+                if (protocolHeaderValue == null) {
+                    // Don't modify the secure, scheme and serverPort attributes
+                    // of the request
+                } else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) {
+                    xRequest.setSecure(true);
+                    xRequest.setScheme("https");
+                    setPorts(xRequest, httpsServerPort);
+                } else {
+                    xRequest.setSecure(false);
+                    xRequest.setScheme("http");
+                    setPorts(xRequest, httpServerPort);
+                }
+            }
+
+            if (hostHeader != null) {
+                String hostHeaderValue = request.getHeader(hostHeader);
+                if (hostHeaderValue != null) {
+                    try {
+                        int portIndex = Host.parse(hostHeaderValue);
+                        if (portIndex > -1) {
+                            log.debug(sm.getString("remoteIpFilter.invalidHostWithPort", hostHeaderValue, hostHeader));
+                            hostHeaderValue = hostHeaderValue.substring(0, portIndex);
+                        }
+
+                        xRequest.setServerName(hostHeaderValue);
+                        if (isChangeLocalName()) {
+                            xRequest.setLocalName(hostHeaderValue);
+                        }
+
+                    } catch (IllegalArgumentException iae) {
+                        log.debug(sm.getString("remoteIpFilter.invalidHostHeader", hostHeaderValue, hostHeader));
+                    }
+                }
+            }
+            request.setAttribute(Globals.REQUEST_FORWARDED_ATTRIBUTE, Boolean.TRUE);
+
+            if (log.isDebugEnabled()) {
+                log.debug("Incoming request " + request.getRequestURI() + " with originalRemoteAddr [" + request.getRemoteAddr() +
+                        "], originalRemoteHost=[" + request.getRemoteHost() + "], originalSecure=[" + request.isSecure() +
+                        "], originalScheme=[" + request.getScheme() + "], originalServerName=[" + request.getServerName() +
+                        "], originalServerPort=[" + request.getServerPort() +
+                        "] will be seen as newRemoteAddr=[" + xRequest.getRemoteAddr() +
+                        "], newRemoteHost=[" + xRequest.getRemoteHost() + "], newSecure=[" + xRequest.isSecure() +
+                        "], newScheme=[" + xRequest.getScheme() + "], newServerName=[" + xRequest.getServerName() +
+                        "], newServerPort=[" + xRequest.getServerPort() + "]");
+            }
+            if (requestAttributesEnabled) {
+                request.setAttribute(AccessLog.REMOTE_ADDR_ATTRIBUTE,
+                        xRequest.getRemoteAddr());
+                request.setAttribute(Globals.REMOTE_ADDR_ATTRIBUTE,
+                        xRequest.getRemoteAddr());
+                request.setAttribute(AccessLog.REMOTE_HOST_ATTRIBUTE,
+                        xRequest.getRemoteHost());
+                request.setAttribute(AccessLog.PROTOCOL_ATTRIBUTE,
+                        xRequest.getProtocol());
+                request.setAttribute(AccessLog.SERVER_NAME_ATTRIBUTE,
+                        xRequest.getServerName());
+                request.setAttribute(AccessLog.SERVER_PORT_ATTRIBUTE,
+                        Integer.valueOf(xRequest.getServerPort()));
+            }
+            chain.doFilter(xRequest, response);
+        } else {
+            if (log.isDebugEnabled()) {
+                log.debug("Skip RemoteIpFilter for request " + request.getRequestURI() + " with originalRemoteAddr '"
+                        + request.getRemoteAddr() + "'");
+            }
+            chain.doFilter(request, response);
+        }
+
+    }
+
+    /*
+     * Considers the value to be secure if it exclusively holds forwards for
+     * {@link #protocolHeaderHttpsValue}.
+     */
+    private boolean isForwardedProtoHeaderValueSecure(String protocolHeaderValue) {
+        if (!protocolHeaderValue.contains(",")) {
+            return protocolHeaderHttpsValue.equalsIgnoreCase(protocolHeaderValue);
+        }
+        String[] forwardedProtocols = commaDelimitedListToStringArray(protocolHeaderValue);
+        if (forwardedProtocols.length == 0) {
+            return false;
+        }
+        for (String forwardedProtocol : forwardedProtocols) {
+            if (!protocolHeaderHttpsValue.equalsIgnoreCase(forwardedProtocol)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void setPorts(XForwardedRequest xrequest, int defaultPort) {
+        int port = defaultPort;
+        if (getPortHeader() != null) {
+            String portHeaderValue = xrequest.getHeader(getPortHeader());
+            if (portHeaderValue != null) {
+                try {
+                    port = Integer.parseInt(portHeaderValue);
+                } catch (NumberFormatException nfe) {
+                    log.debug("Invalid port value [" + portHeaderValue +
+                            "] provided in header [" + getPortHeader() + "]");
+                }
+            }
+        }
+        xrequest.setServerPort(port);
+        if (isChangeLocalPort()) {
+            xrequest.setLocalPort(port);
+        }
+    }
+
+    /**
+     * Wrap the incoming <code>request</code> in a {@link XForwardedRequest} if the http header <code>x-forwarded-for</code> is not empty.
+     * {@inheritDoc}
+     */
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+            doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+    public boolean isChangeLocalName() {
+        return changeLocalName;
+    }
+
+    public boolean isChangeLocalPort() {
+        return changeLocalPort;
+    }
+
+    public int getHttpsServerPort() {
+        return httpsServerPort;
+    }
+
+    public Pattern getInternalProxies() {
+        return internalProxies;
+    }
+
+    public String getProtocolHeader() {
+        return protocolHeader;
+    }
+
+    public String getPortHeader() {
+        return portHeader;
+    }
+
+    public String getProtocolHeaderHttpsValue() {
+        return protocolHeaderHttpsValue;
+    }
+
+    public String getProxiesHeader() {
+        return proxiesHeader;
+    }
+
+    public String getRemoteIpHeader() {
+        return remoteIpHeader;
+    }
+
+    /**
+     * @see #setRequestAttributesEnabled(boolean)
+     * @return <code>true</code> if the attributes will be logged, otherwise
+     *         <code>false</code>
+     */
+    public boolean getRequestAttributesEnabled() {
+        return requestAttributesEnabled;
+    }
+
+    public Pattern getTrustedProxies() {
+        return trustedProxies;
+    }
+
+    public boolean getEnableLookups() {
+        return enableLookups;
+    }
+
+    @Override
+    public void init() throws ServletException {
+        if (getInitParameter(INTERNAL_PROXIES_PARAMETER) != null) {
+            setInternalProxies(getInitParameter(INTERNAL_PROXIES_PARAMETER));
+        }
+
+        if (getInitParameter(PROTOCOL_HEADER_PARAMETER) != null) {
+            setProtocolHeader(getInitParameter(PROTOCOL_HEADER_PARAMETER));
+        }
+
+        if (getInitParameter(PROTOCOL_HEADER_HTTPS_VALUE_PARAMETER) != null) {
+            setProtocolHeaderHttpsValue(getInitParameter(PROTOCOL_HEADER_HTTPS_VALUE_PARAMETER));
+        }
+
+        if (getInitParameter(HOST_HEADER_PARAMETER) != null) {
+            setHostHeader(getInitParameter(HOST_HEADER_PARAMETER));
+        }
+
+        if (getInitParameter(PORT_HEADER_PARAMETER) != null) {
+            setPortHeader(getInitParameter(PORT_HEADER_PARAMETER));
+        }
+
+        if (getInitParameter(CHANGE_LOCAL_NAME_PARAMETER) != null) {
+            setChangeLocalName(Boolean.parseBoolean(getInitParameter(CHANGE_LOCAL_NAME_PARAMETER)));
+        }
+
+        if (getInitParameter(CHANGE_LOCAL_PORT_PARAMETER) != null) {
+            setChangeLocalPort(Boolean.parseBoolean(getInitParameter(CHANGE_LOCAL_PORT_PARAMETER)));
+        }
+
+        if (getInitParameter(PROXIES_HEADER_PARAMETER) != null) {
+            setProxiesHeader(getInitParameter(PROXIES_HEADER_PARAMETER));
+        }
+
+        if (getInitParameter(REMOTE_IP_HEADER_PARAMETER) != null) {
+            setRemoteIpHeader(getInitParameter(REMOTE_IP_HEADER_PARAMETER));
+        }
+
+        if (getInitParameter(TRUSTED_PROXIES_PARAMETER) != null) {
+            setTrustedProxies(getInitParameter(TRUSTED_PROXIES_PARAMETER));
+        }
+
+        if (getInitParameter(HTTP_SERVER_PORT_PARAMETER) != null) {
+            try {
+                setHttpServerPort(Integer.parseInt(getInitParameter(HTTP_SERVER_PORT_PARAMETER)));
+            } catch (NumberFormatException e) {
+                throw new NumberFormatException(sm.getString("remoteIpFilter.invalidNumber", HTTP_SERVER_PORT_PARAMETER, e.getLocalizedMessage()));
+            }
+        }
+
+        if (getInitParameter(HTTPS_SERVER_PORT_PARAMETER) != null) {
+            try {
+                setHttpsServerPort(Integer.parseInt(getInitParameter(HTTPS_SERVER_PORT_PARAMETER)));
+            } catch (NumberFormatException e) {
+                throw new NumberFormatException(sm.getString("remoteIpFilter.invalidNumber", HTTPS_SERVER_PORT_PARAMETER, e.getLocalizedMessage()));
+            }
+        }
+
+        if (getInitParameter(ENABLE_LOOKUPS_PARAMETER) != null) {
+            setEnableLookups(Boolean.parseBoolean(getInitParameter(ENABLE_LOOKUPS_PARAMETER)));
+        }
+    }
+
+    /**
+     * <p>
+     * If <code>true</code>, the return values for both {@link
+     * ServletRequest#getLocalName()} and {@link ServletRequest#getServerName()}
+     * will be modified by this Filter rather than just
+     * {@link ServletRequest#getServerName()}.
+     * </p>
+     * <p>
+     * Default value : <code>false</code>
+     * </p>
+     * @param changeLocalName The new flag value
+     */
+    public void setChangeLocalName(boolean changeLocalName) {
+        this.changeLocalName = changeLocalName;
+    }
+
+    /**
+     * <p>
+     * If <code>true</code>, the return values for both {@link
+     * ServletRequest#getLocalPort()} and {@link ServletRequest#getServerPort()}
+     * will be modified by this Filter rather than just
+     * {@link ServletRequest#getServerPort()}.
+     * </p>
+     * <p>
+     * Default value : <code>false</code>
+     * </p>
+     * @param changeLocalPort The new flag value
+     */
+    public void setChangeLocalPort(boolean changeLocalPort) {
+        this.changeLocalPort = changeLocalPort;
+    }
+
+    /**
+     * <p>
+     * Server Port value if the {@link #protocolHeader} indicates HTTP (i.e. {@link #protocolHeader} is not null and
+     * has a value different of {@link #protocolHeaderHttpsValue}).
+     * </p>
+     * <p>
+     * Default value : 80
+     * </p>
+     * @param httpServerPort The server port to use
+     */
+    public void setHttpServerPort(int httpServerPort) {
+        this.httpServerPort = httpServerPort;
+    }
+
+    /**
+     * <p>
+     * Server Port value if the {@link #protocolHeader} indicates HTTPS
+     * </p>
+     * <p>
+     * Default value : 443
+     * </p>
+     * @param httpsServerPort The server port to use
+     */
+    public void setHttpsServerPort(int httpsServerPort) {
+        this.httpsServerPort = httpsServerPort;
+    }
+
+    /**
+     * <p>
+     * Regular expression that defines the internal proxies.
+     * </p>
+     * <p>
+     * Default value : 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254.\d{1,3}.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1
+     * </p>
+     * @param internalProxies The regexp
+     */
+    public void setInternalProxies(String internalProxies) {
+        if (internalProxies == null || internalProxies.length() == 0) {
+            this.internalProxies = null;
+        } else {
+            this.internalProxies = Pattern.compile(internalProxies);
+        }
+    }
+
+    /**
+     * <p>
+     * Header that holds the incoming host, usually named
+     * <code>X-Forwarded-Host</code>.
+     * </p>
+     * <p>
+     * Default value : <code>null</code>
+     * </p>
+     * @param hostHeader The header name
+     */
+    public void setHostHeader(String hostHeader) {
+        this.hostHeader = hostHeader;
+    }
+
+    /**
+     * <p>
+     * Header that holds the incoming port, usually named
+     * <code>X-Forwarded-Port</code>. If <code>null</code>,
+     * {@link #httpServerPort} or {@link #httpsServerPort} will be used.
+     * </p>
+     * <p>
+     * Default value : <code>null</code>
+     * </p>
+     * @param portHeader The header name
+     */
+    public void setPortHeader(String portHeader) {
+        this.portHeader = portHeader;
+    }
+
+    /**
+     * <p>
+     * Header that holds the incoming protocol, usually named <code>X-Forwarded-Proto</code>. If <code>null</code>, request.scheme and
+     * request.secure will not be modified.
+     * </p>
+     * <p>
+     * Default value : <code>null</code>
+     * </p>
+     * @param protocolHeader The header name
+     */
+    public void setProtocolHeader(String protocolHeader) {
+        this.protocolHeader = protocolHeader;
+    }
+
+    /**
+     * <p>
+     * Case insensitive value of the protocol header to indicate that the incoming http request uses HTTPS.
+     * </p>
+     * <p>
+     * Default value : <code>https</code>
+     * </p>
+     * @param protocolHeaderHttpsValue The header value
+     */
+    public void setProtocolHeaderHttpsValue(String protocolHeaderHttpsValue) {
+        this.protocolHeaderHttpsValue = protocolHeaderHttpsValue;
+    }
+
+    /**
+     * <p>
+     * The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the intermediate client IP
+     * addresses trusted to resolve the actual remote IP. Note that intermediate RemoteIPTrustedProxy addresses are recorded in this header,
+     * while any intermediate RemoteIPInternalProxy addresses are discarded.
+     * </p>
+     * <p>
+     * Name of the http header that holds the list of trusted proxies that has been traversed by the http request.
+     * </p>
+     * <p>
+     * The value of this header can be comma delimited.
+     * </p>
+     * <p>
+     * Default value : <code>X-Forwarded-By</code>
+     * </p>
+     * @param proxiesHeader The header name
+     */
+    public void setProxiesHeader(String proxiesHeader) {
+        this.proxiesHeader = proxiesHeader;
+    }
+
+    /**
+     * <p>
+     * Name of the http header from which the remote ip is extracted.
+     * </p>
+     * <p>
+     * The value of this header can be comma delimited.
+     * </p>
+     * <p>
+     * Default value : <code>X-Forwarded-For</code>
+     * </p>
+     * @param remoteIpHeader The header name
+     */
+    public void setRemoteIpHeader(String remoteIpHeader) {
+        this.remoteIpHeader = remoteIpHeader;
+    }
+
+    /**
+     * Should this filter set request attributes for IP address, Hostname,
+     * protocol and port used for the request? This are typically used in
+     * conjunction with an {@link AccessLog} which will otherwise log the
+     * original values. Default is <code>true</code>.
+     *
+     * The attributes set are:
+     * <ul>
+     * <li>org.apache.catalina.AccessLog.RemoteAddr</li>
+     * <li>org.apache.catalina.AccessLog.RemoteHost</li>
+     * <li>org.apache.catalina.AccessLog.Protocol</li>
+     * <li>org.apache.catalina.AccessLog.ServerPort</li>
+     * <li>org.apache.tomcat.remoteAddr</li>
+     * </ul>
+     *
+     * @param requestAttributesEnabled  <code>true</code> causes the attributes
+     *                                  to be set, <code>false</code> disables
+     *                                  the setting of the attributes.
+     */
+    public void setRequestAttributesEnabled(boolean requestAttributesEnabled) {
+        this.requestAttributesEnabled = requestAttributesEnabled;
+    }
+
+    /**
+     * <p>
+     * Regular expression defining proxies that are trusted when they appear in
+     * the {@link #remoteIpHeader} header.
+     * </p>
+     * <p>
+     * Default value : empty list, no external proxy is trusted.
+     * </p>
+     * @param trustedProxies The trusted proxies regexp
+     */
+    public void setTrustedProxies(String trustedProxies) {
+        if (trustedProxies == null || trustedProxies.length() == 0) {
+            this.trustedProxies = null;
+        } else {
+            this.trustedProxies = Pattern.compile(trustedProxies);
+        }
+    }
+
+    public void setEnableLookups(boolean enableLookups) {
+        this.enableLookups = enableLookups;
+    }
+
+    /*
+     * Log objects are not Serializable but this Filter is because it extends
+     * GenericFilter. Tomcat won't serialize a Filter but in case something else
+     * does...
+     */
+    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
+        ois.defaultReadObject();
+        log = LogFactory.getLog(RemoteIpFilter.class);
+    }
+}


[tomee] 04/04: Patches Tomcat 10.0.27 for CVE-2023-28708 by applying the changeset from https://github.com/apache/tomcat/commit/f509bbf31fc00abe3d9f25ebfabca5e05173da5b

Posted by rz...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch tomee-9.x-cve-patches
in repository https://gitbox.apache.org/repos/asf/tomee.git

commit 1f23bdf62ed2f9f5c151743961b78b15c359ecb0
Author: Richard Zowalla <ri...@hs-heilbronn.de>
AuthorDate: Tue Apr 18 13:34:29 2023 +0200

    Patches Tomcat 10.0.27 for CVE-2023-28708 by applying the changeset from https://github.com/apache/tomcat/commit/f509bbf31fc00abe3d9f25ebfabca5e05173da5b
---
 .../src/patch/java/org/apache/catalina/Globals.java        |  7 +++++++
 .../patch/java/org/apache/catalina/connector/Request.java  | 14 ++++++++++++++
 .../java/org/apache/catalina/filters/RemoteIpFilter.java   |  7 +------
 3 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java
index 916dd38e1c..c56a177d38 100644
--- a/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/Globals.java
@@ -111,6 +111,13 @@ public final class Globals {
     public static final String SENDFILE_SUPPORTED_ATTR = org.apache.coyote.Constants.SENDFILE_SUPPORTED_ATTR;
 
 
+    /**
+     * The request attribute that is set to the value of {@code Boolean.TRUE}
+     * if {@link org.apache.catalina.filters.RemoteIpFilter} determines
+     * that this request was submitted via a secure channel.
+     */
+    public static final String REMOTE_IP_FILTER_SECURE = "org.apache.catalina.filters.RemoteIpFilter.secure";
+
     /**
      * The request attribute that can be used by a servlet to pass
      * to the connector the name of the file that is to be served
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
index 55e7e677fa..5f0b56e826 100644
--- a/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
@@ -3585,5 +3585,19 @@ public class Request implements HttpServletRequest {
                         // NO-OP
                     }
                 });
+        specialAttributes.put(Globals.REMOTE_IP_FILTER_SECURE,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        return Boolean.valueOf(request.isSecure());
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        if (value instanceof Boolean) {
+                            request.setSecure(((Boolean) value).booleanValue());
+                        }
+                    }
+                });
     }
 }
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java
index 75b5404dc9..732300a359 100644
--- a/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/filters/RemoteIpFilter.java
@@ -584,11 +584,6 @@ public class RemoteIpFilter extends GenericFilter {
             return serverPort;
         }
 
-        @Override
-        public boolean isSecure() {
-            return secure;
-        }
-
         public void removeHeader(String name) {
             Map.Entry<String, List<String>> header = getHeaderEntry(name);
             if (header != null) {
@@ -628,7 +623,7 @@ public class RemoteIpFilter extends GenericFilter {
         }
 
         public void setSecure(boolean secure) {
-            this.secure = secure;
+            super.getRequest().setAttribute(Globals.REMOTE_IP_FILTER_SECURE, Boolean.valueOf(secure));
         }
 
         public void setServerName(String serverName) {


[tomee] 01/04: Before Patch for Commons File Upload

Posted by rz...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch tomee-9.x-cve-patches
in repository https://gitbox.apache.org/repos/asf/tomee.git

commit 5ba5876488db193b004641768ffb732df84be43b
Author: Richard Zowalla <ri...@hs-heilbronn.de>
AuthorDate: Tue Apr 18 13:31:42 2023 +0200

    Before Patch for Commons File Upload
---
 .../org/apache/catalina/connector/Request.java     | 3581 ++++++++++++++++++++
 .../org/apache/tomcat/util/http/Parameters.java    |  520 +++
 .../util/http/fileupload/FileUploadBase.java       |  557 +++
 3 files changed, 4658 insertions(+)

diff --git a/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java b/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
new file mode 100644
index 0000000000..d867cdd554
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/catalina/connector/Request.java
@@ -0,0 +1,3581 @@
+/*
+ * 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.connector;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.naming.NamingException;
+import javax.security.auth.Subject;
+
+import jakarta.servlet.AsyncContext;
+import jakarta.servlet.DispatcherType;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.MultipartConfigElement;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletRequestAttributeEvent;
+import jakarta.servlet.ServletRequestAttributeListener;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.SessionTrackingMode;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletMapping;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import jakarta.servlet.http.HttpUpgradeHandler;
+import jakarta.servlet.http.Part;
+import jakarta.servlet.http.PushBuilder;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.Globals;
+import org.apache.catalina.Host;
+import org.apache.catalina.Manager;
+import org.apache.catalina.Realm;
+import org.apache.catalina.Session;
+import org.apache.catalina.TomcatPrincipal;
+import org.apache.catalina.Wrapper;
+import org.apache.catalina.core.ApplicationFilterChain;
+import org.apache.catalina.core.ApplicationMapping;
+import org.apache.catalina.core.ApplicationPart;
+import org.apache.catalina.core.ApplicationPushBuilder;
+import org.apache.catalina.core.ApplicationSessionCookieConfig;
+import org.apache.catalina.core.AsyncContextImpl;
+import org.apache.catalina.mapper.MappingData;
+import org.apache.catalina.util.ParameterMap;
+import org.apache.catalina.util.RequestUtil;
+import org.apache.catalina.util.TLSUtil;
+import org.apache.catalina.util.URLEncoder;
+import org.apache.coyote.ActionCode;
+import org.apache.coyote.UpgradeToken;
+import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.buf.B2CConverter;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.buf.EncodedSolidusHandling;
+import org.apache.tomcat.util.buf.MessageBytes;
+import org.apache.tomcat.util.buf.StringUtils;
+import org.apache.tomcat.util.buf.UDecoder;
+import org.apache.tomcat.util.http.CookieProcessor;
+import org.apache.tomcat.util.http.FastHttpDateFormat;
+import org.apache.tomcat.util.http.Parameters;
+import org.apache.tomcat.util.http.Parameters.FailReason;
+import org.apache.tomcat.util.http.ServerCookie;
+import org.apache.tomcat.util.http.ServerCookies;
+import org.apache.tomcat.util.http.fileupload.FileItem;
+import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
+import org.apache.tomcat.util.http.fileupload.impl.InvalidContentTypeException;
+import org.apache.tomcat.util.http.fileupload.impl.SizeException;
+import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
+import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;
+import org.apache.tomcat.util.http.parser.AcceptLanguage;
+import org.apache.tomcat.util.http.parser.Upgrade;
+import org.apache.tomcat.util.net.SSLSupport;
+import org.apache.tomcat.util.res.StringManager;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+
+/**
+ * Wrapper object for the Coyote request.
+ *
+ * @author Remy Maucherat
+ * @author Craig R. McClanahan
+ */
+public class Request implements HttpServletRequest {
+
+    private static final String HTTP_UPGRADE_HEADER_NAME = "upgrade";
+
+    private static final Log log = LogFactory.getLog(Request.class);
+
+    /**
+     * Create a new Request object associated with the given Connector.
+     *
+     * @param connector The Connector with which this Request object will always
+     *                  be associated. In normal usage this must be non-null. In
+     *                  some test scenarios, it may be possible to use a null
+     *                  Connector without triggering an NPE.
+     */
+    public Request(Connector connector) {
+        this.connector = connector;
+    }
+
+
+    // ------------------------------------------------------------- Properties
+
+
+    /**
+     * Coyote request.
+     */
+    protected org.apache.coyote.Request coyoteRequest;
+
+    /**
+     * Set the Coyote request.
+     *
+     * @param coyoteRequest The Coyote request
+     */
+    public void setCoyoteRequest(org.apache.coyote.Request coyoteRequest) {
+        this.coyoteRequest = coyoteRequest;
+        inputBuffer.setRequest(coyoteRequest);
+    }
+
+    /**
+     * Get the Coyote request.
+     *
+     * @return the Coyote request object
+     */
+    public org.apache.coyote.Request getCoyoteRequest() {
+        return this.coyoteRequest;
+    }
+
+
+    // ----------------------------------------------------- Variables
+
+    /**
+     * The string manager for this package.
+     */
+    protected static final StringManager sm = StringManager.getManager(Request.class);
+
+
+    /**
+     * The set of cookies associated with this Request.
+     */
+    protected Cookie[] cookies = null;
+
+
+    /**
+     * The default Locale if none are specified.
+     */
+    protected static final Locale defaultLocale = Locale.getDefault();
+
+
+    /**
+     * The attributes associated with this Request, keyed by attribute name.
+     */
+    private final Map<String, Object> attributes = new ConcurrentHashMap<>();
+
+
+    /**
+     * Flag that indicates if SSL attributes have been parsed to improve
+     * performance for applications (usually frameworks) that make multiple
+     * calls to {@link Request#getAttributeNames()}.
+     */
+    protected boolean sslAttributesParsed = false;
+
+
+    /**
+     * The preferred Locales associated with this Request.
+     */
+    protected final ArrayList<Locale> locales = new ArrayList<>();
+
+
+    /**
+     * Internal notes associated with this request by Catalina components
+     * and event listeners.
+     */
+    private final transient HashMap<String, Object> notes = new HashMap<>();
+
+
+    /**
+     * Authentication type.
+     */
+    protected String authType = null;
+
+
+    /**
+     * The current dispatcher type.
+     */
+    protected DispatcherType internalDispatcherType = null;
+
+
+    /**
+     * The associated input buffer.
+     */
+    protected final InputBuffer inputBuffer = new InputBuffer();
+
+
+    /**
+     * ServletInputStream.
+     */
+    protected CoyoteInputStream inputStream =
+            new CoyoteInputStream(inputBuffer);
+
+
+    /**
+     * Reader.
+     */
+    protected CoyoteReader reader = new CoyoteReader(inputBuffer);
+
+
+    /**
+     * Using stream flag.
+     */
+    protected boolean usingInputStream = false;
+
+
+    /**
+     * Using reader flag.
+     */
+    protected boolean usingReader = false;
+
+
+    /**
+     * User principal.
+     */
+    protected Principal userPrincipal = null;
+
+
+    /**
+     * Request parameters parsed flag.
+     */
+    protected boolean parametersParsed = false;
+
+
+    /**
+     * Cookie headers parsed flag. Indicates that the cookie headers have been
+     * parsed into ServerCookies.
+     */
+    protected boolean cookiesParsed = false;
+
+
+    /**
+     * Cookie parsed flag. Indicates that the ServerCookies have been converted
+     * into user facing Cookie objects.
+     */
+    protected boolean cookiesConverted = false;
+
+
+    /**
+     * Secure flag.
+     */
+    protected boolean secure = false;
+
+
+    /**
+     * The Subject associated with the current AccessControlContext
+     */
+    protected transient Subject subject = null;
+
+
+    /**
+     * Post data buffer.
+     */
+    protected static final int CACHED_POST_LEN = 8192;
+    protected byte[] postData = null;
+
+
+    /**
+     * Hash map used in the getParametersMap method.
+     */
+    protected ParameterMap<String, String[]> parameterMap = new ParameterMap<>();
+
+
+    /**
+     * The parts, if any, uploaded with this request.
+     */
+    protected Collection<Part> parts = null;
+
+
+    /**
+     * The exception thrown, if any when parsing the parts.
+     */
+    protected Exception partsParseException = null;
+
+
+    /**
+     * The currently active session for this request.
+     */
+    protected Session session = null;
+
+
+    /**
+     * The current request dispatcher path.
+     */
+    protected Object requestDispatcherPath = null;
+
+
+    /**
+     * Was the requested session ID received in a cookie?
+     */
+    protected boolean requestedSessionCookie = false;
+
+
+    /**
+     * The requested session ID (if any) for this request.
+     */
+    protected String requestedSessionId = null;
+
+
+    /**
+     * Was the requested session ID received in a URL?
+     */
+    protected boolean requestedSessionURL = false;
+
+
+    /**
+     * Was the requested session ID obtained from the SSL session?
+     */
+    protected boolean requestedSessionSSL = false;
+
+
+    /**
+     * Parse locales.
+     */
+    protected boolean localesParsed = false;
+
+
+    /**
+     * Local port
+     */
+    protected int localPort = -1;
+
+    /**
+     * Remote address.
+     */
+    protected String remoteAddr = null;
+
+
+    /**
+     * Connection peer address.
+     */
+    protected String peerAddr = null;
+
+
+    /**
+     * Remote host.
+     */
+    protected String remoteHost = null;
+
+
+    /**
+     * Remote port
+     */
+    protected int remotePort = -1;
+
+    /**
+     * Local address
+     */
+    protected String localAddr = null;
+
+
+    /**
+     * Local address
+     */
+    protected String localName = null;
+
+    /**
+     * AsyncContext
+     */
+    private volatile AsyncContextImpl asyncContext = null;
+
+    protected Boolean asyncSupported = null;
+
+    private HttpServletRequest applicationRequest = null;
+
+
+    // --------------------------------------------------------- Public Methods
+
+    protected void addPathParameter(String name, String value) {
+        coyoteRequest.addPathParameter(name, value);
+    }
+
+    protected String getPathParameter(String name) {
+        return coyoteRequest.getPathParameter(name);
+    }
+
+    public void setAsyncSupported(boolean asyncSupported) {
+        this.asyncSupported = Boolean.valueOf(asyncSupported);
+    }
+
+    /**
+     * Release all object references, and initialize instance variables, in
+     * preparation for reuse of this object.
+     */
+    public void recycle() {
+
+        internalDispatcherType = null;
+        requestDispatcherPath = null;
+
+        authType = null;
+        inputBuffer.recycle();
+        usingInputStream = false;
+        usingReader = false;
+        userPrincipal = null;
+        subject = null;
+        parametersParsed = false;
+        if (parts != null) {
+            for (Part part: parts) {
+                try {
+                    part.delete();
+                } catch (IOException ignored) {
+                    // ApplicationPart.delete() never throws an IOEx
+                }
+            }
+            parts = null;
+        }
+        partsParseException = null;
+        locales.clear();
+        localesParsed = false;
+        secure = false;
+        remoteAddr = null;
+        peerAddr = null;
+        remoteHost = null;
+        remotePort = -1;
+        localPort = -1;
+        localAddr = null;
+        localName = null;
+
+        attributes.clear();
+        sslAttributesParsed = false;
+        notes.clear();
+
+        recycleSessionInfo();
+        recycleCookieInfo(false);
+
+        if (getDiscardFacades()) {
+            parameterMap = new ParameterMap<>();
+        } else {
+            parameterMap.setLocked(false);
+            parameterMap.clear();
+        }
+
+        mappingData.recycle();
+        applicationMapping.recycle();
+
+        applicationRequest = null;
+        if (getDiscardFacades()) {
+            if (facade != null) {
+                facade.clear();
+                facade = null;
+            }
+            if (inputStream != null) {
+                inputStream.clear();
+                inputStream = null;
+            }
+            if (reader != null) {
+                reader.clear();
+                reader = null;
+            }
+        }
+
+        asyncSupported = null;
+        if (asyncContext!=null) {
+            asyncContext.recycle();
+        }
+        asyncContext = null;
+    }
+
+
+    protected void recycleSessionInfo() {
+        if (session != null) {
+            try {
+                session.endAccess();
+            } catch (Throwable t) {
+                ExceptionUtils.handleThrowable(t);
+                log.warn(sm.getString("coyoteRequest.sessionEndAccessFail"), t);
+            }
+        }
+        session = null;
+        requestedSessionCookie = false;
+        requestedSessionId = null;
+        requestedSessionURL = false;
+        requestedSessionSSL = false;
+    }
+
+
+    protected void recycleCookieInfo(boolean recycleCoyote) {
+        cookiesParsed = false;
+        cookiesConverted = false;
+        cookies = null;
+        if (recycleCoyote) {
+            getCoyoteRequest().getCookies().recycle();
+        }
+    }
+
+
+    // -------------------------------------------------------- Request Methods
+
+    /**
+     * Associated Catalina connector.
+     */
+    protected final Connector connector;
+
+    /**
+     * @return the Connector through which this Request was received.
+     */
+    public Connector getConnector() {
+        return this.connector;
+    }
+
+
+    /**
+     * Return the Context within which this Request is being processed.
+     * <p>
+     * This is available as soon as the appropriate Context is identified.
+     * Note that availability of a Context allows <code>getContextPath()</code>
+     * to return a value, and thus enables parsing of the request URI.
+     *
+     * @return the Context mapped with the request
+     */
+    public Context getContext() {
+        return mappingData.context;
+    }
+
+
+    /**
+     * Get the recycling strategy of the facade objects.
+     * @return the value of the flag as set on the connector, or
+     *   <code>true</code> if no connector is associated with this request
+     */
+    public boolean getDiscardFacades() {
+        return (connector == null) ? true : connector.getDiscardFacades();
+    }
+
+
+    /**
+     * Filter chain associated with the request.
+     */
+    protected FilterChain filterChain = null;
+
+    /**
+     * Get filter chain associated with the request.
+     *
+     * @return the associated filter chain
+     */
+    public FilterChain getFilterChain() {
+        return this.filterChain;
+    }
+
+    /**
+     * Set filter chain associated with the request.
+     *
+     * @param filterChain new filter chain
+     */
+    public void setFilterChain(FilterChain filterChain) {
+        this.filterChain = filterChain;
+    }
+
+
+    /**
+     * @return the Host within which this Request is being processed.
+     */
+    public Host getHost() {
+        return mappingData.host;
+    }
+
+
+    /**
+     * Mapping data.
+     */
+    protected final MappingData mappingData = new MappingData();
+    private final ApplicationMapping applicationMapping = new ApplicationMapping(mappingData);
+
+    /**
+     * @return mapping data.
+     */
+    public MappingData getMappingData() {
+        return mappingData;
+    }
+
+
+    /**
+     * The facade associated with this request.
+     */
+    protected RequestFacade facade = null;
+
+
+    /**
+     * @return the <code>ServletRequest</code> for which this object
+     * is the facade.  This method must be implemented by a subclass.
+     */
+    public HttpServletRequest getRequest() {
+        if (facade == null) {
+            facade = new RequestFacade(this);
+        }
+        if (applicationRequest == null) {
+            applicationRequest = facade;
+        }
+        return applicationRequest;
+    }
+
+
+    /**
+     * Set a wrapped HttpServletRequest to pass to the application. Components
+     * wishing to wrap the request should obtain the request via
+     * {@link #getRequest()}, wrap it and then call this method with the
+     * wrapped request.
+     *
+     * @param applicationRequest The wrapped request to pass to the application
+     */
+    public void setRequest(HttpServletRequest applicationRequest) {
+        // Check the wrapper wraps this request
+        ServletRequest r = applicationRequest;
+        while (r instanceof HttpServletRequestWrapper) {
+            r = ((HttpServletRequestWrapper) r).getRequest();
+        }
+        if (r != facade) {
+            throw new IllegalArgumentException(sm.getString("request.illegalWrap"));
+        }
+        this.applicationRequest = applicationRequest;
+    }
+
+
+    /**
+     * The response with which this request is associated.
+     */
+    protected org.apache.catalina.connector.Response response = null;
+
+    /**
+     * @return the Response with which this Request is associated.
+     */
+    public org.apache.catalina.connector.Response getResponse() {
+        return this.response;
+    }
+
+    /**
+     * Set the Response with which this Request is associated.
+     *
+     * @param response The new associated response
+     */
+    public void setResponse(org.apache.catalina.connector.Response response) {
+        this.response = response;
+    }
+
+    /**
+     * @return the input stream associated with this Request.
+     */
+    public InputStream getStream() {
+        if (inputStream == null) {
+            inputStream = new CoyoteInputStream(inputBuffer);
+        }
+        return inputStream;
+    }
+
+    /**
+     * URI byte to char converter.
+     */
+    protected B2CConverter URIConverter = null;
+
+    /**
+     * @return the URI converter.
+     */
+    protected B2CConverter getURIConverter() {
+        return URIConverter;
+    }
+
+    /**
+     * Set the URI converter.
+     *
+     * @param URIConverter the new URI converter
+     */
+    protected void setURIConverter(B2CConverter URIConverter) {
+        this.URIConverter = URIConverter;
+    }
+
+
+    /**
+     * @return the Wrapper within which this Request is being processed.
+     */
+    public Wrapper getWrapper() {
+        return mappingData.wrapper;
+    }
+
+
+    // ------------------------------------------------- Request Public Methods
+
+    /**
+     * Create and return a ServletInputStream to read the content
+     * associated with this Request.
+     *
+     * @return the created input stream
+     * @exception IOException if an input/output error occurs
+     */
+    public ServletInputStream createInputStream()
+            throws IOException {
+        if (inputStream == null) {
+            inputStream = new CoyoteInputStream(inputBuffer);
+        }
+        return inputStream;
+    }
+
+
+    /**
+     * Perform whatever actions are required to flush and close the input
+     * stream or reader, in a single operation.
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    public void finishRequest() throws IOException {
+        if (response.getStatus() == HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE) {
+            checkSwallowInput();
+        }
+    }
+
+
+    /**
+     * @return the object bound with the specified name to the internal notes
+     * for this request, or <code>null</code> if no such binding exists.
+     *
+     * @param name Name of the note to be returned
+     */
+    public Object getNote(String name) {
+        return notes.get(name);
+    }
+
+
+    /**
+     * Remove any object bound to the specified name in the internal notes
+     * for this request.
+     *
+     * @param name Name of the note to be removed
+     */
+    public void removeNote(String name) {
+        notes.remove(name);
+    }
+
+
+    /**
+     * Set the port number of the server to process this request.
+     *
+     * @param port The server port
+     */
+    public void setLocalPort(int port) {
+        localPort = port;
+    }
+
+    /**
+     * Bind an object to a specified name in the internal notes associated
+     * with this request, replacing any existing binding for this name.
+     *
+     * @param name Name to which the object should be bound
+     * @param value Object to be bound to the specified name
+     */
+    public void setNote(String name, Object value) {
+        notes.put(name, value);
+    }
+
+
+    /**
+     * Set the IP address of the remote client associated with this Request.
+     *
+     * @param remoteAddr The remote IP address
+     */
+    public void setRemoteAddr(String remoteAddr) {
+        this.remoteAddr = remoteAddr;
+    }
+
+
+    /**
+     * Set the fully qualified name of the remote client associated with this
+     * Request.
+     *
+     * @param remoteHost The remote host name
+     */
+    public void setRemoteHost(String remoteHost) {
+        this.remoteHost = remoteHost;
+    }
+
+
+    /**
+     * Set the value to be returned by <code>isSecure()</code>
+     * for this Request.
+     *
+     * @param secure The new isSecure value
+     */
+    public void setSecure(boolean secure) {
+        this.secure = secure;
+    }
+
+
+    /**
+     * Set the port number of the server to process this request.
+     *
+     * @param port The server port
+     */
+    public void setServerPort(int port) {
+        coyoteRequest.setServerPort(port);
+    }
+
+
+    // ------------------------------------------------- ServletRequest Methods
+
+    /**
+     * @return the specified request attribute if it exists; otherwise, return
+     * <code>null</code>.
+     *
+     * @param name Name of the request attribute to return
+     */
+    @Override
+    public Object getAttribute(String name) {
+        // Special attributes
+        SpecialAttributeAdapter adapter = specialAttributes.get(name);
+        if (adapter != null) {
+            return adapter.get(this, name);
+        }
+
+        Object attr = attributes.get(name);
+
+        if (attr != null) {
+            return attr;
+        }
+
+        attr = coyoteRequest.getAttribute(name);
+        if (attr != null) {
+            return attr;
+        }
+        if (!sslAttributesParsed && TLSUtil.isTLSRequestAttribute(name)) {
+            coyoteRequest.action(ActionCode.REQ_SSL_ATTRIBUTE, coyoteRequest);
+            attr = coyoteRequest.getAttribute(Globals.CERTIFICATES_ATTR);
+            if (attr != null) {
+                attributes.put(Globals.CERTIFICATES_ATTR, attr);
+            }
+            attr = coyoteRequest.getAttribute(Globals.CIPHER_SUITE_ATTR);
+            if (attr != null) {
+                attributes.put(Globals.CIPHER_SUITE_ATTR, attr);
+            }
+            attr = coyoteRequest.getAttribute(Globals.KEY_SIZE_ATTR);
+            if (attr != null) {
+                attributes.put(Globals.KEY_SIZE_ATTR, attr);
+            }
+            attr = coyoteRequest.getAttribute(Globals.SSL_SESSION_ID_ATTR);
+            if (attr != null) {
+                attributes.put(Globals.SSL_SESSION_ID_ATTR, attr);
+            }
+            attr = coyoteRequest.getAttribute(Globals.SSL_SESSION_MGR_ATTR);
+            if (attr != null) {
+                attributes.put(Globals.SSL_SESSION_MGR_ATTR, attr);
+            }
+            attr = coyoteRequest.getAttribute(SSLSupport.PROTOCOL_VERSION_KEY);
+            if (attr != null) {
+                attributes.put(SSLSupport.PROTOCOL_VERSION_KEY, attr);
+            }
+            attr = coyoteRequest.getAttribute(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY);
+            if (attr != null) {
+                attributes.put(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY, attr);
+            }
+            attr = coyoteRequest.getAttribute(SSLSupport.REQUESTED_CIPHERS_KEY);
+            if (attr != null) {
+                attributes.put(SSLSupport.REQUESTED_CIPHERS_KEY, attr);
+            }
+            attr = attributes.get(name);
+            sslAttributesParsed = true;
+        }
+        return attr;
+    }
+
+
+    @Override
+    public long getContentLengthLong() {
+        return coyoteRequest.getContentLengthLong();
+    }
+
+
+    /**
+     * 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 returned 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#ASYNC_SUPPORTED_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#SSL_SESSION_MGR_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>{@link Globals#SENDFILE_SUPPORTED_ATTR}</li>
+     * </ul>
+     * Connector implementations may return some, all or none of these
+     * attributes and may also support additional attributes.
+     *
+     * @return the attribute names enumeration
+     */
+    @Override
+    public Enumeration<String> getAttributeNames() {
+        if (isSecure() && !sslAttributesParsed) {
+            getAttribute(Globals.CERTIFICATES_ATTR);
+        }
+        // Take a copy to prevent ConcurrentModificationExceptions if used to
+        // remove attributes
+        Set<String> names = new HashSet<>(attributes.keySet());
+        return Collections.enumeration(names);
+    }
+
+
+    /**
+     * @return the character encoding for this Request.
+     */
+    @Override
+    public String getCharacterEncoding() {
+        String characterEncoding = coyoteRequest.getCharacterEncoding();
+        if (characterEncoding != null) {
+            return characterEncoding;
+        }
+
+        Context context = getContext();
+        if (context != null) {
+            return context.getRequestCharacterEncoding();
+        }
+
+        return null;
+    }
+
+
+    private Charset getCharset() {
+        Charset charset = null;
+        try {
+            charset = coyoteRequest.getCharset();
+        } catch (UnsupportedEncodingException e) {
+            // Ignore
+        }
+        if (charset != null) {
+            return charset;
+        }
+
+        Context context = getContext();
+        if (context != null) {
+            String encoding = context.getRequestCharacterEncoding();
+            if (encoding != null) {
+                try {
+                    return B2CConverter.getCharset(encoding);
+                } catch (UnsupportedEncodingException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        return org.apache.coyote.Constants.DEFAULT_BODY_CHARSET;
+    }
+
+
+    /**
+     * @return the content length for this Request.
+     */
+    @Override
+    public int getContentLength() {
+        return coyoteRequest.getContentLength();
+    }
+
+
+    /**
+     * @return the content type for this Request.
+     */
+    @Override
+    public String getContentType() {
+        return coyoteRequest.getContentType();
+    }
+
+
+    /**
+     * Set the content type for this Request.
+     *
+     * @param contentType The content type
+     */
+    public void setContentType(String contentType) {
+        coyoteRequest.setContentType(contentType);
+    }
+
+
+    /**
+     * @return the servlet input stream for this Request.  The default
+     * implementation returns a servlet input stream created by
+     * <code>createInputStream()</code>.
+     *
+     * @exception IllegalStateException if <code>getReader()</code> has
+     *  already been called for this request
+     * @exception IOException if an input/output error occurs
+     */
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+
+        if (usingReader) {
+            throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
+        }
+
+        usingInputStream = true;
+        if (inputStream == null) {
+            inputStream = new CoyoteInputStream(inputBuffer);
+        }
+        return inputStream;
+
+    }
+
+
+    /**
+     * @return the preferred Locale that the client will accept content in,
+     * based on the value for the first <code>Accept-Language</code> header
+     * that was encountered.  If the request did not specify a preferred
+     * language, the server's default Locale is returned.
+     */
+    @Override
+    public Locale getLocale() {
+
+        if (!localesParsed) {
+            parseLocales();
+        }
+
+        if (locales.size() > 0) {
+            return locales.get(0);
+        }
+
+        return defaultLocale;
+    }
+
+
+    /**
+     * @return the set of preferred Locales that the client will accept
+     * content in, based on the values for any <code>Accept-Language</code>
+     * headers that were encountered.  If the request did not specify a
+     * preferred language, the server's default Locale is returned.
+     */
+    @Override
+    public Enumeration<Locale> getLocales() {
+
+        if (!localesParsed) {
+            parseLocales();
+        }
+
+        if (locales.size() > 0) {
+            return Collections.enumeration(locales);
+        }
+        ArrayList<Locale> results = new ArrayList<>();
+        results.add(defaultLocale);
+        return Collections.enumeration(results);
+
+    }
+
+
+    /**
+     * @return the value of the specified request parameter, if any; otherwise,
+     * return <code>null</code>.  If there is more than one value defined,
+     * return only the first one.
+     *
+     * @param name Name of the desired request parameter
+     */
+    @Override
+    public String getParameter(String name) {
+
+        if (!parametersParsed) {
+            parseParameters();
+        }
+
+        return coyoteRequest.getParameters().getParameter(name);
+
+    }
+
+
+
+    /**
+     * Returns a <code>Map</code> of the parameters of this request.
+     * Request parameters are extra information sent with the request.
+     * For HTTP servlets, parameters are contained in the query string
+     * or posted form data.
+     *
+     * @return A <code>Map</code> containing parameter names as keys
+     *  and parameter values as map values.
+     */
+    @Override
+    public Map<String, String[]> getParameterMap() {
+
+        if (parameterMap.isLocked()) {
+            return parameterMap;
+        }
+
+        Enumeration<String> enumeration = getParameterNames();
+        while (enumeration.hasMoreElements()) {
+            String name = enumeration.nextElement();
+            String[] values = getParameterValues(name);
+            parameterMap.put(name, values);
+        }
+
+        parameterMap.setLocked(true);
+
+        return parameterMap;
+
+    }
+
+
+    /**
+     * @return the names of all defined request parameters for this request.
+     */
+    @Override
+    public Enumeration<String> getParameterNames() {
+
+        if (!parametersParsed) {
+            parseParameters();
+        }
+
+        return coyoteRequest.getParameters().getParameterNames();
+
+    }
+
+
+    /**
+     * @return the defined values for the specified request parameter, if any;
+     * otherwise, return <code>null</code>.
+     *
+     * @param name Name of the desired request parameter
+     */
+    @Override
+    public String[] getParameterValues(String name) {
+
+        if (!parametersParsed) {
+            parseParameters();
+        }
+
+        return coyoteRequest.getParameters().getParameterValues(name);
+
+    }
+
+
+    /**
+     * @return the protocol and version used to make this Request.
+     */
+    @Override
+    public String getProtocol() {
+        return coyoteRequest.protocol().toString();
+    }
+
+
+    /**
+     * Read the Reader wrapping the input stream for this Request.  The
+     * default implementation wraps a <code>BufferedReader</code> around the
+     * servlet input stream returned by <code>createInputStream()</code>.
+     *
+     * @return a buffered reader for the request
+     * @exception IllegalStateException if <code>getInputStream()</code>
+     *  has already been called for this request
+     * @exception IOException if an input/output error occurs
+     */
+    @Override
+    public BufferedReader getReader() throws IOException {
+
+        if (usingInputStream) {
+            throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
+        }
+
+        // InputBuffer has no easily accessible reference chain to the Context
+        // to check for a default request character encoding at the Context.
+        // Therefore, if a Context default should be used, it is set explicitly
+        // here. Need to do this before setting usingReader.
+        if (coyoteRequest.getCharacterEncoding() == null) {
+            // Nothing currently set explicitly.
+            // Check the content
+            Context context = getContext();
+            if (context != null) {
+                String enc = context.getRequestCharacterEncoding();
+                if (enc != null) {
+                    // Explicitly set the context default so it is visible to
+                    // InputBuffer when creating the Reader.
+                    setCharacterEncoding(enc);
+                }
+            }
+        }
+
+        usingReader = true;
+
+        inputBuffer.checkConverter();
+        if (reader == null) {
+            reader = new CoyoteReader(inputBuffer);
+        }
+        return reader;
+    }
+
+
+    /**
+     * @return the real path of the specified virtual path.
+     *
+     * @param path Path to be translated
+     *
+     * @deprecated As of version 2.1 of the Java Servlet API, use
+     *  <code>ServletContext.getRealPath()</code>.
+     */
+    @Override
+    @Deprecated
+    public String getRealPath(String path) {
+
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+        ServletContext servletContext = context.getServletContext();
+        if (servletContext == null) {
+            return null;
+        }
+
+        try {
+            return servletContext.getRealPath(path);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+
+    /**
+     * @return the remote IP address making this Request.
+     */
+    @Override
+    public String getRemoteAddr() {
+        if (remoteAddr == null) {
+            coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, coyoteRequest);
+            remoteAddr = coyoteRequest.remoteAddr().toString();
+        }
+        return remoteAddr;
+    }
+
+
+    /**
+     * @return the connection peer IP address making this Request.
+     */
+    public String getPeerAddr() {
+        if (peerAddr == null) {
+            coyoteRequest.action(ActionCode.REQ_PEER_ADDR_ATTRIBUTE, coyoteRequest);
+            peerAddr = coyoteRequest.peerAddr().toString();
+        }
+        return peerAddr;
+    }
+
+
+    /**
+     * @return the remote host name making this Request.
+     */
+    @Override
+    public String getRemoteHost() {
+        if (remoteHost == null) {
+            if (!connector.getEnableLookups()) {
+                remoteHost = getRemoteAddr();
+            } else {
+                coyoteRequest.action(ActionCode.REQ_HOST_ATTRIBUTE, coyoteRequest);
+                remoteHost = coyoteRequest.remoteHost().toString();
+            }
+        }
+        return remoteHost;
+    }
+
+    /**
+     * @return the Internet Protocol (IP) source port of the client
+     * or last proxy that sent the request.
+     */
+    @Override
+    public int getRemotePort(){
+        if (remotePort == -1) {
+            coyoteRequest.action(ActionCode.REQ_REMOTEPORT_ATTRIBUTE, coyoteRequest);
+            remotePort = coyoteRequest.getRemotePort();
+        }
+        return remotePort;
+    }
+
+    /**
+     * @return the host name of the Internet Protocol (IP) interface on
+     * which the request was received.
+     */
+    @Override
+    public String getLocalName(){
+        if (localName == null) {
+            coyoteRequest.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, coyoteRequest);
+            localName = coyoteRequest.localName().toString();
+        }
+        return localName;
+    }
+
+    /**
+     * @return the Internet Protocol (IP) address of the interface on
+     * which the request  was received.
+     */
+    @Override
+    public String getLocalAddr(){
+        if (localAddr == null) {
+            coyoteRequest.action(ActionCode.REQ_LOCAL_ADDR_ATTRIBUTE, coyoteRequest);
+            localAddr = coyoteRequest.localAddr().toString();
+        }
+        return localAddr;
+    }
+
+
+    /**
+     * @return the Internet Protocol (IP) port number of the interface
+     * on which the request was received.
+     */
+    @Override
+    public int getLocalPort(){
+        if (localPort == -1){
+            coyoteRequest.action(ActionCode.REQ_LOCALPORT_ATTRIBUTE, coyoteRequest);
+            localPort = coyoteRequest.getLocalPort();
+        }
+        return localPort;
+    }
+
+    /**
+     * @return a RequestDispatcher that wraps the resource at the specified
+     * path, which may be interpreted as relative to the current request path.
+     *
+     * @param path Path of the resource to be wrapped
+     */
+    @Override
+    public RequestDispatcher getRequestDispatcher(String path) {
+
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+
+        if (path == null) {
+            return null;
+        }
+
+        int fragmentPos = path.indexOf('#');
+        if (fragmentPos > -1) {
+            log.warn(sm.getString("request.fragmentInDispatchPath", path));
+            path = path.substring(0, fragmentPos);
+        }
+
+        // If the path is already context-relative, just pass it through
+        if (path.startsWith("/")) {
+            return context.getServletContext().getRequestDispatcher(path);
+        }
+
+        /*
+         * Relative to what, exactly?
+         *
+         * From the Servlet 4.0 Javadoc:
+         * - The pathname specified may be relative, although it cannot extend
+         *   outside the current servlet context.
+         * - If it is relative, it must be relative against the current servlet
+         *
+         * From Section 9.1 of the spec:
+         * - The servlet container uses information in the request object to
+         *   transform the given relative path against the current servlet to a
+         *   complete path.
+         *
+         * It is undefined whether the requestURI is used or whether servletPath
+         * and pathInfo are used. Given that the RequestURI includes the
+         * contextPath (and extracting that is messy) , using the servletPath and
+         * pathInfo looks to be the more reasonable choice.
+         */
+
+        // Convert a request-relative path to a context-relative one
+        String servletPath = (String) getAttribute(
+                RequestDispatcher.INCLUDE_SERVLET_PATH);
+        if (servletPath == null) {
+            servletPath = getServletPath();
+        }
+
+        // Add the path info, if there is any
+        String pathInfo = getPathInfo();
+        String requestPath = null;
+
+        if (pathInfo == null) {
+            requestPath = servletPath;
+        } else {
+            requestPath = servletPath + pathInfo;
+        }
+
+        int pos = requestPath.lastIndexOf('/');
+        String relative = null;
+        if (context.getDispatchersUseEncodedPaths()) {
+            if (pos >= 0) {
+                relative = URLEncoder.DEFAULT.encode(
+                        requestPath.substring(0, pos + 1), StandardCharsets.UTF_8) + path;
+            } else {
+                relative = URLEncoder.DEFAULT.encode(requestPath, StandardCharsets.UTF_8) + path;
+            }
+        } else {
+            if (pos >= 0) {
+                relative = requestPath.substring(0, pos + 1) + path;
+            } else {
+                relative = requestPath + path;
+            }
+        }
+
+        return context.getServletContext().getRequestDispatcher(relative);
+    }
+
+
+    /**
+     * @return the scheme used to make this Request.
+     */
+    @Override
+    public String getScheme() {
+        return coyoteRequest.scheme().toString();
+    }
+
+
+    /**
+     * @return the server name responding to this Request.
+     */
+    @Override
+    public String getServerName() {
+        return coyoteRequest.serverName().toString();
+    }
+
+
+    /**
+     * @return the server port responding to this Request.
+     */
+    @Override
+    public int getServerPort() {
+        return coyoteRequest.getServerPort();
+    }
+
+
+    /**
+     * @return <code>true</code> if this request was received on a secure connection.
+     */
+    @Override
+    public boolean isSecure() {
+        return secure;
+    }
+
+
+    /**
+     * Remove the specified request attribute if it exists.
+     *
+     * @param name Name of the request attribute to remove
+     */
+    @Override
+    public void removeAttribute(String name) {
+        // Remove the specified attribute
+        // Pass special attributes to the native layer
+        if (name.startsWith("org.apache.tomcat.")) {
+            coyoteRequest.getAttributes().remove(name);
+        }
+
+        boolean found = attributes.containsKey(name);
+        if (found) {
+            Object value = attributes.get(name);
+            attributes.remove(name);
+
+            // Notify interested application event listeners
+            notifyAttributeRemoved(name, value);
+        }
+    }
+
+
+    /**
+     * Set the specified request attribute to the specified value.
+     *
+     * @param name Name of the request attribute to set
+     * @param value The associated value
+     */
+    @Override
+    public void setAttribute(String name, Object value) {
+
+        // Name cannot be null
+        if (name == null) {
+            throw new IllegalArgumentException(sm.getString("coyoteRequest.setAttribute.namenull"));
+        }
+
+        // Null value is the same as removeAttribute()
+        if (value == null) {
+            removeAttribute(name);
+            return;
+        }
+
+        // Special attributes
+        SpecialAttributeAdapter adapter = specialAttributes.get(name);
+        if (adapter != null) {
+            adapter.set(this, name, value);
+            return;
+        }
+
+        // Add or replace the specified attribute
+        // Do the security check before any updates are made
+        if (Globals.IS_SECURITY_ENABLED &&
+                name.equals(Globals.SENDFILE_FILENAME_ATTR)) {
+            // Use the canonical file name to avoid any possible symlink and
+            // relative path issues
+            String canonicalPath;
+            try {
+                canonicalPath = new File(value.toString()).getCanonicalPath();
+            } catch (IOException e) {
+                throw new SecurityException(sm.getString(
+                        "coyoteRequest.sendfileNotCanonical", value), e);
+            }
+            // Sendfile is performed in Tomcat's security context so need to
+            // check if the web app is permitted to access the file while still
+            // in the web app's security context
+            System.getSecurityManager().checkRead(canonicalPath);
+            // Update the value so the canonical path is used
+            value = canonicalPath;
+        }
+
+        Object oldValue = attributes.put(name, value);
+
+        // Pass special attributes to the native layer
+        if (name.startsWith("org.apache.tomcat.")) {
+            coyoteRequest.setAttribute(name, value);
+        }
+
+        // Notify interested application event listeners
+        notifyAttributeAssigned(name, value, oldValue);
+    }
+
+
+    /**
+     * Notify interested listeners that attribute has been assigned a value.
+     *
+     * @param name Attribute name
+     * @param value New attribute value
+     * @param oldValue Old attribute value
+     */
+    private void notifyAttributeAssigned(String name, Object value,
+            Object oldValue) {
+        Context context = getContext();
+        if (context == null) {
+            return;
+        }
+        Object listeners[] = context.getApplicationEventListeners();
+        if ((listeners == null) || (listeners.length == 0)) {
+            return;
+        }
+        boolean replaced = (oldValue != null);
+        ServletRequestAttributeEvent event = null;
+        if (replaced) {
+            event = new ServletRequestAttributeEvent(
+                    context.getServletContext(), getRequest(), name, oldValue);
+        } else {
+            event = new ServletRequestAttributeEvent(
+                    context.getServletContext(), getRequest(), name, value);
+        }
+
+        for (Object o : listeners) {
+            if (!(o instanceof ServletRequestAttributeListener)) {
+                continue;
+            }
+            ServletRequestAttributeListener listener = (ServletRequestAttributeListener) o;
+            try {
+                if (replaced) {
+                    listener.attributeReplaced(event);
+                } else {
+                    listener.attributeAdded(event);
+                }
+            } catch (Throwable t) {
+                ExceptionUtils.handleThrowable(t);
+                // Error valve will pick this exception up and display it to user
+                attributes.put(RequestDispatcher.ERROR_EXCEPTION, t);
+                context.getLogger().error(sm.getString("coyoteRequest.attributeEvent"), t);
+            }
+        }
+    }
+
+
+    /**
+     * Notify interested listeners that attribute has been removed.
+     *
+     * @param name Attribute name
+     * @param value Attribute value
+     */
+    private void notifyAttributeRemoved(String name, Object value) {
+        Context context = getContext();
+        Object listeners[] = context.getApplicationEventListeners();
+        if ((listeners == null) || (listeners.length == 0)) {
+            return;
+        }
+        ServletRequestAttributeEvent event =
+                new ServletRequestAttributeEvent(context.getServletContext(),
+                        getRequest(), name, value);
+        for (Object o : listeners) {
+            if (!(o instanceof ServletRequestAttributeListener)) {
+                continue;
+            }
+            ServletRequestAttributeListener listener = (ServletRequestAttributeListener) o;
+            try {
+                listener.attributeRemoved(event);
+            } catch (Throwable t) {
+                ExceptionUtils.handleThrowable(t);
+                // Error valve will pick this exception up and display it to user
+                attributes.put(RequestDispatcher.ERROR_EXCEPTION, t);
+                context.getLogger().error(sm.getString("coyoteRequest.attributeEvent"), t);
+            }
+        }
+    }
+
+
+    /**
+     * Overrides the name of the character encoding used in the body of
+     * this request.  This method must be called prior to reading request
+     * parameters or reading input using <code>getReader()</code>.
+     *
+     * @param enc The character encoding to be used
+     *
+     * @exception UnsupportedEncodingException if the specified encoding
+     *  is not supported
+     *
+     * @since Servlet 2.3
+     */
+    @Override
+    public void setCharacterEncoding(String enc) throws UnsupportedEncodingException {
+
+        if (usingReader) {
+            return;
+        }
+
+        // Confirm that the encoding name is valid
+        Charset charset = B2CConverter.getCharset(enc);
+
+        // Save the validated encoding
+        coyoteRequest.setCharset(charset);
+    }
+
+
+    @Override
+    public ServletContext getServletContext() {
+        return getContext().getServletContext();
+     }
+
+    @Override
+    public AsyncContext startAsync() {
+        return startAsync(getRequest(),response.getResponse());
+    }
+
+    @Override
+    public AsyncContext startAsync(ServletRequest request,
+            ServletResponse response) {
+        if (!isAsyncSupported()) {
+            IllegalStateException ise =
+                    new IllegalStateException(sm.getString("request.asyncNotSupported"));
+            log.warn(sm.getString("coyoteRequest.noAsync",
+                    StringUtils.join(getNonAsyncClassNames())), ise);
+            throw ise;
+        }
+
+        if (asyncContext == null) {
+            asyncContext = new AsyncContextImpl(this);
+        }
+
+        asyncContext.setStarted(getContext(), request, response,
+                request==getRequest() && response==getResponse().getResponse());
+        asyncContext.setTimeout(getConnector().getAsyncTimeout());
+
+        return asyncContext;
+    }
+
+
+    private Set<String> getNonAsyncClassNames() {
+        Set<String> result = new HashSet<>();
+
+        Wrapper wrapper = getWrapper();
+        if (!wrapper.isAsyncSupported()) {
+            result.add(wrapper.getServletClass());
+        }
+
+        FilterChain filterChain = getFilterChain();
+        if (filterChain instanceof ApplicationFilterChain) {
+            ((ApplicationFilterChain) filterChain).findNonAsyncFilters(result);
+        } else {
+            result.add(sm.getString("coyoteRequest.filterAsyncSupportUnknown"));
+        }
+
+        Container c = wrapper;
+        while (c != null) {
+            c.getPipeline().findNonAsyncValves(result);
+            c = c.getParent();
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean isAsyncStarted() {
+        if (asyncContext == null) {
+            return false;
+        }
+
+        return asyncContext.isStarted();
+    }
+
+    public boolean isAsyncDispatching() {
+        if (asyncContext == null) {
+            return false;
+        }
+
+        AtomicBoolean result = new AtomicBoolean(false);
+        coyoteRequest.action(ActionCode.ASYNC_IS_DISPATCHING, result);
+        return result.get();
+    }
+
+    public boolean isAsyncCompleting() {
+        if (asyncContext == null) {
+            return false;
+        }
+
+        AtomicBoolean result = new AtomicBoolean(false);
+        coyoteRequest.action(ActionCode.ASYNC_IS_COMPLETING, result);
+        return result.get();
+    }
+
+    public boolean isAsync() {
+        if (asyncContext == null) {
+            return false;
+        }
+
+        AtomicBoolean result = new AtomicBoolean(false);
+        coyoteRequest.action(ActionCode.ASYNC_IS_ASYNC, result);
+        return result.get();
+    }
+
+    @Override
+    public boolean isAsyncSupported() {
+        if (this.asyncSupported == null) {
+            return true;
+        }
+
+        return asyncSupported.booleanValue();
+    }
+
+    @Override
+    public AsyncContext getAsyncContext() {
+        if (!isAsyncStarted()) {
+            throw new IllegalStateException(sm.getString("request.notAsync"));
+        }
+        return asyncContext;
+    }
+
+    public AsyncContextImpl getAsyncContextInternal() {
+        return asyncContext;
+    }
+
+    @Override
+    public DispatcherType getDispatcherType() {
+        if (internalDispatcherType == null) {
+            return DispatcherType.REQUEST;
+        }
+
+        return this.internalDispatcherType;
+    }
+
+    // ---------------------------------------------------- HttpRequest Methods
+
+    /**
+     * Add a Cookie to the set of Cookies associated with this Request.
+     *
+     * @param cookie The new cookie
+     */
+    public void addCookie(Cookie cookie) {
+
+        if (!cookiesConverted) {
+            convertCookies();
+        }
+
+        int size = 0;
+        if (cookies != null) {
+            size = cookies.length;
+        }
+
+        Cookie[] newCookies = new Cookie[size + 1];
+        if (cookies != null) {
+            System.arraycopy(cookies, 0, newCookies, 0, size);
+        }
+        newCookies[size] = cookie;
+
+        cookies = newCookies;
+
+    }
+
+
+    /**
+     * Add a Locale to the set of preferred Locales for this Request.  The
+     * first added Locale will be the first one returned by getLocales().
+     *
+     * @param locale The new preferred Locale
+     */
+    public void addLocale(Locale locale) {
+        locales.add(locale);
+    }
+
+
+    /**
+     * Clear the collection of Cookies associated with this Request.
+     */
+    public void clearCookies() {
+        cookiesParsed = true;
+        cookiesConverted = true;
+        cookies = null;
+    }
+
+
+    /**
+     * Clear the collection of Locales associated with this Request.
+     */
+    public void clearLocales() {
+        locales.clear();
+    }
+
+
+    /**
+     * Set the authentication type used for this request, if any; otherwise
+     * set the type to <code>null</code>.  Typical values are "BASIC",
+     * "DIGEST", or "SSL".
+     *
+     * @param type The authentication type used
+     */
+    public void setAuthType(String type) {
+        this.authType = type;
+    }
+
+
+    /**
+     * Set the path information for this Request.  This will normally be called
+     * when the associated Context is mapping the Request to a particular
+     * Wrapper.
+     *
+     * @param path The path information
+     */
+    public void setPathInfo(String path) {
+        mappingData.pathInfo.setString(path);
+    }
+
+
+    /**
+     * Set a flag indicating whether or not the requested session ID for this
+     * request came in through a cookie.  This is normally called by the
+     * HTTP Connector, when it parses the request headers.
+     *
+     * @param flag The new flag
+     */
+    public void setRequestedSessionCookie(boolean flag) {
+
+        this.requestedSessionCookie = flag;
+
+    }
+
+
+    /**
+     * Set the requested session ID for this request.  This is normally called
+     * by the HTTP Connector, when it parses the request headers.
+     *
+     * @param id The new session id
+     */
+    public void setRequestedSessionId(String id) {
+
+        this.requestedSessionId = id;
+
+    }
+
+
+    /**
+     * Set a flag indicating whether or not the requested session ID for this
+     * request came in through a URL.  This is normally called by the
+     * HTTP Connector, when it parses the request headers.
+     *
+     * @param flag The new flag
+     */
+    public void setRequestedSessionURL(boolean flag) {
+
+        this.requestedSessionURL = flag;
+
+    }
+
+
+    /**
+     * Set a flag indicating whether or not the requested session ID for this
+     * request came in through SSL.  This is normally called by the
+     * HTTP Connector, when it parses the request headers.
+     *
+     * @param flag The new flag
+     */
+    public void setRequestedSessionSSL(boolean flag) {
+
+        this.requestedSessionSSL = flag;
+
+    }
+
+
+    /**
+     * Get the decoded request URI.
+     *
+     * @return the URL decoded request URI
+     */
+    public String getDecodedRequestURI() {
+        return coyoteRequest.decodedURI().toString();
+    }
+
+
+    /**
+     * Get the decoded request URI.
+     *
+     * @return the URL decoded request URI
+     */
+    public MessageBytes getDecodedRequestURIMB() {
+        return coyoteRequest.decodedURI();
+    }
+
+
+    /**
+     * Set the Principal who has been authenticated for this Request.  This
+     * value is also used to calculate the value to be returned by the
+     * <code>getRemoteUser()</code> method.
+     *
+     * @param principal The user Principal
+     */
+    public void setUserPrincipal(final Principal principal) {
+        if (Globals.IS_SECURITY_ENABLED && principal != null) {
+            if (subject == null) {
+                final HttpSession session = getSession(false);
+                if (session == null) {
+                    // Cache the subject in the request
+                    subject = newSubject(principal);
+                } else {
+                    // Cache the subject in the request and the session
+                    subject = (Subject) session.getAttribute(Globals.SUBJECT_ATTR);
+                    if (subject == null) {
+                        subject = newSubject(principal);
+                        session.setAttribute(Globals.SUBJECT_ATTR, subject);
+                    } else {
+                        subject.getPrincipals().add(principal);
+                    }
+                }
+            } else {
+                subject.getPrincipals().add(principal);
+            }
+        }
+        userPrincipal = principal;
+    }
+
+
+    private Subject newSubject(final Principal principal) {
+        final Subject result = new Subject();
+        result.getPrincipals().add(principal);
+        return result;
+    }
+
+
+    // --------------------------------------------- HttpServletRequest Methods
+
+    @Override
+    public boolean isTrailerFieldsReady() {
+        return coyoteRequest.isTrailerFieldsReady();
+    }
+
+
+    @Override
+    public Map<String, String> getTrailerFields() {
+        if (!isTrailerFieldsReady()) {
+            throw new IllegalStateException(sm.getString("coyoteRequest.trailersNotReady"));
+        }
+        Map<String, String> result = new HashMap<>(coyoteRequest.getTrailerFields());
+        return result;
+    }
+
+
+    @Override
+    public PushBuilder newPushBuilder() {
+        return newPushBuilder(this);
+    }
+
+
+    public PushBuilder newPushBuilder(HttpServletRequest request) {
+        AtomicBoolean result = new AtomicBoolean();
+        coyoteRequest.action(ActionCode.IS_PUSH_SUPPORTED, result);
+        if (result.get()) {
+            return new ApplicationPushBuilder(this, request);
+        } else {
+            return null;
+        }
+    }
+
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends HttpUpgradeHandler> T upgrade(
+            Class<T> httpUpgradeHandlerClass) throws java.io.IOException, ServletException {
+        T handler;
+        InstanceManager instanceManager = null;
+        try {
+            // Do not go through the instance manager for internal Tomcat classes since they don't
+            // need injection
+            if (InternalHttpUpgradeHandler.class.isAssignableFrom(httpUpgradeHandlerClass)) {
+                handler = httpUpgradeHandlerClass.getConstructor().newInstance();
+            } else {
+                instanceManager = getContext().getInstanceManager();
+                handler = (T) instanceManager.newInstance(httpUpgradeHandlerClass);
+            }
+        } catch (ReflectiveOperationException | NamingException | IllegalArgumentException |
+                SecurityException e) {
+            throw new ServletException(e);
+        }
+        UpgradeToken upgradeToken = new UpgradeToken(handler, getContext(), instanceManager,
+                getUpgradeProtocolName(httpUpgradeHandlerClass));
+
+        coyoteRequest.action(ActionCode.UPGRADE, upgradeToken);
+
+        // Output required by RFC2616. Protocol specific headers should have
+        // already been set.
+        response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS);
+
+        return handler;
+    }
+
+
+    private String getUpgradeProtocolName(Class<? extends HttpUpgradeHandler> httpUpgradeHandlerClass) {
+        // Ideal - the caller has already explicitly set the selected protocol
+        // on the response
+        String result = response.getHeader(HTTP_UPGRADE_HEADER_NAME);
+
+        if (result == null) {
+            // If the request's upgrade header contains a single protocol that
+            // is the protocol that must have been selected
+            List<Upgrade> upgradeProtocols = Upgrade.parse(getHeaders(HTTP_UPGRADE_HEADER_NAME));
+            if (upgradeProtocols != null && upgradeProtocols.size() == 1) {
+                result = upgradeProtocols.get(0).toString();
+            }
+        }
+
+        if (result == null) {
+            // Ugly but use the class name - it is better than nothing
+            result = httpUpgradeHandlerClass.getName();
+        }
+        return result;
+    }
+
+
+    /**
+     * Return the authentication type used for this Request.
+     */
+    @Override
+    public String getAuthType() {
+        return authType;
+    }
+
+
+    /**
+     * Return the portion of the request URI used to select the Context
+     * of the Request. The value returned is not decoded which also implies it
+     * is not normalised.
+     */
+    @Override
+    public String getContextPath() {
+        int lastSlash = mappingData.contextSlashCount;
+        // Special case handling for the root context
+        if (lastSlash == 0) {
+            return "";
+        }
+
+        String canonicalContextPath = getServletContext().getContextPath();
+
+        String uri = getRequestURI();
+        int pos = 0;
+        if (!getContext().getAllowMultipleLeadingForwardSlashInPath()) {
+            // Ensure that the returned value only starts with a single '/'.
+            // This prevents the value being misinterpreted as a protocol-
+            // relative URI if used with sendRedirect().
+            do {
+                pos++;
+            } while (pos < uri.length() && uri.charAt(pos) == '/');
+            pos--;
+            uri = uri.substring(pos);
+        }
+
+        char[] uriChars = uri.toCharArray();
+        // Need at least the number of slashes in the context path
+        while (lastSlash > 0) {
+            pos = nextSlash(uriChars, pos + 1);
+            if (pos == -1) {
+                break;
+            }
+            lastSlash--;
+        }
+        // Now allow for path parameters, normalization and/or encoding.
+        // Essentially, keep extending the candidate path up to the next slash
+        // until the decoded and normalized candidate path (with the path
+        // parameters removed) is the same as the canonical path.
+        String candidate;
+        if (pos == -1) {
+            candidate = uri;
+        } else {
+            candidate = uri.substring(0, pos);
+        }
+        candidate = removePathParameters(candidate);
+        candidate = UDecoder.URLDecode(candidate, connector.getURICharset());
+        candidate = org.apache.tomcat.util.http.RequestUtil.normalize(candidate);
+        boolean match = canonicalContextPath.equals(candidate);
+        while (!match && pos != -1) {
+            pos = nextSlash(uriChars, pos + 1);
+            if (pos == -1) {
+                candidate = uri;
+            } else {
+                candidate = uri.substring(0, pos);
+            }
+            candidate = removePathParameters(candidate);
+            candidate = UDecoder.URLDecode(candidate, connector.getURICharset());
+            candidate = org.apache.tomcat.util.http.RequestUtil.normalize(candidate);
+            match = canonicalContextPath.equals(candidate);
+        }
+        if (match) {
+            if (pos == -1) {
+                return uri;
+            } else {
+                return uri.substring(0, pos);
+            }
+        } else {
+            // Should never happen
+            throw new IllegalStateException(sm.getString(
+                    "coyoteRequest.getContextPath.ise", canonicalContextPath, uri));
+        }
+    }
+
+
+    private String removePathParameters(String input) {
+        int nextSemiColon = input.indexOf(';');
+        // Shortcut
+        if (nextSemiColon == -1) {
+            return input;
+        }
+        StringBuilder result = new StringBuilder(input.length());
+        result.append(input.substring(0, nextSemiColon));
+        while (true) {
+            int nextSlash = input.indexOf('/', nextSemiColon);
+            if (nextSlash == -1) {
+                break;
+            }
+            nextSemiColon = input.indexOf(';', nextSlash);
+            if (nextSemiColon == -1) {
+                result.append(input.substring(nextSlash));
+                break;
+            } else {
+                result.append(input.substring(nextSlash, nextSemiColon));
+            }
+        }
+
+        return result.toString();
+    }
+
+
+    private int nextSlash(char[] uri, int startPos) {
+        int len = uri.length;
+        int pos = startPos;
+        while (pos < len) {
+            if (uri[pos] == '/') {
+                return pos;
+            } else if (connector.getEncodedSolidusHandlingInternal() == EncodedSolidusHandling.DECODE &&
+                    uri[pos] == '%' && pos + 2 < len && uri[pos+1] == '2' &&
+                    (uri[pos + 2] == 'f' || uri[pos + 2] == 'F')) {
+                return pos;
+            }
+            pos++;
+        }
+        return -1;
+    }
+
+
+    /**
+     * Return the set of Cookies received with this Request. Triggers parsing of
+     * the Cookie HTTP headers followed by conversion to Cookie objects if this
+     * has not already been performed.
+     *
+     * @return the array of cookies
+     */
+    @Override
+    public Cookie[] getCookies() {
+        if (!cookiesConverted) {
+            convertCookies();
+        }
+        return cookies;
+    }
+
+
+    /**
+     * Return the server representation of the cookies associated with this
+     * request. Triggers parsing of the Cookie HTTP headers (but not conversion
+     * to Cookie objects) if the headers have not yet been parsed.
+     *
+     * @return the server cookies
+     */
+    public ServerCookies getServerCookies() {
+        parseCookies();
+        return coyoteRequest.getCookies();
+    }
+
+
+    /**
+     * Return the value of the specified date header, if any; otherwise
+     * return -1.
+     *
+     * @param name Name of the requested date header
+     * @return the date as a long
+     *
+     * @exception IllegalArgumentException if the specified header value
+     *  cannot be converted to a date
+     */
+    @Override
+    public long getDateHeader(String name) {
+
+        String value = getHeader(name);
+        if (value == null) {
+            return -1L;
+        }
+
+        // Attempt to convert the date header in a variety of formats
+        long result = FastHttpDateFormat.parseDate(value);
+        if (result != (-1L)) {
+            return result;
+        }
+        throw new IllegalArgumentException(value);
+
+    }
+
+
+    /**
+     * Return the first value of the specified header, if any; otherwise,
+     * return <code>null</code>
+     *
+     * @param name Name of the requested header
+     * @return the header value
+     */
+    @Override
+    public String getHeader(String name) {
+        return coyoteRequest.getHeader(name);
+    }
+
+
+    /**
+     * Return all of the values of the specified header, if any; otherwise,
+     * return an empty enumeration.
+     *
+     * @param name Name of the requested header
+     * @return the enumeration with the header values
+     */
+    @Override
+    public Enumeration<String> getHeaders(String name) {
+        return coyoteRequest.getMimeHeaders().values(name);
+    }
+
+
+    /**
+     * @return the names of all headers received with this request.
+     */
+    @Override
+    public Enumeration<String> getHeaderNames() {
+        return coyoteRequest.getMimeHeaders().names();
+    }
+
+
+    /**
+     * Return the value of the specified header as an integer, or -1 if there
+     * is no such header for this request.
+     *
+     * @param name Name of the requested header
+     * @return the header value as an int
+     *
+     * @exception IllegalArgumentException if the specified header value
+     *  cannot be converted to an integer
+     */
+    @Override
+    public int getIntHeader(String name) {
+
+        String value = getHeader(name);
+        if (value == null) {
+            return -1;
+        }
+
+        return Integer.parseInt(value);
+    }
+
+
+    @Override
+    public HttpServletMapping getHttpServletMapping() {
+        return applicationMapping.getHttpServletMapping();
+    }
+
+
+    /**
+     * @return the HTTP request method used in this Request.
+     */
+    @Override
+    public String getMethod() {
+        return coyoteRequest.method().toString();
+    }
+
+
+    /**
+     * @return the path information associated with this Request.
+     */
+    @Override
+    public String getPathInfo() {
+        return mappingData.pathInfo.toString();
+    }
+
+
+    /**
+     * @return the extra path information for this request, translated
+     * to a real path.
+     */
+    @Override
+    public String getPathTranslated() {
+
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+
+        if (getPathInfo() == null) {
+            return null;
+        }
+
+        return context.getServletContext().getRealPath(getPathInfo());
+    }
+
+
+    /**
+     * @return the query string associated with this request.
+     */
+    @Override
+    public String getQueryString() {
+        return coyoteRequest.queryString().toString();
+    }
+
+
+    /**
+     * @return the name of the remote user that has been authenticated
+     * for this Request.
+     */
+    @Override
+    public String getRemoteUser() {
+
+        if (userPrincipal == null) {
+            return null;
+        }
+
+        return userPrincipal.getName();
+    }
+
+
+    /**
+     * Get the request path.
+     *
+     * @return the request path
+     */
+    public MessageBytes getRequestPathMB() {
+        return mappingData.requestPath;
+    }
+
+
+    /**
+     * @return the session identifier included in this request, if any.
+     */
+    @Override
+    public String getRequestedSessionId() {
+        return requestedSessionId;
+    }
+
+
+    /**
+     * @return the request URI for this request.
+     */
+    @Override
+    public String getRequestURI() {
+        return coyoteRequest.requestURI().toString();
+    }
+
+
+    @Override
+    public StringBuffer getRequestURL() {
+        return RequestUtil.getRequestURL(this);
+    }
+
+
+    /**
+     * @return the portion of the request URI used to select the servlet
+     * that will process this request.
+     */
+    @Override
+    public String getServletPath() {
+        return mappingData.wrapperPath.toString();
+    }
+
+
+    /**
+     * @return the session associated with this Request, creating one
+     * if necessary.
+     */
+    @Override
+    public HttpSession getSession() {
+        Session session = doGetSession(true);
+        if (session == null) {
+            return null;
+        }
+
+        return session.getSession();
+    }
+
+
+    /**
+     * @return the session associated with this Request, creating one
+     * if necessary and requested.
+     *
+     * @param create Create a new session if one does not exist
+     */
+    @Override
+    public HttpSession getSession(boolean create) {
+        Session session = doGetSession(create);
+        if (session == null) {
+            return null;
+        }
+
+        return session.getSession();
+    }
+
+
+    /**
+     * @return <code>true</code> if the session identifier included in this
+     * request came from a cookie.
+     */
+    @Override
+    public boolean isRequestedSessionIdFromCookie() {
+
+        if (requestedSessionId == null) {
+            return false;
+        }
+
+        return requestedSessionCookie;
+    }
+
+
+    /**
+     * @return <code>true</code> if the session identifier included in this
+     * request came from the request URI.
+     */
+    @Override
+    public boolean isRequestedSessionIdFromURL() {
+
+        if (requestedSessionId == null) {
+            return false;
+        }
+
+        return requestedSessionURL;
+    }
+
+
+    /**
+     * @return <code>true</code> if the session identifier included in this
+     * request came from the request URI.
+     *
+     * @deprecated As of Version 2.1 of the Java Servlet API, use
+     *  <code>isRequestedSessionIdFromURL()</code> instead.
+     */
+    @Override
+    @Deprecated
+    public boolean isRequestedSessionIdFromUrl() {
+        return isRequestedSessionIdFromURL();
+    }
+
+
+    /**
+     * @return <code>true</code> if the session identifier included in this
+     * request identifies a valid session.
+     */
+    @Override
+    public boolean isRequestedSessionIdValid() {
+
+        if (requestedSessionId == null) {
+            return false;
+        }
+
+        Context context = getContext();
+        if (context == null) {
+            return false;
+        }
+
+        Manager manager = context.getManager();
+        if (manager == null) {
+            return false;
+        }
+
+        Session session = null;
+        try {
+            session = manager.findSession(requestedSessionId);
+        } catch (IOException e) {
+            // Can't find the session
+        }
+
+        if ((session == null) || !session.isValid()) {
+            // Check for parallel deployment contexts
+            if (getMappingData().contexts == null) {
+                return false;
+            } else {
+                for (int i = (getMappingData().contexts.length); i > 0; i--) {
+                    Context ctxt = getMappingData().contexts[i - 1];
+                    try {
+                        if (ctxt.getManager().findSession(requestedSessionId) !=
+                                null) {
+                            return true;
+                        }
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
+    /**
+     * @return <code>true</code> if the authenticated user principal
+     * possesses the specified role name.
+     *
+     * @param role Role name to be validated
+     */
+    @Override
+    public boolean isUserInRole(String role) {
+
+        // Have we got an authenticated principal at all?
+        if (userPrincipal == null) {
+            return false;
+        }
+
+        // Identify the Realm we will use for checking role assignments
+        Context context = getContext();
+        if (context == null) {
+            return false;
+        }
+
+        // If the role is "*" then the return value must be false
+        // Servlet 31, section 13.3
+        if ("*".equals(role)) {
+            return false;
+        }
+
+        // If the role is "**" then, unless the application defines a role with
+        // that name, only check if the user is authenticated
+        if ("**".equals(role) && !context.findSecurityRole("**")) {
+            return userPrincipal != null;
+        }
+
+        Realm realm = context.getRealm();
+        if (realm == null) {
+            return false;
+        }
+
+        // Check for a role defined directly as a <security-role>
+        return realm.hasRole(getWrapper(), userPrincipal, role);
+    }
+
+
+    /**
+     * @return the principal that has been authenticated for this Request.
+     */
+    public Principal getPrincipal() {
+        return userPrincipal;
+    }
+
+
+    /**
+     * @return the principal that has been authenticated for this Request.
+     */
+    @Override
+    public Principal getUserPrincipal() {
+        if (userPrincipal instanceof TomcatPrincipal) {
+            GSSCredential gssCredential =
+                    ((TomcatPrincipal) userPrincipal).getGssCredential();
+            if (gssCredential != null) {
+                int left = -1;
+                try {
+                    // Concurrent calls to this method from an expired session
+                    // can trigger an ISE. If one thread calls logout() below
+                    // before another thread calls getRemainingLifetime() then
+                    // then since logout() eventually calls
+                    // GSSCredential.dispose(), the subsequent call to
+                    // GSSCredential.getRemainingLifetime() will throw an ISE.
+                    // Avoiding the ISE would require locking in this method to
+                    // protect against concurrent access to the GSSCredential.
+                    // That would have a small performance impact. The ISE is
+                    // rare so it is caught and handled rather than avoided.
+                    left = gssCredential.getRemainingLifetime();
+                } catch (GSSException | IllegalStateException e) {
+                    log.warn(sm.getString("coyoteRequest.gssLifetimeFail",
+                            userPrincipal.getName()), e);
+                }
+                // zero is expired. Exception above will mean left == -1
+                // Treat both as expired.
+                if (left <= 0) {
+                    // GSS credential has expired. Need to re-authenticate.
+                    try {
+                        logout();
+                    } catch (ServletException e) {
+                        // Should never happen (no code called by logout()
+                        // throws a ServletException
+                    }
+                    return null;
+                }
+            }
+            return ((TomcatPrincipal) userPrincipal).getUserPrincipal();
+        }
+
+        return userPrincipal;
+    }
+
+
+    /**
+     * @return the session associated with this Request, creating one
+     * if necessary.
+     */
+    public Session getSessionInternal() {
+        return doGetSession(true);
+    }
+
+
+    /**
+     * Change the ID of the session that this request is associated with. There
+     * are several things that may trigger an ID change. These include moving
+     * between nodes in a cluster and session fixation prevention during the
+     * authentication process.
+     *
+     * @param newSessionId   The session to change the session ID for
+     */
+    public void changeSessionId(String newSessionId) {
+        // This should only ever be called if there was an old session ID but
+        // double check to be sure
+        if (requestedSessionId != null && requestedSessionId.length() > 0) {
+            requestedSessionId = newSessionId;
+        }
+
+        Context context = getContext();
+        if (context != null &&
+                !context.getServletContext()
+                        .getEffectiveSessionTrackingModes()
+                        .contains(SessionTrackingMode.COOKIE)) {
+            return;
+        }
+
+        if (response != null) {
+            Cookie newCookie = ApplicationSessionCookieConfig.createSessionCookie(context,
+                    newSessionId, isSecure());
+            response.addSessionCookieInternal(newCookie);
+        }
+    }
+
+
+    @Override
+    public String changeSessionId() {
+
+        Session session = this.getSessionInternal(false);
+        if (session == null) {
+            throw new IllegalStateException(
+                sm.getString("coyoteRequest.changeSessionId"));
+        }
+
+        Manager manager = this.getContext().getManager();
+
+        String newSessionId = manager.rotateSessionId(session);
+        this.changeSessionId(newSessionId);
+
+        return newSessionId;
+    }
+
+    /**
+     * @return the session associated with this Request, creating one
+     * if necessary and requested.
+     *
+     * @param create Create a new session if one does not exist
+     */
+    public Session getSessionInternal(boolean create) {
+        return doGetSession(create);
+    }
+
+
+    /**
+     * @return <code>true</code> if we have parsed parameters
+     */
+    public boolean isParametersParsed() {
+        return parametersParsed;
+    }
+
+
+    /**
+     * @return <code>true</code> if an attempt has been made to read the request
+     *         body and all of the request body has been read.
+     */
+    public boolean isFinished() {
+        return coyoteRequest.isFinished();
+    }
+
+
+    /**
+     * Check the configuration for aborted uploads and if configured to do so,
+     * disable the swallowing of any remaining input and close the connection
+     * once the response has been written.
+     */
+    protected void checkSwallowInput() {
+        Context context = getContext();
+        if (context != null && !context.getSwallowAbortedUploads()) {
+            coyoteRequest.action(ActionCode.DISABLE_SWALLOW_INPUT, null);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean authenticate(HttpServletResponse response)
+            throws IOException, ServletException {
+        if (response.isCommitted()) {
+            throw new IllegalStateException(
+                    sm.getString("coyoteRequest.authenticate.ise"));
+        }
+
+        return getContext().getAuthenticator().authenticate(this, response);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void login(String username, String password)
+            throws ServletException {
+        if (getAuthType() != null || getRemoteUser() != null ||
+                getUserPrincipal() != null) {
+            throw new ServletException(
+                    sm.getString("coyoteRequest.alreadyAuthenticated"));
+        }
+
+        getContext().getAuthenticator().login(username, password, this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void logout() throws ServletException {
+        getContext().getAuthenticator().logout(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Collection<Part> getParts() throws IOException, IllegalStateException,
+            ServletException {
+
+        parseParts(true);
+
+        if (partsParseException != null) {
+            if (partsParseException instanceof IOException) {
+                throw (IOException) partsParseException;
+            } else if (partsParseException instanceof IllegalStateException) {
+                throw (IllegalStateException) partsParseException;
+            } else if (partsParseException instanceof ServletException) {
+                throw (ServletException) partsParseException;
+            }
+        }
+
+        return parts;
+    }
+
+    private void parseParts(boolean explicit) {
+
+        // Return immediately if the parts have already been parsed
+        if (parts != null || partsParseException != null) {
+            return;
+        }
+
+        Context context = getContext();
+        MultipartConfigElement mce = getWrapper().getMultipartConfigElement();
+
+        if (mce == null) {
+            if(context.getAllowCasualMultipartParsing()) {
+                mce = new MultipartConfigElement(null, connector.getMaxPostSize(),
+                        connector.getMaxPostSize(), connector.getMaxPostSize());
+            } else {
+                if (explicit) {
+                    partsParseException = new IllegalStateException(
+                            sm.getString("coyoteRequest.noMultipartConfig"));
+                    return;
+                } else {
+                    parts = Collections.emptyList();
+                    return;
+                }
+            }
+        }
+
+        Parameters parameters = coyoteRequest.getParameters();
+        parameters.setLimit(getConnector().getMaxParameterCount());
+
+        boolean success = false;
+        try {
+            File location;
+            String locationStr = mce.getLocation();
+            if (locationStr == null || locationStr.length() == 0) {
+                location = ((File) context.getServletContext().getAttribute(
+                        ServletContext.TEMPDIR));
+            } else {
+                // If relative, it is relative to TEMPDIR
+                location = new File(locationStr);
+                if (!location.isAbsolute()) {
+                    location = new File(
+                            (File) context.getServletContext().getAttribute(ServletContext.TEMPDIR),
+                            locationStr).getAbsoluteFile();
+                }
+            }
+
+            if (!location.exists() && context.getCreateUploadTargets()) {
+                log.warn(sm.getString("coyoteRequest.uploadCreate",
+                        location.getAbsolutePath(), getMappingData().wrapper.getName()));
+                if (!location.mkdirs()) {
+                    log.warn(sm.getString("coyoteRequest.uploadCreateFail",
+                            location.getAbsolutePath()));
+                }
+            }
+
+            if (!location.isDirectory()) {
+                parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID);
+                partsParseException = new IOException(
+                        sm.getString("coyoteRequest.uploadLocationInvalid",
+                                location));
+                return;
+            }
+
+
+            // Create a new file upload handler
+            DiskFileItemFactory factory = new DiskFileItemFactory();
+            try {
+                factory.setRepository(location.getCanonicalFile());
+            } catch (IOException ioe) {
+                parameters.setParseFailedReason(FailReason.IO_ERROR);
+                partsParseException = ioe;
+                return;
+            }
+            factory.setSizeThreshold(mce.getFileSizeThreshold());
+
+            ServletFileUpload upload = new ServletFileUpload();
+            upload.setFileItemFactory(factory);
+            upload.setFileSizeMax(mce.getMaxFileSize());
+            upload.setSizeMax(mce.getMaxRequestSize());
+
+            parts = new ArrayList<>();
+            try {
+                List<FileItem> items =
+                        upload.parseRequest(new ServletRequestContext(this));
+                int maxPostSize = getConnector().getMaxPostSize();
+                int postSize = 0;
+                Charset charset = getCharset();
+                for (FileItem item : items) {
+                    ApplicationPart part = new ApplicationPart(item, location);
+                    parts.add(part);
+                    if (part.getSubmittedFileName() == null) {
+                        String name = part.getName();
+                        if (maxPostSize >= 0) {
+                            // Have to calculate equivalent size. Not completely
+                            // accurate but close enough.
+                            postSize += name.getBytes(charset).length;
+                            // Equals sign
+                            postSize++;
+                            // Value length
+                            postSize += part.getSize();
+                            // Value separator
+                            postSize++;
+                            if (postSize > maxPostSize) {
+                                parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
+                                throw new IllegalStateException(sm.getString(
+                                        "coyoteRequest.maxPostSizeExceeded"));
+                            }
+                        }
+                        String value = null;
+                        try {
+                            value = part.getString(charset.name());
+                        } catch (UnsupportedEncodingException uee) {
+                            // Not possible
+                        }
+                        parameters.addParameter(name, value);
+                    }
+                }
+
+                success = true;
+            } catch (InvalidContentTypeException e) {
+                parameters.setParseFailedReason(FailReason.INVALID_CONTENT_TYPE);
+                partsParseException = new ServletException(e);
+            } catch (SizeException e) {
+                parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
+                checkSwallowInput();
+                partsParseException = new IllegalStateException(e);
+            } catch (IOException e) {
+                parameters.setParseFailedReason(FailReason.IO_ERROR);
+                partsParseException = new IOException(e);
+            } catch (IllegalStateException e) {
+                // addParameters() will set parseFailedReason
+                checkSwallowInput();
+                partsParseException = e;
+            }
+        } finally {
+            // This might look odd but is correct. setParseFailedReason() only
+            // sets the failure reason if none is currently set. This code could
+            // be more efficient but it is written this way to be robust with
+            // respect to changes in the remainder of the method.
+            if (partsParseException != null || !success) {
+                parameters.setParseFailedReason(FailReason.UNKNOWN);
+            }
+        }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Part getPart(String name) throws IOException, IllegalStateException,
+            ServletException {
+        for (Part part : getParts()) {
+            if (name.equals(part.getName())) {
+                return part;
+            }
+        }
+        return null;
+    }
+
+
+    // ------------------------------------------------------ Protected Methods
+
+    protected Session doGetSession(boolean create) {
+
+        // There cannot be a session if no context has been assigned yet
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+
+        // Return the current session if it exists and is valid
+        if ((session != null) && !session.isValid()) {
+            session = null;
+        }
+        if (session != null) {
+            return session;
+        }
+
+        // Return the requested session if it exists and is valid
+        Manager manager = context.getManager();
+        if (manager == null) {
+            return null;      // Sessions are not supported
+        }
+        if (requestedSessionId != null) {
+            try {
+                session = manager.findSession(requestedSessionId);
+            } catch (IOException e) {
+                if (log.isDebugEnabled()) {
+                    log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
+                } else {
+                    log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
+                }
+                session = null;
+            }
+            if ((session != null) && !session.isValid()) {
+                session = null;
+            }
+            if (session != null) {
+                session.access();
+                return session;
+            }
+        }
+
+        // Create a new session if requested and the response is not committed
+        if (!create) {
+            return null;
+        }
+        boolean trackModesIncludesCookie =
+                context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
+        if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
+            throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
+        }
+
+        // Re-use session IDs provided by the client in very limited
+        // circumstances.
+        String sessionId = getRequestedSessionId();
+        if (requestedSessionSSL) {
+            // If the session ID has been obtained from the SSL handshake then
+            // use it.
+        } else if (("/".equals(context.getSessionCookiePath())
+                && isRequestedSessionIdFromCookie())) {
+            /* This is the common(ish) use case: using the same session ID with
+             * multiple web applications on the same host. Typically this is
+             * used by Portlet implementations. It only works if sessions are
+             * tracked via cookies. The cookie must have a path of "/" else it
+             * won't be provided for requests to all web applications.
+             *
+             * Any session ID provided by the client should be for a session
+             * that already exists somewhere on the host. Check if the context
+             * is configured for this to be confirmed.
+             */
+            if (context.getValidateClientProvidedNewSessionId()) {
+                boolean found = false;
+                for (Container container : getHost().findChildren()) {
+                    Manager m = ((Context) container).getManager();
+                    if (m != null) {
+                        try {
+                            if (m.findSession(sessionId) != null) {
+                                found = true;
+                                break;
+                            }
+                        } catch (IOException e) {
+                            // Ignore. Problems with this manager will be
+                            // handled elsewhere.
+                        }
+                    }
+                }
+                if (!found) {
+                    sessionId = null;
+                }
+            }
+        } else {
+            sessionId = null;
+        }
+        session = manager.createSession(sessionId);
+
+        // Creating a new session cookie based on that session
+        if (session != null && trackModesIncludesCookie) {
+            Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
+                    context, session.getIdInternal(), isSecure());
+
+            response.addSessionCookieInternal(cookie);
+        }
+
+        if (session == null) {
+            return null;
+        }
+
+        session.access();
+        return session;
+    }
+
+    protected String unescape(String s) {
+        if (s==null) {
+            return null;
+        }
+        if (s.indexOf('\\') == -1) {
+            return s;
+        }
+        StringBuilder buf = new StringBuilder();
+        for (int i=0; i<s.length(); i++) {
+            char c = s.charAt(i);
+            if (c!='\\') {
+                buf.append(c);
+            } else {
+                if (++i >= s.length()) {
+                    throw new IllegalArgumentException();//invalid escape, hence invalid cookie
+                }
+                c = s.charAt(i);
+                buf.append(c);
+            }
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Parse cookies. This only parses the cookies into the memory efficient
+     * ServerCookies structure. It does not populate the Cookie objects.
+     */
+    protected void parseCookies() {
+        if (cookiesParsed) {
+            return;
+        }
+
+        cookiesParsed = true;
+
+        ServerCookies serverCookies = coyoteRequest.getCookies();
+        serverCookies.setLimit(connector.getMaxCookieCount());
+        CookieProcessor cookieProcessor = getContext().getCookieProcessor();
+        cookieProcessor.parseCookieHeader(coyoteRequest.getMimeHeaders(), serverCookies);
+    }
+
+    /**
+     * Converts the parsed cookies (parsing the Cookie headers first if they
+     * have not been parsed) into Cookie objects.
+     */
+    protected void convertCookies() {
+        if (cookiesConverted) {
+            return;
+        }
+
+        cookiesConverted = true;
+
+        if (getContext() == null) {
+            return;
+        }
+
+        parseCookies();
+
+        ServerCookies serverCookies = coyoteRequest.getCookies();
+        CookieProcessor cookieProcessor = getContext().getCookieProcessor();
+
+        int count = serverCookies.getCookieCount();
+        if (count <= 0) {
+            return;
+        }
+
+        cookies = new Cookie[count];
+
+        int idx=0;
+        for (int i = 0; i < count; i++) {
+            ServerCookie scookie = serverCookies.getCookie(i);
+            try {
+                // We must unescape the '\\' escape character
+                Cookie cookie = new Cookie(scookie.getName().toString(),null);
+                int version = scookie.getVersion();
+                cookie.setVersion(version);
+                scookie.getValue().getByteChunk().setCharset(cookieProcessor.getCharset());
+                cookie.setValue(unescape(scookie.getValue().toString()));
+                cookie.setPath(unescape(scookie.getPath().toString()));
+                String domain = scookie.getDomain().toString();
+                if (domain!=null) {
+                    cookie.setDomain(unescape(domain));//avoid NPE
+                }
+                String comment = scookie.getComment().toString();
+                cookie.setComment(version==1?unescape(comment):null);
+                cookies[idx++] = cookie;
+            } catch(IllegalArgumentException e) {
+                // Ignore bad cookie
+            }
+        }
+        if( idx < count ) {
+            Cookie [] ncookies = new Cookie[idx];
+            System.arraycopy(cookies, 0, ncookies, 0, idx);
+            cookies = ncookies;
+        }
+    }
+
+
+    /**
+     * Parse request parameters.
+     */
+    protected void parseParameters() {
+
+        parametersParsed = true;
+
+        Parameters parameters = coyoteRequest.getParameters();
+        boolean success = false;
+        try {
+            // 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
+            Charset charset = getCharset();
+
+            boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
+            parameters.setCharset(charset);
+            if (useBodyEncodingForURI) {
+                parameters.setQueryStringCharset(charset);
+            }
+            // Note: If !useBodyEncodingForURI, the query string encoding is
+            //       that set towards the start of CoyoyeAdapter.service()
+
+            parameters.handleQueryParameters();
+
+            if (usingInputStream || usingReader) {
+                success = true;
+                return;
+            }
+
+            String contentType = getContentType();
+            if (contentType == null) {
+                contentType = "";
+            }
+            int semicolon = contentType.indexOf(';');
+            if (semicolon >= 0) {
+                contentType = contentType.substring(0, semicolon).trim();
+            } else {
+                contentType = contentType.trim();
+            }
+
+            if ("multipart/form-data".equals(contentType)) {
+                parseParts(false);
+                success = true;
+                return;
+            }
+
+            if( !getConnector().isParseBodyMethod(getMethod()) ) {
+                success = true;
+                return;
+            }
+
+            if (!("application/x-www-form-urlencoded".equals(contentType))) {
+                success = true;
+                return;
+            }
+
+            int len = getContentLength();
+
+            if (len > 0) {
+                int maxPostSize = connector.getMaxPostSize();
+                if ((maxPostSize >= 0) && (len > maxPostSize)) {
+                    Context context = getContext();
+                    if (context != null && context.getLogger().isDebugEnabled()) {
+                        context.getLogger().debug(
+                                sm.getString("coyoteRequest.postTooLarge"));
+                    }
+                    checkSwallowInput();
+                    parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
+                    return;
+                }
+                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 (readPostBody(formData, len) != len) {
+                        parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
+                        return;
+                    }
+                } catch (IOException e) {
+                    // Client disconnect
+                    Context context = getContext();
+                    if (context != null && context.getLogger().isDebugEnabled()) {
+                        context.getLogger().debug(
+                                sm.getString("coyoteRequest.parseParameters"), e);
+                    }
+                    parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
+                    return;
+                }
+                parameters.processParameters(formData, 0, len);
+            } else if ("chunked".equalsIgnoreCase(
+                    coyoteRequest.getHeader("transfer-encoding"))) {
+                byte[] formData = null;
+                try {
+                    formData = readChunkedPostBody();
+                } catch (IllegalStateException ise) {
+                    // chunkedPostTooLarge error
+                    parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
+                    Context context = getContext();
+                    if (context != null && context.getLogger().isDebugEnabled()) {
+                        context.getLogger().debug(
+                                sm.getString("coyoteRequest.parseParameters"),
+                                ise);
+                    }
+                    return;
+                } catch (IOException e) {
+                    // Client disconnect
+                    parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
+                    Context context = getContext();
+                    if (context != null && context.getLogger().isDebugEnabled()) {
+                        context.getLogger().debug(
+                                sm.getString("coyoteRequest.parseParameters"), e);
+                    }
+                    return;
+                }
+                if (formData != null) {
+                    parameters.processParameters(formData, 0, formData.length);
+                }
+            }
+            success = true;
+        } finally {
+            if (!success) {
+                parameters.setParseFailedReason(FailReason.UNKNOWN);
+            }
+        }
+
+    }
+
+
+    /**
+     * Read post body in an array.
+     *
+     * @param body The bytes array in which the body will be read
+     * @param len The body length
+     * @return the bytes count that has been read
+     * @throws IOException if an IO exception occurred
+     */
+    protected int readPostBody(byte[] body, int len)
+            throws IOException {
+
+        int offset = 0;
+        do {
+            int inputLen = getStream().read(body, offset, len - offset);
+            if (inputLen <= 0) {
+                return offset;
+            }
+            offset += inputLen;
+        } while ((len - offset) > 0);
+        return len;
+
+    }
+
+
+    /**
+     * Read chunked post body.
+     *
+     * @return the post body as a bytes array
+     * @throws IOException if an IO exception occurred
+     */
+    protected byte[] readChunkedPostBody() throws IOException {
+        ByteChunk body = new ByteChunk();
+
+        byte[] buffer = new byte[CACHED_POST_LEN];
+
+        int len = 0;
+        while (len > -1) {
+            len = getStream().read(buffer, 0, CACHED_POST_LEN);
+            if (connector.getMaxPostSize() >= 0 &&
+                    (body.getLength() + len) > connector.getMaxPostSize()) {
+                // Too much data
+                checkSwallowInput();
+                throw new IllegalStateException(
+                        sm.getString("coyoteRequest.chunkedPostTooLarge"));
+            }
+            if (len > 0) {
+                body.append(buffer, 0, len);
+            }
+        }
+        if (body.getLength() == 0) {
+            return null;
+        }
+        if (body.getLength() < body.getBuffer().length) {
+            int length = body.getLength();
+            byte[] result = new byte[length];
+            System.arraycopy(body.getBuffer(), 0, result, 0, length);
+            return result;
+        }
+
+        return body.getBuffer();
+    }
+
+
+    /**
+     * Parse request locales.
+     */
+    protected void parseLocales() {
+
+        localesParsed = true;
+
+        // Store the accumulated languages that have been requested in
+        // a local collection, sorted by the quality value (so we can
+        // add Locales in descending order).  The values will be ArrayLists
+        // containing the corresponding Locales to be added
+        TreeMap<Double, ArrayList<Locale>> locales = new TreeMap<>();
+
+        Enumeration<String> values = getHeaders("accept-language");
+
+        while (values.hasMoreElements()) {
+            String value = values.nextElement();
+            parseLocalesHeader(value, locales);
+        }
+
+        // Process the quality values in highest->lowest order (due to
+        // negating the Double value when creating the key)
+        for (ArrayList<Locale> list : locales.values()) {
+            for (Locale locale : list) {
+                addLocale(locale);
+            }
+        }
+    }
+
+
+    /**
+     * Parse accept-language header value.
+     *
+     * @param value the header value
+     * @param locales the map that will hold the result
+     */
+    protected void parseLocalesHeader(String value, TreeMap<Double, ArrayList<Locale>> locales) {
+
+        List<AcceptLanguage> acceptLanguages;
+        try {
+            acceptLanguages = AcceptLanguage.parse(new StringReader(value));
+        } catch (IOException e) {
+            // Mal-formed headers are ignore. Do the same in the unlikely event
+            // of an IOException.
+            return;
+        }
+
+        for (AcceptLanguage acceptLanguage : acceptLanguages) {
+            // Add a new Locale to the list of Locales for this quality level
+            Double key = Double.valueOf(-acceptLanguage.getQuality());  // Reverse the order
+            locales.computeIfAbsent(key, k -> new ArrayList<>()).add(acceptLanguage.getLocale());
+        }
+    }
+
+
+    // ----------------------------------------------------- Special attributes handling
+
+    private static interface SpecialAttributeAdapter {
+        Object get(Request request, String name);
+
+        void set(Request request, String name, Object value);
+
+        // None of special attributes support removal
+        // void remove(Request request, String name);
+    }
+
+    private static final Map<String, SpecialAttributeAdapter> specialAttributes = new HashMap<>();
+
+    static {
+        specialAttributes.put(Globals.DISPATCHER_TYPE_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        return (request.internalDispatcherType == null) ? DispatcherType.REQUEST
+                                : request.internalDispatcherType;
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        request.internalDispatcherType = (DispatcherType) value;
+                    }
+                });
+        specialAttributes.put(Globals.DISPATCHER_REQUEST_PATH_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        return (request.requestDispatcherPath == null) ? request
+                                .getRequestPathMB().toString()
+                                : request.requestDispatcherPath.toString();
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        request.requestDispatcherPath = value;
+                    }
+                });
+        specialAttributes.put(Globals.ASYNC_SUPPORTED_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        return request.asyncSupported;
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        Boolean oldValue = request.asyncSupported;
+                        request.asyncSupported = (Boolean)value;
+                        request.notifyAttributeAssigned(name, value, oldValue);
+                    }
+                });
+        specialAttributes.put(Globals.GSS_CREDENTIAL_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        if (request.userPrincipal instanceof TomcatPrincipal) {
+                            return ((TomcatPrincipal) request.userPrincipal)
+                                    .getGssCredential();
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        // NO-OP
+                    }
+                });
+        specialAttributes.put(Globals.PARAMETER_PARSE_FAILED_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        if (request.getCoyoteRequest().getParameters()
+                                .isParseFailed()) {
+                            return Boolean.TRUE;
+                        }
+                        return null;
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        // NO-OP
+                    }
+                });
+        specialAttributes.put(Globals.PARAMETER_PARSE_FAILED_REASON_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        return request.getCoyoteRequest().getParameters().getParseFailedReason();
+                    }
+
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        // NO-OP
+                    }
+                });
+        specialAttributes.put(Globals.SENDFILE_SUPPORTED_ATTR,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        return Boolean.valueOf(
+                                request.getConnector().getProtocolHandler(
+                                        ).isSendfileSupported() && request.getCoyoteRequest().getSendfile());
+                    }
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        // NO-OP
+                    }
+                });
+        specialAttributes.put(Globals.CONNECTION_ID,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        AtomicReference<Object> result = new AtomicReference<>();
+                        request.getCoyoteRequest().action(ActionCode.CONNECTION_ID, result);
+                        return result.get();
+                    }
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        // NO-OP
+                    }
+                });
+        specialAttributes.put(Globals.STREAM_ID,
+                new SpecialAttributeAdapter() {
+                    @Override
+                    public Object get(Request request, String name) {
+                        AtomicReference<Object> result = new AtomicReference<>();
+                        request.getCoyoteRequest().action(ActionCode.STREAM_ID, result);
+                        return result.get();
+                    }
+                    @Override
+                    public void set(Request request, String name, Object value) {
+                        // NO-OP
+                    }
+                });
+    }
+}
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java
new file mode 100644
index 0000000000..ce765374e7
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/Parameters.java
@@ -0,0 +1,520 @@
+/*
+ *  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.tomcat.util.http;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.buf.MessageBytes;
+import org.apache.tomcat.util.buf.StringUtils;
+import org.apache.tomcat.util.buf.UDecoder;
+import org.apache.tomcat.util.log.UserDataHelper;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ *
+ * @author Costin Manolache
+ */
+public final class Parameters {
+
+    private static final Log log = LogFactory.getLog(Parameters.class);
+
+    private static final UserDataHelper userDataLog = new UserDataHelper(log);
+
+    private static final UserDataHelper maxParamCountLog = new UserDataHelper(log);
+
+    private static final StringManager sm =
+        StringManager.getManager("org.apache.tomcat.util.http");
+
+    private final Map<String,ArrayList<String>> paramHashValues =
+            new LinkedHashMap<>();
+    private boolean didQueryParameters=false;
+
+    private MessageBytes queryMB;
+
+    private UDecoder urlDec;
+    private final MessageBytes decodedQuery = MessageBytes.newInstance();
+
+    private Charset charset = StandardCharsets.ISO_8859_1;
+    private Charset queryStringCharset = StandardCharsets.UTF_8;
+
+    private int limit = -1;
+    private int parameterCount = 0;
+
+    /**
+     * Set to the reason for the failure (the first failure if there is more
+     * than one) if there were failures during parameter parsing.
+     */
+    private FailReason parseFailedReason = null;
+
+    public Parameters() {
+        // NO-OP
+    }
+
+    public void setQuery( MessageBytes queryMB ) {
+        this.queryMB=queryMB;
+    }
+
+    public void setLimit(int limit) {
+        this.limit = limit;
+    }
+
+    public Charset getCharset() {
+        return charset;
+    }
+
+    public void setCharset(Charset charset) {
+        if (charset == null) {
+            charset = DEFAULT_BODY_CHARSET;
+        }
+        this.charset = charset;
+        if(log.isDebugEnabled()) {
+            log.debug("Set encoding to " + charset.name());
+        }
+    }
+
+    public void setQueryStringCharset(Charset queryStringCharset) {
+        if (queryStringCharset == null) {
+            queryStringCharset = DEFAULT_URI_CHARSET;
+        }
+        this.queryStringCharset = queryStringCharset;
+
+        if(log.isDebugEnabled()) {
+            log.debug("Set query string encoding to " + queryStringCharset.name());
+        }
+    }
+
+
+    public boolean isParseFailed() {
+        return parseFailedReason != null;
+    }
+
+
+    public FailReason getParseFailedReason() {
+        return parseFailedReason;
+    }
+
+
+    public void setParseFailedReason(FailReason failReason) {
+        if (this.parseFailedReason == null) {
+            this.parseFailedReason = failReason;
+        }
+    }
+
+
+    public void recycle() {
+        parameterCount = 0;
+        paramHashValues.clear();
+        didQueryParameters = false;
+        charset = DEFAULT_BODY_CHARSET;
+        decodedQuery.recycle();
+        parseFailedReason = null;
+    }
+
+
+    // -------------------- Data access --------------------
+    // Access to the current name/values, no side effect ( processing ).
+    // You must explicitly call handleQueryParameters and the post methods.
+
+    public String[] getParameterValues(String name) {
+        handleQueryParameters();
+        // no "facade"
+        ArrayList<String> values = paramHashValues.get(name);
+        if (values == null) {
+            return null;
+        }
+        return values.toArray(new String[0]);
+    }
+
+    public Enumeration<String> getParameterNames() {
+        handleQueryParameters();
+        return Collections.enumeration(paramHashValues.keySet());
+    }
+
+    public String getParameter(String name ) {
+        handleQueryParameters();
+        ArrayList<String> values = paramHashValues.get(name);
+        if (values != null) {
+            if(values.size() == 0) {
+                return "";
+            }
+            return values.get(0);
+        } else {
+            return null;
+        }
+    }
+    // -------------------- Processing --------------------
+    /** Process the query string into parameters
+     */
+    public void handleQueryParameters() {
+        if (didQueryParameters) {
+            return;
+        }
+
+        didQueryParameters = true;
+
+        if (queryMB == null || queryMB.isNull()) {
+            return;
+        }
+
+        if(log.isDebugEnabled()) {
+            log.debug("Decoding query " + decodedQuery + " " + queryStringCharset.name());
+        }
+
+        try {
+            decodedQuery.duplicate(queryMB);
+        } catch (IOException e) {
+            // Can't happen, as decodedQuery can't overflow
+            e.printStackTrace();
+        }
+        processParameters(decodedQuery, queryStringCharset);
+    }
+
+
+    public void addParameter( String key, String value )
+            throws IllegalStateException {
+
+        if( key==null ) {
+            return;
+        }
+
+        parameterCount ++;
+        if (limit > -1 && parameterCount > limit) {
+            // Processing this parameter will push us over the limit. ISE is
+            // what Request.parseParts() uses for requests that are too big
+            setParseFailedReason(FailReason.TOO_MANY_PARAMETERS);
+            throw new IllegalStateException(sm.getString(
+                    "parameters.maxCountFail", Integer.valueOf(limit)));
+        }
+
+        paramHashValues.computeIfAbsent(key, k -> new ArrayList<>(1)).add(value);
+    }
+
+    public void setURLDecoder( UDecoder u ) {
+        urlDec=u;
+    }
+
+    // -------------------- Parameter parsing --------------------
+    // we are called from a single thread - we can do it the hard way
+    // if needed
+    private final ByteChunk tmpName=new ByteChunk();
+    private final ByteChunk tmpValue=new ByteChunk();
+    private final ByteChunk origName=new ByteChunk();
+    private final ByteChunk origValue=new ByteChunk();
+    private static final Charset DEFAULT_BODY_CHARSET = StandardCharsets.ISO_8859_1;
+    private static final Charset DEFAULT_URI_CHARSET = StandardCharsets.UTF_8;
+
+
+    public void processParameters( byte bytes[], int start, int len ) {
+        processParameters(bytes, start, len, charset);
+    }
+
+    private void processParameters(byte bytes[], int start, int len, Charset charset) {
+
+        if(log.isDebugEnabled()) {
+            log.debug(sm.getString("parameters.bytes",
+                    new String(bytes, start, len, DEFAULT_BODY_CHARSET)));
+        }
+
+        int decodeFailCount = 0;
+
+        int pos = start;
+        int end = start + len;
+
+        while(pos < end) {
+            int nameStart = pos;
+            int nameEnd = -1;
+            int valueStart = -1;
+            int valueEnd = -1;
+
+            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;
+                }
+            }
+
+            if (log.isDebugEnabled() && valueStart == -1) {
+                log.debug(sm.getString("parameters.noequal",
+                        Integer.valueOf(nameStart), Integer.valueOf(nameEnd),
+                        new String(bytes, nameStart, nameEnd-nameStart, DEFAULT_BODY_CHARSET)));
+            }
+
+            if (nameEnd <= nameStart ) {
+                if (valueStart == -1) {
+                    // &&
+                    if (log.isDebugEnabled()) {
+                        log.debug(sm.getString("parameters.emptyChunk"));
+                    }
+                    // Do not flag as error
+                    continue;
+                }
+                // &=foo&
+                UserDataHelper.Mode logMode = userDataLog.getNextMode();
+                if (logMode != null) {
+                    String extract;
+                    if (valueEnd > nameStart) {
+                        extract = new String(bytes, nameStart, valueEnd - nameStart,
+                                DEFAULT_BODY_CHARSET);
+                    } else {
+                        extract = "";
+                    }
+                    String message = sm.getString("parameters.invalidChunk",
+                            Integer.valueOf(nameStart),
+                            Integer.valueOf(valueEnd), extract);
+                    switch (logMode) {
+                        case INFO_THEN_DEBUG:
+                            message += sm.getString("parameters.fallToDebug");
+                            //$FALL-THROUGH$
+                        case INFO:
+                            log.info(message);
+                            break;
+                        case DEBUG:
+                            log.debug(message);
+                    }
+                }
+                setParseFailedReason(FailReason.NO_NAME);
+                continue;
+                // invalid chunk - it's better to ignore
+            }
+
+            tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
+            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.
+            // For performance - only done for debug
+            if (log.isDebugEnabled()) {
+                try {
+                    origName.append(bytes, nameStart, nameEnd - nameStart);
+                    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);
+                }
+            }
+
+            try {
+                String name;
+                String value;
+
+                if (decodeName) {
+                    urlDecode(tmpName);
+                }
+                tmpName.setCharset(charset);
+                name = tmpName.toString();
+
+                if (valueStart >= 0) {
+                    if (decodeValue) {
+                        urlDecode(tmpValue);
+                    }
+                    tmpValue.setCharset(charset);
+                    value = tmpValue.toString();
+                } else {
+                    value = "";
+                }
+
+                try {
+                    addParameter(name, value);
+                } catch (IllegalStateException ise) {
+                    // Hitting limit stops processing further params but does
+                    // not cause request to fail.
+                    UserDataHelper.Mode logMode = maxParamCountLog.getNextMode();
+                    if (logMode != null) {
+                        String message = ise.getMessage();
+                        switch (logMode) {
+                            case INFO_THEN_DEBUG:
+                                message += sm.getString(
+                                        "parameters.maxCountFail.fallToDebug");
+                                //$FALL-THROUGH$
+                            case INFO:
+                                log.info(message);
+                                break;
+                            case DEBUG:
+                                log.debug(message);
+                        }
+                    }
+                    break;
+                }
+            } catch (IOException e) {
+                setParseFailedReason(FailReason.URL_DECODING);
+                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()) {
+                        UserDataHelper.Mode logMode = userDataLog.getNextMode();
+                        if (logMode != null) {
+                            String message = sm.getString(
+                                    "parameters.decodeFail.info",
+                                    tmpName.toString(), tmpValue.toString());
+                            switch (logMode) {
+                                case INFO_THEN_DEBUG:
+                                    message += sm.getString("parameters.fallToDebug");
+                                    //$FALL-THROUGH$
+                                case INFO:
+                                    log.info(message);
+                                    break;
+                                case DEBUG:
+                                    log.debug(message);
+                            }
+                        }
+                    }
+                }
+            }
+
+            tmpName.recycle();
+            tmpValue.recycle();
+            // Only recycle copies if we used them
+            if (log.isDebugEnabled()) {
+                origName.recycle();
+                origValue.recycle();
+            }
+        }
+
+        if (decodeFailCount > 1 && !log.isDebugEnabled()) {
+            UserDataHelper.Mode logMode = userDataLog.getNextMode();
+            if (logMode != null) {
+                String message = sm.getString(
+                        "parameters.multipleDecodingFail",
+                        Integer.valueOf(decodeFailCount));
+                switch (logMode) {
+                    case INFO_THEN_DEBUG:
+                        message += sm.getString("parameters.fallToDebug");
+                        //$FALL-THROUGH$
+                    case INFO:
+                        log.info(message);
+                        break;
+                    case DEBUG:
+                        log.debug(message);
+                }
+            }
+        }
+    }
+
+    private void urlDecode(ByteChunk bc)
+        throws IOException {
+        if( urlDec==null ) {
+            urlDec=new UDecoder();
+        }
+        urlDec.convert(bc, true);
+    }
+
+    public void processParameters(MessageBytes data, Charset charset) {
+        if( data==null || data.isNull() || data.getLength() <= 0 ) {
+            return;
+        }
+
+        if( data.getType() != MessageBytes.T_BYTES ) {
+            data.toBytes();
+        }
+        ByteChunk bc=data.getByteChunk();
+        processParameters(bc.getBytes(), bc.getOffset(), bc.getLength(), charset);
+    }
+
+    /**
+     * Debug purpose
+     */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, ArrayList<String>> e : paramHashValues.entrySet()) {
+            sb.append(e.getKey()).append('=');
+            StringUtils.join(e.getValue(), ',', sb);
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+
+
+    public enum FailReason {
+        CLIENT_DISCONNECT,
+        MULTIPART_CONFIG_INVALID,
+        INVALID_CONTENT_TYPE,
+        IO_ERROR,
+        NO_NAME,
+        POST_TOO_LARGE,
+        REQUEST_BODY_INCOMPLETE,
+        TOO_MANY_PARAMETERS,
+        UNKNOWN,
+        URL_DECODING
+    }
+}
diff --git a/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
new file mode 100644
index 0000000000..7d678c24d8
--- /dev/null
+++ b/tomee/apache-tomee/src/patch/java/org/apache/tomcat/util/http/fileupload/FileUploadBase.java
@@ -0,0 +1,557 @@
+/*
+ * 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.tomcat.util.http.fileupload;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl;
+import org.apache.tomcat.util.http.fileupload.impl.FileUploadIOException;
+import org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException;
+import org.apache.tomcat.util.http.fileupload.util.FileItemHeadersImpl;
+import org.apache.tomcat.util.http.fileupload.util.Streams;
+
+
+/**
+ * <p>High level API for processing file uploads.</p>
+ *
+ * <p>This class handles multiple files per single HTML widget, sent using
+ * {@code multipart/mixed} encoding type, as specified by
+ * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.  Use {@link
+ * #parseRequest(RequestContext)} to acquire a list of {@link
+ * org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML
+ * widget.</p>
+ *
+ * <p>How the data for individual parts is stored is determined by the factory
+ * used to create them; a given part may be in memory, on disk, or somewhere
+ * else.</p>
+ */
+public abstract class FileUploadBase {
+
+    // ---------------------------------------------------------- Class methods
+
+    /**
+     * <p>Utility method that determines whether the request contains multipart
+     * content.</p>
+     *
+     * <p><strong>NOTE:</strong>This method will be moved to the
+     * {@code ServletFileUpload} class after the FileUpload 1.1 release.
+     * Unfortunately, since this method is static, it is not possible to
+     * provide its replacement until this method is removed.</p>
+     *
+     * @param ctx The request context to be evaluated. Must be non-null.
+     *
+     * @return {@code true} if the request is multipart;
+     *         {@code false} otherwise.
+     */
+    public static final boolean isMultipartContent(final RequestContext ctx) {
+        final String contentType = ctx.getContentType();
+        if (contentType == null) {
+            return false;
+        }
+        return contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART);
+    }
+
+    // ----------------------------------------------------- Manifest constants
+
+    /**
+     * HTTP content type header name.
+     */
+    public static final String CONTENT_TYPE = "Content-type";
+
+    /**
+     * HTTP content disposition header name.
+     */
+    public static final String CONTENT_DISPOSITION = "Content-disposition";
+
+    /**
+     * HTTP content length header name.
+     */
+    public static final String CONTENT_LENGTH = "Content-length";
+
+    /**
+     * Content-disposition value for form data.
+     */
+    public static final String FORM_DATA = "form-data";
+
+    /**
+     * Content-disposition value for file attachment.
+     */
+    public static final String ATTACHMENT = "attachment";
+
+    /**
+     * Part of HTTP content type header.
+     */
+    public static final String MULTIPART = "multipart/";
+
+    /**
+     * HTTP content type header for multipart forms.
+     */
+    public static final String MULTIPART_FORM_DATA = "multipart/form-data";
+
+    /**
+     * HTTP content type header for multiple uploads.
+     */
+    public static final String MULTIPART_MIXED = "multipart/mixed";
+
+    // ----------------------------------------------------------- Data members
+
+    /**
+     * The maximum size permitted for the complete request, as opposed to
+     * {@link #fileSizeMax}. A value of -1 indicates no maximum.
+     */
+    private long sizeMax = -1;
+
+    /**
+     * The maximum size permitted for a single uploaded file, as opposed
+     * to {@link #sizeMax}. A value of -1 indicates no maximum.
+     */
+    private long fileSizeMax = -1;
+
+    /**
+     * The content encoding to use when reading part headers.
+     */
+    private String headerEncoding;
+
+    /**
+     * The progress listener.
+     */
+    private ProgressListener listener;
+
+    // ----------------------------------------------------- Property accessors
+
+    /**
+     * Returns the factory class used when creating file items.
+     *
+     * @return The factory class for new file items.
+     */
+    public abstract FileItemFactory getFileItemFactory();
+
+    /**
+     * Sets the factory class to use when creating file items.
+     *
+     * @param factory The factory class for new file items.
+     */
+    public abstract void setFileItemFactory(FileItemFactory factory);
+
+    /**
+     * Returns the maximum allowed size of a complete request, as opposed
+     * to {@link #getFileSizeMax()}.
+     *
+     * @return The maximum allowed size, in bytes. The default value of
+     *   -1 indicates, that there is no limit.
+     *
+     * @see #setSizeMax(long)
+     *
+     */
+    public long getSizeMax() {
+        return sizeMax;
+    }
+
+    /**
+     * Sets the maximum allowed size of a complete request, as opposed
+     * to {@link #setFileSizeMax(long)}.
+     *
+     * @param sizeMax The maximum allowed size, in bytes. The default value of
+     *   -1 indicates, that there is no limit.
+     *
+     * @see #getSizeMax()
+     *
+     */
+    public void setSizeMax(final long sizeMax) {
+        this.sizeMax = sizeMax;
+    }
+
+    /**
+     * Returns the maximum allowed size of a single uploaded file,
+     * as opposed to {@link #getSizeMax()}.
+     *
+     * @see #setFileSizeMax(long)
+     * @return Maximum size of a single uploaded file.
+     */
+    public long getFileSizeMax() {
+        return fileSizeMax;
+    }
+
+    /**
+     * Sets the maximum allowed size of a single uploaded file,
+     * as opposed to {@link #getSizeMax()}.
+     *
+     * @see #getFileSizeMax()
+     * @param fileSizeMax Maximum size of a single uploaded file.
+     */
+    public void setFileSizeMax(final long fileSizeMax) {
+        this.fileSizeMax = fileSizeMax;
+    }
+
+    /**
+     * Retrieves the character encoding used when reading the headers of an
+     * individual part. When not specified, or {@code null}, the request
+     * encoding is used. If that is also not specified, or {@code null},
+     * the platform default encoding is used.
+     *
+     * @return The encoding used to read part headers.
+     */
+    public String getHeaderEncoding() {
+        return headerEncoding;
+    }
+
+    /**
+     * Specifies the character encoding to be used when reading the headers of
+     * individual part. When not specified, or {@code null}, the request
+     * encoding is used. If that is also not specified, or {@code null},
+     * the platform default encoding is used.
+     *
+     * @param encoding The encoding used to read part headers.
+     */
+    public void setHeaderEncoding(final String encoding) {
+        headerEncoding = encoding;
+    }
+
+    // --------------------------------------------------------- Public methods
+
+    /**
+     * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
+     * compliant {@code multipart/form-data} stream.
+     *
+     * @param ctx The context for the request to be parsed.
+     *
+     * @return An iterator to instances of {@code FileItemStream}
+     *         parsed from the request, in the order that they were
+     *         transmitted.
+     *
+     * @throws FileUploadException if there are problems reading/parsing
+     *                             the request or storing files.
+     * @throws IOException An I/O error occurred. This may be a network
+     *   error while communicating with the client or a problem while
+     *   storing the uploaded content.
+     */
+    public FileItemIterator getItemIterator(final RequestContext ctx)
+    throws FileUploadException, IOException {
+        try {
+            return new FileItemIteratorImpl(this, ctx);
+        } catch (final FileUploadIOException e) {
+            // unwrap encapsulated SizeException
+            throw (FileUploadException) e.getCause();
+        }
+    }
+
+    /**
+     * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
+     * compliant {@code multipart/form-data} stream.
+     *
+     * @param ctx The context for the request to be parsed.
+     *
+     * @return A list of {@code FileItem} instances parsed from the
+     *         request, in the order that they were transmitted.
+     *
+     * @throws FileUploadException if there are problems reading/parsing
+     *                             the request or storing files.
+     */
+    public List<FileItem> parseRequest(final RequestContext ctx)
+            throws FileUploadException {
+        final List<FileItem> items = new ArrayList<>();
+        boolean successful = false;
+        try {
+            final FileItemIterator iter = getItemIterator(ctx);
+            final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
+                    "No FileItemFactory has been set.");
+            final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
+            while (iter.hasNext()) {
+                final FileItemStream item = iter.next();
+                // Don't use getName() here to prevent an InvalidFileNameException.
+                final String fileName = item.getName();
+                final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
+                                                   item.isFormField(), fileName);
+                items.add(fileItem);
+                try {
+                    Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
+                } catch (final FileUploadIOException e) {
+                    throw (FileUploadException) e.getCause();
+                } catch (final IOException e) {
+                    throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
+                                                           MULTIPART_FORM_DATA, e.getMessage()), e);
+                }
+                final FileItemHeaders fih = item.getHeaders();
+                fileItem.setHeaders(fih);
+            }
+            successful = true;
+            return items;
+        } catch (final FileUploadException e) {
+            throw e;
+        } catch (final IOException e) {
+            throw new FileUploadException(e.getMessage(), e);
+        } finally {
+            if (!successful) {
+                for (final FileItem fileItem : items) {
+                    try {
+                        fileItem.delete();
+                    } catch (final Exception ignored) {
+                        // ignored TODO perhaps add to tracker delete failure list somehow?
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
+     * compliant {@code multipart/form-data} stream.
+     *
+     * @param ctx The context for the request to be parsed.
+     *
+     * @return A map of {@code FileItem} instances parsed from the request.
+     *
+     * @throws FileUploadException if there are problems reading/parsing
+     *                             the request or storing files.
+     *
+     * @since 1.3
+     */
+    public Map<String, List<FileItem>> parseParameterMap(final RequestContext ctx)
+            throws FileUploadException {
+        final List<FileItem> items = parseRequest(ctx);
+        final Map<String, List<FileItem>> itemsMap = new HashMap<>(items.size());
+
+        for (final FileItem fileItem : items) {
+            final String fieldName = fileItem.getFieldName();
+            List<FileItem> mappedItems = itemsMap.computeIfAbsent(fieldName, k -> new ArrayList<>());
+
+            mappedItems.add(fileItem);
+        }
+
+        return itemsMap;
+    }
+
+    // ------------------------------------------------------ Protected methods
+
+    /**
+     * Retrieves the boundary from the {@code Content-type} header.
+     *
+     * @param contentType The value of the content type header from which to
+     *                    extract the boundary value.
+     *
+     * @return The boundary, as a byte array.
+     */
+    public byte[] getBoundary(final String contentType) {
+        final ParameterParser parser = new ParameterParser();
+        parser.setLowerCaseNames(true);
+        // Parameter parser can handle null input
+        final Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
+        final String boundaryStr = params.get("boundary");
+
+        if (boundaryStr == null) {
+            return null;
+        }
+        byte[] boundary;
+        boundary = boundaryStr.getBytes(StandardCharsets.ISO_8859_1);
+        return boundary;
+    }
+
+    /**
+     * Retrieves the file name from the {@code Content-disposition}
+     * header.
+     *
+     * @param headers The HTTP headers object.
+     *
+     * @return The file name for the current {@code encapsulation}.
+     */
+    public String getFileName(final FileItemHeaders headers) {
+        return getFileName(headers.getHeader(CONTENT_DISPOSITION));
+    }
+
+    /**
+     * Returns the given content-disposition headers file name.
+     * @param pContentDisposition The content-disposition headers value.
+     * @return The file name
+     */
+    private String getFileName(final String pContentDisposition) {
+        String fileName = null;
+        if (pContentDisposition != null) {
+            final String cdl = pContentDisposition.toLowerCase(Locale.ENGLISH);
+            if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
+                final ParameterParser parser = new ParameterParser();
+                parser.setLowerCaseNames(true);
+                // Parameter parser can handle null input
+                final Map<String, String> params = parser.parse(pContentDisposition, ';');
+                if (params.containsKey("filename")) {
+                    fileName = params.get("filename");
+                    if (fileName != null) {
+                        fileName = fileName.trim();
+                    } else {
+                        // Even if there is no value, the parameter is present,
+                        // so we return an empty file name rather than no file
+                        // name.
+                        fileName = "";
+                    }
+                }
+            }
+        }
+        return fileName;
+    }
+
+    /**
+     * Retrieves the field name from the {@code Content-disposition}
+     * header.
+     *
+     * @param headers A {@code Map} containing the HTTP request headers.
+     *
+     * @return The field name for the current {@code encapsulation}.
+     */
+    public String getFieldName(final FileItemHeaders headers) {
+        return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
+    }
+
+    /**
+     * Returns the field name, which is given by the content-disposition
+     * header.
+     * @param pContentDisposition The content-dispositions header value.
+     * @return The field jake
+     */
+    private String getFieldName(final String pContentDisposition) {
+        String fieldName = null;
+        if (pContentDisposition != null
+                && pContentDisposition.toLowerCase(Locale.ENGLISH).startsWith(FORM_DATA)) {
+            final ParameterParser parser = new ParameterParser();
+            parser.setLowerCaseNames(true);
+            // Parameter parser can handle null input
+            final Map<String, String> params = parser.parse(pContentDisposition, ';');
+            fieldName = params.get("name");
+            if (fieldName != null) {
+                fieldName = fieldName.trim();
+            }
+        }
+        return fieldName;
+    }
+
+    /**
+     * <p> Parses the {@code header-part} and returns as key/value
+     * pairs.
+     *
+     * <p> If there are multiple headers of the same names, the name
+     * will map to a comma-separated list containing the values.
+     *
+     * @param headerPart The {@code header-part} of the current
+     *                   {@code encapsulation}.
+     *
+     * @return A {@code Map} containing the parsed HTTP request headers.
+     */
+    public FileItemHeaders getParsedHeaders(final String headerPart) {
+        final int len = headerPart.length();
+        final FileItemHeadersImpl headers = newFileItemHeaders();
+        int start = 0;
+        for (;;) {
+            int end = parseEndOfLine(headerPart, start);
+            if (start == end) {
+                break;
+            }
+            final StringBuilder header = new StringBuilder(headerPart.substring(start, end));
+            start = end + 2;
+            while (start < len) {
+                int nonWs = start;
+                while (nonWs < len) {
+                    final char c = headerPart.charAt(nonWs);
+                    if (c != ' '  &&  c != '\t') {
+                        break;
+                    }
+                    ++nonWs;
+                }
+                if (nonWs == start) {
+                    break;
+                }
+                // Continuation line found
+                end = parseEndOfLine(headerPart, nonWs);
+                header.append(' ').append(headerPart, nonWs, end);
+                start = end + 2;
+            }
+            parseHeaderLine(headers, header.toString());
+        }
+        return headers;
+    }
+
+    /**
+     * Creates a new instance of {@link FileItemHeaders}.
+     * @return The new instance.
+     */
+    protected FileItemHeadersImpl newFileItemHeaders() {
+        return new FileItemHeadersImpl();
+    }
+
+    /**
+     * Skips bytes until the end of the current line.
+     * @param headerPart The headers, which are being parsed.
+     * @param end Index of the last byte, which has yet been
+     *   processed.
+     * @return Index of the \r\n sequence, which indicates
+     *   end of line.
+     */
+    private int parseEndOfLine(final String headerPart, final int end) {
+        int index = end;
+        for (;;) {
+            final int offset = headerPart.indexOf('\r', index);
+            if (offset == -1  ||  offset + 1 >= headerPart.length()) {
+                throw new IllegalStateException(
+                    "Expected headers to be terminated by an empty line.");
+            }
+            if (headerPart.charAt(offset + 1) == '\n') {
+                return offset;
+            }
+            index = offset + 1;
+        }
+    }
+
+    /**
+     * Reads the next header line.
+     * @param headers String with all headers.
+     * @param header Map where to store the current header.
+     */
+    private void parseHeaderLine(final FileItemHeadersImpl headers, final String header) {
+        final int colonOffset = header.indexOf(':');
+        if (colonOffset == -1) {
+            // This header line is malformed, skip it.
+            return;
+        }
+        final String headerName = header.substring(0, colonOffset).trim();
+        final String headerValue =
+            header.substring(colonOffset + 1).trim();
+        headers.addHeader(headerName, headerValue);
+    }
+
+    /**
+     * Returns the progress listener.
+     *
+     * @return The progress listener, if any, or null.
+     */
+    public ProgressListener getProgressListener() {
+        return listener;
+    }
+
+    /**
+     * Sets the progress listener.
+     *
+     * @param pListener The progress listener, if any. Defaults to null.
+     */
+    public void setProgressListener(final ProgressListener pListener) {
+        listener = pListener;
+    }
+
+}