Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Redirect Policy to Azure core #23617

Merged
merged 9 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
samvaity marked this conversation as resolved.
Show resolved Hide resolved
*/
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>() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static final Set<HttpMethod> DEFAULT_REDIRECT_ALLOWED_METHODS = new HashSet<HttpMethod>() {
private static final Set<HttpMethod> DEFAULT_REDIRECT_ALLOWED_METHODS = new HashSet<>() {

{
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;
samvaity marked this conversation as resolved.
Show resolved Hide resolved
} 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());
samvaity marked this conversation as resolved.
Show resolved Hide resolved

return next.clone().process()
samvaity marked this conversation as resolved.
Show resolved Hide resolved
.flatMap(httpResponse -> {
if (redirectStrategy.shouldAttemptRedirect(context, httpResponse, redirectAttempt,
attemptedRedirectUrls)) {
samvaity marked this conversation as resolved.
Show resolved Hide resolved
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.
samvaity marked this conversation as resolved.
Show resolved Hide resolved
*/
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);
samvaity marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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