/*
* Copyright 2015 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 com.netflix.discovery.shared.transport.decorator;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.netflix.discovery.shared.dns.DnsService;
import com.netflix.discovery.shared.dns.DnsServiceImpl;
import com.netflix.discovery.shared.resolver.DefaultEndpoint;
import com.netflix.discovery.shared.resolver.EurekaEndpoint;
import com.netflix.discovery.shared.transport.EurekaHttpClient;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import com.netflix.discovery.shared.transport.TransportClientFactory;
import com.netflix.discovery.shared.transport.TransportException;
import com.netflix.discovery.shared.transport.TransportUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link EurekaHttpClient} that follows redirect links, and executes the requests against
* the finally resolved endpoint.
* If registration and query requests must handled separately, two different instances shall be created.
* <h3>Thread safety</h3>
* Methods in this class may be called concurrently.
*
* @author Tomasz Bak
*/
public class RedirectingEurekaHttpClient extends EurekaHttpClientDecorator {
private static final Logger logger = LoggerFactory.getLogger(RedirectingEurekaHttpClient.class);
public static final int MAX_FOLLOWED_REDIRECTS = 10;
private static final Pattern REDIRECT_PATH_REGEX = Pattern.compile("(.*/v2/)apps(/.*)?$");
private final EurekaEndpoint serviceEndpoint;
private final TransportClientFactory factory;
private final DnsService dnsService;
private final AtomicReference<EurekaHttpClient> delegateRef = new AtomicReference<>();
/**
* The delegate client should pass through 3xx responses without further processing.
*/
public RedirectingEurekaHttpClient(String serviceUrl, TransportClientFactory factory, DnsService dnsService) {
this.serviceEndpoint = new DefaultEndpoint(serviceUrl);
this.factory = factory;
this.dnsService = dnsService;
}
@Override
public void shutdown() {
TransportUtils.shutdown(delegateRef.getAndSet(null));
}
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
EurekaHttpClient currentEurekaClient = delegateRef.get();
if (currentEurekaClient == null) {
AtomicReference<EurekaHttpClient> currentEurekaClientRef = new AtomicReference<>(factory.newClient(serviceEndpoint));
try {
EurekaHttpResponse<R> response = executeOnNewServer(requestExecutor, currentEurekaClientRef);
TransportUtils.shutdown(delegateRef.getAndSet(currentEurekaClientRef.get()));
return response;
} catch (Exception e) {
logger.error("Request execution error", e);
TransportUtils.shutdown(currentEurekaClientRef.get());
throw e;
}
} else {
try {
return requestExecutor.execute(currentEurekaClient);
} catch (Exception e) {
logger.error("Request execution error", e);
delegateRef.compareAndSet(currentEurekaClient, null);
currentEurekaClient.shutdown();
throw e;
}
}
}
public static TransportClientFactory createFactory(final TransportClientFactory delegateFactory) {
final DnsServiceImpl dnsService = new DnsServiceImpl();
return new TransportClientFactory() {
@Override
public EurekaHttpClient newClient(EurekaEndpoint endpoint) {
return new RedirectingEurekaHttpClient(endpoint.getServiceUrl(), delegateFactory, dnsService);
}
@Override
public void shutdown() {
delegateFactory.shutdown();
}
};
}
private <R> EurekaHttpResponse<R> executeOnNewServer(RequestExecutor<R> requestExecutor,
AtomicReference<EurekaHttpClient> currentHttpClientRef) {
URI targetUrl = null;
for (int followRedirectCount = 0; followRedirectCount < MAX_FOLLOWED_REDIRECTS; followRedirectCount++) {
EurekaHttpResponse<R> httpResponse = requestExecutor.execute(currentHttpClientRef.get());
if (httpResponse.getStatusCode() != 302) {
if (followRedirectCount == 0) {
logger.debug("Pinning to endpoint {}", targetUrl);
} else {
logger.info("Pinning to endpoint {}, after {} redirect(s)", targetUrl, followRedirectCount);
}
return httpResponse;
}
targetUrl = getRedirectBaseUri(httpResponse.getLocation());
if (targetUrl == null) {
throw new TransportException("Invalid redirect URL " + httpResponse.getLocation());
}
currentHttpClientRef.getAndSet(null).shutdown();
currentHttpClientRef.set(factory.newClient(new DefaultEndpoint(targetUrl.toString())));
}
String message = "Follow redirect limit crossed for URI " + serviceEndpoint.getServiceUrl();
logger.warn(message);
throw new TransportException(message);
}
private URI getRedirectBaseUri(URI locationURI) {
if (locationURI == null) {
throw new TransportException("Missing Location header in the redirect reply");
}
Matcher pathMatcher = REDIRECT_PATH_REGEX.matcher(locationURI.getPath());
if (pathMatcher.matches()) {
return UriBuilder.fromUri(locationURI)
.host(dnsService.resolveIp(locationURI.getHost()))
.replacePath(pathMatcher.group(1))
.replaceQuery(null)
.build();
}
logger.warn("Invalid redirect URL {}", locationURI);
return null;
}
}