From 56e58e4807a366384aa95cb1d0c132f6a6304111 Mon Sep 17 00:00:00 2001 From: agherardi Date: Sun, 17 Jun 2018 09:59:29 -0600 Subject: [PATCH] Patch for client connection leak when using digest authentication Signed-off-by: agherardi --- .../jersey/apache/connector/AuthTest.java | 32 +++++++++++++ .../authentication/AuthenticationUtil.java | 46 +++++++++++++++++++ .../HttpAuthenticationFilter.java | 20 ++++++-- 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 core-client/src/main/java/org/glassfish/jersey/client/authentication/AuthenticationUtil.java diff --git a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java index 6a9813037f..9b9a1578f3 100644 --- a/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java +++ b/connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java @@ -40,6 +40,7 @@ import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -144,6 +145,19 @@ public String getFilter(@Context HttpHeaders h) { return "GET"; } + @GET + @Path("digest") + public String getDigest(@Context HttpHeaders h) { + String value = h.getRequestHeaders().getFirst("Authorization"); + if (value == null) { + throw new WebApplicationException( + Response.status(401).header("WWW-Authenticate", "Digest realm=\"WallyWorld\"") + .entity("Forbidden").build()); + } + + return "GET"; + } + @POST public String post(@Context HttpHeaders h, String e) { requestCount++; @@ -254,6 +268,24 @@ public void testAuthGetWithClientFilter() { assertEquals("GET", r.request().get(String.class)); } + @Test + public void testAuthGetWithDigestFilter() { + ClientConfig cc = new ClientConfig(); + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + cc.connectorProvider(new ApacheConnectorProvider()); + cc.property(ApacheClientProperties.CONNECTION_MANAGER, cm); + Client client = ClientBuilder.newClient(cc); + client.register(HttpAuthenticationFeature.universal("name", "password")); + WebTarget r = client.target(getBaseUri()).path("test/digest"); + + assertEquals("GET", r.request().get(String.class)); + + // Verify the connection that was used for the request is available for reuse + // and no connections are leased + assertEquals(cm.getTotalStats().getAvailable(), 1); + assertEquals(cm.getTotalStats().getLeased(), 0); + } + @Test @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?" + " Allow repeatable write in jersey?") diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/AuthenticationUtil.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/AuthenticationUtil.java new file mode 100644 index 0000000000..ace18fe336 --- /dev/null +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/AuthenticationUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.client.authentication; + +import java.io.IOException; +import java.io.InputStream; + +import javax.ws.rs.client.ClientRequestContext; + +/** + * Common authentication utilities + */ +class AuthenticationUtil { + static void discardInputAndClose(InputStream is) { + byte[] buf = new byte[4096]; + try { + while (true) { + if (is.read(buf) <= 0) { + break; + } + } + } catch (IOException ex) { + // ignore + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore + } + } + } +} diff --git a/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java b/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java index 97675c3e3b..26a2af2665 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java @@ -25,9 +25,9 @@ import java.util.List; import java.util.Map; +import javax.annotation.Priority; import javax.ws.rs.Priorities; import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; import javax.ws.rs.client.ClientResponseContext; @@ -42,8 +42,6 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import javax.annotation.Priority; - import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.internal.LocalizationMessages; @@ -270,8 +268,16 @@ private void updateCache(ClientRequestContext request, boolean success, Type ope * {@code false} otherwise). */ static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader) { - Client client = request.getClient(); + // If the failed response has an entity stream, close it. We must do this to avoid leaking a connection + // when we replace the entity stream of the failed response with that of the repeated response (see below). + // Notice that by closing the entity stream before sending the repeated request we allow the connection allocated + // to the failed request to be reused, if possible, for the repeated request. + if (response.hasEntity()) { + AuthenticationUtil.discardInputAndClose(response.getEntityStream()); + response.setEntityStream(null); + } + Client client = request.getClient(); String method = request.getMethod(); MediaType mediaType = request.getMediaType(); URI lUri = request.getUri(); @@ -294,6 +300,12 @@ static boolean repeatRequest(ClientRequestContext request, ClientResponseContext builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true"); + // Copy other properties, if any, from the original request + for (String propertyName : request.getPropertyNames()) { + Object propertyValue = request.getProperty(propertyName); + builder.property(propertyName, propertyValue); + } + Invocation invocation; if (request.getEntity() == null) { invocation = builder.build(method);