diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java index 93e20c25a39..365b35faca8 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2022 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 @@ -18,12 +18,14 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeoutException; import javax.ws.rs.core.Response; +import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.netty.connector.internal.NettyInputStream; @@ -35,6 +37,7 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.timeout.IdleStateEvent; @@ -46,21 +49,27 @@ */ class JerseyClientHandler extends SimpleChannelInboundHandler { + private static final String LOCATION_HEADER = "Location"; + private final ClientRequest jerseyRequest; private final CompletableFuture responseAvailable; private final CompletableFuture responseDone; + private final boolean followRedirects; + private final NettyConnector connector; private NettyInputStream nis; private ClientResponse jerseyResponse; private boolean readTimedOut; - JerseyClientHandler(ClientRequest request, - CompletableFuture responseAvailable, - CompletableFuture responseDone) { + JerseyClientHandler(ClientRequest request, CompletableFuture responseAvailable, + CompletableFuture responseDone, NettyConnector connector) { this.jerseyRequest = request; this.responseAvailable = responseAvailable; this.responseDone = responseDone; + // Follow redirects by default + this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true); + this.connector = connector; } @Override @@ -83,7 +92,29 @@ protected void notifyResponse() { if (jerseyResponse != null) { ClientResponse cr = jerseyResponse; jerseyResponse = null; - responseAvailable.complete(cr); + int responseStatus = cr.getStatus(); + if (followRedirects + && (responseStatus == HttpResponseStatus.MOVED_PERMANENTLY.code() + || responseStatus == HttpResponseStatus.FOUND.code() + || responseStatus == HttpResponseStatus.SEE_OTHER.code() + || responseStatus == HttpResponseStatus.TEMPORARY_REDIRECT.code() + || responseStatus == HttpResponseStatus.PERMANENT_REDIRECT.code())) { + String location = cr.getHeaderString(LOCATION_HEADER); + try { + URI newUri = URI.create(location); + ClientRequest newReq = new ClientRequest(jerseyRequest); + newReq.setUri(newUri); + // Do not complete responseAvailable and try with new URI + // FIXME: This loops forever if HTTP response code is always a redirect. + // Currently there is no client property to specify a limit of redirections. + connector.execute(newReq, responseAvailable); + } catch (RuntimeException e) { + // It could happen if location header is wrong + responseAvailable.completeExceptionally(e); + } + } else { + responseAvailable.complete(cr); + } } } @@ -91,7 +122,6 @@ protected void notifyResponse() { public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { if (msg instanceof HttpResponse) { final HttpResponse response = (HttpResponse) msg; - jerseyResponse = new ClientResponse(new Response.StatusType() { @Override public int getStatusCode() { diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java index 056ef318027..54808098c72 100644 --- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java +++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2022 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 @@ -145,7 +145,9 @@ class NettyConnector implements Connector { @Override public ClientResponse apply(ClientRequest jerseyRequest) { try { - return execute(jerseyRequest).join(); + CompletableFuture response = new CompletableFuture<>(); + execute(jerseyRequest, response); + return response.join(); } catch (CompletionException cex) { final Throwable t = cex.getCause() == null ? cex : cex.getCause(); throw new ProcessingException(t.getMessage(), t); @@ -156,19 +158,25 @@ public ClientResponse apply(ClientRequest jerseyRequest) { @Override public Future apply(final ClientRequest jerseyRequest, final AsyncConnectorCallback jerseyCallback) { - return execute(jerseyRequest).whenCompleteAsync((r, th) -> { - if (th == null) jerseyCallback.response(r); - else jerseyCallback.failure(th); - }, executorService); + CompletableFuture response = new CompletableFuture<>(); + response.whenCompleteAsync((r, th) -> { + if (th == null) { + jerseyCallback.response(r); + } else { + jerseyCallback.failure(th); + } + }, executorService); + execute(jerseyRequest, response); + return response; } - protected CompletableFuture execute(final ClientRequest jerseyRequest) { + protected void execute(final ClientRequest jerseyRequest, + final CompletableFuture responseAvailable) { Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0); if (timeout == null || timeout < 0) { throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout)); } - final CompletableFuture responseAvailable = new CompletableFuture<>(); final CompletableFuture responseDone = new CompletableFuture<>(); final URI requestUri = jerseyRequest.getUri(); @@ -262,7 +270,7 @@ protected void initChannel(SocketChannel ch) throws Exception { // assert: it is ok to abort the entire response, if responseDone is completed exceptionally - in particular, nothing // will leak final Channel ch = chan; - JerseyClientHandler clientHandler = new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone); + JerseyClientHandler clientHandler = new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, this); // read timeout makes sense really as an inactivity timeout ch.pipeline().addLast(READ_TIMEOUT_HANDLER, new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS)); @@ -383,8 +391,6 @@ public void run() { } catch (InterruptedException e) { responseDone.completeExceptionally(e); } - - return responseAvailable; } private String buildPathWithQueryParameters(URI requestUri) { diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java new file mode 100644 index 00000000000..8a3031e5b4a --- /dev/null +++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/FollowRedirectsTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022 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.netty.connector; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; +import java.util.logging.Logger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.logging.LoggingFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +public class FollowRedirectsTest extends JerseyTest { + + private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.class.getName()); + private static final String REDIRECT_URL = "http://localhost:9998/test"; + + @Path("/test") + public static class RedirectResource { + @GET + public String get() { + return "GET"; + } + + @GET + @Path("redirect") + public Response redirect() { + return Response.seeOther(URI.create(REDIRECT_URL)).build(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(RedirectResource.class); + config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY)); + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.property(ClientProperties.FOLLOW_REDIRECTS, false); + config.connectorProvider(new NettyConnectorProvider()); + } + + @Test + public void testDoFollow() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new NettyConnectorProvider()); + Client c = ClientBuilder.newClient(config); + WebTarget t = c.target(u); + Response r = t.path("test/redirect") + .request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + c.close(); + } + + @Test + public void testDoFollowPerRequestOverride() { + WebTarget t = target("test/redirect"); + t.property(ClientProperties.FOLLOW_REDIRECTS, true); + Response r = t.request().get(); + assertEquals(200, r.getStatus()); + assertEquals("GET", r.readEntity(String.class)); + } + + @Test + public void testDontFollow() { + WebTarget t = target("test/redirect"); + assertEquals(303, t.request().get().getStatus()); + } + + @Test + public void testDontFollowPerRequestOverride() { + final URI u = target().getUri(); + ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true); + config.connectorProvider(new NettyConnectorProvider()); + Client client = ClientBuilder.newClient(config); + WebTarget t = client.target(u); + t.property(ClientProperties.FOLLOW_REDIRECTS, false); + Response r = t.path("test/redirect").request().get(); + assertEquals(303, r.getStatus()); + client.close(); + } +}