diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java index ee19ea7ea86..d6fa0d34eea 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java @@ -42,6 +42,7 @@ import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.filter.ThreadContextMapFilter; import org.apache.logging.log4j.core.test.junit.LoggerContextSource; +import org.apache.logging.log4j.test.junit.SetTestProperty; import org.apache.logging.log4j.test.junit.TempLoggingDir; import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.Tag; @@ -103,6 +104,7 @@ void xml(final LoggerContext context) throws IOException { } @Test + @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = "true") @LoggerContextSource("log4j-xinclude.xml") void xinclude(final LoggerContext context) throws IOException { checkConfiguration(context); diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurity.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurity.java deleted file mode 100644 index 64aa3aa4d63..00000000000 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurity.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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. - */ -package org.apache.logging.log4j.core.config.xml; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configurator; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -@Tag("functional") -@Tag("security") -class XmlConfigurationSecurity { - - @Test - @Timeout(5) - void xmlSecurity() { - final LoggerContext context = - Configurator.initialize("XmlConfigurationSecurity", "XmlConfigurationSecurity.xml"); - assertNotNull(context.getConfiguration().getAppender("list")); - } -} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurityTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurityTest.java new file mode 100644 index 00000000000..f9ae9e37bf1 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationSecurityTest.java @@ -0,0 +1,81 @@ +/* + * 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. + */ +package org.apache.logging.log4j.core.config.xml; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationException; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.test.junit.SetTestProperty; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** + * Verifies the contract of {@link XmlConfiguration} regarding the resolution of external resources. + * + *

Configuration files are part of the trusted computing base (see the + * Log4j security policy); the hardenings checked here are + * defense in depth, not a security boundary against a malicious configuration. These tests only cover what Log4j is + * responsible for:

+ * + * + *

All external fetches are forbidden; whether a forbidden fetch raises an error or is silently skipped is part of + * the contract of the {@code eu.copernik:copernik-xml-factory} library and is tested there.

+ */ +@Tag("functional") +@Tag("security") +class XmlConfigurationSecurityTest { + + private static final ConfigurationFactory FACTORY = new XmlConfigurationFactory(); + + private static Configuration getConfiguration(final String resource) { + return FACTORY.getConfiguration(null, null, URI.create("classpath:XmlConfigurationSecurity/" + resource)); + } + + /** + * A configuration that triggers a forbidden external fetch fails to load with a {@link ConfigurationException}. + * The {@code @Timeout} guards against an SSRF: a fetch attempt against the unreachable host would block until the + * connection times out. + */ + @Test + @Timeout(5) + void forbiddenExternalFetchThrowsConfigurationException() { + assertThatThrownBy(() -> getConfiguration("external-parameter-entity.xml")) + .isInstanceOf(ConfigurationException.class); + } + + /** + * XInclude resources are resolved through {@code ConfigurationSource}, so they are subject to the same + * {@code ALLOWED_PROTOCOLS} restrictions as the configuration file. With {@code http} excluded, an {@code http} + * include cannot be resolved and the configuration fails to load. + */ + @Test + @Timeout(5) + @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = "true") + @SetTestProperty(key = "log4j2.configurationAllowedProtocols", value = "file") + void xIncludeRespectsAllowedProtocols() { + assertThatThrownBy(() -> getConfiguration("xinclude.xml")).isInstanceOf(ConfigurationException.class); + } +} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationXIncludeTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationXIncludeTest.java new file mode 100644 index 00000000000..a5f921d0719 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/xml/XmlConfigurationXIncludeTest.java @@ -0,0 +1,130 @@ +/* + * 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. + */ +package org.apache.logging.log4j.core.config.xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.Node; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +/** + * Unit tests for the XInclude handling of {@link XmlConfiguration}. + *

+ * The {@code parse}-based tests exercise the {@link DocumentBuilder} returned by + * {@link XmlConfiguration#newDocumentBuilder(boolean)} directly, so they are independent of the + * {@code log4j2.configurationEnableXInclude} property plumbing covered by the integration tests in + * {@code ConfigurationFactoryTest}. {@link #fixupAttributesAreStripped()} drives {@link XmlConfiguration#constructHierarchy} + * to verify that the {@code xml:base}/{@code xml:lang} attributes added by the XInclude fix-up features are not + * exposed as Log4j configuration attributes. + *

+ */ +class XmlConfigurationXIncludeTest { + + private Document parse(final boolean enableXInclude) throws Exception { + return parse("/log4j-xinclude.xml", enableXInclude); + } + + private Document parse(final String resource, final boolean enableXInclude) throws Exception { + final URL url = getClass().getResource(resource); + final DocumentBuilder builder = XmlConfiguration.newDocumentBuilder(enableXInclude); + try (final java.io.InputStream is = url.openStream()) { + final InputSource source = new InputSource(is); + // Required so that relative `href` attributes can be resolved against the configuration location. + source.setSystemId(url.toExternalForm()); + return builder.parse(source); + } + } + + @Test + void xInclude_enabled_resolves_includes() throws Exception { + final Document document = parse(true); + // The `xi:include` elements have been replaced by the content of the included files. + assertEquals(0, document.getElementsByTagNameNS("*", "include").getLength(), "no `xi:include` should remain"); + assertEquals(1, document.getElementsByTagName("Console").getLength(), "Console appender from include"); + assertEquals(1, document.getElementsByTagName("File").getLength(), "File appender from include"); + assertEquals(1, document.getElementsByTagName("List").getLength(), "List appender from include"); + assertEquals(2, document.getElementsByTagName("Logger").getLength(), "Loggers from include"); + } + + @Test + void xInclude_disabled_keeps_includes_unresolved() throws Exception { + final Document document = parse(false); + // Without XInclude support the `xi:include` elements are left untouched and nothing is included. + assertEquals(2, document.getElementsByTagNameNS("*", "include").getLength(), "`xi:include` elements remain"); + assertEquals(0, document.getElementsByTagName("Console").getLength(), "no appenders should be included"); + } + + @Test + void xInclude_resolves_classpath_scheme() throws Exception { + // The custom resolver delegates to `ConfigurationSource`, which understands the `classpath:` URI scheme. + final Document document = parse("/log4j-xinclude-classpath.xml", true); + assertEquals(0, document.getElementsByTagNameNS("*", "include").getLength(), "no `xi:include` should remain"); + assertEquals( + 1, document.getElementsByTagName("Console").getLength(), "Console appender from classpath include"); + assertEquals(2, document.getElementsByTagName("Logger").getLength(), "Loggers from classpath include"); + } + + /** + * The XInclude {@code fixup-base-uris} and {@code fixup-language} features (both enabled by default) add + * {@code xml:base} and {@code xml:lang} attributes to the top-level included elements. Those belong to the + * reserved XML namespace and must not be exposed as Log4j configuration attributes. + */ + @Test + void fixupAttributesAreStripped() throws Exception { + // `log4j-xinclude-fixup.xml` carries `xml:lang` on the root and includes a `` appender, so the + // included element receives both `xml:base` (from `fixup-base-uris`) and `xml:lang` (from `fixup-language`). + final Element rootElement = parse("/log4j-xinclude-fixup.xml", true).getDocumentElement(); + + final ConfigurationSource source = ConfigurationSource.fromResource( + "log4j-xinclude-fixup.xml", getClass().getClassLoader()); + final XmlConfiguration configuration = new XmlConfiguration(new LoggerContext("test"), source); + // `constructHierarchy` resolves child elements to plugin types, so the plugins must be collected first. + configuration.getPluginManager().collectPlugins(); + + final Node root = new Node(); + configuration.constructHierarchy(root, rootElement); + + // Sanity check: the include was resolved and the `` node is present in the tree. + assertThat(collectNodeNames(root, new ArrayList<>())).contains("Console"); + // None of the nodes carries an `xml:`-namespaced attribute. + assertThat(collectXmlNamespaceAttributes(root, new ArrayList<>())).isEmpty(); + } + + private static List collectNodeNames(final Node node, final List names) { + names.add(node.getName()); + node.getChildren().forEach(child -> collectNodeNames(child, names)); + return names; + } + + private static List collectXmlNamespaceAttributes(final Node node, final List found) { + node.getAttributes().keySet().stream() + .filter(key -> key.startsWith("xml:")) + .forEach(found::add); + node.getChildren().forEach(child -> collectXmlNamespaceAttributes(child, found)); + return found; + } +} diff --git a/log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml b/log4j-core-test/src/test/resources/XmlConfigurationSecurity/external-parameter-entity.xml similarity index 100% rename from log4j-core-test/src/test/resources/XmlConfigurationSecurity.xml rename to log4j-core-test/src/test/resources/XmlConfigurationSecurity/external-parameter-entity.xml diff --git a/log4j-core-test/src/test/resources/XmlConfigurationSecurity/xinclude.xml b/log4j-core-test/src/test/resources/XmlConfigurationSecurity/xinclude.xml new file mode 100644 index 00000000000..98b2b76128e --- /dev/null +++ b/log4j-core-test/src/test/resources/XmlConfigurationSecurity/xinclude.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/log4j-core-test/src/test/resources/log4j-xinclude-classpath.xml b/log4j-core-test/src/test/resources/log4j-xinclude-classpath.xml new file mode 100644 index 00000000000..e9e9802fc9a --- /dev/null +++ b/log4j-core-test/src/test/resources/log4j-xinclude-classpath.xml @@ -0,0 +1,26 @@ + + + + + ${test:logging.path}/test-xinclude.log + + + + + diff --git a/log4j-core-test/src/test/resources/log4j-xinclude-fixup-console.xml b/log4j-core-test/src/test/resources/log4j-xinclude-fixup-console.xml new file mode 100644 index 00000000000..d82ba1bfb71 --- /dev/null +++ b/log4j-core-test/src/test/resources/log4j-xinclude-fixup-console.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/log4j-core-test/src/test/resources/log4j-xinclude-fixup.xml b/log4j-core-test/src/test/resources/log4j-xinclude-fixup.xml new file mode 100644 index 00000000000..8c83023d319 --- /dev/null +++ b/log4j-core-test/src/test/resources/log4j-xinclude-fixup.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml index b2f34d4ab95..4edf0860c43 100644 --- a/log4j-core/pom.xml +++ b/log4j-core/pom.xml @@ -157,6 +157,11 @@ commons-csv true + + + eu.copernik + copernik-xml-factory + com.conversantmedia diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java index 3218589bb1d..6605334ba11 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java @@ -16,9 +16,12 @@ */ package org.apache.logging.log4j.core.config.xml; +import eu.copernik.xml.factory.XmlFactories; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -35,6 +38,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.AbstractConfiguration; import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationException; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.Reconfigurable; @@ -45,13 +49,14 @@ import org.apache.logging.log4j.core.util.Integers; import org.apache.logging.log4j.core.util.Loader; import org.apache.logging.log4j.core.util.Patterns; -import org.apache.logging.log4j.core.util.Throwables; +import org.apache.logging.log4j.util.PropertiesUtil; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NodeList; import org.w3c.dom.Text; +import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -60,8 +65,7 @@ */ public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable { - private static final String XINCLUDE_FIXUP_LANGUAGE = "http://apache.org/xml/features/xinclude/fixup-language"; - private static final String XINCLUDE_FIXUP_BASE_URIS = "http://apache.org/xml/features/xinclude/fixup-base-uris"; + private static final String ENABLE_XINCLUDE_PROP = "log4j2.configurationEnableXInclude"; private final List status = new ArrayList<>(); private Element rootElement; @@ -84,24 +88,9 @@ public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSo } final InputSource source = new InputSource(new ByteArrayInputStream(buffer)); source.setSystemId(configSource.getLocation()); - final DocumentBuilder documentBuilder = newDocumentBuilder(true); - Document document; - try { - document = documentBuilder.parse(source); - } catch (final Exception e) { - // LOG4J2-1127 - final Throwable throwable = Throwables.getRootCause(e); - if (throwable instanceof UnsupportedOperationException) { - LOGGER.warn( - "The DocumentBuilder {} does not support an operation: {}." - + "Trying again without XInclude...", - documentBuilder, - e); - document = newDocumentBuilder(false).parse(source); - } else { - throw e; - } - } + final boolean xIncludeAware = PropertiesUtil.getProperties().getBooleanProperty(ENABLE_XINCLUDE_PROP); + final DocumentBuilder documentBuilder = newDocumentBuilder(xIncludeAware); + final Document document = documentBuilder.parse(source); rootElement = document.getDocumentElement(); final Map attrs = processAttributes(rootNode, rootElement); final StatusConfiguration statusConfig = new StatusConfiguration().withStatus(getDefaultStatus()); @@ -135,6 +124,7 @@ public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSo statusConfig.initialize(); } catch (final SAXException | IOException | ParserConfigurationException e) { LOGGER.error("Error parsing " + configSource.getLocation(), e); + throw new ConfigurationException("Error parsing " + configSource.getLocation(), e); } if (strict && schemaResource != null && buffer != null) { try (final InputStream is = @@ -172,67 +162,22 @@ public XmlConfiguration(final LoggerContext loggerContext, final ConfigurationSo /** * Creates a new DocumentBuilder suitable for parsing a configuration file. * - * @param xIncludeAware enabled XInclude + * @param enableXInclude enabled XInclude * @return a new DocumentBuilder * @throws ParserConfigurationException if a DocumentBuilder cannot be created, which satisfies the configuration requested. */ - static DocumentBuilder newDocumentBuilder(final boolean xIncludeAware) throws ParserConfigurationException { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + static DocumentBuilder newDocumentBuilder(final boolean enableXInclude) throws ParserConfigurationException { + final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory(); factory.setNamespaceAware(true); - - disableDtdProcessing(factory); - - if (xIncludeAware) { - enableXInclude(factory); + if (enableXInclude) { + factory.setXIncludeAware(true); } - return factory.newDocumentBuilder(); - } - - private static void disableDtdProcessing(final DocumentBuilderFactory factory) { - factory.setValidating(false); - factory.setExpandEntityReferences(false); - setFeature(factory, "http://xml.org/sax/features/external-general-entities", false); - setFeature(factory, "http://xml.org/sax/features/external-parameter-entities", false); - setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); - } - private static void setFeature( - final DocumentBuilderFactory factory, final String featureName, final boolean value) { - try { - factory.setFeature(featureName, value); - } catch (final ParserConfigurationException e) { - LOGGER.warn( - "The DocumentBuilderFactory [{}] does not support the feature [{}]: {}", factory, featureName, e); - } catch (final AbstractMethodError err) { - LOGGER.warn( - "The DocumentBuilderFactory [{}] is out of date and does not support setFeature: {}", factory, err); + final DocumentBuilder builder = factory.newDocumentBuilder(); + if (enableXInclude) { + builder.setEntityResolver(ConfigurationSourceEntityResolver.INSTANCE); } - } - - /** - * Enables XInclude for the given DocumentBuilderFactory - * - * @param factory a DocumentBuilderFactory - */ - private static void enableXInclude(final DocumentBuilderFactory factory) { - try { - factory.setXIncludeAware(true); - // LOG4J2-3531: Xerces only checks if the feature is supported when creating a factory. To reproduce: - // -Dorg.apache.xerces.xni.parser.XMLParserConfiguration=org.apache.xerces.parsers.XML11NonValidatingConfiguration - try { - factory.newDocumentBuilder(); - } catch (final ParserConfigurationException e) { - factory.setXIncludeAware(false); - LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e); - } - } catch (final UnsupportedOperationException e) { - LOGGER.warn("The DocumentBuilderFactory [{}] does not support XInclude: {}", factory, e); - } catch (final AbstractMethodError | NoSuchMethodError err) { - LOGGER.warn( - "The DocumentBuilderFactory [{}] is out of date and does not support XInclude: {}", factory, err); - } - setFeature(factory, XINCLUDE_FIXUP_BASE_URIS, true); - setFeature(factory, XINCLUDE_FIXUP_LANGUAGE, true); + return builder; } @Override @@ -266,7 +211,8 @@ public Configuration reconfigure() { return null; } - private void constructHierarchy(final Node node, final Element element) { + // Package-private for testing + void constructHierarchy(final Node node, final Element element) { processAttributes(node, element); final StringBuilder buffer = new StringBuilder(); final NodeList list = element.getChildNodes(); @@ -327,7 +273,9 @@ private Map processAttributes(final Node node, final Element ele final org.w3c.dom.Node w3cNode = attrs.item(i); if (w3cNode instanceof Attr) { final Attr attr = (Attr) w3cNode; - if (attr.getName().equals("xml:base")) { + // The XInclude `fixup-base-uris` and `fixup-language` features (both enabled by default) add + // `xml:base` and `xml:lang` attributes to the top-level included elements. + if (XMLConstants.XML_NS_URI.equals(attr.getNamespaceURI())) { continue; } attributes.put(attr.getName(), attr.getValue()); @@ -368,4 +316,34 @@ public String toString() { return "Status [name=" + name + ", element=" + element + ", errorType=" + errorType + "]"; } } + + /** + * Entity resolver that resolves external entities the same way the configuration itself is resolved. + */ + private static class ConfigurationSourceEntityResolver implements EntityResolver { + + private static final EntityResolver INSTANCE = new ConfigurationSourceEntityResolver(); + + @Override + public InputSource resolveEntity(final String publicId, final String systemId) throws SAXException { + if (systemId == null) { + throw new SAXException("Unable to resolve entity: missing system id"); + } + InputSource source = null; + try { + final ConfigurationSource configurationSource = ConfigurationSource.fromUri(new URI(systemId)); + if (configurationSource != null) { + source = new InputSource(configurationSource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + } + } catch (final URISyntaxException e) { + throw new SAXException("Unable to resolve system id " + systemId, e); + } + if (source == null) { + throw new SAXException("Unable to resolve entity: " + systemId); + } + return source; + } + } } diff --git a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java index 06bb63611d2..9d370ccf5b0 100644 --- a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java +++ b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/AbstractLoadBundleTest.java @@ -75,6 +75,10 @@ private Bundle getApiTestsBundle() throws BundleException { return installBundle("org.apache.logging.log4j.api.test"); } + private Bundle getXmlFactoryBundle() throws BundleException { + return installBundle("eu.copernik.xml.factory"); + } + /** * Tests starting, then stopping, then restarting, then stopping, and finally uninstalling the API and Core bundles */ @@ -82,19 +86,20 @@ private Bundle getApiTestsBundle() throws BundleException { public void testApiCoreStartStopStartStop() throws BundleException { final Bundle api = getApiBundle(); + final Bundle xmlFactory = getXmlFactoryBundle(); final Bundle core = getCoreBundle(); assertEquals(Bundle.INSTALLED, api.getState(), "api is not in INSTALLED state"); assertEquals(Bundle.INSTALLED, core.getState(), "core is not in INSTALLED state"); // 1st start-stop - doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core); - doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api); + doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, xmlFactory, core); + doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, xmlFactory, api); // 2nd start-stop - doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core); - doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api); + doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, xmlFactory, core); + doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, xmlFactory, api); - doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, api); + doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, xmlFactory, api); } /** @@ -104,9 +109,10 @@ public void testApiCoreStartStopStartStop() throws BundleException { public void testClassNotFoundErrorLogger() throws BundleException { final Bundle api = getApiBundle(); + final Bundle xmlFactory = getXmlFactoryBundle(); final Bundle core = getCoreBundle(); - doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api); + doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, xmlFactory); // fails if LOG4J2-1637 is not fixed try { core.start(); @@ -126,8 +132,8 @@ public void testClassNotFoundErrorLogger() throws BundleException { } assertEquals(Bundle.ACTIVE, core.getState(), String.format("`%s` bundle state mismatch", core)); - doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api); - doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, api); + doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, xmlFactory, api); + doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, core, xmlFactory, api); } /** @@ -138,10 +144,11 @@ public void testClassNotFoundErrorLogger() throws BundleException { public void testLog4J12Fragement() throws BundleException, ReflectiveOperationException { final Bundle api = getApiBundle(); + final Bundle xmlFactory = getXmlFactoryBundle(); final Bundle core = getCoreBundle(); final Bundle compat = get12ApiBundle(); - doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core); + doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, xmlFactory, core); final Class coreClassFromCore = core.loadClass("org.apache.logging.log4j.core.Core"); final Class levelClassFrom12API = core.loadClass("org.apache.log4j.Level"); @@ -156,8 +163,8 @@ public void testLog4J12Fragement() throws BundleException, ReflectiveOperationEx levelClassFromAPI.getClassLoader(), "expected 1.2 API Level NOT to have the same class loader as API Level"); - doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, api); - doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, compat, core, api); + doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, core, xmlFactory, api); + doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, compat, core, xmlFactory, api); } /** @@ -166,13 +173,14 @@ public void testLog4J12Fragement() throws BundleException, ReflectiveOperationEx @Test public void testServiceLoader() throws BundleException, ReflectiveOperationException { final Bundle api = getApiBundle(); + final Bundle xmlFactory = getXmlFactoryBundle(); final Bundle core = getCoreBundle(); final Bundle apiTests = getApiTestsBundle(); final Class osgiServiceLocator = api.loadClass("org.apache.logging.log4j.util.OsgiServiceLocator"); assertTrue((boolean) osgiServiceLocator.getMethod("isAvailable").invoke(null), "OsgiServiceLocator is active"); - doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, core, apiTests); + doOnBundlesAndVerifyState(Bundle::start, Bundle.ACTIVE, api, xmlFactory, core, apiTests); final Class osgiServiceLocatorTest = apiTests.loadClass("org.apache.logging.log4j.test.util.OsgiServiceLocatorTest"); @@ -187,8 +195,8 @@ public void testServiceLoader() throws BundleException, ReflectiveOperationExcep "org.apache.logging.log4j.core.impl.Log4jProvider", services.get(0).getClass().getName()); - doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, apiTests, core, api); - doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, apiTests, core, api); + doOnBundlesAndVerifyState(Bundle::stop, Bundle.RESOLVED, apiTests, core, xmlFactory, api); + doOnBundlesAndVerifyState(Bundle::uninstall, Bundle.UNINSTALLED, apiTests, core, xmlFactory, api); } private static void doOnBundlesAndVerifyState( diff --git a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java index 493bb61c9e4..77d7f54cdce 100644 --- a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java +++ b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/CoreOsgiTest.java @@ -46,6 +46,7 @@ public class CoreOsgiTest { public Option[] config() { return options( linkBundle("org.apache.logging.log4j.api"), + linkBundle("eu.copernik.xml.factory"), linkBundle("org.apache.logging.log4j.core"), linkBundle("org.apache.logging.log4j.1.2.api").start(false), // required by Pax Exam's logging diff --git a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java index 813331210c5..018edca9f54 100644 --- a/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java +++ b/log4j-osgi-test/src/test/java/org/apache/logging/log4j/osgi/tests/DisruptorTest.java @@ -52,6 +52,7 @@ public class DisruptorTest { public Option[] config() { return options( linkBundle("org.apache.logging.log4j.api"), + linkBundle("eu.copernik.xml.factory"), linkBundle("org.apache.logging.log4j.core"), linkBundle("com.lmax.disruptor"), // required by Pax Exam's logging diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml index 9a7b5aa2c87..869fab6c90f 100644 --- a/log4j-parent/pom.xml +++ b/log4j-parent/pom.xml @@ -78,6 +78,7 @@ 1.3.5 1.2.15 + 0.1.1 3.4.4 0.9.0 2.38.0 @@ -392,6 +393,12 @@ ${commons-pool2.version} + + eu.copernik + copernik-xml-factory + ${copernik-xml-factory.version} + + com.conversantmedia disruptor diff --git a/src/changelog/.2.x.x/4064_xinclude_opt_in.xml b/src/changelog/.2.x.x/4064_xinclude_opt_in.xml new file mode 100644 index 00000000000..24a23488c69 --- /dev/null +++ b/src/changelog/.2.x.x/4064_xinclude_opt_in.xml @@ -0,0 +1,12 @@ + + + + + XInclude support in XML configurations is now opt-in (disabled by default) and can be enabled with the `log4j2.configurationEnableXInclude` property. + + diff --git a/src/changelog/.2.x.x/4064_xinclude_resolver.xml b/src/changelog/.2.x.x/4064_xinclude_resolver.xml new file mode 100644 index 00000000000..89ce4d936e3 --- /dev/null +++ b/src/changelog/.2.x.x/4064_xinclude_resolver.xml @@ -0,0 +1,11 @@ + + + + XInclude resources are now resolved through `ConfigurationSource`, so they support the Log4j URI conventions. + +