Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,38 @@
*/
public class RedirectHandler implements Interceptor {
@Nonnull private final RedirectHandlerOption mRedirectOption;
@Nullable private final java.net.ProxySelector mProxySelector;

/**
* Initialize using default redirect options, default IShouldRedirect and max redirect value
*/
public RedirectHandler() {
this(null);
this(null, null);
}

/**
* Initialize using custom redirect options.
* @param redirectOption pass instance of redirect options to be used
*/
public RedirectHandler(@Nullable final RedirectHandlerOption redirectOption) {
this(redirectOption, null);
}

/**
* Initialize using custom redirect options and proxy selector.
* @param redirectOption pass instance of redirect options to be used
* @param proxySelector The ProxySelector to use for determining proxy configuration, or null to use the system default
*/
public RedirectHandler(
@Nullable final RedirectHandlerOption redirectOption,
@Nullable final java.net.ProxySelector proxySelector) {
if (redirectOption == null) {
this.mRedirectOption = new RedirectHandlerOption();
} else {
this.mRedirectOption = redirectOption;
}
this.mProxySelector =
proxySelector != null ? proxySelector : java.net.ProxySelector.getDefault();
}

boolean isRedirected(
Expand Down Expand Up @@ -81,7 +95,11 @@ boolean isRedirected(
return false;
}

Request getRedirect(final Request request, final Response userResponse)
Request getRedirect(
final Request request,
final Response userResponse,
final RedirectHandlerOption redirectOption,
final Chain chain)
throws ProtocolException {
String location = userResponse.header("Location");
if (location == null || location.length() == 0) return null;
Expand All @@ -95,25 +113,22 @@ Request getRedirect(final Request request, final Response userResponse)
location = request.url() + location;
}

HttpUrl requestUrl = userResponse.request().url();
HttpUrl requestUrl = request.url();

HttpUrl locationUrl = userResponse.request().url().resolve(location);
HttpUrl locationUrl = request.url().resolve(location);

// Don't follow redirects to unsupported protocols.
if (locationUrl == null) return null;

// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();

// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
boolean sameScheme = locationUrl.scheme().equalsIgnoreCase(requestUrl.scheme());
boolean sameHost =
locationUrl.host().toString().equalsIgnoreCase(requestUrl.host().toString());
if (!sameScheme || !sameHost) {
requestBuilder.removeHeader("Authorization");
}
// Scrub sensitive headers before following the redirect
java.util.function.Function<HttpUrl, java.net.Proxy> proxyResolver =
RedirectHandlerOption.getProxyResolver(mProxySelector);
redirectOption
.scrubSensitiveHeaders()
.scrubHeaders(requestBuilder, requestUrl, locationUrl, proxyResolver);

// Response status code 303 See Other then POST changes to GET
if (userResponse.code() == HTTP_SEE_OTHER) {
Expand Down Expand Up @@ -163,7 +178,10 @@ Request getRedirect(final Request request, final Response userResponse)
isRedirected(request, response, requestsCount, redirectOption)
&& redirectOption.shouldRedirect().shouldRedirect(response);

final Request followup = shouldRedirect ? getRedirect(request, response) : null;
final Request followup =
shouldRedirect
? getRedirect(request, response, redirectOption, chain)
: null;
if (followup != null) {
response.close();
request = followup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

import okhttp3.HttpUrl;
import okhttp3.Request;

import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

/**
* Options to be passed to the redirect middleware.
*/
Expand All @@ -28,11 +38,62 @@ public class RedirectHandlerOption implements RequestOption {
*/
@Nonnull public static final IShouldRedirect DEFAULT_SHOULD_REDIRECT = response -> true;

@Nonnull private final IScrubSensitiveHeaders scrubSensitiveHeaders;

/**
* Functional interface for scrubbing sensitive headers during redirects.
*/
@FunctionalInterface
public interface IScrubSensitiveHeaders {
/**
* Scrubs sensitive headers from the request before following a redirect.
* @param requestBuilder The request builder to modify
* @param originalUrl The original request URL
* @param newUrl The new redirect URL
* @param proxyResolver A function that returns the proxy for a given destination, or null if no proxy applies
*/
void scrubHeaders(
@Nonnull Request.Builder requestBuilder,
@Nonnull HttpUrl originalUrl,
@Nonnull HttpUrl newUrl,
@Nullable Function<HttpUrl, Proxy> proxyResolver);
}

/**
* The default implementation for scrubbing sensitive headers during redirects.
* This method removes Authorization and Cookie headers when the host or scheme changes,
* and removes Proxy-Authorization headers when no proxy is configured or the proxy is bypassed for the new URL.
*/
@Nonnull public static final IScrubSensitiveHeaders DEFAULT_SCRUB_SENSITIVE_HEADERS =
(requestBuilder, originalUrl, newUrl, proxyResolver) -> {
Objects.requireNonNull(requestBuilder, "parameter requestBuilder cannot be null");
Objects.requireNonNull(originalUrl, "parameter originalUrl cannot be null");
Objects.requireNonNull(newUrl, "parameter newUrl cannot be null");

// Remove Authorization and Cookie headers if the request's scheme, host, or port
// changes
boolean isDifferentHostOrScheme =
!newUrl.host().equalsIgnoreCase(originalUrl.host())
|| !newUrl.scheme().equalsIgnoreCase(originalUrl.scheme())
|| newUrl.port() != originalUrl.port();
if (isDifferentHostOrScheme) {
requestBuilder.removeHeader("Authorization");
requestBuilder.removeHeader("Cookie");
}

// Remove Proxy-Authorization if no proxy is configured or the URL is bypassed
boolean isProxyInactive =
proxyResolver == null || proxyResolver.apply(newUrl) == null;
if (isProxyInactive) {
requestBuilder.removeHeader("Proxy-Authorization");
}
};

/**
* Create default instance of redirect options, with default values of max redirects and should redirect
*/
public RedirectHandlerOption() {
this(DEFAULT_MAX_REDIRECTS, DEFAULT_SHOULD_REDIRECT);
this(DEFAULT_MAX_REDIRECTS, DEFAULT_SHOULD_REDIRECT, DEFAULT_SCRUB_SENSITIVE_HEADERS);
}

/**
Expand All @@ -41,13 +102,30 @@ public RedirectHandlerOption() {
* @param shouldRedirect Should redirect callback called before every redirect
*/
public RedirectHandlerOption(int maxRedirects, @Nullable final IShouldRedirect shouldRedirect) {
this(maxRedirects, shouldRedirect, DEFAULT_SCRUB_SENSITIVE_HEADERS);
}

/**
* Create an instance with provided values
* @param maxRedirects Max redirects to occur
* @param shouldRedirect Should redirect callback called before every redirect
* @param scrubSensitiveHeaders Callback to scrub sensitive headers during redirects
*/
public RedirectHandlerOption(
int maxRedirects,
@Nullable final IShouldRedirect shouldRedirect,
@Nullable final IScrubSensitiveHeaders scrubSensitiveHeaders) {
if (maxRedirects < 0)
throw new IllegalArgumentException("Max redirects cannot be negative");
if (maxRedirects > MAX_REDIRECTS)
throw new IllegalArgumentException("Max redirect cannot exceed " + MAX_REDIRECTS);

this.maxRedirects = maxRedirects;
this.shouldRedirect = shouldRedirect != null ? shouldRedirect : DEFAULT_SHOULD_REDIRECT;
this.scrubSensitiveHeaders =
scrubSensitiveHeaders != null
? scrubSensitiveHeaders
: DEFAULT_SCRUB_SENSITIVE_HEADERS;
}

/**
Expand All @@ -66,6 +144,40 @@ public int maxRedirects() {
return this.shouldRedirect;
}

/**
* Gets the callback for scrubbing sensitive headers during redirects.
* @return scrub sensitive headers callback
*/
@Nonnull public IScrubSensitiveHeaders scrubSensitiveHeaders() {
return this.scrubSensitiveHeaders;
}

/**
* Helper method to get a proxy resolver from a ProxySelector.
* @param proxySelector The ProxySelector to use, or null if no proxy is configured
* @return A function that resolves proxies for a given HttpUrl, or null if no proxy selector is provided
*/
@Nullable public static Function<HttpUrl, Proxy> getProxyResolver(
@Nullable final ProxySelector proxySelector) {
if (proxySelector == null) {
return null;
}
return url -> {
try {
URI uri = new URI(url.scheme(), null, url.host(), url.port(), null, null, null);
List<Proxy> proxies = proxySelector.select(uri);
if (proxies != null && !proxies.isEmpty()) {
Proxy proxy = proxies.get(0);
// Return null for DIRECT proxies (no proxy)
return proxy.type() == Proxy.Type.DIRECT ? null : proxy;
}
return null;
} catch (Exception e) {
return null;
}
};
}

/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
Expand Down
Loading
Loading