/* * Copyright 2016 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package io.reactivex.netty.protocol.http.client.internal; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.netty.internal.VoidToAnythingCast; import io.reactivex.netty.protocol.http.client.HttpClientRequest; import io.reactivex.netty.protocol.http.client.HttpClientResponse; import io.reactivex.netty.protocol.http.client.HttpRedirectException; import io.reactivex.netty.protocol.tcp.client.TcpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.functions.Func1; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static io.reactivex.netty.protocol.http.client.HttpRedirectException.Reason.*; public class Redirector<I, O> implements Func1<HttpClientResponse<O>, Observable<HttpClientResponse<O>>> { public static final int DEFAULT_MAX_REDIRECTS = 5; private static final Logger logger = LoggerFactory.getLogger(Redirector.class); private static final int[] REDIRECTABLE_STATUS_CODES = {301, 302, 303, 307, 308}; static { Arrays.sort(REDIRECTABLE_STATUS_CODES); // Required as we do binary search. This is a safety net in case the // array is modified (code change) & is not sorted. } private final List<String> visitedLocations; // Is never updated concurrently as redirects are sequential. private final int maxHops; private final AtomicInteger redirectCount; // Can be shared across multiple event loops, so needs to be thread-safe. private volatile HttpResponseStatus lastRedirectStatus; private final TcpClient<?, HttpClientResponse<O>> client; private RawRequest<I, O> originalRequest; public Redirector(int maxHops, TcpClient<?, HttpClientResponse<O>> client) { this.maxHops = maxHops; this.client = client; visitedLocations = new ArrayList<>(); redirectCount = new AtomicInteger(); } public Redirector(TcpClient<?, HttpClientResponse<O>> client) { this(DEFAULT_MAX_REDIRECTS, client); } public void setOriginalRequest(RawRequest<I, O> originalRequest) { if (null != this.originalRequest) { throw new IllegalStateException("Original request is already set."); } this.originalRequest = originalRequest; visitedLocations.add(originalRequest.getHeaders().uri()); } @Override public Observable<HttpClientResponse<O>> call(HttpClientResponse<O> response) { Observable<HttpClientResponse<O>> toReturn; if (null == originalRequest) { toReturn = Observable.error(new IllegalStateException("Raw request not available to the redirector.")); } else if (requiresRedirect(response)) { String location = extractRedirectLocation(response); if (location == null) { toReturn = Observable.error(new HttpRedirectException(InvalidRedirect, "No redirect location found.")); } else if (visitedLocations.contains(location)) { // this forms a loop toReturn = Observable.error(new HttpRedirectException(RedirectLoop, "Redirection contains a loop. Last requested location: " + location)); } else if (redirectCount.get() >= maxHops) { toReturn = Observable.error(new HttpRedirectException(TooManyRedirects, "Too many redirects. Max redirects: " + maxHops)); } else { URI redirectUri; try { redirectUri = new URI(location); lastRedirectStatus = response.getStatus(); redirectCount.incrementAndGet(); toReturn = createRedirectRequest(originalRequest, redirectUri, lastRedirectStatus.code()); } catch (Exception e) { toReturn = Observable.error(new HttpRedirectException(InvalidRedirect, "Location is not a valid URI. Provided location: " + location, e)); } } } else { return Observable.just(response); } return response.discardContent() .map(new VoidToAnythingCast<HttpClientResponse<O>>()) .ignoreElements() .concatWith(toReturn); } public boolean requiresRedirect(HttpClientResponse<O> response) { int statusCode = response.getStatus().code(); boolean requiresRedirect = false; // This class only supports relative redirects as an HttpClient is always tied to a host:port combo and hence // can not do an absolute redirect. if (Arrays.binarySearch(REDIRECTABLE_STATUS_CODES, statusCode) >= 0) { String location = extractRedirectLocation(response); // Only process relative URIs: Issue https://github.com/ReactiveX/RxNetty/issues/270 requiresRedirect = null == location || !location.startsWith("http"); } if (requiresRedirect && statusCode != HttpResponseStatus.SEE_OTHER.code()) { HttpMethod originalMethod = originalRequest.getHeaders().method(); // If the Method is not HEAD/GET do not auto redirect requiresRedirect = originalMethod == HttpMethod.GET || originalMethod == HttpMethod.HEAD; } return requiresRedirect; } protected String extractRedirectLocation(HttpClientResponse<O> redirectedResponse) { return redirectedResponse.getHeader(HttpHeaderNames.LOCATION); } protected HttpClientRequest<I, O> createRedirectRequest(RawRequest<I, O> original, URI redirectLocation, int redirectStatus) { String redirectUri = getNettyRequestUri(redirectLocation, original.getHeaders().uri(), redirectStatus); RawRequest<I, O> redirectRequest = original.setUri(redirectUri); if (redirectStatus == 303) { // according to HTTP spec, 303 mandates the change of request type to GET // If it is a get, then the content is not to be sent. redirectRequest = RawRequest.create(redirectRequest.getHeaders().protocolVersion(), HttpMethod.GET, redirectUri, this); } return HttpClientRequestImpl.create(redirectRequest, client); } protected static String getNettyRequestUri(URI uri, String originalUriString, int redirectStatus) { StringBuilder sb = new StringBuilder(); if (uri.getRawPath() != null) { sb.append(uri.getRawPath()); } if (uri.getRawQuery() != null) { sb.append('?').append(uri.getRawQuery()); } if (uri.getRawFragment() != null) { sb.append('#').append(uri.getRawFragment()); } else if(redirectStatus >= 300) { // http://tools.ietf.org/html/rfc7231#section-7.1.2 suggests that the URI fragment should be carried over to // the redirect location if not exists in the redirect location. // Issue: https://github.com/ReactiveX/RxNetty/issues/271 try { URI originalUri = new URI(originalUriString); if (originalUri.getRawFragment() != null) { sb.append('#').append(originalUri.getRawFragment()); } } catch (URISyntaxException e) { logger.warn("Error parsing original request URI during redirect. " + "This means that the path fragment if any in the original request will not be inherited " + "by the redirect.", e); } } return sb.toString(); } }