From 81c7d7c616c0a20f28400a9c49e7a38b2409f61f Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Thu, 19 Nov 2020 09:35:29 -0500 Subject: [PATCH] Support for new property to ignore responses in exceptions thrown by 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 --- .../jersey/client/ClientProperties.java | 21 +++ .../jersey/client/JerseyInvocation.java | 47 ++++--- .../client/IgnoreExceptionResponseTest.java | 128 ++++++++++++++++++ 3 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/IgnoreExceptionResponseTest.java diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java index b01857adcc..c0e8515f65 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java @@ -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}. + *

+ * The value MUST be an instance convertible to {@link java.lang.Boolean}. + *

+ *

+ * The default value is {@code false}. + *

+ *

+ * The name of the configuration property is {@value}. + *

+ * + * @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. *

diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java index ceb9009c51..0c9453f679 100644 --- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java +++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyInvocation.java @@ -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); } @@ -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 { @@ -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); } } diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/IgnoreExceptionResponseTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/IgnoreExceptionResponseTest.java new file mode 100644 index 0000000000..88125d8a21 --- /dev/null +++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/IgnoreExceptionResponseTest.java @@ -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 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 + } + } +}