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

feat: add option to pass redirect Location: header value as-is without encoding, decoding, or escaping #871

Merged
merged 3 commits into from Nov 20, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -80,6 +80,13 @@ public class GenericUrl extends GenericData {
/** Fragment component or {@code null} for none. */
private String fragment;

/**
* If true, the URL string originally given is used as is (without encoding, decoding and
* escaping) whenever referenced; otherwise, part of the URL string may be encoded or decoded as
* deemed appropriate or necessary.
*/
private boolean verbatim;

public GenericUrl() {}

/**
Expand All @@ -99,24 +106,52 @@ public GenericUrl() {}
* @throws IllegalArgumentException if URL has a syntax error
*/
public GenericUrl(String encodedUrl) {
this(parseURL(encodedUrl));
this(encodedUrl, false);
}

/**
* Constructs from an encoded URL.
*
* <p>Any known query parameters with pre-defined fields as data keys are parsed based on
* their data type. Any unrecognized query parameter are always parsed as a string.
*
* <p>Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
*
* @param encodedUrl encoded URL, including any existing query parameters that should be parsed
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
* @throws IllegalArgumentException if URL has a syntax error
*/
public GenericUrl(String encodedUrl, boolean verbatim) {
this(parseURL(encodedUrl), verbatim);
}


/**
* Constructs from a URI.
*
* @param uri URI
* @since 1.14
*/
public GenericUrl(URI uri) {
this(uri, false);
}

/**
* Constructs from a URI.
*
* @param uri URI
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
*/
public GenericUrl(URI uri, boolean verbatim) {
this(
uri.getScheme(),
uri.getHost(),
uri.getPort(),
uri.getRawPath(),
uri.getRawFragment(),
uri.getRawQuery(),
uri.getRawUserInfo());
uri.getRawUserInfo(),
verbatim);
}

/**
Expand All @@ -126,14 +161,26 @@ public GenericUrl(URI uri) {
* @since 1.14
*/
public GenericUrl(URL url) {
this(url, false);
}

/**
* Constructs from a URL.
*
* @param url URL
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
* @since 1.14
*/
public GenericUrl(URL url, boolean verbatim) {
this(
url.getProtocol(),
url.getHost(),
url.getPort(),
url.getPath(),
url.getRef(),
url.getQuery(),
url.getUserInfo());
url.getUserInfo(),
verbatim);
}

private GenericUrl(
Expand All @@ -143,16 +190,26 @@ private GenericUrl(
String path,
String fragment,
String query,
String userInfo) {
String userInfo,
boolean verbatim) {
this.scheme = scheme.toLowerCase(Locale.US);
this.host = host;
this.port = port;
this.pathParts = toPathParts(path);
this.fragment = fragment != null ? CharEscapers.decodeUri(fragment) : null;
if (query != null) {
UrlEncodedParser.parse(query, this);
}
this.userInfo = userInfo != null ? CharEscapers.decodeUri(userInfo) : null;
this.pathParts = toPathParts(path, verbatim);
this.verbatim = verbatim;
if (verbatim) {
this.fragment = fragment;
if (query != null) {
UrlEncodedParser.parse(query, this, false);
}
this.userInfo = userInfo;
} else {
this.fragment = fragment != null ? CharEscapers.decodeUri(fragment) : null;
if (query != null) {
UrlEncodedParser.parse(query, this);
}
this.userInfo = userInfo != null ? CharEscapers.decodeUri(userInfo) : null;
}
}

@Override
Expand Down Expand Up @@ -333,7 +390,7 @@ public final String buildAuthority() {
buf.append(Preconditions.checkNotNull(scheme));
buf.append("://");
if (userInfo != null) {
buf.append(CharEscapers.escapeUriUserInfo(userInfo)).append('@');
buf.append(verbatim ? userInfo : CharEscapers.escapeUriUserInfo(userInfo)).append('@');
}
buf.append(Preconditions.checkNotNull(host));
int port = this.port;
Expand All @@ -357,12 +414,12 @@ public final String buildRelativeUrl() {
if (pathParts != null) {
appendRawPathFromParts(buf);
}
addQueryParams(entrySet(), buf);
addQueryParams(entrySet(), buf, verbatim);

// URL fragment
String fragment = this.fragment;
if (fragment != null) {
buf.append('#').append(URI_FRAGMENT_ESCAPER.escape(fragment));
buf.append('#').append(verbatim ? fragment : URI_FRAGMENT_ESCAPER.escape(fragment));
}
return buf.toString();
}
Expand Down Expand Up @@ -467,7 +524,7 @@ public String getRawPath() {
* @param encodedPath raw encoded path or {@code null} to set {@link #pathParts} to {@code null}
*/
public void setRawPath(String encodedPath) {
pathParts = toPathParts(encodedPath);
pathParts = toPathParts(encodedPath, verbatim);
}

/**
Expand All @@ -482,7 +539,7 @@ public void setRawPath(String encodedPath) {
*/
public void appendRawPath(String encodedPath) {
if (encodedPath != null && encodedPath.length() != 0) {
List<String> appendedPathParts = toPathParts(encodedPath);
List<String> appendedPathParts = toPathParts(encodedPath, verbatim);
if (pathParts == null || pathParts.isEmpty()) {
this.pathParts = appendedPathParts;
} else {
Expand All @@ -492,7 +549,6 @@ public void appendRawPath(String encodedPath) {
}
}
}

/**
* Returns the decoded path parts for the given encoded path.
*
Expand All @@ -503,6 +559,20 @@ public void appendRawPath(String encodedPath) {
* or {@code ""} input
*/
public static List<String> toPathParts(String encodedPath) {
return toPathParts(encodedPath, false);
}

/**
* Returns the path parts (decoded if not {@code verbatim}).
*
* @param encodedPath slash-prefixed encoded path, for example {@code
* "/m8/feeds/contacts/default/full"}
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
* @return path parts (decoded if not {@code verbatim}), with each part assumed to be preceded by a {@code '/'}, for example
* {@code "", "m8", "feeds", "contacts", "default", "full"}, or {@code null} for {@code null}
* or {@code ""} input
*/
public static List<String> toPathParts(String encodedPath, boolean verbatim) {
if (encodedPath == null || encodedPath.length() == 0) {
return null;
}
Expand All @@ -518,7 +588,7 @@ public static List<String> toPathParts(String encodedPath) {
} else {
sub = encodedPath.substring(cur);
}
result.add(CharEscapers.decodeUri(sub));
result.add(verbatim ? sub : CharEscapers.decodeUri(sub));
cur = slash + 1;
}
return result;
Expand All @@ -532,40 +602,40 @@ private void appendRawPathFromParts(StringBuilder buf) {
buf.append('/');
}
if (pathPart.length() != 0) {
buf.append(CharEscapers.escapeUriPath(pathPart));
buf.append(verbatim ? pathPart : CharEscapers.escapeUriPath(pathPart));
}
}
}

/** Adds query parameters from the provided entrySet into the buffer. */
static void addQueryParams(Set<Entry<String, Object>> entrySet, StringBuilder buf) {
static void addQueryParams(Set<Entry<String, Object>> entrySet, StringBuilder buf, boolean verbatim) {
// (similar to UrlEncodedContent)
boolean first = true;
for (Map.Entry<String, Object> nameValueEntry : entrySet) {
Object value = nameValueEntry.getValue();
if (value != null) {
String name = CharEscapers.escapeUriQuery(nameValueEntry.getKey());
String name = verbatim ? nameValueEntry.getKey() : CharEscapers.escapeUriQuery(nameValueEntry.getKey());
if (value instanceof Collection<?>) {
Collection<?> collectionValue = (Collection<?>) value;
for (Object repeatedValue : collectionValue) {
first = appendParam(first, buf, name, repeatedValue);
first = appendParam(first, buf, name, repeatedValue, verbatim);
}
} else {
first = appendParam(first, buf, name, value);
first = appendParam(first, buf, name, value, verbatim);
}
}
}
}

private static boolean appendParam(boolean first, StringBuilder buf, String name, Object value) {
private static boolean appendParam(boolean first, StringBuilder buf, String name, Object value, boolean verbatim) {
if (first) {
first = false;
buf.append('?');
} else {
buf.append('&');
}
buf.append(name);
String stringValue = CharEscapers.escapeUriQuery(value.toString());
String stringValue = verbatim ? value.toString() : CharEscapers.escapeUriQuery(value.toString());
if (stringValue.length() != 0) {
buf.append('=').append(stringValue);
}
Expand Down
Expand Up @@ -141,7 +141,7 @@ public final class HttpRequest {

/** HTTP request URL. */
private GenericUrl url;

/** Timeout in milliseconds to establish a connection or {@code 0} for an infinite timeout. */
private int connectTimeout = 20 * 1000;

Expand Down Expand Up @@ -172,9 +172,12 @@ public final class HttpRequest {
/** The {@link BackOffPolicy} to use between retry attempts or {@code null} for none. */
@Deprecated @Beta private BackOffPolicy backOffPolicy;

/** Whether to automatically follow redirects ({@code true} by default). */
/** Whether to automatically follow redirects ({@code true} by default). */
private boolean followRedirects = true;

/** Whether to use raw redirect URLs ({@code false} by default). */
private boolean useRawRedirectUrls = false;

/**
* Whether to throw an exception at the end of {@link #execute()} on an HTTP error code (non-2XX)
* after all retries and response handlers have been exhausted ({@code true} by default).
Expand Down Expand Up @@ -695,6 +698,23 @@ public HttpRequest setFollowRedirects(boolean followRedirects) {
return this;
}

/**
* Return whether to use raw redirect URLs.
*/
public boolean getUseRawRedirectUrls() {
return useRawRedirectUrls;
}

/**
* Sets whether to use raw redirect URLs.
*
* <p>The default value is {@code false}.
*/
public HttpRequest setUseRawRedirectUrls(boolean useRawRedirectUrls) {
this.useRawRedirectUrls = useRawRedirectUrls;
return this;
}

/**
* Returns whether to throw an exception at the end of {@link #execute()} on an HTTP error code
* (non-2XX) after all retries and response handlers have been exhausted.
Expand Down Expand Up @@ -1159,7 +1179,7 @@ public boolean handleRedirect(int statusCode, HttpHeaders responseHeaders) {
&& HttpStatusCodes.isRedirect(statusCode)
&& redirectLocation != null) {
// resolve the redirect location relative to the current location
setUrl(new GenericUrl(url.toURL(redirectLocation)));
setUrl(new GenericUrl(url.toURL(redirectLocation), useRawRedirectUrls));
// on 303 change method to GET
if (statusCode == HttpStatusCodes.STATUS_CODE_SEE_OTHER) {
setRequestMethod(HttpMethods.GET);
Expand Down
Expand Up @@ -318,7 +318,7 @@ public static String expand(
}
if (addUnusedParamsAsQueryParams) {
// Add the parameters remaining in the variableMap as query parameters.
GenericUrl.addQueryParams(variableMap.entrySet(), pathBuf);
GenericUrl.addQueryParams(variableMap.entrySet(), pathBuf, false);
}
return pathBuf.toString();
}
Expand Down