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

remm pushed a commit to branch 8.5.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 54a40e6ed8459f24f7e45b941f40aaf7f5d9eeb3
Author: remm <r...@apache.org>
AuthorDate: Thu Feb 9 15:45:43 2023 +0100

    Add proxy and redirect error report valve
    
    Although this could be merged in the default valve, I prefer keeping it
    separate. The code duplication is minimal after refactoring.
    Keep the ability to use a properties file as an option for that (with
    URLs, the server.xml could become very verbose very fast), although it
    will default to the status code configuration from the superclass.
    Based on PR#506 submitted by Max Fortun.
---
 .../apache/catalina/valves/ErrorReportValve.java   |  36 ++--
 .../catalina/valves/ProxyErrorReportValve.java     | 227 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   5 +
 webapps/docs/config/host.xml                       |   3 +-
 webapps/docs/config/valve.xml                      |  61 +++++-
 5 files changed, 318 insertions(+), 14 deletions(-)

diff --git a/java/org/apache/catalina/valves/ErrorReportValve.java 
b/java/org/apache/catalina/valves/ErrorReportValve.java
index 42ae752c77..f950785aed 100644
--- a/java/org/apache/catalina/valves/ErrorReportValve.java
+++ b/java/org/apache/catalina/valves/ErrorReportValve.java
@@ -151,6 +151,29 @@ public class ErrorReportValve extends ValveBase {
     // ------------------------------------------------------ Protected Methods
 
 
+    /**
+     * Return the error page associated with the specified status and 
exception.
+     *
+     * @param statusCode the status code
+     * @param throwable the exception
+     * @return the associated error page
+     */
+    protected ErrorPage findErrorPage(int statusCode, Throwable throwable) {
+        ErrorPage errorPage = null;
+        if (throwable != null) {
+            errorPage = errorPageSupport.find(throwable);
+        }
+        if (errorPage == null) {
+            errorPage = errorPageSupport.find(statusCode);
+        }
+        if (errorPage == null) {
+            // Default error page
+            errorPage = errorPageSupport.find(0);
+        }
+        return errorPage;
+    }
+
+
     /**
      * Prints out an error report.
      *
@@ -179,18 +202,7 @@ public class ErrorReportValve extends ValveBase {
             return;
         }
 
-        ErrorPage errorPage = null;
-        if (throwable != null) {
-            errorPage = errorPageSupport.find(throwable);
-        }
-        if (errorPage == null) {
-            errorPage = errorPageSupport.find(statusCode);
-        }
-        if (errorPage == null) {
-            // Default error page
-            errorPage = errorPageSupport.find(0);
-        }
-
+        ErrorPage errorPage = findErrorPage(statusCode, throwable);
 
         if (errorPage != null) {
             if (sendErrorPage(errorPage.getLocation(), response)) {
diff --git a/java/org/apache/catalina/valves/ProxyErrorReportValve.java 
b/java/org/apache/catalina/valves/ProxyErrorReportValve.java
new file mode 100644
index 0000000000..0132c83a49
--- /dev/null
+++ b/java/org/apache/catalina/valves/ProxyErrorReportValve.java
@@ -0,0 +1,227 @@
+/*
+ * 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.valves;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.coyote.ActionCode;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.descriptor.web.ErrorPage;
+import org.apache.tomcat.util.http.fileupload.IOUtils;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * <p>
+ * Implementation of a Valve that proxies or redirects error reporting to 
other urls.
+ * </p>
+ * <p>
+ * This Valve should be attached at the Host level, although it will work if 
attached to a Context.
+ * </p>
+ */
+public class ProxyErrorReportValve extends ErrorReportValve {
+    private static final Log log = 
LogFactory.getLog(ProxyErrorReportValve.class);
+
+    /**
+     * Use a redirect or proxy the response to the specified location. Default 
to redirect.
+     */
+    protected boolean useRedirect = true;
+
+    /**
+     * @return the useRedirect
+     */
+    public boolean getUseRedirect() {
+        return this.useRedirect;
+    }
+
+    /**
+     * @param useRedirect the useRedirect to set
+     */
+    public void setUseRedirect(boolean useRedirect) {
+        this.useRedirect = useRedirect;
+    }
+
+    /**
+     * Use a properties file for the URLs.
+     */
+    protected boolean usePropertiesFile = false;
+
+    /**
+     * @return the usePropertiesFile
+     */
+    public boolean getUsePropertiesFile() {
+        return this.usePropertiesFile;
+    }
+
+    /**
+     * @param usePropertiesFile the usePropertiesFile to set
+     */
+    public void setUsePropertiesFile(boolean usePropertiesFile) {
+        this.usePropertiesFile = usePropertiesFile;
+    }
+
+    private String getRedirectUrl(Response response) {
+        ResourceBundle resourceBundle = 
ResourceBundle.getBundle(this.getClass().getSimpleName(),
+                response.getLocale());
+        String redirectUrl = null;
+        try {
+            redirectUrl = 
resourceBundle.getString(Integer.toString(response.getStatus()));
+        } catch (MissingResourceException e) {
+            // Ignore
+        }
+        if (redirectUrl == null) {
+            try {
+                redirectUrl = resourceBundle.getString(Integer.toString(0));
+            } catch (MissingResourceException ex) {
+                // Ignore
+            }
+        }
+        return redirectUrl;
+    }
+
+    @Override
+    protected void report(Request request, Response response, Throwable 
throwable) {
+
+        int statusCode = response.getStatus();
+
+        // Do nothing on a 1xx, 2xx and 3xx status
+        // Do nothing if anything has been written already
+        // Do nothing if the response hasn't been explicitly marked as in error
+        //    and that error has not been reported.
+        if (statusCode < 400 || response.getContentWritten() > 0) {
+            return;
+        }
+
+        // If an error has occurred that prevents further I/O, don't waste time
+        // producing an error report that will never be read
+        AtomicBoolean result = new AtomicBoolean(false);
+        response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
+        if (!result.get()) {
+            return;
+        }
+
+        String urlString = null;
+        if (usePropertiesFile) {
+            urlString = getRedirectUrl(response);
+        } else {
+            ErrorPage errorPage = findErrorPage(statusCode, throwable);
+            if (errorPage != null) {
+                urlString = errorPage.getLocation();
+            }
+        }
+        if (urlString == null) {
+            super.report(request, response, throwable);
+            return;
+        }
+
+        // No need to delegate anymore
+        if (!response.setErrorReported()) {
+            return;
+        }
+
+        StringBuilder stringBuilder = new StringBuilder(urlString);
+        if (urlString.indexOf("?") > -1) {
+            stringBuilder.append("&");
+        } else {
+            stringBuilder.append("?");
+        }
+        stringBuilder.append("requestUri=");
+        stringBuilder.append(URLEncoder.encode(request.getDecodedRequestURI(), 
request.getConnector().getURICharset()));
+        stringBuilder.append("&statusCode=");
+        stringBuilder.append(URLEncoder.encode(String.valueOf(statusCode), 
StandardCharsets.UTF_8));
+
+        String reason = null;
+        String description = null;
+        StringManager smClient = StringManager.getManager(Constants.Package, 
request.getLocales());
+        response.setLocale(smClient.getLocale());
+        try {
+            reason = smClient.getString("http." + statusCode + ".reason");
+            description = smClient.getString("http." + statusCode + ".desc");
+        } catch (Throwable t) {
+            ExceptionUtils.handleThrowable(t);
+        }
+        if (reason == null || description == null) {
+            reason = smClient.getString("errorReportValve.unknownReason");
+            description = smClient.getString("errorReportValve.noDescription");
+        }
+        stringBuilder.append("&statusDescription=");
+        stringBuilder.append(URLEncoder.encode(description, 
StandardCharsets.UTF_8));
+        stringBuilder.append("&statusReason=");
+        stringBuilder.append(URLEncoder.encode(reason, 
StandardCharsets.UTF_8));
+
+        String message = response.getMessage();
+        if (message != null) {
+            stringBuilder.append("&message=");
+            stringBuilder.append(URLEncoder.encode(message, 
StandardCharsets.UTF_8));
+        }
+        if (throwable != null) {
+            stringBuilder.append("&throwable=");
+            stringBuilder.append(URLEncoder.encode(throwable.toString(), 
StandardCharsets.UTF_8));
+        }
+
+        urlString = stringBuilder.toString();
+        if (useRedirect) {
+            if (log.isTraceEnabled()) {
+                log.trace("Redirecting error reporting to " + urlString);
+            }
+            try {
+                response.sendRedirect(urlString);
+            } catch (IOException e) {
+                // Ignore
+            }
+        } else {
+            if (log.isTraceEnabled()) {
+                log.trace("Proxying error reporting to " + urlString);
+            }
+            HttpURLConnection httpURLConnection = null;
+            try {
+                URL url = (new URI(urlString)).toURL();
+                httpURLConnection = (HttpURLConnection) url.openConnection();
+                httpURLConnection.connect();
+                response.setContentType(httpURLConnection.getContentType());
+                
response.setContentLength(httpURLConnection.getContentLength());
+                OutputStream outputStream = response.getOutputStream();
+                InputStream inputStream = url.openStream();
+                IOUtils.copy(inputStream, outputStream);
+            } catch (URISyntaxException | IOException e) {
+                if (log.isDebugEnabled()) {
+                    log.debug("Proxy error to " + urlString, e);
+                }
+                // Ignore
+            } finally {
+                if (httpURLConnection != null) {
+                    httpURLConnection.disconnect();
+                }
+            }
+        }
+    }
+}
+
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index f8aabefb51..8bb1f17c46 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -116,6 +116,11 @@
         <code>String.replace()</code> where regular expressions where not being
         used. Pull request <pr>581</pr> provided by Andrei Briukhov. (markt)
       </fix>
+      <add>
+        Add error report valve that allows redirecting to of proxying from an
+        external web server. Based on code and ideas from pull request
+        <pr>506</pr> provided by Max Fortun. (remm)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/host.xml b/webapps/docs/config/host.xml
index 687e46c48a..4b112e70ca 100644
--- a/webapps/docs/config/host.xml
+++ b/webapps/docs/config/host.xml
@@ -277,7 +277,8 @@
         implement the
         <code>org.apache.catalina.Valve</code> interface. If none is specified,
         the value <code>org.apache.catalina.valves.ErrorReportValve</code>
-        will be used by default.</p>
+        will be used by default. if set to an empty string, the error report
+        will be disabled.</p>
       </attribute>
 
       <attribute name="unpackWARs" required="false">
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index ce72d991b7..b8cac337a6 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -2181,7 +2181,7 @@
       for HTTP status codes that will return Json error messages.</p>
 
     <p>By specifying this class in <code>errorReportValveClass</code> attribute
-      in <code>HOST</code>, it will be used instead of
+      in <code>Host</code>, it will be used instead of
       <code>ErrorReportValve</code> and will return JSON response instead of 
HTML.
     </p>
 
@@ -2205,6 +2205,65 @@
 
 </section>
 
+<section name="Proxy Error Report Valve">
+
+  <subsection name="Introduction">
+
+    <p>The <strong>Proxy Error Report Valve</strong> is a simple error handler
+    for HTTP status codes that will redirect or proxy to another location
+    responsible for the generation of the error report.</p>
+
+    <p>By specifying this class in <code>errorReportValveClass</code> attribute
+    in <code>Host</code>, it will be used instead of
+    <code>ErrorReportValve</code> with the default attribute values. To
+    configure the attributes, the valve can be defined nested in the
+    <code>Host</code> element.</p>
+
+  </subsection>
+
+  <subsection name="Attributes">
+
+    <p>The <strong>Proxy Error Report Valve</strong> supports the following
+    configuration attributes:</p>
+
+    <attributes>
+
+      <attribute name="className" required="true">
+        <p>Java class name of the implementation to use.  This MUST be set to
+        <strong>org.apache.catalina.valves.ProxyErrorReportValve</strong>.</p>
+      </attribute>
+
+      <attribute name="usePropertiesFile" required="false">
+        <p>If <code>true</code>, the valve will use the properties file
+        described below to associate the URLs with the status code.
+        If <code>false</code>, the configuration mechanism of the default
+        <code>ErrorReportValve</code> will be used instead. The default
+        value is <code>false</code>.</p>
+      </attribute>
+
+      <attribute name="useRedirect" required="false">
+        <p>If <code>true</code>, the valve will send a redirect to the URL.
+        If <code>false</code>, the valve will instead proxy the content from
+        the specified URL. The default value is <code>true</code>.</p>
+      </attribute>
+
+    </attributes>
+
+  </subsection>
+
+  <subsection name="Configuration">
+
+    <p>The <strong>Proxy Error Report Valve</strong> can use a resource file
+    <strong>ProxyErrorReportValve.properties</strong>
+    from the class path, where each entry is a statusCode=baseUrl. baseUrl
+    should not include any url parameters, statusCode, statusDescription,
+    requestUri, and throwable which will be automatically appended. A special
+    key named <code>0</code> should be used to match any other unmapped
+    code to a redirect or proxy URL.</p>
+
+  </subsection>
+
+</section>
 
 <section name="Crawler Session Manager Valve">
 


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to