Skip to content

Commit 2c4f49e

Browse files
iocanelcodyoss
authored andcommitted
feat: add option to pass redirect Location: header value as-is without encoding, decoding, or escaping (#871)
1 parent 9d81ee3 commit 2c4f49e

File tree

5 files changed

+170
-35
lines changed

5 files changed

+170
-35
lines changed

google-http-client/src/main/java/com/google/api/client/http/GenericUrl.java

Lines changed: 94 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ public class GenericUrl extends GenericData {
8080
/** Fragment component or {@code null} for none. */
8181
private String fragment;
8282

83+
/**
84+
* If true, the URL string originally given is used as is (without encoding, decoding and
85+
* escaping) whenever referenced; otherwise, part of the URL string may be encoded or decoded as
86+
* deemed appropriate or necessary.
87+
*/
88+
private boolean verbatim;
89+
8390
public GenericUrl() {}
8491

8592
/**
@@ -99,24 +106,52 @@ public GenericUrl() {}
99106
* @throws IllegalArgumentException if URL has a syntax error
100107
*/
101108
public GenericUrl(String encodedUrl) {
102-
this(parseURL(encodedUrl));
109+
this(encodedUrl, false);
110+
}
111+
112+
/**
113+
* Constructs from an encoded URL.
114+
*
115+
* <p>Any known query parameters with pre-defined fields as data keys are parsed based on
116+
* their data type. Any unrecognized query parameter are always parsed as a string.
117+
*
118+
* <p>Any {@link MalformedURLException} is wrapped in an {@link IllegalArgumentException}.
119+
*
120+
* @param encodedUrl encoded URL, including any existing query parameters that should be parsed
121+
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
122+
* @throws IllegalArgumentException if URL has a syntax error
123+
*/
124+
public GenericUrl(String encodedUrl, boolean verbatim) {
125+
this(parseURL(encodedUrl), verbatim);
103126
}
104127

128+
105129
/**
106130
* Constructs from a URI.
107131
*
108132
* @param uri URI
109133
* @since 1.14
110134
*/
111135
public GenericUrl(URI uri) {
136+
this(uri, false);
137+
}
138+
139+
/**
140+
* Constructs from a URI.
141+
*
142+
* @param uri URI
143+
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
144+
*/
145+
public GenericUrl(URI uri, boolean verbatim) {
112146
this(
113147
uri.getScheme(),
114148
uri.getHost(),
115149
uri.getPort(),
116150
uri.getRawPath(),
117151
uri.getRawFragment(),
118152
uri.getRawQuery(),
119-
uri.getRawUserInfo());
153+
uri.getRawUserInfo(),
154+
verbatim);
120155
}
121156

122157
/**
@@ -126,14 +161,26 @@ public GenericUrl(URI uri) {
126161
* @since 1.14
127162
*/
128163
public GenericUrl(URL url) {
164+
this(url, false);
165+
}
166+
167+
/**
168+
* Constructs from a URL.
169+
*
170+
* @param url URL
171+
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
172+
* @since 1.14
173+
*/
174+
public GenericUrl(URL url, boolean verbatim) {
129175
this(
130176
url.getProtocol(),
131177
url.getHost(),
132178
url.getPort(),
133179
url.getPath(),
134180
url.getRef(),
135181
url.getQuery(),
136-
url.getUserInfo());
182+
url.getUserInfo(),
183+
verbatim);
137184
}
138185

139186
private GenericUrl(
@@ -143,16 +190,26 @@ private GenericUrl(
143190
String path,
144191
String fragment,
145192
String query,
146-
String userInfo) {
193+
String userInfo,
194+
boolean verbatim) {
147195
this.scheme = scheme.toLowerCase(Locale.US);
148196
this.host = host;
149197
this.port = port;
150-
this.pathParts = toPathParts(path);
151-
this.fragment = fragment != null ? CharEscapers.decodeUri(fragment) : null;
152-
if (query != null) {
153-
UrlEncodedParser.parse(query, this);
154-
}
155-
this.userInfo = userInfo != null ? CharEscapers.decodeUri(userInfo) : null;
198+
this.pathParts = toPathParts(path, verbatim);
199+
this.verbatim = verbatim;
200+
if (verbatim) {
201+
this.fragment = fragment;
202+
if (query != null) {
203+
UrlEncodedParser.parse(query, this, false);
204+
}
205+
this.userInfo = userInfo;
206+
} else {
207+
this.fragment = fragment != null ? CharEscapers.decodeUri(fragment) : null;
208+
if (query != null) {
209+
UrlEncodedParser.parse(query, this);
210+
}
211+
this.userInfo = userInfo != null ? CharEscapers.decodeUri(userInfo) : null;
212+
}
156213
}
157214

158215
@Override
@@ -333,7 +390,7 @@ public final String buildAuthority() {
333390
buf.append(Preconditions.checkNotNull(scheme));
334391
buf.append("://");
335392
if (userInfo != null) {
336-
buf.append(CharEscapers.escapeUriUserInfo(userInfo)).append('@');
393+
buf.append(verbatim ? userInfo : CharEscapers.escapeUriUserInfo(userInfo)).append('@');
337394
}
338395
buf.append(Preconditions.checkNotNull(host));
339396
int port = this.port;
@@ -357,12 +414,12 @@ public final String buildRelativeUrl() {
357414
if (pathParts != null) {
358415
appendRawPathFromParts(buf);
359416
}
360-
addQueryParams(entrySet(), buf);
417+
addQueryParams(entrySet(), buf, verbatim);
361418

362419
// URL fragment
363420
String fragment = this.fragment;
364421
if (fragment != null) {
365-
buf.append('#').append(URI_FRAGMENT_ESCAPER.escape(fragment));
422+
buf.append('#').append(verbatim ? fragment : URI_FRAGMENT_ESCAPER.escape(fragment));
366423
}
367424
return buf.toString();
368425
}
@@ -467,7 +524,7 @@ public String getRawPath() {
467524
* @param encodedPath raw encoded path or {@code null} to set {@link #pathParts} to {@code null}
468525
*/
469526
public void setRawPath(String encodedPath) {
470-
pathParts = toPathParts(encodedPath);
527+
pathParts = toPathParts(encodedPath, verbatim);
471528
}
472529

473530
/**
@@ -482,7 +539,7 @@ public void setRawPath(String encodedPath) {
482539
*/
483540
public void appendRawPath(String encodedPath) {
484541
if (encodedPath != null && encodedPath.length() != 0) {
485-
List<String> appendedPathParts = toPathParts(encodedPath);
542+
List<String> appendedPathParts = toPathParts(encodedPath, verbatim);
486543
if (pathParts == null || pathParts.isEmpty()) {
487544
this.pathParts = appendedPathParts;
488545
} else {
@@ -492,7 +549,6 @@ public void appendRawPath(String encodedPath) {
492549
}
493550
}
494551
}
495-
496552
/**
497553
* Returns the decoded path parts for the given encoded path.
498554
*
@@ -503,6 +559,20 @@ public void appendRawPath(String encodedPath) {
503559
* or {@code ""} input
504560
*/
505561
public static List<String> toPathParts(String encodedPath) {
562+
return toPathParts(encodedPath, false);
563+
}
564+
565+
/**
566+
* Returns the path parts (decoded if not {@code verbatim}).
567+
*
568+
* @param encodedPath slash-prefixed encoded path, for example {@code
569+
* "/m8/feeds/contacts/default/full"}
570+
* @param verbatim flag, to specify if URL should be used as is (without encoding, decoding and escaping)
571+
* @return path parts (decoded if not {@code verbatim}), with each part assumed to be preceded by a {@code '/'}, for example
572+
* {@code "", "m8", "feeds", "contacts", "default", "full"}, or {@code null} for {@code null}
573+
* or {@code ""} input
574+
*/
575+
public static List<String> toPathParts(String encodedPath, boolean verbatim) {
506576
if (encodedPath == null || encodedPath.length() == 0) {
507577
return null;
508578
}
@@ -518,7 +588,7 @@ public static List<String> toPathParts(String encodedPath) {
518588
} else {
519589
sub = encodedPath.substring(cur);
520590
}
521-
result.add(CharEscapers.decodeUri(sub));
591+
result.add(verbatim ? sub : CharEscapers.decodeUri(sub));
522592
cur = slash + 1;
523593
}
524594
return result;
@@ -532,40 +602,40 @@ private void appendRawPathFromParts(StringBuilder buf) {
532602
buf.append('/');
533603
}
534604
if (pathPart.length() != 0) {
535-
buf.append(CharEscapers.escapeUriPath(pathPart));
605+
buf.append(verbatim ? pathPart : CharEscapers.escapeUriPath(pathPart));
536606
}
537607
}
538608
}
539609

540610
/** Adds query parameters from the provided entrySet into the buffer. */
541-
static void addQueryParams(Set<Entry<String, Object>> entrySet, StringBuilder buf) {
611+
static void addQueryParams(Set<Entry<String, Object>> entrySet, StringBuilder buf, boolean verbatim) {
542612
// (similar to UrlEncodedContent)
543613
boolean first = true;
544614
for (Map.Entry<String, Object> nameValueEntry : entrySet) {
545615
Object value = nameValueEntry.getValue();
546616
if (value != null) {
547-
String name = CharEscapers.escapeUriQuery(nameValueEntry.getKey());
617+
String name = verbatim ? nameValueEntry.getKey() : CharEscapers.escapeUriQuery(nameValueEntry.getKey());
548618
if (value instanceof Collection<?>) {
549619
Collection<?> collectionValue = (Collection<?>) value;
550620
for (Object repeatedValue : collectionValue) {
551-
first = appendParam(first, buf, name, repeatedValue);
621+
first = appendParam(first, buf, name, repeatedValue, verbatim);
552622
}
553623
} else {
554-
first = appendParam(first, buf, name, value);
624+
first = appendParam(first, buf, name, value, verbatim);
555625
}
556626
}
557627
}
558628
}
559629

560-
private static boolean appendParam(boolean first, StringBuilder buf, String name, Object value) {
630+
private static boolean appendParam(boolean first, StringBuilder buf, String name, Object value, boolean verbatim) {
561631
if (first) {
562632
first = false;
563633
buf.append('?');
564634
} else {
565635
buf.append('&');
566636
}
567637
buf.append(name);
568-
String stringValue = CharEscapers.escapeUriQuery(value.toString());
638+
String stringValue = verbatim ? value.toString() : CharEscapers.escapeUriQuery(value.toString());
569639
if (stringValue.length() != 0) {
570640
buf.append('=').append(stringValue);
571641
}

google-http-client/src/main/java/com/google/api/client/http/HttpRequest.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public final class HttpRequest {
141141

142142
/** HTTP request URL. */
143143
private GenericUrl url;
144-
144+
145145
/** Timeout in milliseconds to establish a connection or {@code 0} for an infinite timeout. */
146146
private int connectTimeout = 20 * 1000;
147147

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

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

178+
/** Whether to use raw redirect URLs ({@code false} by default). */
179+
private boolean useRawRedirectUrls = false;
180+
178181
/**
179182
* Whether to throw an exception at the end of {@link #execute()} on an HTTP error code (non-2XX)
180183
* after all retries and response handlers have been exhausted ({@code true} by default).
@@ -695,6 +698,23 @@ public HttpRequest setFollowRedirects(boolean followRedirects) {
695698
return this;
696699
}
697700

701+
/**
702+
* Return whether to use raw redirect URLs.
703+
*/
704+
public boolean getUseRawRedirectUrls() {
705+
return useRawRedirectUrls;
706+
}
707+
708+
/**
709+
* Sets whether to use raw redirect URLs.
710+
*
711+
* <p>The default value is {@code false}.
712+
*/
713+
public HttpRequest setUseRawRedirectUrls(boolean useRawRedirectUrls) {
714+
this.useRawRedirectUrls = useRawRedirectUrls;
715+
return this;
716+
}
717+
698718
/**
699719
* Returns whether to throw an exception at the end of {@link #execute()} on an HTTP error code
700720
* (non-2XX) after all retries and response handlers have been exhausted.
@@ -1159,7 +1179,7 @@ public boolean handleRedirect(int statusCode, HttpHeaders responseHeaders) {
11591179
&& HttpStatusCodes.isRedirect(statusCode)
11601180
&& redirectLocation != null) {
11611181
// resolve the redirect location relative to the current location
1162-
setUrl(new GenericUrl(url.toURL(redirectLocation)));
1182+
setUrl(new GenericUrl(url.toURL(redirectLocation), useRawRedirectUrls));
11631183
// on 303 change method to GET
11641184
if (statusCode == HttpStatusCodes.STATUS_CODE_SEE_OTHER) {
11651185
setRequestMethod(HttpMethods.GET);

google-http-client/src/main/java/com/google/api/client/http/UriTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public static String expand(
318318
}
319319
if (addUnusedParamsAsQueryParams) {
320320
// Add the parameters remaining in the variableMap as query parameters.
321-
GenericUrl.addQueryParams(variableMap.entrySet(), pathBuf);
321+
GenericUrl.addQueryParams(variableMap.entrySet(), pathBuf, false);
322322
}
323323
return pathBuf.toString();
324324
}

0 commit comments

Comments
 (0)