From 2c4f49e0e5f9c6b8f21f35edae373eaada87119b Mon Sep 17 00:00:00 2001 From: Ioannis Canellos Date: Wed, 20 Nov 2019 20:30:26 +0200 Subject: [PATCH] feat: add option to pass redirect Location: header value as-is without encoding, decoding, or escaping (#871) --- .../google/api/client/http/GenericUrl.java | 118 ++++++++++++++---- .../google/api/client/http/HttpRequest.java | 26 +++- .../google/api/client/http/UriTemplate.java | 2 +- .../api/client/http/UrlEncodedParser.java | 47 ++++++- .../api/client/http/GenericUrlTest.java | 12 +- 5 files changed, 170 insertions(+), 35 deletions(-) diff --git a/google-http-client/src/main/java/com/google/api/client/http/GenericUrl.java b/google-http-client/src/main/java/com/google/api/client/http/GenericUrl.java index e18810c89..68c5ea6c3 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/GenericUrl.java +++ b/google-http-client/src/main/java/com/google/api/client/http/GenericUrl.java @@ -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() {} /** @@ -99,9 +106,26 @@ 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. + * + *

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. + * + *

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. * @@ -109,6 +133,16 @@ public GenericUrl(String encodedUrl) { * @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(), @@ -116,7 +150,8 @@ public GenericUrl(URI uri) { uri.getRawPath(), uri.getRawFragment(), uri.getRawQuery(), - uri.getRawUserInfo()); + uri.getRawUserInfo(), + verbatim); } /** @@ -126,6 +161,17 @@ 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(), @@ -133,7 +179,8 @@ public GenericUrl(URL url) { url.getPath(), url.getRef(), url.getQuery(), - url.getUserInfo()); + url.getUserInfo(), + verbatim); } private GenericUrl( @@ -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 @@ -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; @@ -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(); } @@ -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); } /** @@ -482,7 +539,7 @@ public void setRawPath(String encodedPath) { */ public void appendRawPath(String encodedPath) { if (encodedPath != null && encodedPath.length() != 0) { - List appendedPathParts = toPathParts(encodedPath); + List appendedPathParts = toPathParts(encodedPath, verbatim); if (pathParts == null || pathParts.isEmpty()) { this.pathParts = appendedPathParts; } else { @@ -492,7 +549,6 @@ public void appendRawPath(String encodedPath) { } } } - /** * Returns the decoded path parts for the given encoded path. * @@ -503,6 +559,20 @@ public void appendRawPath(String encodedPath) { * or {@code ""} input */ public static List 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 toPathParts(String encodedPath, boolean verbatim) { if (encodedPath == null || encodedPath.length() == 0) { return null; } @@ -518,7 +588,7 @@ public static List 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; @@ -532,32 +602,32 @@ 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> entrySet, StringBuilder buf) { + static void addQueryParams(Set> entrySet, StringBuilder buf, boolean verbatim) { // (similar to UrlEncodedContent) boolean first = true; for (Map.Entry 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('?'); @@ -565,7 +635,7 @@ private static boolean appendParam(boolean first, StringBuilder buf, String name 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); } diff --git a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java index f6cd29ef4..a2c01d425 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java +++ b/google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java @@ -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; @@ -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). @@ -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. + * + *

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. @@ -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); diff --git a/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java b/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java index f3e7d63d1..fcf25fa49 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java +++ b/google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java @@ -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(); } diff --git a/google-http-client/src/main/java/com/google/api/client/http/UrlEncodedParser.java b/google-http-client/src/main/java/com/google/api/client/http/UrlEncodedParser.java index cd5e8a63a..fb5ec5375 100644 --- a/google-http-client/src/main/java/com/google/api/client/http/UrlEncodedParser.java +++ b/google-http-client/src/main/java/com/google/api/client/http/UrlEncodedParser.java @@ -73,7 +73,6 @@ public class UrlEncodedParser implements ObjectParser { */ public static final String MEDIA_TYPE = new HttpMediaType(UrlEncodedParser.CONTENT_TYPE).setCharsetParameter(Charsets.UTF_8).build(); - /** * Parses the given URL-encoded content into the given data object of data key name/value pairs * using {@link #parse(Reader, Object)}. @@ -82,17 +81,28 @@ public class UrlEncodedParser implements ObjectParser { * @param data data key name/value pairs */ public static void parse(String content, Object data) { + parse(content, data, true); + } + + /** + * Parses the given URL-encoded content into the given data object of data key name/value pairs + * using {@link #parse(Reader, Object)}. + * + * @param content URL-encoded content or {@code null} to ignore content + * @param data data key name/value pairs + * @param decodeEnabled flag that specifies whether decoding should be enabled. + */ + public static void parse(String content, Object data, boolean decodeEnabled) { if (content == null) { return; } try { - parse(new StringReader(content), data); + parse(new StringReader(content), data, decodeEnabled); } catch (IOException exception) { // I/O exception not expected on a string throw Throwables.propagate(exception); } } - /** * Parses the given URL-encoded content into the given data object of data key name/value pairs, * including support for repeating data key names. @@ -113,7 +123,32 @@ public static void parse(String content, Object data) { * @param data data key name/value pairs * @since 1.14 */ - public static void parse(Reader reader, Object data) throws IOException { + public static void parse(Reader reader, Object data) throws IOException { + parse(reader, data, true); + } + + /** + * Parses the given URL-encoded content into the given data object of data key name/value pairs, + * including support for repeating data key names. + * + *

Declared fields of a "primitive" type (as defined by {@link Data#isPrimitive(Type)} are + * parsed using {@link Data#parsePrimitiveValue(Type, String)} where the {@link Class} parameter + * is the declared field class. Declared fields of type {@link Collection} are used to support + * repeating data key names, so each member of the collection is an additional data key value. + * They are parsed the same as "primitive" fields, except that the generic type parameter of the + * collection is used as the {@link Class} parameter. + * + *

If there is no declared field for an input parameter name, it is ignored unless the + * input {@code data} parameter is a {@link Map}. If it is a map, the parameter value is + * stored either as a string, or as a {@link ArrayList}<String> in the case of repeated + * parameters. + * + * @param reader URL-encoded reader + * @param data data key name/value pairs + * @param decodeEnabled flag that specifies whether data should be decoded. + * @since 1.14 + */ + public static void parse(Reader reader, Object data, boolean decodeEnabled) throws IOException { Class clazz = data.getClass(); ClassInfo classInfo = ClassInfo.of(clazz); List context = Arrays.asList(clazz); @@ -132,9 +167,9 @@ public static void parse(Reader reader, Object data) throws IOException { // falls through case '&': // parse name/value pair - String name = CharEscapers.decodeUri(nameWriter.toString()); + String name = decodeEnabled ? CharEscapers.decodeUri(nameWriter.toString()) : nameWriter.toString(); if (name.length() != 0) { - String stringValue = CharEscapers.decodeUri(valueWriter.toString()); + String stringValue = decodeEnabled ? CharEscapers.decodeUri(valueWriter.toString()) : valueWriter.toString(); // get the field from the type information FieldInfo fieldInfo = classInfo.getFieldInfo(name); if (fieldInfo != null) { diff --git a/google-http-client/src/test/java/com/google/api/client/http/GenericUrlTest.java b/google-http-client/src/test/java/com/google/api/client/http/GenericUrlTest.java index 3ef972479..c83acc7ff 100644 --- a/google-http-client/src/test/java/com/google/api/client/http/GenericUrlTest.java +++ b/google-http-client/src/test/java/com/google/api/client/http/GenericUrlTest.java @@ -150,6 +150,10 @@ public TestUrl() {} public TestUrl(String encodedUrl) { super(encodedUrl); } + + public TestUrl(String encodedUrl, boolean verbatim) { + super(encodedUrl, verbatim); + } } private static final String FULL = @@ -193,6 +197,12 @@ public void testParse_full() { assertEquals("bar", url.foo); } + public void testParse_full_verbatim() { + TestUrl url = new TestUrl(FULL, true); + assertNull(url.hidden); + assertEquals("Go%3D%23/%25%26%20?%3Co%3Egle", url.getFirst("q")); + } + public void testConstructor_url() throws MalformedURLException { GenericUrl url = new GenericUrl(new URL(FULL)); subtestFull(url); @@ -473,7 +483,7 @@ public void testToPathParts() { } private void subtestToPathParts(String encodedPath, String... expectedDecodedParts) { - List result = GenericUrl.toPathParts(encodedPath); + List result = GenericUrl.toPathParts(encodedPath, false); if (encodedPath == null) { assertNull(result); } else {