| /* | |
| * Copyright (c) 2022, 2024, 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. | |
| */ | |
| import com.sun.net.httpserver.HttpExchange; | |
| import com.sun.net.httpserver.HttpServer; | |
| import jdk.test.lib.Utils; | |
| import jdk.test.lib.process.OutputAnalyzer; | |
| import jdk.test.lib.process.ProcessTools; | |
| import jdk.test.lib.util.FileUtils; | |
| import sun.net.www.ParseUtil; | |
| import java.io.Closeable; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| import java.io.PrintWriter; | |
| import java.io.UncheckedIOException; | |
| import java.lang.reflect.Method; | |
| import java.net.HttpURLConnection; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.URI; | |
| import java.nio.CharBuffer; | |
| import java.nio.file.Files; | |
| import java.nio.file.InvalidPathException; | |
| import java.nio.file.Path; | |
| import java.nio.file.StandardCopyOption; | |
| import java.nio.file.StandardOpenOption; | |
| import java.security.Security; | |
| import java.time.Instant; | |
| import java.util.ArrayList; | |
| import java.util.Collections; | |
| import java.util.HashSet; | |
| import java.util.Iterator; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Set; | |
| import java.util.stream.Stream; | |
| /* | |
| * @test | |
| * @summary Tests security properties passed through java.security, | |
| * java.security.properties or included from other properties files. | |
| * @bug 8155246 8292297 8292177 8281658 8319332 | |
| * @modules java.base/sun.net.www | |
| * @library /test/lib | |
| * @run main ConfigFileTest | |
| */ | |
| public class ConfigFileTest { | |
| static final String SEPARATOR_THIN = "----------------------------"; | |
| private static void printTestHeader(String testName) { | |
| System.out.println(); | |
| System.out.println(SEPARATOR_THIN); | |
| System.out.println(testName); | |
| System.out.println(SEPARATOR_THIN); | |
| System.out.println(); | |
| } | |
| public static void main(String[] args) throws Exception { | |
| if (args.length == 1 && Executor.RUNNER_ARG.equals(args[0])) { | |
| // Executed by a test-launched JVM. | |
| // Force the initialization of java.security.Security. | |
| Security.getProviders(); | |
| Security.setProperty("postInitTest", "shouldNotRecord"); | |
| System.out.println(FilesManager.LAST_FILE_PROP_NAME + ": " + | |
| Security.getProperty(FilesManager.LAST_FILE_PROP_NAME)); | |
| assertTestSecuritySetPropertyShouldNotInclude(); | |
| } else { | |
| // Executed by the test JVM. | |
| try (FilesManager filesMgr = new FilesManager()) { | |
| for (Method m : ConfigFileTest.class.getDeclaredMethods()) { | |
| if (m.getName().startsWith("test")) { | |
| printTestHeader(m.getName()); | |
| Executor.run(m, filesMgr); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /* | |
| * Success cases | |
| */ | |
| static void testShowSettings(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| // Sanity test passing the -XshowSettings:security option. | |
| ex.addJvmArg("-XshowSettings:security"); | |
| ex.setMasterFile(filesMgr.newMasterFile()); | |
| ex.assertSuccess(); | |
| ex.getOutputAnalyzer() | |
| .shouldContain("Security properties:") | |
| .shouldContain("Security provider static configuration:") | |
| .shouldContain("Security TLS configuration"); | |
| } | |
| static void testIncludeBasic(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| ExtraPropsFile extraFile = filesMgr.newExtraFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| PropsFile file1 = filesMgr.newFile("dir1/file1.properties"); | |
| PropsFile file2 = filesMgr.newFile("dir1/dir2/file2.properties"); | |
| masterFile.addAbsoluteInclude(file0); | |
| extraFile.addRelativeInclude(file2); | |
| file2.addAbsoluteInclude(file1); | |
| ex.setMasterFile(masterFile); | |
| ex.setExtraFile(extraFile, Executor.ExtraMode.FILE_URI, false); | |
| ex.assertSuccess(); | |
| } | |
| static void testRepeatedInclude(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| PropsFile file1 = filesMgr.newFile("dir1/file1.properties"); | |
| masterFile.addAbsoluteInclude(file0); | |
| masterFile.addAbsoluteInclude(file1); | |
| masterFile.addAbsoluteInclude(file0); | |
| file1.addRelativeInclude(file0); | |
| ex.setMasterFile(masterFile); | |
| ex.assertSuccess(); | |
| } | |
| static void testIncludeWithOverrideAll(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| ExtraPropsFile extraFile = filesMgr.newExtraFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| PropsFile file1 = filesMgr.newFile("dir1/file1.properties"); | |
| masterFile.addRelativeInclude(file0); | |
| extraFile.addAbsoluteInclude(file1); | |
| ex.setMasterFile(masterFile); | |
| ex.setExtraFile(extraFile, Executor.ExtraMode.HTTP_SERVED, true); | |
| ex.assertSuccess(); | |
| } | |
| static void extraPropertiesByHelper(Executor ex, FilesManager filesMgr, | |
| Executor.ExtraMode mode) throws Exception { | |
| ExtraPropsFile extraFile = filesMgr.newExtraFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| extraFile.addRelativeInclude(file0); | |
| ex.setMasterFile(filesMgr.newMasterFile()); | |
| ex.setExtraFile(extraFile, mode, true); | |
| ex.assertSuccess(); | |
| } | |
| static void testExtraPropertiesByPathAbsolute(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| extraPropertiesByHelper(ex, filesMgr, Executor.ExtraMode.PATH_ABS); | |
| } | |
| static void testExtraPropertiesByPathRelative(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| extraPropertiesByHelper(ex, filesMgr, Executor.ExtraMode.PATH_REL); | |
| } | |
| static void specialCharsIncludes(Executor ex, FilesManager filesMgr, | |
| char specialChar, Executor.ExtraMode extraMode, | |
| boolean useRelativeIncludes) throws Exception { | |
| String suffix = specialChar + ".properties"; | |
| ExtraPropsFile extraFile; | |
| PropsFile file0, file1; | |
| try { | |
| extraFile = filesMgr.newExtraFile("extra" + suffix); | |
| file0 = filesMgr.newFile("file0" + suffix); | |
| file1 = filesMgr.newFile("file1" + suffix); | |
| } catch (InvalidPathException ipe) { | |
| // The platform encoding may not allow to create files with some | |
| // special characters. Skip the test in these cases. | |
| return; | |
| } | |
| if (useRelativeIncludes) { | |
| extraFile.addRelativeInclude(file0); | |
| } else { | |
| extraFile.addAbsoluteInclude(file0); | |
| } | |
| extraFile.addAbsoluteInclude(file1); | |
| ex.setMasterFile(filesMgr.newMasterFile()); | |
| ex.setExtraFile(extraFile, extraMode, false); | |
| ex.assertSuccess(); | |
| } | |
| static void testUnicodeIncludes1(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| specialCharsIncludes(ex, filesMgr, '\u2022', | |
| Executor.ExtraMode.PATH_ABS, true); | |
| } | |
| static void testUnicodeIncludes2(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| specialCharsIncludes(ex, filesMgr, '\u2022', | |
| Executor.ExtraMode.FILE_URI, true); | |
| } | |
| static void testUnicodeIncludes3(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| // Backward compatibility check. Malformed URLs such as | |
| // file:/tmp/extra•.properties are supported for the extra file. | |
| // However, relative includes are not allowed in these cases. | |
| specialCharsIncludes(ex, filesMgr, '\u2022', | |
| Executor.ExtraMode.RAW_FILE_URI1, false); | |
| } | |
| static void testUnicodeIncludes4(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| // Backward compatibility check. Malformed URLs such as | |
| // file:///tmp/extra•.properties are supported for the extra file. | |
| // However, relative includes are not allowed in these cases. | |
| specialCharsIncludes(ex, filesMgr, '\u2022', | |
| Executor.ExtraMode.RAW_FILE_URI2, false); | |
| } | |
| static void testSpaceIncludes1(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| specialCharsIncludes(ex, filesMgr, ' ', | |
| Executor.ExtraMode.PATH_ABS, true); | |
| } | |
| static void testSpaceIncludes2(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| specialCharsIncludes(ex, filesMgr, ' ', | |
| Executor.ExtraMode.FILE_URI, true); | |
| } | |
| static void testSpaceIncludes3(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| // Backward compatibility check. Malformed URLs such as | |
| // file:/tmp/extra .properties are supported for the extra file. | |
| // However, relative includes are not allowed in these cases. | |
| specialCharsIncludes(ex, filesMgr, ' ', | |
| Executor.ExtraMode.RAW_FILE_URI1, false); | |
| } | |
| static void testSpaceIncludes4(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| // Backward compatibility check. Malformed URLs such as | |
| // file:///tmp/extra .properties are supported for the extra file. | |
| // However, relative includes are not allowed in these cases. | |
| specialCharsIncludes(ex, filesMgr, ' ', | |
| Executor.ExtraMode.RAW_FILE_URI2, false); | |
| } | |
| static void notOverrideOnFailureHelper(Executor ex, FilesManager filesMgr, | |
| String nonExistentExtraFile) throws Exception { | |
| // An overriding extra properties file that does not exist | |
| // should not erase properties from the master file. | |
| ex.setIgnoredExtraFile(nonExistentExtraFile, true); | |
| ex.setMasterFile(filesMgr.newMasterFile()); | |
| ex.assertSuccess(); | |
| ex.getOutputAnalyzer().shouldContain("unable to load security " + | |
| "properties from " + nonExistentExtraFile); | |
| } | |
| static void testNotOverrideOnEmptyFailure(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| notOverrideOnFailureHelper(ex, filesMgr, ""); | |
| ex.getOutputAnalyzer() | |
| .shouldContain("Empty extra properties file path"); | |
| } | |
| static void testNotOverrideOnURLFailure(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| notOverrideOnFailureHelper(ex, filesMgr, | |
| "file:///nonExistentFile.properties"); | |
| } | |
| static void testNotOverrideOnPathFailure(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| notOverrideOnFailureHelper(ex, filesMgr, "nonExistentFile.properties"); | |
| } | |
| static void testNotOverrideOnDirFailure(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| notOverrideOnFailureHelper(ex, filesMgr, "file:///"); | |
| ex.getOutputAnalyzer().shouldContain("Is a directory"); | |
| } | |
| static void testNotOverrideOnBadFileURLFailure(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| notOverrideOnFailureHelper(ex, filesMgr, "file:///%00"); | |
| } | |
| static void testDisabledExtraPropertiesFile(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| masterFile.addRawProperty("security.overridePropertiesFile", "false"); | |
| ex.setMasterFile(masterFile); | |
| ex.setIgnoredExtraFile(file0.path.toString(), true); | |
| ex.assertSuccess(); | |
| } | |
| static final String SECURITY_SET_PROP_FILE_PATH = | |
| "testSecuritySetPropertyShouldNotInclude.propsFilePath"; | |
| static void testSecuritySetPropertyShouldNotInclude(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| ex.addSystemProp(SECURITY_SET_PROP_FILE_PATH, file0.path.toString()); | |
| ex.setMasterFile(masterFile); | |
| ex.assertSuccess(); | |
| } | |
| static void assertTestSecuritySetPropertyShouldNotInclude() { | |
| // This check is executed by the launched JVM. | |
| String propsFilePath = System.getProperty(SECURITY_SET_PROP_FILE_PATH); | |
| if (propsFilePath != null) { | |
| String name = Path.of(propsFilePath).getFileName().toString(); | |
| String setPropInvokeRepr = "Security.setProperty(\"include\", " + | |
| "\"" + propsFilePath + "\")"; | |
| try { | |
| Security.setProperty("include", propsFilePath); | |
| throw new RuntimeException(setPropInvokeRepr + " was " + | |
| "expected to throw IllegalArgumentException."); | |
| } catch (IllegalArgumentException expected) {} | |
| if (FilesManager.APPLIED_PROP_VALUE.equals( | |
| Security.getProperty(name))) { | |
| throw new RuntimeException(setPropInvokeRepr + " caused " + | |
| "a file inclusion."); | |
| } | |
| try { | |
| Security.getProperty("include"); | |
| throw new RuntimeException("Security.getProperty(\"include\")" + | |
| " was expected to throw IllegalArgumentException."); | |
| } catch (IllegalArgumentException expected) {} | |
| } | |
| } | |
| /* | |
| * Error cases | |
| */ | |
| static void testCannotResolveRelativeFromHTTPServed(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| ExtraPropsFile extraFile = filesMgr.newExtraFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| extraFile.addRelativeInclude(file0); | |
| ex.setMasterFile(filesMgr.newMasterFile()); | |
| ex.setExtraFile(extraFile, Executor.ExtraMode.HTTP_SERVED, true); | |
| ex.assertError("InternalError: Cannot resolve '" + file0.fileName + | |
| "' relative path when included from a non-regular " + | |
| "properties file (e.g. HTTP served file)"); | |
| } | |
| static void testCannotIncludeCycles(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| PropsFile file0 = filesMgr.newFile("file0.properties"); | |
| PropsFile file1 = filesMgr.newFile("dir1/file1.properties"); | |
| // Includes chain: master -> file0 -> file1 -> master. | |
| file1.addRelativeInclude(masterFile); | |
| file0.addRelativeInclude(file1); | |
| masterFile.addRelativeInclude(file0); | |
| ex.setMasterFile(masterFile); | |
| ex.assertError( | |
| "InternalError: Cyclic include of '" + masterFile.path + "'"); | |
| } | |
| static void testCannotIncludeURL(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| ExtraPropsFile extraFile = filesMgr.newExtraFile(); | |
| masterFile.addRawProperty("include", extraFile.url.toString()); | |
| ex.setMasterFile(masterFile); | |
| ex.assertError("InternalError: Unable to include 'http://127.0.0.1:"); | |
| } | |
| static void testCannotIncludeNonexistentFile(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| PropsFile masterFile = filesMgr.newMasterFile(); | |
| String nonexistentPath = "/nonExistentFile.properties"; | |
| masterFile.addRawProperty("include", nonexistentPath); | |
| ex.setMasterFile(masterFile); | |
| ex.assertError( | |
| "InternalError: Unable to include '" + nonexistentPath + "'"); | |
| } | |
| static void testMustHaveMasterFile(Executor ex, FilesManager filesMgr) | |
| throws Exception { | |
| // Launch a JDK without a master java.security file present. | |
| ex.assertError("InternalError: Error loading java.security file"); | |
| } | |
| static void testMustHaveMasterFileEvenWithExtraFile(Executor ex, | |
| FilesManager filesMgr) throws Exception { | |
| // Launch a JDK without a master java.security file present, but with an | |
| // extra file passed. Since the "security.overridePropertiesFile=true" | |
| // security property is missing, it should fail anyway. | |
| ex.setExtraFile( | |
| filesMgr.newExtraFile(), Executor.ExtraMode.FILE_URI, true); | |
| ex.assertError("InternalError: Error loading java.security file"); | |
| } | |
| } | |
| sealed class PropsFile permits ExtraPropsFile { | |
| protected static final class Include { | |
| final PropsFile propsFile; | |
| final String value; | |
| private Include(PropsFile propsFile, String value) { | |
| this.propsFile = propsFile; | |
| this.value = value; | |
| } | |
| static Include of(PropsFile propsFile) { | |
| return new Include(propsFile, propsFile.path.toString()); | |
| } | |
| static Include of(PropsFile propsFile, String value) { | |
| return new Include(propsFile, value); | |
| } | |
| } | |
| protected final List<Include> includes = new ArrayList<>(); | |
| protected final PrintWriter writer; | |
| protected boolean includedFromExtra = false; | |
| final String fileName; | |
| final Path path; | |
| PropsFile(String fileName, Path path) throws IOException { | |
| this.fileName = fileName; | |
| this.path = path; | |
| this.writer = new PrintWriter(Files.newOutputStream(path, | |
| StandardOpenOption.CREATE, StandardOpenOption.APPEND), true); | |
| } | |
| private static String escape(String text, boolean escapeSpace) { | |
| StringBuilder sb = new StringBuilder(text.length()); | |
| CharBuffer cb = CharBuffer.wrap(text); | |
| while (cb.hasRemaining()) { | |
| char c = cb.get(); | |
| if (c == '\\' || escapeSpace && c == ' ') { | |
| sb.append('\\'); | |
| } | |
| if (Character.UnicodeBlock.of(c) == | |
| Character.UnicodeBlock.BASIC_LATIN) { | |
| sb.append(c); | |
| } else { | |
| sb.append("\\u%04x".formatted((int) c)); | |
| } | |
| } | |
| return sb.toString(); | |
| } | |
| private void addRawProperty(String key, String value, String sep) { | |
| writer.println(escape(key, true) + sep + escape(value, false)); | |
| } | |
| protected void addIncludeDefinition(Include include) { | |
| if (include.propsFile instanceof ExtraPropsFile) { | |
| throw new RuntimeException("ExtraPropsFile should not be included"); | |
| } | |
| includes.add(include); | |
| addRawProperty("include", include.value, " "); | |
| } | |
| void addComment(String comment) { | |
| writer.println("# " + comment); | |
| } | |
| void addRawProperty(String key, String value) { | |
| addRawProperty(key, value, "="); | |
| } | |
| void addAbsoluteInclude(PropsFile propsFile) { | |
| addIncludeDefinition(Include.of(propsFile)); | |
| } | |
| void addRelativeInclude(PropsFile propsFile) { | |
| addIncludeDefinition(Include.of(propsFile, | |
| path.getParent().relativize(propsFile.path).toString())); | |
| } | |
| void assertApplied(OutputAnalyzer oa) { | |
| oa.shouldContain(Executor.INITIAL_PROP_LOG_MSG + fileName + "=" + | |
| FilesManager.APPLIED_PROP_VALUE); | |
| for (Include include : includes) { | |
| include.propsFile.assertApplied(oa); | |
| oa.shouldContain("processing include: '" + include.value + "'"); | |
| oa.shouldContain("finished processing " + include.propsFile.path); | |
| } | |
| } | |
| void assertWasOverwritten(OutputAnalyzer oa) { | |
| oa.shouldNotContain(Executor.INITIAL_PROP_LOG_MSG + fileName + "=" + | |
| FilesManager.APPLIED_PROP_VALUE); | |
| for (Include include : includes) { | |
| if (!include.propsFile.includedFromExtra) { | |
| include.propsFile.assertWasOverwritten(oa); | |
| } | |
| oa.shouldContain("processing include: '" + include.value + "'"); | |
| oa.shouldContain("finished processing " + include.propsFile.path); | |
| } | |
| } | |
| void markAsIncludedFromExtra() { | |
| includedFromExtra = true; | |
| for (Include include : includes) { | |
| include.propsFile.markAsIncludedFromExtra(); | |
| } | |
| } | |
| PropsFile getLastFile() { | |
| return includes.isEmpty() ? | |
| this : includes.getLast().propsFile.getLastFile(); | |
| } | |
| void close() { | |
| writer.close(); | |
| } | |
| } | |
| final class ExtraPropsFile extends PropsFile { | |
| private final Map<String, String> systemProps = new LinkedHashMap<>(); | |
| final URI url; | |
| ExtraPropsFile(String fileName, URI url, Path path) throws IOException { | |
| super(fileName, path); | |
| this.url = url; | |
| } | |
| @Override | |
| protected void addIncludeDefinition(Include include) { | |
| if (includes.isEmpty()) { | |
| String propName = "props.fileName"; | |
| systemProps.put(propName, include.propsFile.fileName); | |
| include = Include.of(include.propsFile, | |
| include.value.replace(include.propsFile.fileName, | |
| "${props.none}${" + propName + "}")); | |
| } | |
| include.propsFile.markAsIncludedFromExtra(); | |
| super.addIncludeDefinition(include); | |
| } | |
| Map<String, String> getSystemProperties() { | |
| return Collections.unmodifiableMap(systemProps); | |
| } | |
| } | |
| final class FilesManager implements Closeable { | |
| private static final Path ROOT_DIR = | |
| Path.of(ConfigFileTest.class.getSimpleName()).toAbsolutePath(); | |
| private static final Path PROPS_DIR = ROOT_DIR.resolve("properties"); | |
| private static final Path JDK_DIR = ROOT_DIR.resolve("jdk"); | |
| private static final Path MASTER_FILE = | |
| JDK_DIR.resolve("conf/security/java.security"); | |
| private static final Path MASTER_FILE_TEMPLATE = | |
| MASTER_FILE.resolveSibling("java.security.template"); | |
| static final String JAVA_EXECUTABLE = | |
| JDK_DIR.resolve("bin/java").toString(); | |
| static final String LAST_FILE_PROP_NAME = "last-file"; | |
| static final String APPLIED_PROP_VALUE = "applied"; | |
| private final List<PropsFile> createdFiles; | |
| private final Set<String> fileNamesInUse; | |
| private final HttpServer httpServer; | |
| private final URI serverUri; | |
| private final long masterFileLines; | |
| FilesManager() throws Exception { | |
| createdFiles = new ArrayList<>(); | |
| fileNamesInUse = new HashSet<>(); | |
| httpServer = HttpServer.create( | |
| new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); | |
| httpServer.createContext("/", this::handleRequest); | |
| InetSocketAddress address = httpServer.getAddress(); | |
| httpServer.start(); | |
| serverUri = new URI("http", null, address.getHostString(), | |
| address.getPort(), null, null, null); | |
| copyJDK(); | |
| try (Stream<String> s = Files.lines(MASTER_FILE_TEMPLATE)) { | |
| masterFileLines = s.count(); | |
| } | |
| } | |
| private static void copyJDK() throws Exception { | |
| Path testJDK = Path.of(Objects.requireNonNull( | |
| System.getProperty("test.jdk"), "unspecified test.jdk")); | |
| if (!Files.exists(testJDK)) { | |
| throw new RuntimeException("test.jdk -> nonexistent JDK"); | |
| } | |
| Files.createDirectories(JDK_DIR); | |
| try (Stream<Path> pathStream = Files.walk(testJDK)) { | |
| pathStream.skip(1).forEach((Path file) -> { | |
| try { | |
| Files.copy(file, JDK_DIR.resolve(testJDK.relativize(file)), | |
| StandardCopyOption.COPY_ATTRIBUTES); | |
| } catch (IOException ioe) { | |
| throw new UncheckedIOException(ioe); | |
| } | |
| }); | |
| } | |
| Files.move(MASTER_FILE, MASTER_FILE_TEMPLATE); | |
| } | |
| private void handleRequest(HttpExchange x) throws IOException { | |
| String rawPath = x.getRequestURI().getRawPath(); | |
| Path f = ROOT_DIR.resolve(x.getRequestURI().getPath().substring(1)); | |
| int statusCode; | |
| byte[] responseBody; | |
| // Check for unescaped space, unresolved parent or backward slash. | |
| if (rawPath.matches("^.*( |(\\.|%2[Ee]){2}|\\\\|%5[Cc]).*$")) { | |
| statusCode = HttpURLConnection.HTTP_BAD_REQUEST; | |
| responseBody = new byte[0]; | |
| } else if (Files.isRegularFile(f)) { | |
| x.getResponseHeaders().add("Content-type", "text/plain"); | |
| statusCode = HttpURLConnection.HTTP_OK; | |
| responseBody = Files.readAllBytes(f); | |
| } else { | |
| statusCode = HttpURLConnection.HTTP_NOT_FOUND; | |
| responseBody = new byte[0]; | |
| } | |
| System.out.println("[" + Instant.now() + "] " + | |
| getClass().getSimpleName() + ": " + | |
| x.getRequestMethod() + " " + rawPath + " -> " + | |
| statusCode + " (" + responseBody.length + " bytes)"); | |
| try (OutputStream responseStream = x.getResponseBody()) { | |
| x.sendResponseHeaders(statusCode, responseBody.length); | |
| responseStream.write(responseBody); | |
| } | |
| } | |
| @FunctionalInterface | |
| private interface PropsFileBuilder { | |
| PropsFile build(String fileName, Path path) throws IOException; | |
| } | |
| private PropsFile newFile(Path path, PropsFileBuilder builder) | |
| throws IOException { | |
| String fileName = path.getFileName().toString(); | |
| if (!fileNamesInUse.add(fileName)) { | |
| // Names must be unique in order for the special | |
| // property <fileName>=<APPLIED_PROP_VALUE> to work. | |
| throw new RuntimeException(fileName + " is repeated"); | |
| } | |
| Files.createDirectories(path.getParent()); | |
| PropsFile propsFile = builder.build(fileName, path); | |
| propsFile.addComment("Property to determine if this properties file " + | |
| "was parsed and not overwritten:"); | |
| propsFile.addRawProperty(fileName, APPLIED_PROP_VALUE); | |
| propsFile.addComment(ConfigFileTest.SEPARATOR_THIN); | |
| propsFile.addComment("Property to be overwritten by every properties " + | |
| "file (master, extra or included):"); | |
| propsFile.addRawProperty(LAST_FILE_PROP_NAME, fileName); | |
| propsFile.addComment(ConfigFileTest.SEPARATOR_THIN); | |
| createdFiles.add(propsFile); | |
| return propsFile; | |
| } | |
| PropsFile newFile(String relPathStr) throws IOException { | |
| return newFile(PROPS_DIR.resolve(relPathStr), PropsFile::new); | |
| } | |
| PropsFile newMasterFile() throws IOException { | |
| Files.copy(MASTER_FILE_TEMPLATE, MASTER_FILE); | |
| return newFile(MASTER_FILE, PropsFile::new); | |
| } | |
| ExtraPropsFile newExtraFile() throws IOException { | |
| return newExtraFile("extra.properties"); | |
| } | |
| ExtraPropsFile newExtraFile(String extraFileName) throws IOException { | |
| return (ExtraPropsFile) newFile(PROPS_DIR.resolve(extraFileName), | |
| (fileName, path) -> { | |
| URI uri = serverUri.resolve(ParseUtil.encodePath( | |
| ROOT_DIR.relativize(path).toString())); | |
| return new ExtraPropsFile(fileName, uri, path); | |
| }); | |
| } | |
| void reportCreatedFiles() throws IOException { | |
| for (PropsFile propsFile : createdFiles) { | |
| System.err.println(); | |
| System.err.println(propsFile.path.toString()); | |
| System.err.println(ConfigFileTest.SEPARATOR_THIN.repeat(3)); | |
| try (Stream<String> lines = Files.lines(propsFile.path)) { | |
| long lineNumber = 1L; | |
| Iterator<String> it = lines.iterator(); | |
| while (it.hasNext()) { | |
| String line = it.next(); | |
| if (!propsFile.path.equals(MASTER_FILE) || | |
| lineNumber > masterFileLines) { | |
| System.err.println(line); | |
| } | |
| lineNumber++; | |
| } | |
| } | |
| System.err.println(); | |
| } | |
| } | |
| void clear() throws IOException { | |
| if (!createdFiles.isEmpty()) { | |
| for (PropsFile propsFile : createdFiles) { | |
| propsFile.close(); | |
| Files.delete(propsFile.path); | |
| } | |
| FileUtils.deleteFileTreeUnchecked(PROPS_DIR); | |
| createdFiles.clear(); | |
| fileNamesInUse.clear(); | |
| } | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| clear(); | |
| httpServer.stop(0); | |
| FileUtils.deleteFileTreeUnchecked(ROOT_DIR); | |
| } | |
| } | |
| final class Executor { | |
| enum ExtraMode { | |
| HTTP_SERVED, FILE_URI, RAW_FILE_URI1, RAW_FILE_URI2, PATH_ABS, PATH_REL | |
| } | |
| static final String RUNNER_ARG = "runner"; | |
| static final String INITIAL_PROP_LOG_MSG = "Initial security property: "; | |
| private static final String OVERRIDING_LOG_MSG = | |
| "overriding other security properties files!"; | |
| private static final String[] ALWAYS_UNEXPECTED_LOG_MSGS = { | |
| "java.lang.AssertionError", | |
| INITIAL_PROP_LOG_MSG + "postInitTest=shouldNotRecord", | |
| INITIAL_PROP_LOG_MSG + "include=", | |
| }; | |
| private static final Path CWD = Path.of(".").toAbsolutePath(); | |
| private static final String JAVA_SEC_PROPS = "java.security.properties"; | |
| private static final String CLASS_PATH = Objects.requireNonNull( | |
| System.getProperty("test.classes"), "unspecified test.classes"); | |
| private static final String DEBUG_ARG = | |
| "-Xrunjdwp:transport=dt_socket,address=localhost:8000,suspend=y"; | |
| private final Map<String, String> systemProps = new LinkedHashMap<>( | |
| Map.of("java.security.debug", "all", "javax.net.debug", "all", | |
| // Ensure we get UTF-8 debug outputs in Windows: | |
| "stderr.encoding", "UTF-8", "stdout.encoding", "UTF-8")); | |
| private final List<String> jvmArgs = new ArrayList<>( | |
| List.of(FilesManager.JAVA_EXECUTABLE, "-enablesystemassertions", | |
| // Uncomment DEBUG_ARG to debug test-launched JVMs: | |
| "-classpath", CLASS_PATH//, DEBUG_ARG | |
| )); | |
| private PropsFile masterPropsFile; | |
| private ExtraPropsFile extraPropsFile; | |
| private boolean expectedOverrideAll = false; | |
| private OutputAnalyzer oa; | |
| static void run(Method m, FilesManager filesMgr) throws Exception { | |
| try { | |
| m.invoke(null, new Executor(), filesMgr); | |
| } catch (Throwable e) { | |
| filesMgr.reportCreatedFiles(); | |
| throw e; | |
| } finally { | |
| filesMgr.clear(); | |
| } | |
| } | |
| void addSystemProp(String key, String value) { | |
| systemProps.put(key, value); | |
| } | |
| private void setRawExtraFile(String extraFile, boolean overrideAll) { | |
| addSystemProp(JAVA_SEC_PROPS, (overrideAll ? "=" : "") + extraFile); | |
| } | |
| void setMasterFile(PropsFile masterPropsFile) { | |
| this.masterPropsFile = masterPropsFile; | |
| } | |
| void setExtraFile(ExtraPropsFile extraPropsFile, ExtraMode mode, | |
| boolean overrideAll) { | |
| this.extraPropsFile = extraPropsFile; | |
| expectedOverrideAll = overrideAll; | |
| setRawExtraFile(switch (mode) { | |
| case HTTP_SERVED -> extraPropsFile.url.toString(); | |
| case FILE_URI -> extraPropsFile.path.toUri().toString(); | |
| case RAW_FILE_URI1 -> "file:" + extraPropsFile.path; | |
| case RAW_FILE_URI2 -> "file://" + | |
| (extraPropsFile.path.startsWith("/") ? "" : "/") + | |
| extraPropsFile.path; | |
| case PATH_ABS -> extraPropsFile.path.toString(); | |
| case PATH_REL -> CWD.relativize(extraPropsFile.path).toString(); | |
| }, overrideAll); | |
| } | |
| void setIgnoredExtraFile(String extraPropsFile, boolean overrideAll) { | |
| setRawExtraFile(extraPropsFile, overrideAll); | |
| expectedOverrideAll = false; | |
| } | |
| void addJvmArg(String arg) { | |
| jvmArgs.add(arg); | |
| } | |
| private void execute(boolean successExpected) throws Exception { | |
| List<String> command = new ArrayList<>(jvmArgs); | |
| Collections.addAll(command, Utils.getTestJavaOpts()); | |
| addSystemPropertiesAsJvmArgs(command); | |
| command.add(ConfigFileTest.class.getSimpleName()); | |
| command.add(RUNNER_ARG); | |
| oa = ProcessTools.executeProcess(new ProcessBuilder(command)); | |
| oa.shouldHaveExitValue(successExpected ? 0 : 1); | |
| for (String output : ALWAYS_UNEXPECTED_LOG_MSGS) { | |
| oa.shouldNotContain(output); | |
| } | |
| } | |
| private void addSystemPropertiesAsJvmArgs(List<String> command) { | |
| Map<String, String> allSystemProps = new LinkedHashMap<>(systemProps); | |
| if (extraPropsFile != null) { | |
| allSystemProps.putAll(extraPropsFile.getSystemProperties()); | |
| } | |
| for (Map.Entry<String, String> e : allSystemProps.entrySet()) { | |
| command.add("-D" + e.getKey() + "=" + e.getValue()); | |
| } | |
| } | |
| void assertSuccess() throws Exception { | |
| execute(true); | |
| // Ensure every file was processed by checking a unique property used as | |
| // a flag. Each file defines <fileName>=applied. | |
| // | |
| // For example: | |
| // | |
| // file0 | |
| // --------------- | |
| // file0=applied | |
| // include file1 | |
| // | |
| // file1 | |
| // --------------- | |
| // file1=applied | |
| // | |
| // The assertion would be file0 == applied AND file1 == applied. | |
| // | |
| if (extraPropsFile != null) { | |
| extraPropsFile.assertApplied(oa); | |
| } | |
| if (expectedOverrideAll) { | |
| // When overriding with an extra file, check that neither | |
| // the master file nor its includes are visible. | |
| oa.shouldContain(OVERRIDING_LOG_MSG); | |
| masterPropsFile.assertWasOverwritten(oa); | |
| } else { | |
| oa.shouldNotContain(OVERRIDING_LOG_MSG); | |
| masterPropsFile.assertApplied(oa); | |
| } | |
| // Ensure the last included file overwrote a fixed property. Each file | |
| // defines last-file=<fileName>. | |
| // | |
| // For example: | |
| // | |
| // file0 | |
| // --------------- | |
| // last-file=file0 | |
| // include file1 | |
| // | |
| // file1 | |
| // --------------- | |
| // last-file=file1 | |
| // | |
| // The assertion would be last-file == file1. | |
| // | |
| PropsFile lastFile = (extraPropsFile == null ? | |
| masterPropsFile : extraPropsFile).getLastFile(); | |
| oa.shouldContain(FilesManager.LAST_FILE_PROP_NAME + "=" + | |
| lastFile.fileName); | |
| oa.stdoutShouldContain(FilesManager.LAST_FILE_PROP_NAME + ": " + | |
| lastFile.fileName); | |
| } | |
| void assertError(String message) throws Exception { | |
| execute(false); | |
| oa.shouldContain(message); | |
| } | |
| OutputAnalyzer getOutputAnalyzer() { | |
| return oa; | |
| } | |
| } |