Skip to content

Commit

Permalink
Netty Connector doesn't support Followredirects (#5048)
Browse files Browse the repository at this point in the history
Signed-off-by: Jorge Bescos Gascon <[email protected]>
  • Loading branch information
jbescos committed May 17, 2022
1 parent d5b6823 commit c0ed81d
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,15 +18,20 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;

import javax.ws.rs.core.HttpHeaders;
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;
import org.glassfish.jersey.netty.connector.internal.RedirectException;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
Expand All @@ -35,6 +40,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;
Expand All @@ -46,21 +52,32 @@
*/
class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {

private static final int DEFAULT_MAX_REDIRECTS = 5;

// Modified only by the same thread. No need to synchronize it.
private final Set<URI> redirectUriHistory;
private final ClientRequest jerseyRequest;
private final CompletableFuture<ClientResponse> responseAvailable;
private final CompletableFuture<?> responseDone;
private final boolean followRedirects;
private final int maxRedirects;
private final NettyConnector connector;

private NettyInputStream nis;
private ClientResponse jerseyResponse;

private boolean readTimedOut;

JerseyClientHandler(ClientRequest request,
CompletableFuture<ClientResponse> responseAvailable,
CompletableFuture<?> responseDone) {
JerseyClientHandler(ClientRequest request, CompletableFuture<ClientResponse> responseAvailable,
CompletableFuture<?> responseDone, Set<URI> redirectUriHistory, NettyConnector connector) {
this.redirectUriHistory = redirectUriHistory;
this.jerseyRequest = request;
this.responseAvailable = responseAvailable;
this.responseDone = responseDone;
// Follow redirects by default
this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true);
this.maxRedirects = jerseyRequest.resolveProperty(NettyClientProperties.MAX_REDIRECTS, DEFAULT_MAX_REDIRECTS);
this.connector = connector;
}

@Override
Expand All @@ -83,15 +100,48 @@ 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(HttpHeaders.LOCATION);
if (location == null || location.isEmpty()) {
responseAvailable.completeExceptionally(new RedirectException(LocalizationMessages.REDIRECT_NO_LOCATION()));
} else {
try {
URI newUri = URI.create(location);
boolean alreadyRequested = !redirectUriHistory.add(newUri);
if (alreadyRequested) {
// infinite loop detection
responseAvailable.completeExceptionally(
new RedirectException(LocalizationMessages.REDIRECT_INFINITE_LOOP()));
} else if (redirectUriHistory.size() > maxRedirects) {
// maximal number of redirection
responseAvailable.completeExceptionally(
new RedirectException(LocalizationMessages.REDIRECT_LIMIT_REACHED(maxRedirects)));
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
connector.execute(newReq, redirectUriHistory, responseAvailable);
}
} catch (IllegalArgumentException e) {
responseAvailable.completeExceptionally(
new RedirectException(LocalizationMessages.REDIRECT_ERROR_DETERMINING_LOCATION(location)));
}
}
} else {
responseAvailable.complete(cr);
}
}
}

@Override
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,18 @@ public class NettyClientProperties {
* @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";

/**
* The maximal number of redirects during single request.
* <p/>
* Value is expected to be positive {@link Integer}. Default value is {@value #DEFAULT_MAX_REDIRECTS}.
* <p/>
* HTTP redirection must be enabled by property {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS},
* otherwise {@code MAX_REDIRECTS} is not applied.
*
* @since 2.36
* @see org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS
* @see org.glassfish.jersey.netty.connector.internal.RedirectException
*/
public static final String MAX_REDIRECTS = "jersey.config.client.NettyConnectorProvider.maxRedirects";
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -151,7 +153,9 @@ class NettyConnector implements Connector {
@Override
public ClientResponse apply(ClientRequest jerseyRequest) {
try {
return execute(jerseyRequest).join();
CompletableFuture<ClientResponse> response = new CompletableFuture<>();
execute(jerseyRequest, new HashSet<>(), response);
return response.join();
} catch (CompletionException cex) {
final Throwable t = cex.getCause() == null ? cex : cex.getCause();
throw new ProcessingException(t.getMessage(), t);
Expand All @@ -162,19 +166,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<ClientResponse> response = new CompletableFuture<>();
response.whenCompleteAsync((r, th) -> {
if (th == null) {
jerseyCallback.response(r);
} else {
jerseyCallback.failure(th);
}
}, executorService);
execute(jerseyRequest, new HashSet<>(), response);
return response;
}

protected CompletableFuture<ClientResponse> execute(final ClientRequest jerseyRequest) {
protected void execute(final ClientRequest jerseyRequest, final Set<URI> redirectUriHistory,
final CompletableFuture<ClientResponse> responseAvailable) {
Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0);
if (timeout == null || timeout < 0) {
throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout));
}

final CompletableFuture<ClientResponse> responseAvailable = new CompletableFuture<>();
final CompletableFuture<?> responseDone = new CompletableFuture<>();

final URI requestUri = jerseyRequest.getUri();
Expand Down Expand Up @@ -290,7 +300,8 @@ 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, redirectUriHistory, this);
// read timeout makes sense really as an inactivity timeout
ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
Expand Down Expand Up @@ -411,8 +422,6 @@ public void run() {
} catch (InterruptedException e) {
responseDone.completeExceptionally(e);
}

return responseAvailable;
}

private String buildPathWithQueryParameters(URI requestUri) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.internal;

import org.glassfish.jersey.client.ClientProperties;

/**
* This Exception is used only if {@link ClientProperties#FOLLOW_REDIRECTS} is set to {@code true}.
* <p/>
* This exception is thrown when any of the Redirect HTTP response status codes (301, 302, 303, 307, 308) is received and:
* <ul>
* <li>
* the chained redirection count exceeds the value of
* {@link org.glassfish.jersey.netty.connector.NettyClientProperties#MAX_REDIRECTS}
* </li>
* <li>
* or an infinite redirection loop is detected
* </li>
* <li>
* or Location response header is missing, empty or does not contain a valid {@link java.net.URI}.
* </li>
* </ul>
*
*/
public class RedirectException extends Exception {

private static final long serialVersionUID = 4357724300486801294L;

/**
* Constructor.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public RedirectException(String message) {
super(message);
}

/**
* Constructor.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public RedirectException(String message, Throwable t) {
super(message, t);
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,4 +19,7 @@ wrong.read.timeout=Unexpected ("{0}") READ_TIMEOUT.
wrong.max.pool.size=Unexpected ("{0}") maximum number of connections per destination.
wrong.max.pool.total=Unexpected ("{0}") maximum number of connections total.
wrong.max.pool.idle=Unexpected ("{0}") maximum number of idle seconds.

redirect.no.location="Received redirect that does not contain a location or the location is empty."
redirect.error.determining.location="Error determining redirect location: ({0})."
redirect.infinite.loop="Infinite loop in chained redirects detected."
redirect.limit.reached="Max chained redirect limit ({0}) exceeded."
Loading

0 comments on commit c0ed81d

Please sign in to comment.