-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 All,
Any objections to me back-porting this (and r1799677 as well) at least back to 8.0? Thanks, - -chris On 6/21/17 3:05 PM, schu...@apache.org wrote: > Author: schultz Date: Wed Jun 21 19:05:38 2017 New Revision: > 1799498 > > URL: http://svn.apache.org/viewvc?rev=1799498&view=rev Log: Add > LoadBalancerDrainingValve. > > Added: > tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve .java > (with props) > tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV alve.java > (with props) Modified: tomcat/trunk/webapps/docs/changelog.xml > tomcat/trunk/webapps/docs/config/valve.xml > > Added: > tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve .java > > URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valve s/LoadBalancerDrainingValve.java?rev=1799498&view=auto > ====================================================================== ======== > > - --- tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve.j ava (added) > +++ > tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve .java > Wed Jun 21 19:05:38 2017 @@ -0,0 +1,277 @@ +/* + * 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 javax.servlet.ServletException; > +import javax.servlet.http.Cookie; +import > javax.servlet.http.HttpServletResponse; + +import > org.apache.catalina.LifecycleException; +import > org.apache.catalina.connector.Request; +import > org.apache.catalina.connector.Response; +import > org.apache.catalina.util.SessionConfig; +import > org.apache.juli.logging.Log; + +/** + * <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>A request parameter is added to > the redirect URI in order to avoid + * repeated redirects in the > event of an error or misconfiguration.</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 > http://tomcat.apache.org/connectors-doc/generic_howto/loadbalancers.ht ml > > + */ > +public class LoadBalancerDrainingValve + extends ValveBase +{ + > /** + * The request attribute key where the load-balancer's > activation state + * can be found. + */ + 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 #_ignoreCookieValue}. The cookie name > and value must match + * to avoid being re-balanced. + */ + > private String _ignoreCookieValue; + + /** + * Local > reference to the container log. + */ + protected Log > containerLog = null; + + 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). + */ + 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 initInternal() + throws LifecycleException + > { + super.initInternal(); + + containerLog = > getContainer().getLogger(); + } + + @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("Load-balancer is in DISABLED state; draining > this node"); + + boolean ignoreRebalance = false; // > Allow certain clients + Cookie sessionCookie = null; + + > // Kill any session cookie present + final Cookie[] > cookies = request.getCookies(); + + final String > sessionCookieName = > request.getServletContext().getSessionCookieConfig().getName(); + + > // Kill any session cookie present + 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 + > // Is the client presenting a valid ignore-cookie value? + > if(null != _ignoreCookieName + && > _ignoreCookieName.equals(cookieName) + > && null != _ignoreCookieValue + && > _ignoreCookieValue.equals(cookie.getValue())) { + > ignoreRebalance = true; + } + } + > } + + if(ignoreRebalance) { + > if(containerLog.isDebugEnabled()) + > containerLog.debug("Client is presenting a valid " + > _ignoreCookieName + + " cookie, > re-balancing is being skipped"); + + > getNext().invoke(request, response); + + return; + > } + + // Kill any session cookie that was found + > // TODO: Consider implications of SSO cookies + if(null > != sessionCookie) { + String cookiePath = > request.getServletContext().getSessionCookieConfig().getPath(); + + > if(request.getContext().getSessionCookiePathUsesTrailingSlash()) { > + // Handle special case of ROOT context where > cookies require a path of + // '/' but the > servlet spec uses an empty string + // Also > ensure the cookies for a context with a path of /foo don't get + > // sent for requests with a path of /foobar + if > (!cookiePath.endsWith("/")) + cookiePath = > cookiePath + "/"; + + > sessionCookie.setPath(cookiePath); + > sessionCookie.setMaxAge(0); // Delete + > sessionCookie.setValue(""); // Purge the cookie's value + > response.addCookie(sessionCookie); + } + > } + + // Re-write the URI if it contains a ;jsessionid > parameter + String uri = request.getRequestURI(); + > String sessionURIParamName = "jsessionid"; + > 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); + } +} > > Propchange: > tomcat/trunk/java/org/apache/catalina/valves/LoadBalancerDrainingValve .java > > - ------------------------------------------------------------------------ - ------ > svn:eol-style = native > > Added: > tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV alve.java > > URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/valve s/TestLoadBalancerDrainingValve.java?rev=1799498&view=auto > ====================================================================== ======== > > - --- tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingVal ve.java (added) > +++ > tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV alve.java > Wed Jun 21 19:05:38 2017 @@ -0,0 +1,257 @@ +/* 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.util.ArrayList; +import java.util.List; + +import > javax.servlet.ServletContext; +import > javax.servlet.SessionCookieConfig; +import > javax.servlet.http.Cookie; + +import org.junit.Test; + +import > org.apache.catalina.Context; +import org.apache.catalina.Valve; > +import org.apache.catalina.connector.Request; +import > org.apache.catalina.connector.Response; +import > org.apache.catalina.core.StandardPipeline; +import > org.easymock.EasyMock; +import org.easymock.IMocksControl; + > +public class TestLoadBalancerDrainingValve { + + static class > MockResponse extends Response { + private List<Cookie> > cookies; + @Override + public boolean isCommitted() > { + return false; + } + @Override + > public void addCookie(Cookie cookie) + { + > if(null == cookies) + cookies = new > ArrayList<Cookie>(1); + cookies.add(cookie); + } > + public List<Cookie> getCookies() { + return > cookies; + } + } + + static class CookieConfig > implements SessionCookieConfig { + + private String name; + > private String domain; + private String path; + > private String comment; + private boolean httpOnly; + > private boolean secure; + private int maxAge; + + > @Override + public String getName() { + return > name; + } + @Override + public void > setName(String name) { + this.name = name; + } + > @Override + public String getDomain() { + return > domain; + } + @Override + public void > setDomain(String domain) { + this.domain = domain; + > } + @Override + public String getPath() { + > return path; + } + @Override + public void > setPath(String path) { + this.path = path; + } + > @Override + public String getComment() { + return > comment; + } + @Override + public void > setComment(String comment) { + this.comment = comment; + > } + @Override + public boolean isHttpOnly() { + > return httpOnly; + } + @Override + public void > setHttpOnly(boolean httpOnly) { + this.httpOnly = > httpOnly; + } + @Override + public boolean > isSecure() { + return secure; + } + > @Override + public void setSecure(boolean secure) { + > this.secure = secure; + } + @Override + public > int getMaxAge() { + return maxAge; + } + > @Override + public void setMaxAge(int maxAge) { + > this.maxAge = maxAge; + } + } + + // A Cookie subclass > that knows how to compare itself to other Cookie objects + > static class MyCookie extends Cookie { + public > MyCookie(String name, String value) { super(name, value); } + + > @Override + public boolean equals(Object o) { + if(null > == o) return false; + MyCookie mc = (MyCookie)o; + + > return mc.getName().equals(this.getName()) + && > mc.getPath().equals(this.getPath()) + && > mc.getValue().equals(this.getValue()) + && > mc.getMaxAge() == this.getMaxAge(); + } + + @Override + > public String toString() { + return "Cookie { name=" + > getName() + ", value=" + getValue() + ", path=" + getPath() + ", > maxAge=" + getMaxAge() + " }"; + } + } + + @Test + > public void testNormalRequest() throws Exception { + > runValve("ACT", true, true, false, null); + } + + @Test + > public void testDisabledValidSession() throws Exception { + > runValve("DIS", true, true, false, null); + } + + @Test + > public void testDisabledInvalidSession() throws Exception { + > runValve("DIS", false, false, false, "foo=bar"); + } + + > @Test + public void testDisabledInvalidSessionWithIgnore() > throws Exception { + runValve("DIS", false, true, true, > "foo=bar"); + } + + private void runValve(String > jkActivation, + boolean validSessionId, + > boolean expectInvokeNext, + boolean > enableIgnore, + String queryString) throws > Exception { + IMocksControl control = > EasyMock.createControl(); + ServletContext servletContext = > control.createMock(ServletContext.class); + Context ctx = > control.createMock(Context.class); + Request request = > control.createMock(Request.class); + Response response = > control.createMock(Response.class); + + String > sessionCookieName = "JSESSIONID"; + String sessionId = > "cafebabe"; + String requestURI = "/test/path"; + > SessionCookieConfig cookieConfig = new CookieConfig(); + > cookieConfig.setDomain("example.com"); + > cookieConfig.setName(sessionCookieName); + > cookieConfig.setPath("/"); + + // Valve.init requires all of > this stuff + > EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn(""); + > EasyMock.expect(ctx.getName()).andStubReturn(""); + > EasyMock.expect(ctx.getPipeline()).andStubReturn(new > StandardPipeline()); + > EasyMock.expect(ctx.getDomain()).andStubReturn("foo"); + > EasyMock.expect(ctx.getLogger()).andStubReturn(org.apache.juli.logging .LogFactory.getLog(LoadBalancerDrainingValve.class)); > > + EasyMock.expect(ctx.getServletContext()).andStubReturn(servletContext); > + + // Set up the actual test + > EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.ATTRIBU TE_KEY_JK_LB_ACTIVATION)).andStubReturn(jkActivation); > > + EasyMock.expect(request.isRequestedSessionIdValid()).andStubReturn(valid SessionId); > + + ArrayList<Cookie> cookies = new ArrayList<Cookie>(); + > if(enableIgnore) { + cookies.add(new Cookie("ignore", > "true")); + } + + if(!validSessionId) { + > MyCookie cookie = new MyCookie(cookieConfig.getName(), sessionId); > + cookie.setPath(cookieConfig.getPath()); + > cookie.setValue(sessionId); + + cookies.add(cookie); + + > EasyMock.expect(request.getRequestedSessionId()).andStubReturn(session Id); > > + EasyMock.expect(request.getRequestURI()).andStubReturn(requestURI); > + > EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(ne w > Cookie[cookies.size()])); + > EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn (cookieConfig); > > + EasyMock.expect(request.getServletContext()).andStubReturn(servletContex t); > + > EasyMock.expect(request.getContext()).andStubReturn(ctx); + > EasyMock.expect(ctx.getSessionCookiePathUsesTrailingSlash()).andStubRe turn(true); > > + EasyMock.expect(servletContext.getSessionCookieConfig()).andStubReturn(c ookieConfig); > + > EasyMock.expect(request.getQueryString()).andStubReturn(queryString); > > + > + if(!enableIgnore) { + // Response will > have cookie deleted + MyCookie expectedCookie = new > MyCookie(cookieConfig.getName(), ""); + > expectedCookie.setPath(cookieConfig.getPath()); + > expectedCookie.setMaxAge(0); + + // These two lines > just mean EasyMock.expect(response.addCookie) but for a void > method + response.addCookie(expectedCookie); + > EasyMock.expect(ctx.getSessionCookieName()).andReturn(sessionCookieNam e); > // Indirect call + String expectedRequestURI = > requestURI; + if(null != queryString) + > expectedRequestURI = expectedRequestURI + '?' + queryString; + > response.setHeader("Location", expectedRequestURI); + > response.setStatus(307); + } + } + + Valve > next = control.createMock(Valve.class); + + > if(expectInvokeNext) { + // Expect the "next" Valve to > fire + // Next 2 lines are basically > EasyMock.expect(next.invoke(req,res)) but for a void method + > next.invoke(request, response); + > EasyMock.expectLastCall(); + } + + // Get set to > actually test + control.replay(); + + > LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve(); > + valve.setContainer(ctx); + valve.init(); + > valve.setNext(next); + valve.setIgnoreCookieName("ignore"); > + valve.setIgnoreCookieValue("true"); + + > valve.invoke(request, response); + + control.verify(); + > } +} > > Propchange: > tomcat/trunk/test/org/apache/catalina/valves/TestLoadBalancerDrainingV alve.java > > - ------------------------------------------------------------------------ - ------ > svn:eol-style = native > > Modified: tomcat/trunk/webapps/docs/changelog.xml URL: > http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?r ev=1799498&r1=1799497&r2=1799498&view=diff > > ======================================================================== ====== > --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ > tomcat/trunk/webapps/docs/changelog.xml Wed Jun 21 19:05:38 2017 @@ > -138,6 +138,10 @@ variable for CGI executables is populated in a > consistent way regardless of how the CGI servlet is mapped to a > request. (markt) </fix> + <add> + Add > LoadBalancerDrainingValve, a Valve designed to reduce the amount > of + time required for a node to drain its authenticated > users. (schultz) + </add> </changelog> </subsection> > <subsection name="Coyote"> > > Modified: tomcat/trunk/webapps/docs/config/valve.xml URL: > http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/valve.xm l?rev=1799498&r1=1799497&r2=1799498&view=diff > > ======================================================================== ====== > --- tomcat/trunk/webapps/docs/config/valve.xml (original) +++ > tomcat/trunk/webapps/docs/config/valve.xml Wed Jun 21 19:05:38 > 2017 @@ -700,6 +700,81 @@ > > > <section name="Proxies Support"> + <subsection name="Load Balancer > Draining Valve"> + <subsection name="Introduction"> + <p> + > When using mod_jk or mod_proxy_ajp, the client's session id is used > to + determine which back-end server will be used to serve > the request. If the + target node is being "drained" (in > mod_jk, this is the <i>DISABLED</i> + state; in > mod_proxy_ajp, this is the <i>Drain (N)</i> state), requests + > for expired sessions can actually cause the draining node to fail > to + drain. + </p> + <p> + Unfortunately, > AJP-based load-balancers cannot prove whether the + > client-provided session id is valid or not and therefore will send > any + requests for a session that appears to be targeted to > that node to the + disabled (or "draining") node, causing > the "draining" process to take + longer than necessary. + > </p> + <p> + This Valve detects requests for invalid > sessions, strips the session + information from the request, > and redirects back to the same URL, where + the > load-balancer should choose a different (active) node to handle > the + request. This will accelerate the "draining" process > for the disabled + node(s). + </p> + + <p> + > The activation state of the node is sent by the load-balancer in > the + request, so no state change on the node being disabled > is necessary. Simply + configure this Valve in your valve > pipeline and it will take action when + the activation state > is set to "disabled". + </p> + + <p> + You should > take care to register this Valve earlier in the Valve pipeline + > than any authentication Valves, because this Valve should be able > to + redirect a request before any authentication Valve > saves a request to a + protected resource. If this happens, > a new session will be created and + the draining process > will stall because a new, valid session will be + > established. + </p> + </subsection><!-- / Introduction --> > + + <subsection name="Attributes"> + <p>The <strong>Load > Balancer Draining 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.LoadBalancerDrainingValve</strong>. > > + </p> > + </attribute> + + <attribute > name="redirectStatusCode" required="false"> + <p>Allows > setting a custom redirect code to be used when the client + > is redirected to be re-balanced by the load-balancer. The default > is + 307 TEMPORARY_REDIRECT.</p> + </attribute> + + > <attribute name="ignoreCookieName" required="false"> + > <p>When used with <code>ignoreCookieValue</code>, a client can > present + this cookie (and accompanying value) that will > cause this Valve to + do nothing. This will allow you to > probe your <i>disabled</i> node + before re-enabling it to > make sure that it is working as expected.</p> + > </attribute> + + <attribute name="ignoreCookieValue" > required="false"> + <p>When used with > <code>ignoreCookieName</code>, a client can present + a > cookie (and accompanying value) that will cause this Valve to + > do nothing. This will allow you to probe your <i>disabled</i> node > + before re-enabling it to make sure that it is working as > expected.</p> + </attribute> + </attributes> + > </subsection><!-- /Attributes --> + </subsection><!-- /Load > Balancer Draining Valve --> > > <subsection name="Remote IP Valve"> > > > > > --------------------------------------------------------------------- > > To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org > For additional commands, e-mail: dev-h...@tomcat.apache.org > -----BEGIN PGP SIGNATURE----- Comment: GPGTools - http://gpgtools.org Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/ iQIcBAEBCAAGBQJZUAAeAAoJEBzwKT+lPKRYdlcP/Avpl70k41yWB64rqlwbBwrB ISxrnU3YLAOJXu8Lc0+/QRIhl2VXKt73ETblCPmVLYluPfu4MLjFvTouX9ZElL6r UX36tNPTiWCaiocQ9P7jWLHvBdeX48ck+MbC8EE3bKfJf1MQvXL62RuIfuLnoj6P r1/SsKXu63Y1CekWm68TlKPVqIhqk0sEzSG7X12w5QxVCOuDh2KA68Glkue86I5F z6mS+eTetF8+inRbiB8EXBrCxThwpq7NACBOcBlOVOlGZHF7JWj/V5djBDDFRuKv pFO5kbOeDaG5ruGNgD9UDvzlIikLIncLGFr01kYDnbIlfhf6sNJLtbd6m8dE2hAp UYciBxj7RDjaCLhW6ltLlldP8kGdfKiUWkH6SELH6FVPlSvXp+7RqZoY2cqCFYEV Xy1/QXAWbTlUPbLiC6z9YCHI0i9vMQL8JH3bfKVX9qIM6mGLpgZsCiRu+CD2PK0j XXCb+F/X0Qq6v1sJFUGab8JYAOQz8pXNYjGde5xtHX6TUpnz8pgKsOe2wuxH0LP4 7oRXVDKYs57XjwPF7jZzEkE72JCYu2jgmLSk+1CXMmDTfs2/RPFaY8/rDpoWv67I ZB6R+4zFBmL13DWRREHKWzZ3qUuCPy5+Pyzso4olrL3Gw/vvXt63Gatrg+hXm6RS LAPDvxZ3I5pO6ceuNnle =GX3V -----END PGP SIGNATURE----- --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org