LoadBalancerDrainingValve.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.catalina.valves;

  18. import java.io.IOException;

  19. import javax.servlet.ServletException;
  20. import javax.servlet.SessionCookieConfig;
  21. import javax.servlet.http.Cookie;
  22. import javax.servlet.http.HttpServletResponse;

  23. import org.apache.catalina.connector.Request;
  24. import org.apache.catalina.connector.Response;
  25. import org.apache.catalina.util.SessionConfig;

  26. /**
  27.  * <p>
  28.  * A Valve to detect situations where a load-balanced node receiving a request has been deactivated by the load balancer
  29.  * (JK_LB_ACTIVATION=DIS) and the incoming request has no valid session.
  30.  * </p>
  31.  * <p>
  32.  * In these cases, the user's session cookie should be removed if it exists, any ";jsessionid" parameter should be
  33.  * removed from the request URI, and the client should be redirected to the same URI. This will cause the load-balanced
  34.  * to re-balance the client to another server.
  35.  * </p>
  36.  * <p>
  37.  * All this work is required because when the activation state of a node is DISABLED, the load-balancer will still send
  38.  * requests to the node if they appear to have a session on that node. Since mod_jk doesn't actually know whether the
  39.  * session id is valid, it will send the request blindly to the disabled node, which makes it take much longer to drain
  40.  * the node than strictly necessary.
  41.  * </p>
  42.  * <p>
  43.  * For testing purposes, a special cookie can be configured and used by a client to ignore the normal behavior of this
  44.  * Valve and allow a client to get a new session on a DISABLED node. See {@link #setIgnoreCookieName} and
  45.  * {@link #setIgnoreCookieValue} to configure those values.
  46.  * </p>
  47.  * <p>
  48.  * This Valve should be installed earlier in the Valve pipeline than any authentication valves, as the redirection
  49.  * should take place before an authentication valve would save a request to a protected resource.
  50.  * </p>
  51.  *
  52.  * @see <a href="https://tomcat.apache.org/connectors-doc/generic_howto/loadbalancers.html">Load balancer
  53.  *          documentation</a>
  54.  */
  55. public class LoadBalancerDrainingValve extends ValveBase {

  56.     /**
  57.      * The request attribute key where the load-balancer's activation state can be found.
  58.      */
  59.     public static final String ATTRIBUTE_KEY_JK_LB_ACTIVATION = "JK_LB_ACTIVATION";

  60.     /**
  61.      * The HTTP response code that will be used to redirect the request back to the load-balancer for re-balancing.
  62.      * Defaults to 307 (TEMPORARY_REDIRECT). HTTP status code 305 (USE_PROXY) might be an option, here. too.
  63.      */
  64.     private int _redirectStatusCode = HttpServletResponse.SC_TEMPORARY_REDIRECT;

  65.     /**
  66.      * The name of the cookie which can be set to ignore the "draining" action of this Filter. This will allow a client
  67.      * to contact the server without being re-balanced to another server. The expected cookie value can be set in the
  68.      * {@link #_ignoreCookieValue}. The cookie name and value must match to avoid being re-balanced.
  69.      */
  70.     private String _ignoreCookieName;

  71.     /**
  72.      * The value of the cookie which can be set to ignore the "draining" action of this Filter. This will allow a client
  73.      * to contact the server without being re-balanced to another server. The expected cookie name can be set in the
  74.      * {@link #_ignoreCookieName}. The cookie name and value must match to avoid being re-balanced.
  75.      */
  76.     private String _ignoreCookieValue;

  77.     public LoadBalancerDrainingValve() {
  78.         super(true); // Supports async
  79.     }

  80.     //
  81.     // Configuration parameters
  82.     //

  83.     /**
  84.      * Sets the HTTP response code that will be used to redirect the request back to the load-balancer for re-balancing.
  85.      * Defaults to 307 (TEMPORARY_REDIRECT).
  86.      *
  87.      * @param code The code to use for the redirect
  88.      */
  89.     public void setRedirectStatusCode(int code) {
  90.         _redirectStatusCode = code;
  91.     }

  92.     /**
  93.      * Gets the name of the cookie that can be used to override the re-balancing behavior of this Valve when the current
  94.      * node is in the DISABLED activation state.
  95.      *
  96.      * @return The cookie name used to ignore normal processing rules.
  97.      *
  98.      * @see #setIgnoreCookieValue
  99.      */
  100.     public String getIgnoreCookieName() {
  101.         return _ignoreCookieName;
  102.     }

  103.     /**
  104.      * Sets the name of the cookie that can be used to override the re-balancing behavior of this Valve when the current
  105.      * node is in the DISABLED activation state. There is no default value for this setting: the ability to override the
  106.      * re-balancing behavior of this Valve is <i>disabled</i> by default.
  107.      *
  108.      * @param cookieName The cookie name to use to ignore normal processing rules.
  109.      *
  110.      * @see #getIgnoreCookieValue
  111.      */
  112.     public void setIgnoreCookieName(String cookieName) {
  113.         _ignoreCookieName = cookieName;
  114.     }

  115.     /**
  116.      * Gets the expected value of the cookie that can be used to override the re-balancing behavior of this Valve when
  117.      * the current node is in the DISABLED activation state.
  118.      *
  119.      * @return The cookie value used to ignore normal processing rules.
  120.      *
  121.      * @see #setIgnoreCookieValue
  122.      */
  123.     public String getIgnoreCookieValue() {
  124.         return _ignoreCookieValue;
  125.     }

  126.     /**
  127.      * Sets the expected value of the cookie that can be used to override the re-balancing behavior of this Valve when
  128.      * the current node is in the DISABLED activation state. The "ignore" cookie's value <b>must</b> be exactly equal to
  129.      * this value in order to allow the client to override the re-balancing behavior.
  130.      *
  131.      * @param cookieValue The cookie value to use to ignore normal processing rules.
  132.      *
  133.      * @see #getIgnoreCookieValue
  134.      */
  135.     public void setIgnoreCookieValue(String cookieValue) {
  136.         _ignoreCookieValue = cookieValue;
  137.     }

  138.     @Override
  139.     public void invoke(Request request, Response response) throws IOException, ServletException {
  140.         if ("DIS".equals(request.getAttribute(ATTRIBUTE_KEY_JK_LB_ACTIVATION)) &&
  141.                 !request.isRequestedSessionIdValid()) {

  142.             if (containerLog.isDebugEnabled()) {
  143.                 containerLog.debug(sm.getString("loadBalancerDrainingValve.draining"));
  144.             }

  145.             boolean ignoreRebalance = false;
  146.             Cookie sessionCookie = null;

  147.             final Cookie[] cookies = request.getCookies();

  148.             final String sessionCookieName = SessionConfig.getSessionCookieName(request.getContext());

  149.             if (null != cookies) {
  150.                 for (Cookie cookie : cookies) {
  151.                     final String cookieName = cookie.getName();
  152.                     if (containerLog.isTraceEnabled()) {
  153.                         containerLog.trace("Checking cookie " + cookieName + "=" + cookie.getValue());
  154.                     }

  155.                     if (sessionCookieName.equals(cookieName) &&
  156.                             request.getRequestedSessionId().equals(cookie.getValue())) {
  157.                         sessionCookie = cookie;
  158.                     } else if (null != _ignoreCookieName && _ignoreCookieName.equals(cookieName) &&
  159.                             null != _ignoreCookieValue && _ignoreCookieValue.equals(cookie.getValue())) {
  160.                         // The client presenting a valid ignore-cookie value?
  161.                         ignoreRebalance = true;
  162.                     }
  163.                 }
  164.             }

  165.             if (ignoreRebalance) {
  166.                 if (containerLog.isDebugEnabled()) {
  167.                     containerLog.debug(sm.getString("loadBalancerDrainingValve.skip", _ignoreCookieName));
  168.                 }

  169.                 getNext().invoke(request, response);

  170.                 return;
  171.             }

  172.             // Kill any session cookie that was found
  173.             // TODO: Consider implications of SSO cookies
  174.             if (null != sessionCookie) {
  175.                 sessionCookie.setPath(SessionConfig.getSessionCookiePath(request.getContext()));
  176.                 sessionCookie.setMaxAge(0); // Delete
  177.                 sessionCookie.setValue(""); // Purge the cookie's value
  178.                 // Replicate logic used to set secure attribute for session cookies
  179.                 SessionCookieConfig sessionCookieConfig =
  180.                         request.getContext().getServletContext().getSessionCookieConfig();
  181.                 sessionCookie.setSecure(request.isSecure() || sessionCookieConfig.isSecure());
  182.                 response.addCookie(sessionCookie);
  183.             }

  184.             // Re-write the URI if it contains a ;jsessionid parameter
  185.             String uri = request.getRequestURI();
  186.             String sessionURIParamName = SessionConfig.getSessionUriParamName(request.getContext());
  187.             if (uri.contains(";" + sessionURIParamName + "=")) {
  188.                 uri = uri.replaceFirst(";" + sessionURIParamName + "=[^&?]*", "");
  189.             }

  190.             String queryString = request.getQueryString();

  191.             if (null != queryString) {
  192.                 uri = uri + "?" + queryString;
  193.             }

  194.             // NOTE: Do not call response.encodeRedirectURL or the bad
  195.             // sessionid will be restored
  196.             response.setHeader("Location", uri);
  197.             response.setStatus(_redirectStatusCode);
  198.         } else {
  199.             getNext().invoke(request, response);
  200.         }
  201.     }
  202. }