/* * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * @test * @bug 8293590 * @summary URL built-in protocol handlers should parse the URL early * to avoid constructing URLs for which openConnection * would later throw an exception, when possible. * A jdk.net.url.delayParsing property allows to switch that * behavior off to mitigate risks of regression * @run junit EarlyOrDelayedParsing * @run junit/othervm -Djdk.net.url.delayParsing EarlyOrDelayedParsing * @run junit/othervm -Djdk.net.url.delayParsing=true EarlyOrDelayedParsing * @run junit/othervm -Djdk.net.url.delayParsing=false EarlyOrDelayedParsing */ import java.io.IOException; import java.net.ConnectException; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.stream.IntStream; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static java.lang.System.err; import static org.junit.jupiter.api.Assertions.*; public class EarlyOrDelayedParsing { public final boolean EARLY_PARSING; { String value = System.getProperty("jdk.net.url.delayParsing", "false"); EARLY_PARSING = !value.isEmpty() && !Boolean.parseBoolean(value); } // Some characters that when included at the wrong place // in the authority component, without being escaped, would // cause an exception. private static final String EXCLUDED_DELIMS = "<>\" "; private static final String UNWISE = "{}|\\^`"; private static final String DELIMS = "[]/?#@"; // Test data used to test exceptions thrown by URL // at some point, when constructed with some illegal input. sealed interface URLArgTest permits OneArgTest, TwoArgsTest, ThreeArgsTest, FourArgsTest { // Some character that is expected to cause an exception // at some point, and which this test case is built for int character(); // An URL string containing the illegal character String url(); // Some characters are already checked at construction // time. They will cause an exception to be thrown, // whether delayed parsing is activated or not. // This method returns true if an exception is // expected at construction time for this test case, // even when delayed parsing is activated. boolean early(int c); // The URL scheme this test case is built for. // Typically, one of "http", "https", "ftp"... default String scheme() { return scheme(url()); } // Return the URL string of this test case, after // substituting its scheme with the given scheme. default String urlWithScheme(String scheme) { String url = url(); int colon = url.indexOf(':'); String urlWithScheme = scheme + url.substring(colon); return urlWithScheme; } // Which exception to expect when parsing is delayed default boolean acceptDelayedException(Throwable exception) { return exception instanceof MalformedURLException || exception instanceof UnknownHostException; } default String describe() { return this.getClass().getSimpleName() + "(url=" + url() + ")"; } static int port(String protocol) { return switch (protocol) { case "http" -> 80; case "https" -> 443; case "ftp" -> 21; default -> -1; }; } static String scheme(String url) { return url.substring(0, url.indexOf(':')); } } // Test data for the one arg constructor // public URL(String spec) throws MalformedURLException sealed interface OneArgTest extends URLArgTest { // Create a new test case identical to this one but // with a different URL scheme default OneArgTest withScheme(String scheme) { String urlWithScheme = urlWithScheme(scheme); if (this instanceof OfHost) { return new OfHost(character(), urlWithScheme); } if (this instanceof OfUserInfo) { return new OfUserInfo(character(), urlWithScheme); } throw new AssertionError("unexpected subclass: " + this.getClass()); } @Override default boolean early(int c) { return this instanceof OfHost && (c < 31 || c == 127); } @Override default boolean acceptDelayedException(Throwable exception) { return URLArgTest.super.acceptDelayedException(exception) || "file".equalsIgnoreCase(scheme()) && character() == '\\' && exception instanceof IOException; } record OfHost(int character, String url) implements OneArgTest { } record OfUserInfo(int character, String url) implements OneArgTest { } static OneArgTest ofHost(int c) { return new OfHost(c, "http://local%shost/".formatted(Character.toString(c))); } static OneArgTest ofUserInfo(int c) { return new OfUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c))); } } // Test data for the two arg constructor // public URL(URL context, String spec) throws MalformedURLException sealed interface TwoArgsTest extends URLArgTest { // Create a new test case identical to this one but // with a different URL scheme default TwoArgsTest withScheme(String scheme) { String urlWithScheme = urlWithScheme(scheme); if (this instanceof OfTwoArgsHost) { return new OfTwoArgsHost(character(), urlWithScheme); } if (this instanceof OfTwoArgsUserInfo) { return new OfTwoArgsUserInfo(character(), urlWithScheme); } throw new AssertionError("unexpected subclass: " + this.getClass()); } @Override default boolean early(int c) { return this instanceof OfTwoArgsHost && (c < 31 || c == 127); } @Override default boolean acceptDelayedException(Throwable exception) { return URLArgTest.super.acceptDelayedException(exception) || "file".equalsIgnoreCase(scheme()) && character() == '\\' && exception instanceof IOException; } record OfTwoArgsHost(int character, String url) implements TwoArgsTest { } record OfTwoArgsUserInfo(int character, String url) implements TwoArgsTest { } static TwoArgsTest ofHost(int c) { return new OfTwoArgsHost(c, "http://local%shost/".formatted(Character.toString(c))); } static TwoArgsTest ofUserInfo(int c) { return new OfTwoArgsUserInfo(c, "http://user%sinfo@localhost:9999/".formatted(Character.toString(c))); } static TwoArgsTest ofOneArgTest(OneArgTest test) { if (test instanceof OneArgTest.OfHost) { return ofHost(test.character()); } else if (test instanceof OneArgTest.OfUserInfo) { return ofUserInfo(test.character()); } throw new AssertionError("can't convert to TwoArgsTest: " + test.getClass()); } } // Test data for the three args constructor // public URL(String scheme, String host, String file) // throws MalformedURLException sealed interface ThreeArgsTest extends URLArgTest { // the host component String host(); // the path + query components String file(); // Create a new test case identical to this one but // with a different URL scheme and port default ThreeArgsTest withScheme(String scheme) { String urlWithScheme = urlWithScheme(scheme); if (this instanceof OfHostFile) { return new OfHostFile(character(), host(), file(), urlWithScheme); } throw new AssertionError("unexpected subclass: " + this.getClass()); } @Override default boolean early(int c) { return (c < 31 || c == 127 || c == '/'); } @Override default boolean acceptDelayedException(Throwable exception) { return URLArgTest.super.acceptDelayedException(exception) || "file".equalsIgnoreCase(scheme()) && exception instanceof IOException; } record OfHostFile(int character, String host, String file, String url) implements ThreeArgsTest { } static ThreeArgsTest ofHostFile(int c) { String host = "local%shost".formatted(Character.toString(c)); String url = "http://" + host + "/"; return new OfHostFile(c, host, "/", url); } } // Test data for the four args constructor // public URL(String scheme, String host, int port, String file) // throws MalformedURLException sealed interface FourArgsTest extends URLArgTest { // the host component String host(); // the port component int port(); // the path + query components String file(); // Create a new test case identical to this one but // with a different URL scheme and port default FourArgsTest withScheme(String scheme) { String urlWithScheme = urlWithScheme(scheme); if (this instanceof OfHostFilePort) { int port = URLArgTest.port(scheme); return new OfHostFilePort(character(), host(), port, file(), urlWithScheme); } throw new AssertionError("unexpected subclass: " + this.getClass()); } @Override default boolean early(int c) { return (c < 31 || c == 127 || c == '/'); } @Override default boolean acceptDelayedException(Throwable exception) { return URLArgTest.super.acceptDelayedException(exception) || "file".equalsIgnoreCase(scheme()) && exception instanceof IOException; } record OfHostFilePort(int character, String host, int port, String file, String url) implements FourArgsTest { } static FourArgsTest ofHostPortFile(int c) { String host = "local%shost".formatted(Character.toString(c)); String url = "http://" + host + "/"; int port = URLArgTest.port(URLArgTest.scheme(url)); return new OfHostFilePort(c, host, port, "/", url); } } // Generate test data for the URL one arg constructor, with variations // of the host component. static Stream oneArgHostTests() { List tests = new ArrayList<>(); List urls = new ArrayList<>(); urls.addAll((UNWISE + EXCLUDED_DELIMS).chars() .mapToObj(OneArgTest::ofHost).toList()); urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127)) .mapToObj(OneArgTest::ofHost).toList()); for (String scheme : List.of("http", "https", "ftp")) { for (var test : urls) { tests.add(test.withScheme(scheme)); } } return tests.stream(); } // Generate test data for the URL one arg constructor, with variations // of the user info component. static Stream oneArgUserInfoTests() { List tests = new ArrayList<>(); List urls = new ArrayList<>(); urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127)) .mapToObj(OneArgTest::ofUserInfo).toList()); urls.add(OneArgTest.ofUserInfo('\\')); for (String scheme : List.of("http", "https", "ftp")) { for (var test : urls) { tests.add(test.withScheme(scheme)); } } return tests.stream(); } // Test data with all variations for the URL one arg // constructor (spec) static Stream oneArgTests() { return Stream.concat(oneArgHostTests(), oneArgUserInfoTests()); } // Test data with all variations for the URL two arg // constructor (URL, spec) static Stream twoArgTests() { return oneArgTests().map(TwoArgsTest::ofOneArgTest); } // Generate test data for the URL three arguments constructor // (scheme, host, file) static Stream threeArgsTests() { List urls = new ArrayList<>(); urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars() .mapToObj(ThreeArgsTest::ofHostFile).toList()); urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127)) .mapToObj(ThreeArgsTest::ofHostFile).toList()); List tests = new ArrayList<>(); for (String scheme : List.of("http", "https", "ftp", "file")) { for (var test : urls) { tests.add(test.withScheme(scheme)); } } return tests.stream(); } // Generate test data for the URL four arguments constructor // (scheme, host, port, file) static Stream fourArgsTests() { List urls = new ArrayList<>(); urls.addAll((UNWISE + EXCLUDED_DELIMS + DELIMS).chars() .mapToObj(FourArgsTest::ofHostPortFile).toList()); urls.addAll(IntStream.concat(IntStream.range(0, 31), IntStream.of(127)) .mapToObj(FourArgsTest::ofHostPortFile).toList()); List tests = new ArrayList<>(); for (String scheme : List.of("http", "https", "ftp", "file")) { for (var test : urls) { tests.add(test.withScheme(scheme)); } } return tests.stream(); } @ParameterizedTest @MethodSource("oneArgTests") public void testOneArgConstructor(OneArgTest test) throws Exception { int c = test.character(); String url = test.url(); if (EARLY_PARSING || test.early(c)) { err.println("Early parsing: " + test.describe()); var exception = assertThrows(MalformedURLException.class, () -> { new URL(url); }); err.println("Got expected exception: " + exception); } else { err.println("Delayed parsing: " + test.describe()); URL u = new URL(url); var exception = assertThrows(IOException.class, () -> { u.openConnection().connect(); }); if (!test.acceptDelayedException(exception)) { err.println("unexpected exception type: " + exception); throw exception; } err.println("Got expected exception: " + exception); assertFalse(exception instanceof ConnectException); } } @ParameterizedTest @MethodSource("twoArgTests") public void testTwoArgConstructor(TwoArgsTest test) throws Exception { int c = test.character(); String url = test.url(); String scheme = URLArgTest.scheme(url); URL u = new URL(scheme, null,""); if (EARLY_PARSING || test.early(c)) { err.println("Early parsing: " + test.describe()); var exception = assertThrows(MalformedURLException.class, () -> { new URL(u, url); }); err.println("Got expected exception: " + exception); } else { err.println("Delayed parsing: " + test.describe()); URL u2 = new URL(u, url); var exception = assertThrows(IOException.class, () -> { u2.openConnection().connect(); }); if (!test.acceptDelayedException(exception)) { err.println("unexpected exception type: " + exception); throw exception; } err.println("Got expected exception: " + exception); assertFalse(exception instanceof ConnectException); } } @ParameterizedTest @MethodSource("threeArgsTests") public void testThreeArgsConstructor(ThreeArgsTest test) throws Exception { int c = test.character(); String url = test.url(); if (EARLY_PARSING || test.early(c)) { err.println("Early parsing: " + url); var exception = assertThrows(MalformedURLException.class, () -> { new URL(test.scheme(), test.host(), test.file()); }); err.println("Got expected exception: " + exception); } else { err.println("Delayed parsing: " + url); URL u = new URL(test.scheme(), test.host(), test.file()); var exception = assertThrows(IOException.class, () -> { u.openConnection().connect(); }); if (!test.acceptDelayedException(exception)) { err.println("unexpected exception type: " + exception); throw exception; } err.println("Got expected exception: " + exception); assertFalse(exception instanceof ConnectException); } } @ParameterizedTest @MethodSource("fourArgsTests") public void testFourArgsConstructor(FourArgsTest test) throws Exception { int c = test.character(); String url = test.url(); if (EARLY_PARSING || test.early(c)) { err.println("Early parsing: " + url); var exception = assertThrows(MalformedURLException.class, () -> { new URL(test.scheme(), test.host(), test.port(), test.file()); }); err.println("Got expected exception: " + exception); } else { err.println("Delayed parsing: " + url); URL u = new URL(test.scheme(), test.host(), test.port(), test.file()); var exception = assertThrows(IOException.class, () -> { u.openConnection().connect(); }); if (!test.acceptDelayedException(exception)) { err.println("unexpected exception type: " + exception); throw exception; } err.println("Got expected exception: " + exception); assertFalse(exception instanceof ConnectException); } } }