diff --git a/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java b/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java index d2d10298745..6b5c508eb95 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/WhatWgUrlParser.java @@ -55,6 +55,7 @@ import org.springframework.util.Assert; * with {@code EXTRA}. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 6.2 */ @SuppressWarnings({"SameParameterValue", "BooleanMethodIsAlwaysInverted"}) @@ -277,31 +278,12 @@ final class WhatWgUrlParser { } private static String domainToAscii(String domain, boolean beStrict) { - // If beStrict is false, domain is an ASCII string, and strictly splitting domain on U+002E (.) - // does not produce any item that starts with an ASCII case-insensitive match for "xn--", - // this step is equivalent to ASCII lowercasing domain. + // If beStrict is false and domain is an ASCII string, the algorithm returns domain lowercased + // regardless of Unicode ToASCII's outcome, due to web compatibility. In particular, the WhatWG + // spec deliberately does not reject invalid or ambiguous "xn--" (ACE) labels here. See the note in + // https://url.spec.whatwg.org/#concept-domain-to-ascii and web-platform-tests url cases if (!beStrict && containsOnlyAscii(domain)) { - int dotIdx = domain.indexOf('.'); - boolean onlyLowerCase = true; - while (dotIdx != -1) { - if (domain.length() - dotIdx > 4) { - // ASCII case-insensitive match for "xn--" - int ch0 = domain.codePointAt(dotIdx + 1); - int ch1 = domain.codePointAt(dotIdx + 2); - int ch2 = domain.codePointAt(dotIdx + 3); - int ch3 = domain.codePointAt(dotIdx + 4); - if ((ch0 == 'x' || ch0 == 'X') && - (ch1 == 'n' || ch1 == 'N') && - ch2 == '-' && ch3 == '_') { - onlyLowerCase = false; - break; - } - } - dotIdx = domain.indexOf('.', dotIdx + 1); - } - if (onlyLowerCase) { - return domain.toLowerCase(Locale.ENGLISH); - } + return domain.toLowerCase(Locale.ENGLISH); } // Let result be the result of running Unicode ToASCII (https://www.unicode.org/reports/tr46/#ToASCII) // with domain_name set to domain, UseSTD3ASCIIRules set to beStrict, CheckHyphens set to false, diff --git a/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java b/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java index c4fadb5f0fa..ad4c4e697a3 100644 --- a/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/WhatWgUrlParserTests.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Arjen Poutsma + * @author Sebastien Deleuze */ class WhatWgUrlParserTests { @@ -43,6 +44,25 @@ class WhatWgUrlParserTests { testParse("//other.info/parent/../foo/bar", "", "other.info", null, "/foo/bar", null, null); } + @Test + void parseAsciiHost() { + // Pure ASCII host is lowercased + testParse("https://EXAMPLE.com/foo", "https", "example.com", null, "/foo", null, null); + // ASCII "xn--" (ACE) labels are accepted and lowercased (in any case), not validated or rejected. + // See https://url.spec.whatwg.org/#concept-domain-to-ascii and web-platform-tests url cases. + testParse("https://a.b.c.xn--pokxncvks", "https", "a.b.c.xn--pokxncvks", null, "", null, null); + testParse("https://a.b.c.XN--pokxncvks", "https", "a.b.c.xn--pokxncvks", null, "", null, null); + testParse("https://a.b.c.Xn--pokxncvks", "https", "a.b.c.xn--pokxncvks", null, "", null, null); + // A trailing non-numeric "xn--" label keeps a numeric-looking host as a domain, not an IPv4 address. + testParse("https://10.0.0.xn--pokxncvks", "https", "10.0.0.xn--pokxncvks", null, "", null, null); + testParse("https://10.0.0.XN--pokxncvks", "https", "10.0.0.xn--pokxncvks", null, "", null, null); + testParse("https://10.0.0.xN--pokxncvks", "https", "10.0.0.xn--pokxncvks", null, "", null, null); + // Leading ACE label is handled too, and an empty "xn--" label is accepted (not a failure) + testParse("https://XN--pokxncvks.example", "https", "xn--pokxncvks.example", null, "", null, null); + testParse("https://xn--/", "https", "xn--", null, "/", null, null); + testParse("file://xn--/p", "file", "xn--", null, "/p", null, null); + } + private void testParse(String input, String scheme, @Nullable String host, @Nullable String port, String path, @Nullable String query, @Nullable String fragment) { WhatWgUrlParser.UrlRecord result = WhatWgUrlParser.parse(input, EMPTY_URL_RECORD, null, null); assertThat(result.scheme()).as("Invalid scheme").isEqualTo(scheme);