From 0216c364b0bddb717c054ab7d0ff61b1aaf032e9 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Tue, 27 Aug 2019 12:33:06 -0300 Subject: [PATCH] Add a Header to a Jersey SSE Client Request (#7662) * Add a Header to a Jersey SSE Client Request (cherry picked from commit ee70714e7885cf8713e9c2698a8a8d93fb6a53c8) * Class and Methods rename --- jersey/pom.xml | 17 +- .../jersey/client/JerseyClientHeaders.java | 189 +++++++++++++++++ .../filter/AddHeaderOnRequestFilter.java | 16 ++ .../baeldung/jersey/server/EchoHeaders.java | 72 +++++++ .../jersey/server/EchoHeadersUnitTest.java | 197 ++++++++++++++++++ 5 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 jersey/src/main/java/com/baeldung/jersey/client/JerseyClientHeaders.java create mode 100644 jersey/src/main/java/com/baeldung/jersey/client/filter/AddHeaderOnRequestFilter.java create mode 100644 jersey/src/main/java/com/baeldung/jersey/server/EchoHeaders.java create mode 100644 jersey/src/test/java/com/baeldung/jersey/server/EchoHeadersUnitTest.java diff --git a/jersey/pom.xml b/jersey/pom.xml index 708b36ce41..0b4d469e3a 100644 --- a/jersey/pom.xml +++ b/jersey/pom.xml @@ -32,7 +32,7 @@ org.glassfish.jersey.containers jersey-container-grizzly2-servlet - ${jersey.version} + ${jersey.version} org.glassfish.jersey.ext @@ -44,6 +44,21 @@ jersey-bean-validation ${jersey.version} + + org.glassfish.jersey.security + oauth1-client + ${jersey.version} + + + org.glassfish.jersey.security + oauth2-client + ${jersey.version} + + + org.glassfish.jersey.media + jersey-media-sse + ${jersey.version} + org.glassfish.jersey.test-framework jersey-test-framework-core diff --git a/jersey/src/main/java/com/baeldung/jersey/client/JerseyClientHeaders.java b/jersey/src/main/java/com/baeldung/jersey/client/JerseyClientHeaders.java new file mode 100644 index 0000000000..ebcbe1d4ab --- /dev/null +++ b/jersey/src/main/java/com/baeldung/jersey/client/JerseyClientHeaders.java @@ -0,0 +1,189 @@ +package com.baeldung.jersey.client; + +import com.baeldung.jersey.client.filter.AddHeaderOnRequestFilter; +import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; +import org.glassfish.jersey.client.oauth1.AccessToken; +import org.glassfish.jersey.client.oauth1.ConsumerCredentials; +import org.glassfish.jersey.client.oauth1.OAuth1ClientSupport; +import org.glassfish.jersey.client.oauth2.OAuth2ClientSupport; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.Response; +import javax.ws.rs.sse.InboundSseEvent; +import javax.ws.rs.sse.SseEventSource; + +import static org.glassfish.jersey.client.authentication.HttpAuthenticationFeature.*; + +public class JerseyClientHeaders { + + private static final String BEARER_CONSUMER_SECRET = "my-consumer-secret"; + private static final String BEARER_ACCESS_TOKEN_SECRET = "my-access-token-secret"; + private static final String TARGET = "http://localhost:9998/"; + private static final String MAIN_RESOURCE = "echo-headers"; + private static final String RESOURCE_AUTH_DIGEST = "digest"; + + private static String sseHeaderValue; + + public static Response simpleHeader(String headerKey, String headerValue) { + Client client = ClientBuilder.newClient(); + WebTarget webTarget = client.target(TARGET); + WebTarget resourceWebTarget = webTarget.path(MAIN_RESOURCE); + Invocation.Builder invocationBuilder = resourceWebTarget.request(); + invocationBuilder.header(headerKey, headerValue); + return invocationBuilder.get(); + } + + public static Response simpleHeaderFluently(String headerKey, String headerValue) { + Client client = ClientBuilder.newClient(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .header(headerKey, headerValue) + .get(); + } + + public static Response basicAuthenticationAtClientLevel(String username, String password) { + //To simplify we removed de SSL/TLS protection, but it's required to have an encryption + // when using basic authentication schema as it's send only on Base64 encoding + HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic(username, password); + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .get(); + } + + public static Response basicAuthenticationAtRequestLevel(String username, String password) { + //To simplify we removed de SSL/TLS protection, but it's required to have an encryption + // when using basic authentication schema as it's send only on Base64 encoding + HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder().build(); + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .property(HTTP_AUTHENTICATION_BASIC_USERNAME, username) + .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, password) + .get(); + } + + public static Response digestAuthenticationAtClientLevel(String username, String password) { + HttpAuthenticationFeature feature = HttpAuthenticationFeature.digest(username, password); + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .path(RESOURCE_AUTH_DIGEST) + .request() + .get(); + } + + public static Response digestAuthenticationAtRequestLevel(String username, String password) { + HttpAuthenticationFeature feature = HttpAuthenticationFeature.digest(); + + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .path(RESOURCE_AUTH_DIGEST) + .request() + .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, username) + .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, password) + .get(); + } + + public static Response bearerAuthenticationWithOAuth1AtClientLevel(String token, String consumerKey) { + ConsumerCredentials consumerCredential = new ConsumerCredentials(consumerKey, BEARER_CONSUMER_SECRET); + AccessToken accessToken = new AccessToken(token, BEARER_ACCESS_TOKEN_SECRET); + Feature feature = OAuth1ClientSupport + .builder(consumerCredential) + .feature() + .accessToken(accessToken) + .build(); + + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .get(); + } + + public static Response bearerAuthenticationWithOAuth1AtRequestLevel(String token, String consumerKey) { + ConsumerCredentials consumerCredential = new ConsumerCredentials(consumerKey, BEARER_CONSUMER_SECRET); + AccessToken accessToken = new AccessToken(token, BEARER_ACCESS_TOKEN_SECRET); + Feature feature = OAuth1ClientSupport + .builder(consumerCredential) + .feature() + .build(); + + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .property(OAuth1ClientSupport.OAUTH_PROPERTY_ACCESS_TOKEN, accessToken) + .get(); + } + + public static Response bearerAuthenticationWithOAuth2AtClientLevel(String token) { + Feature feature = OAuth2ClientSupport.feature(token); + + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .get(); + } + + public static Response bearerAuthenticationWithOAuth2AtRequestLevel(String token, String otherToken) { + Feature feature = OAuth2ClientSupport.feature(token); + + Client client = ClientBuilder.newBuilder().register(feature).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .property(OAuth2ClientSupport.OAUTH2_PROPERTY_ACCESS_TOKEN, otherToken) + .get(); + } + + public static Response filter() { + Client client = ClientBuilder.newBuilder().register(AddHeaderOnRequestFilter.class).build(); + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .get(); + } + + public static Response sendRestrictedHeaderThroughDefaultTransportConnector(String headerKey, String headerValue) { + Client client = ClientBuilder.newClient(); + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + + return client.target(TARGET) + .path(MAIN_RESOURCE) + .request() + .header(headerKey, headerValue) + .get(); + } + + public static String simpleSSEHeader() throws InterruptedException { + Client client = ClientBuilder.newBuilder() + .register(AddHeaderOnRequestFilter.class) + .build(); + + WebTarget webTarget = client.target(TARGET) + .path(MAIN_RESOURCE) + .path("events"); + + SseEventSource sseEventSource = SseEventSource.target(webTarget).build(); + sseEventSource.register(JerseyClientHeaders::receiveEvent); + sseEventSource.open(); + Thread.sleep(3_000); + sseEventSource.close(); + + return sseHeaderValue; + } + + private static void receiveEvent(InboundSseEvent event) { + sseHeaderValue = event.readData(); + } +} diff --git a/jersey/src/main/java/com/baeldung/jersey/client/filter/AddHeaderOnRequestFilter.java b/jersey/src/main/java/com/baeldung/jersey/client/filter/AddHeaderOnRequestFilter.java new file mode 100644 index 0000000000..a874928c16 --- /dev/null +++ b/jersey/src/main/java/com/baeldung/jersey/client/filter/AddHeaderOnRequestFilter.java @@ -0,0 +1,16 @@ +package com.baeldung.jersey.client.filter; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import java.io.IOException; + +public class AddHeaderOnRequestFilter implements ClientRequestFilter { + + public static final String FILTER_HEADER_VALUE = "filter-header-value"; + public static final String FILTER_HEADER_KEY = "x-filter-header"; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(FILTER_HEADER_KEY, FILTER_HEADER_VALUE); + } +} diff --git a/jersey/src/main/java/com/baeldung/jersey/server/EchoHeaders.java b/jersey/src/main/java/com/baeldung/jersey/server/EchoHeaders.java new file mode 100644 index 0000000000..a8df3c10a8 --- /dev/null +++ b/jersey/src/main/java/com/baeldung/jersey/server/EchoHeaders.java @@ -0,0 +1,72 @@ +package com.baeldung.jersey.server; + +import com.baeldung.jersey.client.filter.AddHeaderOnRequestFilter; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.*; +import javax.ws.rs.sse.OutboundSseEvent; +import javax.ws.rs.sse.Sse; +import javax.ws.rs.sse.SseEventSink; + +@Path("/echo-headers") +public class EchoHeaders { + + static final String REALM_KEY = "realm"; + static final String REALM_VALUE = "Baeldung"; + static final String QOP_KEY = "qop"; + static final String QOP_VALUE = "auth"; + static final String NONCE_KEY = "nonce"; + static final String NONCE_VALUE = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + static final String OPAQUE_KEY = "opaque"; + static final String OPAQUE_VALUE = "5ccc069c403ebaf9f0171e9517f40e41"; + static final String SSE_HEADER_KEY = "x-sse-header-key"; + + @Context + HttpHeaders headers; + + @GET + public Response getHeadersBack() { + return echoHeaders(); + } + + @RolesAllowed("ADMIN") + @GET + @Path("/digest") + public Response getHeadersBackFromDigestAuthentication() { + // As the Digest authentication require some complex steps to work we'll simulate the process + // https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation + if (headers.getHeaderString("authorization") == null) { + String authenticationRequired = "Digest " + REALM_KEY + "=\"" + REALM_VALUE + "\", " + QOP_KEY + "=\"" + QOP_VALUE + "\", " + NONCE_KEY + "=\"" + NONCE_VALUE + "\", " + OPAQUE_KEY + "=\"" + OPAQUE_VALUE + "\""; + return Response.status(Response.Status.UNAUTHORIZED) + .header("WWW-Authenticate", authenticationRequired) + .build(); + } else { + return echoHeaders(); + } + } + + @GET + @Path("/events") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void getServerSentEvents(@Context SseEventSink eventSink, @Context Sse sse) { + OutboundSseEvent event = sse.newEventBuilder() + .name("echo-headers") + .data(String.class, headers.getHeaderString(AddHeaderOnRequestFilter.FILTER_HEADER_KEY)) + .build(); + eventSink.send(event); + } + + private Response echoHeaders() { + Response.ResponseBuilder responseBuilder = Response.noContent(); + + headers.getRequestHeaders() + .forEach((k, v) -> { + v.forEach(value -> responseBuilder.header(k, value)); + }); + + return responseBuilder.build(); + } +} diff --git a/jersey/src/test/java/com/baeldung/jersey/server/EchoHeadersUnitTest.java b/jersey/src/test/java/com/baeldung/jersey/server/EchoHeadersUnitTest.java new file mode 100644 index 0000000000..ac2cc2c4eb --- /dev/null +++ b/jersey/src/test/java/com/baeldung/jersey/server/EchoHeadersUnitTest.java @@ -0,0 +1,197 @@ +package com.baeldung.jersey.server; + +import com.baeldung.jersey.client.JerseyClientHeaders; +import com.baeldung.jersey.client.filter.AddHeaderOnRequestFilter; +import org.glassfish.jersey.media.sse.SseFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class EchoHeadersUnitTest extends JerseyTest { + + private static final String SIMPLE_HEADER_KEY = "my-header-key"; + private static final String SIMPLE_HEADER_VALUE = "my-header-value"; + private static final String USERNAME = "baeldung"; + private static final String PASSWORD = "super-secret"; + private static final String AUTHORIZATION_HEADER_KEY = "authorization"; + private static final String BEARER_TOKEN_VALUE = "my-token"; + private static final String BEARER_CONSUMER_KEY_VALUE = "my-consumer-key"; + private static final String BEARER_REQUEST_TOKEN_VALUE = "my-request-token"; + + @Test + public void whenCallingSimpleHeader_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.simpleHeader(SIMPLE_HEADER_KEY, SIMPLE_HEADER_VALUE); + + assertEquals(response.getHeaderString(SIMPLE_HEADER_KEY), SIMPLE_HEADER_VALUE); + } + + @Test + public void whenCallingSimpleHeaderFluently_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.simpleHeaderFluently(SIMPLE_HEADER_KEY, SIMPLE_HEADER_VALUE); + + assertEquals(response.getHeaderString(SIMPLE_HEADER_KEY), SIMPLE_HEADER_VALUE); + } + + @Test + public void whenCallingBasicAuthenticationAtClientLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.basicAuthenticationAtClientLevel(USERNAME, PASSWORD); + + assertBasicAuthenticationHeaders(response); + } + + @Test + public void whenCallingBasicAuthenticationAtRequestLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.basicAuthenticationAtRequestLevel(USERNAME, PASSWORD); + + assertBasicAuthenticationHeaders(response); + } + + @Test + public void whenCallingDigestAuthenticationAtClientLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.digestAuthenticationAtClientLevel(USERNAME, PASSWORD); + + Map subHeadersMap = parseAuthenticationSubHeader(response, 7); + + assertDigestAuthenticationHeaders(subHeadersMap); + } + + @Test + public void whenCallingDigestAuthenticationAtRequestLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.digestAuthenticationAtRequestLevel(USERNAME, PASSWORD); + + Map subHeadersMap = parseAuthenticationSubHeader(response, 7); + + assertDigestAuthenticationHeaders(subHeadersMap); + } + + @Test + public void whenCallingBearerAuthenticationWithOAuth1AtClientLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.bearerAuthenticationWithOAuth1AtClientLevel(BEARER_TOKEN_VALUE, BEARER_CONSUMER_KEY_VALUE); + + Map subHeadersMap = parseAuthenticationSubHeader(response, 6); + + assertBearerAuthenticationHeaders(subHeadersMap); + } + + @Test + public void whenCallingBearerAuthenticationWithOAuth1AtRequestLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.bearerAuthenticationWithOAuth1AtRequestLevel(BEARER_TOKEN_VALUE, BEARER_CONSUMER_KEY_VALUE); + + Map subHeadersMap = parseAuthenticationSubHeader(response, 6); + + assertBearerAuthenticationHeaders(subHeadersMap); + } + + @Test + public void whenCallingBearerAuthenticationWithOAuth2AtClientLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.bearerAuthenticationWithOAuth2AtClientLevel(BEARER_TOKEN_VALUE); + + assertEquals("Bearer " + BEARER_TOKEN_VALUE, response.getHeaderString(AUTHORIZATION_HEADER_KEY)); + } + + @Test + public void whenCallingBearerAuthenticationWithOAuth2AtRequestLevel_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.bearerAuthenticationWithOAuth2AtRequestLevel(BEARER_TOKEN_VALUE, BEARER_REQUEST_TOKEN_VALUE); + + assertEquals("Bearer " + BEARER_REQUEST_TOKEN_VALUE, response.getHeaderString(AUTHORIZATION_HEADER_KEY)); + } + + @Test + public void whenCallingFilter_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.filter(); + + assertEquals(AddHeaderOnRequestFilter.FILTER_HEADER_VALUE, response.getHeaderString(AddHeaderOnRequestFilter.FILTER_HEADER_KEY)); + } + + @Test + public void whenCallingSendRestrictedHeaderThroughDefaultTransportConnector_thenHeadersReturnedBack() { + Response response = JerseyClientHeaders.sendRestrictedHeaderThroughDefaultTransportConnector("keep-alive", "keep-alive-value"); + + assertEquals("keep-alive-value", response.getHeaderString("keep-alive")); + } + + @Test + public void whenCallingSimpleSSEHeader_thenHeadersReturnedBack() throws InterruptedException { + String sseHeaderBackValue = JerseyClientHeaders.simpleSSEHeader(); + + assertEquals(AddHeaderOnRequestFilter.FILTER_HEADER_VALUE, sseHeaderBackValue); + } + + private void assertBearerAuthenticationHeaders(Map subHeadersMap) { + + assertEquals(BEARER_TOKEN_VALUE, subHeadersMap.get("oauth_token")); + assertEquals(BEARER_CONSUMER_KEY_VALUE, subHeadersMap.get("oauth_consumer_key")); + assertNotNull(subHeadersMap.get("oauth_nonce")); + assertNotNull(subHeadersMap.get("oauth_signature")); + assertNotNull(subHeadersMap.get("oauth_callback")); + assertNotNull(subHeadersMap.get("oauth_signature_method")); + assertNotNull(subHeadersMap.get("oauth_version")); + assertNotNull(subHeadersMap.get("oauth_timestamp")); + } + + private void assertDigestAuthenticationHeaders(Map subHeadersMap) { + assertEquals(EchoHeaders.NONCE_VALUE, subHeadersMap.get(EchoHeaders.NONCE_KEY)); + assertEquals(EchoHeaders.OPAQUE_VALUE, subHeadersMap.get(EchoHeaders.OPAQUE_KEY)); + assertEquals(EchoHeaders.QOP_VALUE, subHeadersMap.get(EchoHeaders.QOP_KEY)); + assertEquals(EchoHeaders.REALM_VALUE, subHeadersMap.get(EchoHeaders.REALM_KEY)); + + assertEquals(USERNAME, subHeadersMap.get("username")); + assertEquals("/echo-headers/digest", subHeadersMap.get("uri")); + assertNotNull(subHeadersMap.get("cnonce")); + assertEquals("00000001", subHeadersMap.get("nc")); + assertNotNull(subHeadersMap.get("response")); + } + + private Map parseAuthenticationSubHeader(Response response, int startAt) { + String authorizationHeader = response.getHeaderString(AUTHORIZATION_HEADER_KEY); + // The substring(startAt) is used to cut off the authentication schema part from the value returned. + String[] subHeadersKeyValue = authorizationHeader.substring(startAt).split(","); + Map subHeadersMap = new HashMap<>(); + + for (String subHeader : subHeadersKeyValue) { + String[] keyValue = subHeader.split("="); + + if (keyValue[1].startsWith("\"")) { + keyValue[1] = keyValue[1].substring(1, keyValue[1].length() - 1); + } + + subHeadersMap.put(keyValue[0].trim(), keyValue[1].trim()); + } + return subHeadersMap; + } + + private void assertBasicAuthenticationHeaders(Response response) { + String base64Credentials = response.getHeaderString(AUTHORIZATION_HEADER_KEY); + // The substring(6) is used to cut the "Basic " part of the value returned, + // as it's used to indicates the authentication schema and does not belong to the credentials + byte[] credentials = Base64.getDecoder().decode(base64Credentials.substring(6)); + String[] credentialsParsed = new String(credentials).split(":"); + + assertEquals(credentialsParsed[0], USERNAME); + assertEquals(credentialsParsed[1], PASSWORD); + } + + @Override + protected Application configure() { + return new ResourceConfig() + .register(EchoHeaders.class); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + // We need this definition here, because if you are running + // the complete suit test the sendingRestrictedHeaderThroughDefaultTransportConnector_shouldReturnThanBack + // will fail if only defined on the client method, since the JerseyTest is created once. + System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); + } +} \ No newline at end of file