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:
+ *
+ * - a forbidden external fetch is surfaced as a {@link ConfigurationException} (rather than a half-built
+ * configuration), and
+ * - XInclude resources are subject to the same {@code ALLOWED_PROTOCOLS} restrictions as the configuration
+ * file itself.
+ *
+ *
+ * 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.
+
+