Skip to content

Commit

Permalink
feat: add option to pass redirect Location: header value as-is withou…
Browse files Browse the repository at this point in the history
…t encoding, decoding, or escaping (#871)
  • Loading branch information
iocanel authored and codyoss committed Nov 20, 2019
1 parent 9d81ee3 commit 2c4f49e
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 35 deletions.
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

0 comments on commit 2c4f49e

Please sign in to comment.