Skip to content
This repository was archived by the owner on May 28, 2018. It is now read-only.

Commit 24b3490

Browse files
author
alessandro.gherardi
committed
Patch for client connection leak when using digest authentication Choose Digest over Basic if the server returns both
1 parent 7d4c8b0 commit 24b3490

File tree

3 files changed

+101
-14
lines changed

3 files changed

+101
-14
lines changed

connectors/apache-connector/src/test/java/org/glassfish/jersey/apache/connector/AuthTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import org.apache.http.auth.AuthScope;
6565
import org.apache.http.auth.UsernamePasswordCredentials;
6666
import org.apache.http.client.CredentialsProvider;
67+
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
6768
import org.junit.Ignore;
6869
import org.junit.Test;
6970
import static org.junit.Assert.assertEquals;
@@ -168,6 +169,38 @@ public String getFilter(@Context HttpHeaders h) {
168169
return "GET";
169170
}
170171

172+
@GET
173+
@Path("digest")
174+
public String getDigest(@Context HttpHeaders h) {
175+
String value = h.getRequestHeaders().getFirst("Authorization");
176+
if (value == null) {
177+
throw new WebApplicationException(
178+
Response.status(401).header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
179+
.entity("Forbidden").build());
180+
}
181+
182+
return "GET";
183+
}
184+
185+
@GET
186+
@Path("basicAndDigest")
187+
public String getBasicAndDigest(@Context HttpHeaders h) {
188+
String value = h.getRequestHeaders().getFirst("Authorization");
189+
if (value == null) {
190+
throw new WebApplicationException(
191+
Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
192+
.header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
193+
.entity("Forbidden").build());
194+
} else if (value.startsWith("Basic")) {
195+
throw new WebApplicationException(
196+
Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
197+
.header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
198+
.entity("Digest authentication expected").build());
199+
}
200+
201+
return "GET";
202+
}
203+
171204
@POST
172205
public String post(@Context HttpHeaders h, String e) {
173206
requestCount++;
@@ -259,6 +292,37 @@ public void testAuthGetWithClientFilter() {
259292
assertEquals("GET", r.request().get(String.class));
260293
}
261294

295+
@Test
296+
public void testAuthGetWithDigestFilter() {
297+
ClientConfig cc = new ClientConfig();
298+
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
299+
cc.connectorProvider(new ApacheConnectorProvider());
300+
cc.property(ApacheClientProperties.CONNECTION_MANAGER, cm);
301+
Client client = ClientBuilder.newClient(cc);
302+
client.register(HttpAuthenticationFeature.universal("name", "password"));
303+
WebTarget r = client.target(getBaseUri()).path("test/digest");
304+
305+
assertEquals("GET", r.request().get(String.class));
306+
307+
// Verify the connection that was used for the request is available for reuse
308+
// and no connections are leased
309+
assertEquals(cm.getTotalStats().getAvailable(), 1);
310+
assertEquals(cm.getTotalStats().getLeased(), 0);
311+
}
312+
313+
@Test
314+
public void testAuthGetWithBasicAndDigestFilter() {
315+
ClientConfig cc = new ClientConfig();
316+
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
317+
cc.connectorProvider(new ApacheConnectorProvider());
318+
cc.property(ApacheClientProperties.CONNECTION_MANAGER, cm);
319+
Client client = ClientBuilder.newClient(cc);
320+
client.register(HttpAuthenticationFeature.universal("name", "password"));
321+
WebTarget r = client.target(getBaseUri()).path("test/basicAndDigest");
322+
323+
assertEquals("GET", r.request().get(String.class));
324+
}
325+
262326
@Test
263327
@Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?"
264328
+ " Allow repeatable write in jersey?")

core-client/src/main/java/org/glassfish/jersey/client/authentication/BasicAuthenticator.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040

4141
package org.glassfish.jersey.client.authentication;
4242

43+
import java.io.IOException;
44+
4345
import javax.ws.rs.client.ClientRequestContext;
4446
import javax.ws.rs.client.ClientResponseContext;
4547
import javax.ws.rs.core.HttpHeaders;
@@ -113,7 +115,8 @@ public void filterRequest(ClientRequestContext request) throws RequestAuthentica
113115
* new request was done with digest authentication information and authentication was successful.
114116
* @throws ResponseAuthenticationException in case that basic credentials missing or are in invalid format
115117
*/
116-
public boolean filterResponseAndAuthenticate(ClientRequestContext request, ClientResponseContext response) {
118+
public boolean filterResponseAndAuthenticate(ClientRequestContext request, ClientResponseContext response)
119+
throws IOException {
117120
final String authenticate = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE);
118121
if (authenticate != null && authenticate.trim().toUpperCase().startsWith("BASIC")) {
119122
HttpAuthenticationFilter.Credentials credentials = HttpAuthenticationFilter

core-client/src/main/java/org/glassfish/jersey/client/authentication/HttpAuthenticationFilter.java

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@
4949
import java.util.List;
5050
import java.util.Map;
5151

52+
import javax.annotation.Priority;
5253
import javax.ws.rs.Priorities;
5354
import javax.ws.rs.client.Client;
54-
import javax.ws.rs.client.ClientBuilder;
5555
import javax.ws.rs.client.ClientRequestContext;
5656
import javax.ws.rs.client.ClientRequestFilter;
5757
import javax.ws.rs.client.ClientResponseContext;
@@ -66,8 +66,6 @@
6666
import javax.ws.rs.core.MultivaluedMap;
6767
import javax.ws.rs.core.Response;
6868

69-
import javax.annotation.Priority;
70-
7169
import org.glassfish.jersey.client.ClientProperties;
7270
import org.glassfish.jersey.client.internal.LocalizationMessages;
7371

@@ -222,15 +220,21 @@ public void filter(ClientRequestContext request, ClientResponseContext response)
222220
Type result = null; // which authentication is requested: BASIC or DIGEST
223221
boolean authenticate;
224222

223+
// If the server requests both BASIC and DIGEST, prefer DIGEST since it's stronger
224+
// (see https://tools.ietf.org/html/rfc2617#section-4.6)
225225
if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) {
226-
String authString = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE);
227-
if (authString != null) {
228-
final String upperCaseAuth = authString.trim().toUpperCase();
229-
if (upperCaseAuth.startsWith("BASIC")) {
230-
result = Type.BASIC;
231-
} else if (upperCaseAuth.startsWith("DIGEST")) {
232-
result = Type.DIGEST;
233-
} else {
226+
List<String> authStrings = response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE);
227+
if (authStrings != null) {
228+
for (String authString : authStrings) {
229+
final String upperCaseAuth = authString.trim().toUpperCase();
230+
if (result == null && upperCaseAuth.startsWith("BASIC")) {
231+
result = Type.BASIC;
232+
} else if (upperCaseAuth.startsWith("DIGEST")) {
233+
result = Type.DIGEST;
234+
}
235+
}
236+
237+
if (result == null) {
234238
// unknown authentication -> this filter cannot authenticate with this method
235239
return;
236240
}
@@ -292,10 +296,20 @@ private void updateCache(ClientRequestContext request, boolean success, Type ope
292296
* @param newAuthorizationHeader {@code Authorization} header that should be added to the new request.
293297
* @return {@code true} is the authentication was successful ({@code true} if 401 response code was not returned;
294298
* {@code false} otherwise).
299+
* @throws IOException
295300
*/
296-
static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader) {
301+
static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader)
302+
throws IOException {
303+
// If the failed response has an entity stream, close it. We must do this to avoid leaking a connection
304+
// when we replace the entity stream of the failed response with that of the repeated response (see below).
305+
// Notice that by closing the entity stream before sending the repeated request we allow the connection allocated
306+
// to the failed request to be reused, if possible, for the repeated request.
307+
if (response.hasEntity()) {
308+
response.getEntityStream().close();
309+
response.setEntityStream(null);
310+
}
311+
297312
Client client = request.getClient();
298-
299313
String method = request.getMethod();
300314
MediaType mediaType = request.getMediaType();
301315
URI lUri = request.getUri();
@@ -318,6 +332,12 @@ static boolean repeatRequest(ClientRequestContext request, ClientResponseContext
318332

319333
builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true");
320334

335+
// Copy other properties, if any, from the original request
336+
for (String propertyName : request.getPropertyNames()) {
337+
Object propertyValue = request.getProperty(propertyName);
338+
builder.property(propertyName, propertyValue);
339+
}
340+
321341
Invocation invocation;
322342
if (request.getEntity() == null) {
323343
invocation = builder.build(method);

0 commit comments

Comments
 (0)