1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
/*
* 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 jakarta.servlet.ServletException;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.SessionConfig;
/**
* <p>
* A Valve to detect situations where a load-balanced node receiving a request has been deactivated by the load balancer
* (JK_LB_ACTIVATION=DIS) and the incoming request has no valid session.
* </p>
* <p>
* In these cases, the user's session cookie should be removed if it exists, any ";jsessionid" parameter should be
* removed from the request URI, and the client should be redirected to the same URI. This will cause the load-balanced
* to re-balance the client to another server.
* </p>
* <p>
* All this work is required because when the activation state of a node is DISABLED, the load-balancer will still send
* requests to the node if they appear to have a session on that node. Since mod_jk doesn't actually know whether the
* session id is valid, it will send the request blindly to the disabled node, which makes it take much longer to drain
* the node than strictly necessary.
* </p>
* <p>
* For testing purposes, a special cookie can be configured and used by a client to ignore the normal behavior of this
* Valve and allow a client to get a new session on a DISABLED node. See {@link #setIgnoreCookieName} and
* {@link #setIgnoreCookieValue} to configure those values.
* </p>
* <p>
* This Valve should be installed earlier in the Valve pipeline than any authentication valves, as the redirection
* should take place before an authentication valve would save a request to a protected resource.
* </p>
*
* @see <a href="https://tomcat.apache.org/connectors-doc/generic_howto/loadbalancers.html">Load balancer
* documentation</a>
*/
public class LoadBalancerDrainingValve extends ValveBase {
/**
* The request attribute key where the load-balancer's activation state can be found.
*/
public static final String ATTRIBUTE_KEY_JK_LB_ACTIVATION = "JK_LB_ACTIVATION";
/**
* The HTTP response code that will be used to redirect the request back to the load-balancer for re-balancing.
* Defaults to 307 (TEMPORARY_REDIRECT). HTTP status code 305 (USE_PROXY) might be an option, here. too.
*/
private int _redirectStatusCode = HttpServletResponse.SC_TEMPORARY_REDIRECT;
/**
* The name of the cookie which can be set to ignore the "draining" action of this Filter. This will allow a client
* to contact the server without being re-balanced to another server. The expected cookie value can be set in the
* {@link #_ignoreCookieValue}. The cookie name and value must match to avoid being re-balanced.
*/
private String _ignoreCookieName;
/**
* The value of the cookie which can be set to ignore the "draining" action of this Filter. This will allow a client
* to contact the server without being re-balanced to another server. The expected cookie name can be set in the
* {@link #_ignoreCookieName}. The cookie name and value must match to avoid being re-balanced.
*/
private String _ignoreCookieValue;
public LoadBalancerDrainingValve() {
super(true); // Supports async
}
//
// Configuration parameters
//
/**
* Sets the HTTP response code that will be used to redirect the request back to the load-balancer for re-balancing.
* Defaults to 307 (TEMPORARY_REDIRECT).
*
* @param code The code to use for the redirect
*/
public void setRedirectStatusCode(int code) {
_redirectStatusCode = code;
}
/**
* Gets the name of the cookie that can be used to override the re-balancing behavior of this Valve when the current
* node is in the DISABLED activation state.
*
* @return The cookie name used to ignore normal processing rules.
*
* @see #setIgnoreCookieValue
*/
public String getIgnoreCookieName() {
return _ignoreCookieName;
}
/**
* Sets the name of the cookie that can be used to override the re-balancing behavior of this Valve when the current
* node is in the DISABLED activation state. There is no default value for this setting: the ability to override the
* re-balancing behavior of this Valve is <i>disabled</i> by default.
*
* @param cookieName The cookie name to use to ignore normal processing rules.
*
* @see #getIgnoreCookieValue
*/
public void setIgnoreCookieName(String cookieName) {
_ignoreCookieName = cookieName;
}
/**
* Gets the expected value of the cookie that can be used to override the re-balancing behavior of this Valve when
* the current node is in the DISABLED activation state.
*
* @return The cookie value used to ignore normal processing rules.
*
* @see #setIgnoreCookieValue
*/
public String getIgnoreCookieValue() {
return _ignoreCookieValue;
}
/**
* Sets the expected value of the cookie that can be used to override the re-balancing behavior of this Valve when
* the current node is in the DISABLED activation state. The "ignore" cookie's value <b>must</b> be exactly equal to
* this value in order to allow the client to override the re-balancing behavior.
*
* @param cookieValue The cookie value to use to ignore normal processing rules.
*
* @see #getIgnoreCookieValue
*/
public void setIgnoreCookieValue(String cookieValue) {
_ignoreCookieValue = cookieValue;
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if ("DIS".equals(request.getAttribute(ATTRIBUTE_KEY_JK_LB_ACTIVATION)) &&
!request.isRequestedSessionIdValid()) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("loadBalancerDrainingValve.draining"));
}
boolean ignoreRebalance = false;
Cookie sessionCookie = null;
final Cookie[] cookies = request.getCookies();
final String sessionCookieName = SessionConfig.getSessionCookieName(request.getContext());
if (null != cookies) {
for (Cookie cookie : cookies) {
final String cookieName = cookie.getName();
if (containerLog.isTraceEnabled()) {
containerLog.trace("Checking cookie " + cookieName + "=" + cookie.getValue());
}
if (sessionCookieName.equals(cookieName) &&
request.getRequestedSessionId().equals(cookie.getValue())) {
sessionCookie = cookie;
} else if (null != _ignoreCookieName && _ignoreCookieName.equals(cookieName) &&
null != _ignoreCookieValue && _ignoreCookieValue.equals(cookie.getValue())) {
// The client presenting a valid ignore-cookie value?
ignoreRebalance = true;
}
}
}
if (ignoreRebalance) {
if (containerLog.isDebugEnabled()) {
containerLog.debug(sm.getString("loadBalancerDrainingValve.skip", _ignoreCookieName));
}
getNext().invoke(request, response);
return;
}
// Kill any session cookie that was found
// TODO: Consider implications of SSO cookies
if (null != sessionCookie) {
sessionCookie.setPath(SessionConfig.getSessionCookiePath(request.getContext()));
sessionCookie.setMaxAge(0); // Delete
sessionCookie.setValue(""); // Purge the cookie's value
// Replicate logic used to set secure attribute for session cookies
SessionCookieConfig sessionCookieConfig =
request.getContext().getServletContext().getSessionCookieConfig();
sessionCookie.setSecure(request.isSecure() || sessionCookieConfig.isSecure());
response.addCookie(sessionCookie);
}
// Re-write the URI if it contains a ;jsessionid parameter
String uri = request.getRequestURI();
String sessionURIParamName = SessionConfig.getSessionUriParamName(request.getContext());
if (uri.contains(";" + sessionURIParamName + "=")) {
uri = uri.replaceFirst(";" + sessionURIParamName + "=[^&?]*", "");
}
String queryString = request.getQueryString();
if (null != queryString) {
uri = uri + "?" + queryString;
}
// NOTE: Do not call response.encodeRedirectURL or the bad
// sessionid will be restored
response.setHeader("Location", uri);
response.setStatus(_redirectStatusCode);
} else {
getNext().invoke(request, response);
}
}
}
|