Skip to content

Commit

Permalink
Support for new property to ignore responses in exceptions thrown by …
Browse files Browse the repository at this point in the history
…the Client API. If the property jersey.config.client.ignoreExceptionResponse is set to true, any response in an exception thrown by the Client API will be mapped to an empty response that only includes the status code of the original one. This is to prevent accidental leaks of confidential data.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
  • Loading branch information
spericas authored and jansupol committed Nov 30, 2020
1 parent 684bbf8 commit 95c08d3
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,27 @@ public final class ClientProperties {
*/
public static final String USE_ENCODING = "jersey.config.client.useEncoding";

/**
* Ignore a response in an exception thrown by the client API by not forwarding
* it to this service's client. A value of {@code true} indicates that responses
* will be ignored, and only the response status will return to the client. This
* property will normally be specified as a system property; note that system
* properties are only visible if {@link CommonProperties#ALLOW_SYSTEM_PROPERTIES_PROVIDER}
* is set to {@code true}.
* <p>
* The value MUST be an instance convertible to {@link java.lang.Boolean}.
* </p>
* <p>
* The default value is {@code false}.
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
*
* @see org.glassfish.jersey.CommonProperties#ALLOW_SYSTEM_PROPERTIES_PROVIDER
*/
public static final String IGNORE_EXCEPTION_RESPONSE = "jersey.config.client.ignoreExceptionResponse";

/**
* If {@code true} then disable auto-discovery on the client.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public class JerseyInvocation implements javax.ws.rs.client.Invocation {
// Copy request context when invoke or submit methods are invoked.
private final boolean copyRequestContext;

private boolean ignoreResponseException;

private JerseyInvocation(final Builder builder) {
this(builder, false);
}
Expand All @@ -91,6 +93,15 @@ private JerseyInvocation(final Builder builder, final boolean copyRequestContext

this.requestContext = new ClientRequest(builder.requestContext);
this.copyRequestContext = copyRequestContext;

Object value = builder.requestContext.getConfiguration()
.getProperty(ClientProperties.IGNORE_EXCEPTION_RESPONSE);
if (value != null) {
Boolean booleanValue = PropertiesHelper.convertValue(value, Boolean.class);
if (booleanValue != null) {
this.ignoreResponseException = booleanValue;
}
}
}

private enum EntityPresence {
Expand Down Expand Up @@ -875,56 +886,60 @@ public JerseyInvocation property(final String name, final Object value) {
}

private ProcessingException convertToException(final Response response) {
// Use an empty response if ignoring response in exception
final int statusCode = response.getStatus();
final Response finalResponse = ignoreResponseException ? Response.status(statusCode).build() : response;

try {
// Buffer and close entity input stream (if any) to prevent
// leaking connections (see JERSEY-2157).
response.bufferEntity();

final WebApplicationException webAppException;
final int statusCode = response.getStatus();
final Response.Status status = Response.Status.fromStatusCode(statusCode);

if (status == null) {
final Response.Status.Family statusFamily = response.getStatusInfo().getFamily();
webAppException = createExceptionForFamily(response, statusFamily);
final Response.Status.Family statusFamily = finalResponse.getStatusInfo().getFamily();
webAppException = createExceptionForFamily(finalResponse, statusFamily);
} else {
switch (status) {
case BAD_REQUEST:
webAppException = new BadRequestException(response);
webAppException = new BadRequestException(finalResponse);
break;
case UNAUTHORIZED:
webAppException = new NotAuthorizedException(response);
webAppException = new NotAuthorizedException(finalResponse);
break;
case FORBIDDEN:
webAppException = new ForbiddenException(response);
webAppException = new ForbiddenException(finalResponse);
break;
case NOT_FOUND:
webAppException = new NotFoundException(response);
webAppException = new NotFoundException(finalResponse);
break;
case METHOD_NOT_ALLOWED:
webAppException = new NotAllowedException(response);
webAppException = new NotAllowedException(finalResponse);
break;
case NOT_ACCEPTABLE:
webAppException = new NotAcceptableException(response);
webAppException = new NotAcceptableException(finalResponse);
break;
case UNSUPPORTED_MEDIA_TYPE:
webAppException = new NotSupportedException(response);
webAppException = new NotSupportedException(finalResponse);
break;
case INTERNAL_SERVER_ERROR:
webAppException = new InternalServerErrorException(response);
webAppException = new InternalServerErrorException(finalResponse);
break;
case SERVICE_UNAVAILABLE:
webAppException = new ServiceUnavailableException(response);
webAppException = new ServiceUnavailableException(finalResponse);
break;
default:
final Response.Status.Family statusFamily = response.getStatusInfo().getFamily();
webAppException = createExceptionForFamily(response, statusFamily);
final Response.Status.Family statusFamily = finalResponse.getStatusInfo().getFamily();
webAppException = createExceptionForFamily(finalResponse, statusFamily);
}
}

return new ResponseProcessingException(response, webAppException);
return new ResponseProcessingException(finalResponse, webAppException);
} catch (final Throwable t) {
return new ResponseProcessingException(response, LocalizationMessages.RESPONSE_TO_EXCEPTION_CONVERSION_FAILED(), t);
return new ResponseProcessingException(finalResponse,
LocalizationMessages.RESPONSE_TO_EXCEPTION_CONVERSION_FAILED(), t);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2020 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.tests.e2e.client;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;

import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

/**
* Tests ignoring of client responses in exceptions.
*
* @author Santiago Pericas-Geertsen
*/
public class IgnoreExceptionResponseTest extends JerseyTest {

static String lastAllowSystemProperties;
static String lastIgnoreExceptionResponse;
static AtomicReference<URI> baseUri = new AtomicReference<>();

@Override
protected Application configure() {
return new ResourceConfig(TestResource.class);
}

public IgnoreExceptionResponseTest() {
baseUri.set(getBaseUri());
}

/**
* Sets ignore exception response as system property after enabling the provider.
*/
@BeforeClass
public static void startUp() {
lastAllowSystemProperties = System.setProperty(CommonProperties.ALLOW_SYSTEM_PROPERTIES_PROVIDER, "true");
lastIgnoreExceptionResponse = System.setProperty(ClientProperties.IGNORE_EXCEPTION_RESPONSE, "true");
}

/**
* Restores state after completion.
*/
@AfterClass
public static void cleanUp() {
if (lastIgnoreExceptionResponse != null) {
System.setProperty(ClientProperties.IGNORE_EXCEPTION_RESPONSE, lastIgnoreExceptionResponse);
}
if (lastAllowSystemProperties != null) {
System.setProperty(CommonProperties.ALLOW_SYSTEM_PROPERTIES_PROVIDER, lastAllowSystemProperties);
}
}

@Test
public void test() {
Client client = ClientBuilder.newClient();
Response r = client.target(getBaseUri())
.path("test")
.path("first")
.request()
.get();
assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), r.getStatus());
assertNull(r.getHeaderString("confidential"));
assertNull(r.getCookies().get("confidential"));
assertFalse(r.hasEntity());
}

@Path("test")
public static class TestResource {

@Path("first")
@GET
public String first() {
Client client = ClientBuilder.newClient();
String entity = client.target(baseUri.get())
.path("test")
.path("second")
.request()
.get(String.class); // WebApplicationException may be thrown
return processEntity(entity);
}

@Path("second")
@GET
public String second() {
throw new WebApplicationException(
"Leaking confidential information",
Response.status(500)
.header("confidential", "nuke-codes")
.cookie(NewCookie.valueOf("confidential=more-nuke-codes"))
.entity("even-more-nuke-codes")
.build());
}

private String processEntity(String entity) {
return entity; // filter confidential information
}
}
}

0 comments on commit 95c08d3

Please sign in to comment.