Skip to content
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
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,10 @@ boolean isRedirected(
return false;
}

Request getRedirect(final Request request, final Response userResponse)
Request getRedirect(
final Request request,
final Response userResponse,
final RedirectHandlerOption redirectOption)
throws ProtocolException {
String location = userResponse.header("Location");
if (location == null || location.length() == 0) return null;
Expand All @@ -95,32 +112,29 @@ 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");
}
Request.Builder requestBuilder = userResponse.request().newBuilder().url(locationUrl);

// Scrub sensitive headers before following the redirect
java.util.function.Function<HttpUrl, java.net.Proxy> proxyResolver =
RedirectHandlerOption.getProxyResolver(mProxySelector);
redirectOption
.scrubSensitiveHeaders()
.scrubHeaders(requestBuilder, requestUrl, proxyResolver);

// Response status code 303 See Other then POST changes to GET
if (userResponse.code() == HTTP_SEE_OTHER) {
requestBuilder.method("GET", null);
}

return requestBuilder.url(locationUrl).build();
return requestBuilder.build();
}

// Intercept request and response made to network
Expand Down Expand Up @@ -163,7 +177,8 @@ 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) : 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,63 @@ 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 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,
@Nullable Function<HttpUrl, Proxy> proxyResolver);
}

/**
* The default implementation for scrubbing sensitive headers during redirects.
* This method removes Authorization and Cookie headers when the host, scheme, or port 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, proxyResolver) -> {
Objects.requireNonNull(requestBuilder, "parameter requestBuilder cannot be null");
Objects.requireNonNull(originalUrl, "parameter originalUrl cannot be null");

// Get the new URL from the request builder
HttpUrl newUrl = requestBuilder.build().url();
Objects.requireNonNull(newUrl, "The request URL cannot be null");

// Remove Authorization and Cookie headers if the request's scheme, host, or port
// changes
boolean isDifferentOrigin =
!newUrl.host().equalsIgnoreCase(originalUrl.host())
|| !newUrl.scheme().equalsIgnoreCase(originalUrl.scheme())
|| newUrl.port() != originalUrl.port();
if (isDifferentOrigin) {
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 +103,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 +145,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