This is an automated email from the ASF dual-hosted git repository. pkarwasz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 9fcb3c0153b4e4517eb7ee797234d3dfdee04ba1 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Wed Mar 9 22:36:33 2022 +0100 [LOG4J2-3423] Refactoring of URLConnection code Refactors the code to minimize the number of calls to `URLConnection` methods. Conflicts: log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java log4j-core/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java log4j-core/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java --- .../org/apache/log4j/PropertyConfigurator.java | 5 +- .../java/org/apache/log4j/xml/DOMConfigurator.java | 5 +- .../log4j/core/config/ConfigurationSourceTest.java | 48 +++++++++++- .../log4j/core/net/UrlConnectionFactoryTest.java | 83 +++++++++++++++------ .../logging/log4j/core/util/NetUtilsTest.java | 18 ++++- .../log4j/core/config/ConfigurationFactory.java | 44 +++-------- .../log4j/core/config/ConfigurationSource.java | 10 +-- .../logging/log4j/core/config/HttpWatcher.java | 2 + .../log4j/core/net/UrlConnectionFactory.java | 10 +-- log4j-core/src/test/resources/jarfile.jar | Bin 0 -> 1122 bytes .../src/test/resources/jarfile/config/console.xml | 28 +++++++ 11 files changed, 175 insertions(+), 78 deletions(-) diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/PropertyConfigurator.java b/log4j-1.2-api/src/main/java/org/apache/log4j/PropertyConfigurator.java index 348646a..7f82b47 100644 --- a/log4j-1.2-api/src/main/java/org/apache/log4j/PropertyConfigurator.java +++ b/log4j-1.2-api/src/main/java/org/apache/log4j/PropertyConfigurator.java @@ -47,6 +47,7 @@ import org.apache.log4j.spi.RendererSupport; import org.apache.log4j.spi.ThrowableRenderer; import org.apache.log4j.spi.ThrowableRendererSupport; import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.net.UrlConnectionFactory; import org.apache.logging.log4j.util.StackLocatorUtil; /** @@ -381,9 +382,7 @@ public class PropertyConfigurator implements Configurator { Configuration doConfigure(final URL url, final LoggerRepository loggerRepository, final ClassLoader classLoader) { LogLog.debug("Reading configuration from URL " + url); try { - final URLConnection urlConnection = url.openConnection(); - // A "jar:" URL file remains open after the stream is closed, so do not cache it. - urlConnection.setUseCaches(false); + final URLConnection urlConnection = UrlConnectionFactory.createConnection(url); try (InputStream inputStream = urlConnection.getInputStream()) { return doConfigure(inputStream, loggerRepository, classLoader); } diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/xml/DOMConfigurator.java b/log4j-1.2-api/src/main/java/org/apache/log4j/xml/DOMConfigurator.java index e5adf60..b0cbb58 100644 --- a/log4j-1.2-api/src/main/java/org/apache/log4j/xml/DOMConfigurator.java +++ b/log4j-1.2-api/src/main/java/org/apache/log4j/xml/DOMConfigurator.java @@ -39,6 +39,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.net.UrlConnectionFactory; import org.apache.logging.log4j.core.util.IOUtils; import org.w3c.dom.Element; @@ -144,9 +145,7 @@ public class DOMConfigurator { public void doConfigure(final URL url, final LoggerRepository repository) { try { - final URLConnection connection = url.openConnection(); - // A "jar:" URL file remains open after the stream is closed, so do not cache it. - connection.setUseCaches(false); + final URLConnection connection = UrlConnectionFactory.createConnection(url); try (InputStream inputStream = connection.getInputStream()) { doConfigure(new ConfigurationSource(inputStream, url)); } diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java index c067b71..9182e01 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationSourceTest.java @@ -17,11 +17,23 @@ package org.apache.logging.log4j.core.config; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.apache.logging.log4j.core.net.UrlConnectionFactory; +import org.junit.jupiter.api.Test; + +import com.sun.management.UnixOperatingSystemMXBean; public class ConfigurationSourceTest { @@ -30,4 +42,36 @@ public class ConfigurationSourceTest { ConfigurationSource configurationSource = new ConfigurationSource(new ByteArrayInputStream(new byte[] { 'a', 'b' })); assertNotNull(configurationSource.resetInputStream()); } + + /** + * Checks if the usage of 'jar:' URLs does not increase the file descriptor + * count and the jar file can be deleted. + * + * @throws Exception + */ + @Test + public void testNoJarFileLeak() throws Exception { + final Path original = Paths.get("target", "test-classes", "jarfile.jar"); + final Path copy = Paths.get("target", "test-classes", "jarfile-copy.jar"); + Files.copy(original, copy); + final URL jarUrl = new URL("jar:" + copy.toUri().toURL() + "!/config/console.xml"); + final long expected = getOpenFileDescriptorCount(); + UrlConnectionFactory.createConnection(jarUrl).getInputStream().close(); + // This can only fail on UNIX + assertEquals(expected, getOpenFileDescriptorCount()); + // This can only fail on Windows + try { + Files.delete(copy); + } catch (IOException e) { + fail(e); + } + } + + private long getOpenFileDescriptorCount() { + final OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); + if (os instanceof UnixOperatingSystemMXBean) { + return ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount(); + } + return 0L; + } } diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java index 07cd754..16f948e 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java @@ -16,18 +16,33 @@ */ package org.apache.logging.log4j.core.net; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; import java.net.HttpURLConnection; import java.net.URI; +import java.net.URL; import java.nio.file.Files; import java.util.Base64; import java.util.Enumeration; -import java.util.Properties; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -38,16 +53,11 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import com.sun.management.UnixOperatingSystemMXBean; /** * Tests the UrlConnectionFactory @@ -60,9 +70,6 @@ public class UrlConnectionFactoryTest { private static Server server; private static Base64.Decoder decoder = Base64.getDecoder(); private static int port; - private static final int NOT_MODIFIED = 304; - private static final int NOT_AUTHORIZED = 401; - private static final int OK = 200; private static final int BUF_SIZE = 1024; @BeforeAll @@ -110,15 +117,16 @@ public class UrlConnectionFactoryTest { assertNotNull(source, "No ConfigurationSource returned"); InputStream is = source.getInputStream(); assertNotNull(is, "No data returned"); + is.close(); long lastModified = source.getLastModified(); int result = verifyNotModified(uri, lastModified); - assertEquals(NOT_MODIFIED, result,"File was modified"); + assertEquals(SC_NOT_MODIFIED, result,"File was modified"); File file = new File("target/test-classes/log4j2-config.xml"); if (!file.setLastModified(System.currentTimeMillis())) { fail("Unable to set last modified time"); } result = verifyNotModified(uri, lastModified); - assertEquals(OK, result,"File was not modified"); + assertEquals(SC_OK, result,"File was not modified"); } private int verifyNotModified(URI uri, long lastModifiedMillis) throws Exception { @@ -130,10 +138,34 @@ public class UrlConnectionFactoryTest { return urlConnection.getResponseCode(); } catch (final IOException ioe) { LOGGER.error("Error accessing configuration at {}: {}", uri, ioe.getMessage()); - return 500; + return SC_INTERNAL_SERVER_ERROR; } } + @Test + public void testNoJarFileLeak() throws Exception { + final URL url = new File("target/test-classes/jarfile.jar").toURI().toURL(); + // Retrieve using 'file:' + URL jarUrl = new URL("jar:" + url.toString() + "!/config/console.xml"); + long expected = getOpenFileDescriptorCount(); + UrlConnectionFactory.createConnection(jarUrl).getInputStream().close(); + assertEquals(expected, getOpenFileDescriptorCount()); + // Retrieve using 'http:' + jarUrl = new URL("jar:http://localhost:" + port + "/jarfile.jar!/config/console.xml"); + final File tmpDir = new File(System.getProperty("java.io.tmpdir")); + expected = tmpDir.list().length; + UrlConnectionFactory.createConnection(jarUrl).getInputStream().close(); + assertEquals(expected, tmpDir.list().length, "File descriptor leak"); + } + + private long getOpenFileDescriptorCount() { + final OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); + if (os instanceof UnixOperatingSystemMXBean) { + return ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount(); + } + return 0L; + } + public static class TestServlet extends DefaultServlet { private static final long serialVersionUID = -2885158530511450659L; @@ -143,7 +175,7 @@ public class UrlConnectionFactoryTest { HttpServletResponse response) throws ServletException, IOException { Enumeration<String> headers = request.getHeaders(HttpHeader.AUTHORIZATION.toString()); if (headers == null) { - response.sendError(401, "No Auth header"); + response.sendError(SC_UNAUTHORIZED, "No Auth header"); return; } while (headers.hasMoreElements()) { @@ -151,26 +183,31 @@ public class UrlConnectionFactoryTest { assertTrue(authData.startsWith(BASIC), "Not a Basic auth header"); String credentials = new String(decoder.decode(authData.substring(BASIC.length()))); if (!expectedCreds.equals(credentials)) { - response.sendError(401, "Invalid credentials"); + response.sendError(SC_UNAUTHORIZED, "Invalid credentials"); return; } } - if (request.getServletPath().equals("/log4j2-config.xml")) { - File file = new File("target/test-classes/log4j2-config.xml"); + final String servletPath = request.getServletPath(); + if (servletPath != null) { + final File file = new File("target/test-classes" + servletPath); + if (!file.exists()) { + response.sendError(SC_NOT_FOUND); + return; + } long modifiedSince = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.toString()); long lastModified = (file.lastModified() / 1000) * 1000; LOGGER.debug("LastModified: {}, modifiedSince: {}", lastModified, modifiedSince); if (modifiedSince > 0 && lastModified <= modifiedSince) { - response.setStatus(304); + response.setStatus(SC_NOT_MODIFIED); return; } response.setDateHeader(HttpHeader.LAST_MODIFIED.toString(), lastModified); response.setContentLengthLong(file.length()); Files.copy(file.toPath(), response.getOutputStream()); response.getOutputStream().flush(); - response.setStatus(200); + response.setStatus(SC_OK); } else { - response.sendError(400, "Unsupported request"); + response.sendError(SC_BAD_REQUEST, "Unsupported request"); } } } diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java index c7cddc6..c0f7ffa 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; +import java.io.File; import java.net.URI; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -31,10 +32,25 @@ public class NetUtilsTest { @Test public void testToUriWithoutBackslashes() { final String config = "file:///path/to/something/on/unix"; - final URI uri = NetUtils.toURI(config); + URI uri = NetUtils.toURI(config); assertNotNull(uri, "The URI should not be null."); assertEquals("file:///path/to/something/on/unix", uri.toString(), "The URI is not correct."); + + final String properUriPath = "/path/without/spaces"; + uri = NetUtils.toURI(properUriPath); + + assertNotNull(uri, "The URI should not be null."); + assertEquals(properUriPath, uri.toString(), "The URI is not correct."); + } + + @Test + public void testToUriUnixWithSpaces() { + final String pathWithSpaces = "/ path / with / spaces"; + final URI uri = NetUtils.toURI(pathWithSpaces); + + assertNotNull(uri, "The URI should not be null."); + assertEquals(new File(pathWithSpaces).toURI().toString(), uri.toString(), "The URI is not correct."); } @Test diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java index fac845f..8473e25 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java @@ -16,12 +16,23 @@ */ package org.apache.logging.log4j.core.config; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory; import org.apache.logging.log4j.core.lookup.StrSubstitutor; -import org.apache.logging.log4j.core.net.UrlConnectionFactory; import org.apache.logging.log4j.core.util.AuthorizationProvider; import org.apache.logging.log4j.core.util.BasicAuthorizationProvider; import org.apache.logging.log4j.core.util.FileUtils; @@ -235,35 +246,4 @@ public abstract class ConfigurationFactory extends ConfigurationBuilderFactory { return uri.getScheme() == null ? uri.getPath() : uri.getSchemeSpecificPart(); } - /** - * Loads the configuration from the location represented by the String. - * @param config The configuration location. - * @param loader The default ClassLoader to use. - * @return The InputSource to use to read the configuration. - */ - protected ConfigurationSource getInputFromString(final String config, final ClassLoader loader) { - try { - final URL url = new URL(config); - final URLConnection urlConnection = UrlConnectionFactory.createConnection(url); - final File file = FileUtils.fileFromUri(url.toURI()); - if (file != null) { - return new ConfigurationSource(urlConnection.getInputStream(), FileUtils.fileFromUri(url.toURI())); - } else { - return new ConfigurationSource(urlConnection.getInputStream(), url, urlConnection.getLastModified()); - } - } catch (final Exception ex) { - final ConfigurationSource source = ConfigurationSource.fromResource(config, loader); - if (source == null) { - try { - final File file = new File(config); - return new ConfigurationSource(new FileInputStream(file), file); - } catch (final FileNotFoundException fnfe) { - // Ignore the exception - LOGGER.catching(Level.DEBUG, fnfe); - } - } - return source; - } - } - } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java index bb37919..24f7961 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java @@ -332,17 +332,9 @@ public class ConfigurationSource { return null; } try { - final URL url = configLocation.toURL(); - final URLConnection urlConnection = UrlConnectionFactory.createConnection(url); - final InputStream is = urlConnection.getInputStream(); - final long lastModified = urlConnection.getLastModified(); - return new ConfigurationSource(is, configLocation.toURL(), lastModified); - } catch (final FileNotFoundException ex) { - ConfigurationFactory.LOGGER.warn("Could not locate file {}", configLocation.toString()); + return getConfigurationSource(configLocation.toURL()); } catch (final MalformedURLException ex) { ConfigurationFactory.LOGGER.error("Invalid URL {}", configLocation.toString(), ex); - } catch (final Exception ex) { - ConfigurationFactory.LOGGER.error("Unable to access {}", configLocation.toString(), ex); } return null; } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java index b781a4c..f6eb7e2 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java @@ -135,6 +135,8 @@ public class HttpWatcher extends AbstractWatcher { } } catch (final IOException ioe) { LOGGER.error("Error accessing configuration at {}: {}", url, ioe.getMessage()); + } finally { + urlConnection.disconnect(); } } catch (final IOException ioe) { LOGGER.error("Error connecting to configuration at {}: {}", url, ioe.getMessage()); diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java index 65d4a13..fc72141 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java @@ -18,6 +18,7 @@ package org.apache.logging.log4j.core.net; import java.io.IOException; import java.net.HttpURLConnection; +import java.net.JarURLConnection; import java.net.ProtocolException; import java.net.URL; import java.net.URLConnection; @@ -77,8 +78,6 @@ public class UrlConnectionFactory { urlConnection.setDoOutput(true); urlConnection.setDoInput(true); urlConnection.setRequestMethod("GET"); - // A "jar:" URL file remains open after the stream is closed, so do not cache it. - urlConnection.setUseCaches(false); if (connectTimeoutMillis > 0) { urlConnection.setConnectTimeout(connectTimeoutMillis); } @@ -107,13 +106,14 @@ public class UrlConnectionFactory { urlConnection = createConnection(url, 0, SslConfigurationFactory.getSslConfiguration()); } else { urlConnection = url.openConnection(); - // A "jar:" URL file remains open after the stream is closed, so do not cache it. - urlConnection.setUseCaches(false); + if (urlConnection instanceof JarURLConnection) { + // A "jar:" URL file remains open after the stream is closed, so do not cache it. + urlConnection.setUseCaches(false); + } } return urlConnection; } - private static boolean isXml(final String type) { return type.equalsIgnoreCase("xml"); } diff --git a/log4j-core/src/test/resources/jarfile.jar b/log4j-core/src/test/resources/jarfile.jar new file mode 100644 index 0000000..bf34b8c Binary files /dev/null and b/log4j-core/src/test/resources/jarfile.jar differ diff --git a/log4j-core/src/test/resources/jarfile/config/console.xml b/log4j-core/src/test/resources/jarfile/config/console.xml new file mode 100644 index 0000000..015ac0b --- /dev/null +++ b/log4j-core/src/test/resources/jarfile/config/console.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--> +<Configuration status="WARN"> + <Appenders> + <Console name="Console" /> + </Appenders> + <Loggers> + <Root level="TRACE"> + <AppenderRef ref="Console" /> + </Root> + </Loggers> +</Configuration>
