Skip to content

Commit bbb53d0

Browse files
committed
Pluggable URI parsing, use RFC parser by default
See gh-33639
1 parent 52805da commit bbb53d0

File tree

5 files changed

+165
-44
lines changed

5 files changed

+165
-44
lines changed

Diff for: spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java

+31-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public class DefaultUriBuilderFactory implements UriBuilderFactory {
4545
@Nullable
4646
private final UriComponentsBuilder baseUri;
4747

48+
@Nullable
49+
private UriComponentsBuilder.ParserType parserType;
50+
4851
private EncodingMode encodingMode = EncodingMode.TEMPLATE_AND_VALUES;
4952

5053
@Nullable
@@ -92,6 +95,26 @@ public final boolean hasBaseUri() {
9295
return (this.baseUri != null);
9396
}
9497

98+
/**
99+
* Set the {@link UriComponentsBuilder.ParserType} to use.
100+
* <p>By default, if the parser type is not specified,
101+
* {@link UriComponentsBuilder} uses {@link UriComponentsBuilder.ParserType#RFC}.
102+
* @param parserType the parser type
103+
* @since 6.2
104+
*/
105+
public void setParserType(UriComponentsBuilder.ParserType parserType) {
106+
this.parserType = parserType;
107+
}
108+
109+
/**
110+
* Return the configured parser type.
111+
* @since 6.2
112+
*/
113+
@Nullable
114+
public UriComponentsBuilder.ParserType getParserType() {
115+
return this.parserType;
116+
}
117+
95118
/**
96119
* Set the {@link EncodingMode encoding mode} to use.
97120
* <p>By default this is set to {@link EncodingMode#TEMPLATE_AND_VALUES
@@ -265,12 +288,12 @@ private UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) {
265288
result = (baseUri != null ? baseUri.cloneBuilder() : UriComponentsBuilder.newInstance());
266289
}
267290
else if (baseUri != null) {
268-
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate);
291+
UriComponentsBuilder builder = parseUri(uriTemplate);
269292
UriComponents uri = builder.build();
270293
result = (uri.getHost() == null ? baseUri.cloneBuilder().uriComponents(uri) : builder);
271294
}
272295
else {
273-
result = UriComponentsBuilder.fromUriString(uriTemplate);
296+
result = parseUri(uriTemplate);
274297
}
275298
if (encodingMode.equals(EncodingMode.TEMPLATE_AND_VALUES)) {
276299
result.encode();
@@ -279,6 +302,12 @@ else if (baseUri != null) {
279302
return result;
280303
}
281304

305+
private UriComponentsBuilder parseUri(String uriTemplate) {
306+
return (getParserType() != null ?
307+
UriComponentsBuilder.fromUriString(uriTemplate, getParserType()) :
308+
UriComponentsBuilder.fromUriString(uriTemplate));
309+
}
310+
282311
private void parsePathIfNecessary(UriComponentsBuilder result) {
283312
if (parsePath && encodingMode.equals(EncodingMode.URI_COMPONENT)) {
284313
UriComponents uric = result.build();

Diff for: spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java

+107-35
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,19 @@ public static UriComponentsBuilder fromUri(URI uri) {
197197
}
198198

199199
/**
200-
* Create a builder that is initialized with the given URI string.
200+
* Variant of {@link #fromUriString(String, ParserType)} that defaults to
201+
* the {@link ParserType#RFC} parsing.
202+
*/
203+
public static UriComponentsBuilder fromUriString(String uri) throws InvalidUrlException {
204+
Assert.notNull(uri, "URI must not be null");
205+
if (uri.isEmpty()) {
206+
return new UriComponentsBuilder();
207+
}
208+
return fromUriString(uri, ParserType.RFC);
209+
}
210+
211+
/**
212+
* Create a builder that is initialized by parsing the given URI string.
201213
* <p><strong>Note:</strong> The presence of reserved characters can prevent
202214
* correct parsing of the URI string. For example if a query parameter
203215
* contains {@code '='} or {@code '&'} characters, the query string cannot
@@ -208,47 +220,27 @@ public static UriComponentsBuilder fromUri(URI uri) {
208220
* UriComponentsBuilder.fromUriString(uriString).buildAndExpand(&quot;hot&amp;cold&quot;);
209221
* </pre>
210222
* @param uri the URI string to initialize with
223+
* @param parserType the parsing algorithm to use
211224
* @return the new {@code UriComponentsBuilder}
212225
* @throws InvalidUrlException if {@code uri} cannot be parsed
226+
* @since 6.2
213227
*/
214-
public static UriComponentsBuilder fromUriString(String uri) throws InvalidUrlException {
228+
public static UriComponentsBuilder fromUriString(String uri, ParserType parserType) throws InvalidUrlException {
215229
Assert.notNull(uri, "URI must not be null");
216-
230+
if (uri.isEmpty()) {
231+
return new UriComponentsBuilder();
232+
}
217233
UriComponentsBuilder builder = new UriComponentsBuilder();
218-
if (!uri.isEmpty()) {
219-
WhatWgUrlParser.UrlRecord urlRecord = WhatWgUrlParser.parse(uri, EMPTY_URL_RECORD, null, null);
220-
if (!urlRecord.scheme().isEmpty()) {
221-
builder.scheme(urlRecord.scheme());
222-
}
223-
if (urlRecord.includesCredentials()) {
224-
StringBuilder userInfo = new StringBuilder(urlRecord.username());
225-
if (!urlRecord.password().isEmpty()) {
226-
userInfo.append(':');
227-
userInfo.append(urlRecord.password());
228-
}
229-
builder.userInfo(userInfo.toString());
234+
return switch (parserType) {
235+
case RFC -> {
236+
RfcUriParser.UriRecord record = RfcUriParser.parse(uri);
237+
yield builder.rfcUriRecord(record);
230238
}
231-
if (urlRecord.host() != null && !(urlRecord.host() instanceof WhatWgUrlParser.EmptyHost)) {
232-
builder.host(urlRecord.host().toString());
239+
case WHAT_WG -> {
240+
WhatWgUrlParser.UrlRecord record = WhatWgUrlParser.parse(uri, EMPTY_URL_RECORD, null, null);
241+
yield builder.whatWgUrlRecord(record);
233242
}
234-
if (urlRecord.port() != null) {
235-
builder.port(urlRecord.port().toString());
236-
}
237-
if (urlRecord.path().isOpaque()) {
238-
String ssp = urlRecord.path() + urlRecord.search();
239-
builder.schemeSpecificPart(ssp);
240-
}
241-
else {
242-
builder.path(urlRecord.path().toString());
243-
if (StringUtils.hasLength(urlRecord.query())) {
244-
builder.query(urlRecord.query());
245-
}
246-
}
247-
if (StringUtils.hasLength(urlRecord.fragment())) {
248-
builder.fragment(urlRecord.fragment());
249-
}
250-
}
251-
return builder;
243+
};
252244
}
253245

254246
/**
@@ -517,6 +509,58 @@ public UriComponentsBuilder uriComponents(UriComponents uriComponents) {
517509
return this;
518510
}
519511

512+
/**
513+
* Internal method to initialize this builder from an RFC {@code UriRecord}.
514+
*/
515+
private UriComponentsBuilder rfcUriRecord(RfcUriParser.UriRecord record) {
516+
scheme(record.scheme());
517+
if (record.isOpaque()) {
518+
if (record.path() != null) {
519+
schemeSpecificPart(record.path());
520+
}
521+
}
522+
else {
523+
userInfo(record.user());
524+
host(record.host());
525+
port(record.port());
526+
if (record.path() != null) {
527+
path(record.path());
528+
}
529+
query(record.query());
530+
}
531+
fragment(record.fragment());
532+
return this;
533+
}
534+
535+
/**
536+
* Internal method to initialize this builder from a WhatWG {@code UrlRecord}.
537+
*/
538+
private UriComponentsBuilder whatWgUrlRecord(WhatWgUrlParser.UrlRecord record) {
539+
if (!record.scheme().isEmpty()) {
540+
scheme(record.scheme());
541+
}
542+
if (record.path().isOpaque()) {
543+
String ssp = record.path() + record.search();
544+
schemeSpecificPart(ssp);
545+
}
546+
else {
547+
userInfo(record.userInfo());
548+
String hostname = record.hostname();
549+
if (StringUtils.hasText(hostname)) {
550+
host(hostname);
551+
}
552+
if (record.port() != null) {
553+
port(record.portString());
554+
}
555+
path(record.path().toString());
556+
query(record.query());
557+
if (StringUtils.hasText(record.fragment())) {
558+
fragment(record.fragment());
559+
}
560+
}
561+
return this;
562+
}
563+
520564
@Override
521565
public UriComponentsBuilder scheme(@Nullable String scheme) {
522566
this.scheme = scheme;
@@ -790,6 +834,34 @@ private interface PathComponentBuilder {
790834
}
791835

792836

837+
/**
838+
* Enum to represent different URI parsing mechanisms.
839+
*/
840+
public enum ParserType {
841+
842+
/**
843+
* Parser that expects URI's conforming to RFC 3986 syntax.
844+
*/
845+
RFC,
846+
847+
/**
848+
* Parser based on algorithm defined in the WhatWG URL Living standard.
849+
* Browsers use this algorithm to align on lenient parsing of user typed
850+
* URL's that may deviate from RFC syntax.
851+
* <p>For more details, see:
852+
* <ul>
853+
* <li><a href="https://url.spec.whatwg.org">URL Living Standard</a>
854+
* <li><a href="https://url.spec.whatwg.org/#url-parsing">Section 4.4: URL parsing</a>
855+
* <li><a href="https://github.com/web-platform-tests/wpt/tree/master/url">web-platform-tests</a>
856+
* </ul>
857+
* <p>Use this if you need to leniently handle URL's that don't conform
858+
* to RFC syntax, or for alignment with browser parsing.
859+
*/
860+
WHAT_WG
861+
862+
}
863+
864+
793865
private static class CompositePathComponentBuilder implements PathComponentBuilder {
794866

795867
private final Deque<PathComponentBuilder> builders = new ArrayDeque<>();

Diff for: spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@
3838
/**
3939
* Implementation of the
4040
* <a href="https://url.spec.whatwg.org/#url-parsing">URL parsing</a> algorithm
41-
* of the WhatWG URL Living standard.
41+
* of the WhatWG URL Living standard. Browsers use this algorithm to align on
42+
* lenient parsing of user typed URL's that may deviate from RFC syntax.
43+
* Use this, via {@link UriComponentsBuilder.ParserType#WHAT_WG}, if you need to
44+
* leniently handle URL's that don't confirm to RFC syntax, or for alignment
45+
* with browser behavior.
4246
*
4347
* <p>Comments in this class correlate to the parsing algorithm.
4448
* The implementation differs from the spec in the following ways:
@@ -1895,6 +1899,22 @@ void appendToPassword(String s) {
18951899
}
18961900
}
18971901

1902+
/**
1903+
* Convenience method to return the full user info.
1904+
*/
1905+
@Nullable
1906+
public String userInfo() {
1907+
if (!includesCredentials()) {
1908+
return null;
1909+
}
1910+
StringBuilder userInfo = new StringBuilder(username());
1911+
if (!password().isEmpty()) {
1912+
userInfo.append(':');
1913+
userInfo.append(password());
1914+
}
1915+
return userInfo.toString();
1916+
}
1917+
18981918
/**
18991919
* A URL’s host is {@code null} or a {@linkplain Host host}.
19001920
* It is initially {@code null}.

Diff for: spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ void sendRedirectWithLocationSlashSlashParentDotDot() throws Exception {
649649

650650
String location = "//other.info/parent/../foo/bar";
651651
String redirectedUrl = sendRedirect(location);
652-
assertThat(redirectedUrl).isEqualTo(("https://other.info/foo/bar"));
652+
assertThat(redirectedUrl).isEqualTo(("https:" + location));
653653
}
654654

655655
@Test

Diff for: spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,11 @@ void opaqueUriDoesNotResetOnNullInput() {
638638
void relativeUrls() {
639639
String baseUrl = "https://example.com";
640640
assertThat(UriComponentsBuilder.fromUriString(baseUrl + "/foo/../bar").build().toString())
641-
.isEqualTo(baseUrl + "/bar");
641+
.isEqualTo(baseUrl + "/foo/../bar");
642642
assertThat(UriComponentsBuilder.fromUriString(baseUrl + "/foo/../bar").build().toUriString())
643-
.isEqualTo(baseUrl + "/bar");
643+
.isEqualTo(baseUrl + "/foo/../bar");
644644
assertThat(UriComponentsBuilder.fromUriString(baseUrl + "/foo/../bar").build().toUri().getPath())
645-
.isEqualTo("/bar");
645+
.isEqualTo("/foo/../bar");
646646
assertThat(UriComponentsBuilder.fromUriString(baseUrl).path("foo/../bar").build().toString())
647647
.isEqualTo(baseUrl + "/foo/../bar");
648648
assertThat(UriComponentsBuilder.fromUriString(baseUrl).path("foo/../bar").build().toUriString())
@@ -741,9 +741,9 @@ void encodeTemplateWithInvalidPlaceholderSyntax() {
741741

742742
// empty
743743
tester.accept("{}", "%7B%7D");
744-
tester.accept("{ \t}", "%7B%20%7D");
744+
tester.accept("{ \t}", "%7B%20%09%7D");
745745
tester.accept("/a{}b", "/a%7B%7Db");
746-
tester.accept("/a{ \t}b", "/a%7B%20%7Db");
746+
tester.accept("/a{ \t}b", "/a%7B%20%09%7Db");
747747

748748
// nested, matching
749749
tester.accept("{foo{}}", "%7Bfoo%7B%7D%7D");

0 commit comments

Comments
 (0)