You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2023/02/09 14:47:45 UTC

[tomcat] branch main updated: Add proxy and redirect error report valve

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 2c2a1bd248 Add proxy and redirect error report valve
2c2a1bd248 is described below

commit 2c2a1bd248b1ee200a0898c30e355802f0a6f7a6
Author: remm <re...@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                      |  62 +++++-
 5 files changed, 319 insertions(+), 14 deletions(-)

diff --git a/java/org/apache/catalina/valves/ErrorReportValve.java b/java/org/apache/catalina/valves/ErrorReportValve.java
index 365109ebf9..44548e1170 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 a253d54d72..670bd464e3 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -165,6 +165,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 d77425b6a7..541d527cd0 100644
--- a/webapps/docs/config/host.xml
+++ b/webapps/docs/config/host.xml
@@ -290,7 +290,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 7ee19c04f4..8172e28428 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -2179,7 +2179,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>
 
@@ -2203,6 +2203,66 @@
 
 </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">
 
   <subsection name="Introduction">


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