Skip to content

Commit

Permalink
Add Redirect Policy to Azure core (#23617)
Browse files Browse the repository at this point in the history
  • Loading branch information
samvaity authored Aug 31, 2021
1 parent 6dabf5b commit 3277d65
Show file tree
Hide file tree
Showing 6 changed files with 666 additions and 1 deletion.
1 change: 1 addition & 0 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features Added

- Added `HttpAuthorization` which supports configuring a generic `Authorization` header on a request.
- Added `RedirectPolicy` to standardize the ability to redirect HTTP requests.

## 1.19.0 (2021-08-06)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineN
* Authorizes the request with the bearer token acquired using the specified {@code tokenRequestContext}
*
* @param context the HTTP pipeline context.
* @param tokenRequestContext the token request conext to be used for token acquisition.
* @param tokenRequestContext the token request context to be used for token acquisition.
* @return a {@link Mono} containing {@link Void}
*/
public Mono<Void> setAuthorizationHeader(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http.policy;

import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.util.CoreUtils;
import com.azure.core.util.logging.ClientLogger;

import java.net.HttpURLConnection;
import java.util.HashSet;
import java.util.Set;

/**
* A default implementation of {@link RedirectStrategy} that uses the provided maximum retry attempts,
* header name to look up redirect url value for, http methods and a known set of
* redirect status response codes (301, 302, 307, 308) to determine if request should be redirected.
*/
public final class DefaultRedirectStrategy implements RedirectStrategy {
private final ClientLogger logger = new ClientLogger(DefaultRedirectStrategy.class);

private static final int DEFAULT_MAX_REDIRECT_ATTEMPTS = 3;
private static final String DEFAULT_REDIRECT_LOCATION_HEADER_NAME = "Location";
private static final int PERMANENT_REDIRECT_STATUS_CODE = 308;
private static final int TEMPORARY_REDIRECT_STATUS_CODE = 307;
private static final Set<HttpMethod> DEFAULT_REDIRECT_ALLOWED_METHODS = new HashSet<HttpMethod>() {
{
add(HttpMethod.GET);
add(HttpMethod.HEAD);
}
};

private final int maxAttempts;
private final String locationHeader;
private final Set<HttpMethod> redirectMethods;

/**
* Creates an instance of {@link DefaultRedirectStrategy} with a maximum number of redirect attempts 3,
* header name "Location" to locate the redirect url in the response headers and {@link HttpMethod#GET}
* and {@link HttpMethod#HEAD} as allowed methods for performing the redirect.
*/
public DefaultRedirectStrategy() {
this(DEFAULT_MAX_REDIRECT_ATTEMPTS, DEFAULT_REDIRECT_LOCATION_HEADER_NAME, DEFAULT_REDIRECT_ALLOWED_METHODS);
}

/**
* Creates an instance of {@link DefaultRedirectStrategy} with the provided number of redirect attempts and
* default header name "Location" to locate the redirect url in the response headers and {@link HttpMethod#GET}
* and {@link HttpMethod#HEAD} as allowed methods for performing the redirect.
*
* @param maxAttempts The max number of redirect attempts that can be made.
* @throws IllegalArgumentException if {@code maxAttempts} is less than 0.
*/
public DefaultRedirectStrategy(int maxAttempts) {
this(maxAttempts, DEFAULT_REDIRECT_LOCATION_HEADER_NAME, DEFAULT_REDIRECT_ALLOWED_METHODS);
}

/**
* Creates an instance of {@link DefaultRedirectStrategy}.
*
* @param maxAttempts The max number of redirect attempts that can be made.
* @param locationHeader The header name containing the redirect URL.
* @param allowedMethods The set of {@link HttpMethod} that are allowed to be redirected.
* @throws IllegalArgumentException if {@code maxAttempts} is less than 0.
*/
public DefaultRedirectStrategy(int maxAttempts, String locationHeader, Set<HttpMethod> allowedMethods) {
if (maxAttempts < 0) {
throw logger.logExceptionAsError(new IllegalArgumentException("Max attempts cannot be less than 0."));
}
this.maxAttempts = maxAttempts;
if (CoreUtils.isNullOrEmpty(locationHeader)) {
logger.error("'locationHeader' provided as null will be defaulted to {}",
DEFAULT_REDIRECT_LOCATION_HEADER_NAME);
this.locationHeader = DEFAULT_REDIRECT_LOCATION_HEADER_NAME;
} else {
this.locationHeader = locationHeader;
}
if (CoreUtils.isNullOrEmpty(allowedMethods)) {
logger.error("'allowedMethods' provided as null will be defaulted to {}", DEFAULT_REDIRECT_ALLOWED_METHODS);
this.redirectMethods = DEFAULT_REDIRECT_ALLOWED_METHODS;
} else {
this.redirectMethods = allowedMethods;
}
}

@Override
public boolean shouldAttemptRedirect(HttpPipelineCallContext context,
HttpResponse httpResponse, int tryCount,
Set<String> attemptedRedirectUrls) {
String redirectUrl =
tryGetRedirectHeader(httpResponse.getHeaders(), this.getLocationHeader());

if (isValidRedirectCount(tryCount)
&& redirectUrl != null
&& !alreadyAttemptedRedirectUrl(redirectUrl, attemptedRedirectUrls)
&& isValidRedirectStatusCode(httpResponse.getStatusCode())
&& isAllowedRedirectMethod(httpResponse.getRequest().getHttpMethod())) {
logger.verbose("[Redirecting] Try count: {}, Attempted Redirect URLs: {}", tryCount,
attemptedRedirectUrls.toString());
attemptedRedirectUrls.add(redirectUrl);
return true;
} else {
return false;
}
}

@Override
public HttpRequest createRedirectRequest(HttpResponse httpResponse) {
String responseLocation =
tryGetRedirectHeader(httpResponse.getHeaders(), this.getLocationHeader());
return httpResponse.getRequest().setUrl(responseLocation);
}

@Override
public int getMaxAttempts() {
return maxAttempts;
}

@Override
public String getLocationHeader() {
return locationHeader;
}

@Override
public Set<HttpMethod> getAllowedMethods() {
return redirectMethods;
}

/**
* Check if the redirect url provided in the response headers is already attempted.
*
* @param redirectUrl the redirect url provided in the response header.
* @param attemptedRedirectUrls the set containing a list of attempted redirect locations.
* @return {@code true} if the redirectUrl provided in the response header is already being attempted for redirect
* , {@code false} otherwise.
*/
private boolean alreadyAttemptedRedirectUrl(String redirectUrl,
Set<String> attemptedRedirectUrls) {
if (attemptedRedirectUrls.contains(redirectUrl)) {
logger.error("Request was redirected more than once to: {}", redirectUrl);
return true;
}
return false;
}

/**
* Check if the attempt count of the redirect is less than the {@code maxAttempts}
*
* @param tryCount the try count for the HTTP request associated to the HTTP response.
* @return {@code true} if the {@code tryCount} is greater than the {@code maxAttempts}, {@code false} otherwise.
*/
private boolean isValidRedirectCount(int tryCount) {
if (tryCount >= getMaxAttempts()) {
logger.error("Request has been redirected more than {} times.", getMaxAttempts());
return false;
}
return true;
}

/**
* Check if the request http method is a valid redirect method.
*
* @param httpMethod the http method of the request.
* @return {@code true} if the request {@code httpMethod} is a valid http redirect method, {@code false} otherwise.
*/
private boolean isAllowedRedirectMethod(HttpMethod httpMethod) {
if (getAllowedMethods().contains(httpMethod)) {
return true;
} else {
logger.error("Request was redirected from an invalid redirect allowed method: {}", httpMethod);
return false;
}
}

/**
* Checks if the incoming request status code is a valid redirect status code.
*
* @param statusCode the status code of the incoming request.
* @return {@code true} if the request {@code statusCode} is a valid http redirect method, {@code false} otherwise.
*/
private boolean isValidRedirectStatusCode(int statusCode) {
return statusCode == HttpURLConnection.HTTP_MOVED_TEMP
|| statusCode == HttpURLConnection.HTTP_MOVED_PERM
|| statusCode == PERMANENT_REDIRECT_STATUS_CODE
|| statusCode == TEMPORARY_REDIRECT_STATUS_CODE;
}

/**
* Gets the redirect url from the response headers.
*
* @param headers the http response headers.
* @param headerName the header name to look up value for.
* @return the header value for the provided header name, {@code null} otherwise.
*/
String tryGetRedirectHeader(HttpHeaders headers, String headerName) {
String headerValue = headers.getValue(headerName);
if (CoreUtils.isNullOrEmpty(headerValue)) {
logger.error("Redirect url was null for header name: {}, Request redirect was terminated", headerName);
return null;
} else {
return headerValue;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http.policy;

import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import reactor.core.publisher.Mono;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
* A {@link HttpPipelinePolicy} that redirects a {@link HttpRequest} when an HTTP Redirect is received as
* {@link HttpResponse response}.
*/
public final class RedirectPolicy implements HttpPipelinePolicy {
private final RedirectStrategy redirectStrategy;

/**
* Creates {@link RedirectPolicy} with default {@link DefaultRedirectStrategy} as {@link RedirectStrategy} and
* uses the redirect status response code (301, 302, 307, 308) to determine if this request should be redirected.
*/
public RedirectPolicy() {
this(new DefaultRedirectStrategy());
}

/**
* Creates {@link RedirectPolicy} with the provided {@code redirectStrategy} as {@link RedirectStrategy}
* to determine if this request should be redirected.
*
* @param redirectStrategy The {@link RedirectStrategy} used for redirection.
* @throws NullPointerException When {@code redirectStrategy} is {@code null}.
*/
public RedirectPolicy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = Objects.requireNonNull(redirectStrategy, "'redirectStrategy' cannot be null.");
}

@Override
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
// Reset the attemptedRedirectUrls for each individual request.
return attemptRedirect(context, next, context.getHttpRequest(), 1, new HashSet<>());
}

/**
* Function to process through the HTTP Response received in the pipeline
* and redirect sending the request with new redirect url.
*/
private Mono<HttpResponse> attemptRedirect(final HttpPipelineCallContext context,
final HttpPipelineNextPolicy next,
final HttpRequest originalHttpRequest,
final int redirectAttempt,
Set<String> attemptedRedirectUrls) {
// make sure the context is not modified during retry, except for the URL
context.setHttpRequest(originalHttpRequest.copy());

return next.clone().process()
.flatMap(httpResponse -> {
if (redirectStrategy.shouldAttemptRedirect(context, httpResponse, redirectAttempt,
attemptedRedirectUrls)) {
HttpRequest redirectRequestCopy = redirectStrategy.createRedirectRequest(httpResponse);
return httpResponse.getBody()
.ignoreElements()
.then(attemptRedirect(context, next, redirectRequestCopy, redirectAttempt + 1, attemptedRedirectUrls));
} else {
return Mono.just(httpResponse);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.http.policy;

import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;

import java.util.Set;

/**
* The interface for determining the {@link RedirectStrategy redirect strategy} used in {@link RedirectPolicy}.
*/
public interface RedirectStrategy {
/**
* Max number of redirect attempts to be made.
*
* @return The max number of redirect attempts.
*/
int getMaxAttempts();

/**
* The header name to look up the value for the redirect url in response headers.
*
* @return the value of the header, or null if the header doesn't exist in the response.
*/
String getLocationHeader();

/**
* The {@link HttpMethod http methods} that are allowed to be redirected.
*
* @return the set of redirect allowed methods.
*/
Set<HttpMethod> getAllowedMethods();

/**
* Determines if the url should be redirected between each try.
*
* @param context the {@link HttpPipelineCallContext HTTP pipeline context}.
* @param httpResponse the {@link HttpRequest} containing the redirect url present in the response headers
* @param tryCount redirect attempts so far
* @param attemptedRedirectUrls attempted redirect locations used so far.
* @return {@code true} if the request should be redirected, {@code false} otherwise
*/
boolean shouldAttemptRedirect(HttpPipelineCallContext context, HttpResponse httpResponse, int tryCount,
Set<String> attemptedRedirectUrls);

/**
* Creates an {@link HttpRequest request} for the redirect attempt.
*
* @param httpResponse the {@link HttpResponse} containing the redirect url present in the response headers
* @return the modified {@link HttpRequest} to redirect the incoming request.
*/
HttpRequest createRedirectRequest(HttpResponse httpResponse);
}
Loading

0 comments on commit 3277d65

Please sign in to comment.