Extract ResourceEntityResolver HTTPS schema resolution fallback
This commit extracts the DTD/XSD remote lookup fallback from the resolveEntity() method into a protected method. A WARN-level logging statement is added to the extracted fallback in order to make it clear that remote lookup happened. Overriding the protected method would allow users to avoid this fallback entirely if it isn't desirable, without the need to duplicate the local resolution code. Closes gh-29697
This commit is contained in:
parent
57cfb94f1f
commit
5965917d16
|
@ -110,27 +110,53 @@ public class ResourceEntityResolver extends DelegatingEntityResolver {
|
|||
}
|
||||
}
|
||||
else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) {
|
||||
// External dtd/xsd lookup via https even for canonical http declaration
|
||||
String url = systemId;
|
||||
if (url.startsWith("http:")) {
|
||||
url = "https:" + url.substring(5);
|
||||
}
|
||||
try {
|
||||
source = new InputSource(ResourceUtils.toURL(url).openStream());
|
||||
source.setPublicId(publicId);
|
||||
source.setSystemId(systemId);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
|
||||
}
|
||||
// Fall back to the parser's default behavior.
|
||||
source = null;
|
||||
}
|
||||
source = resolveSchemaEntity(publicId, systemId);
|
||||
}
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fallback method for {@link #resolveEntity(String, String)} that is used when a
|
||||
* "schema" entity (DTD or XSD) cannot be resolved as a local resource. The default
|
||||
* behavior is to perform a remote resolution over HTTPS.
|
||||
* <p>Subclasses can override this method to change the default behavior.
|
||||
* <ul>
|
||||
* <li>Return {@code null} to fall back to the parser's
|
||||
* {@linkplain org.xml.sax.EntityResolver#resolveEntity(String, String) default behavior}.</li>
|
||||
* <li>Throw an exception to prevent remote resolution of the XSD or DTD.</li>
|
||||
* </ul>
|
||||
* @param publicId the public identifier of the external entity being referenced,
|
||||
* or null if none was supplied
|
||||
* @param systemId the system identifier of the external entity being referenced
|
||||
* @return an InputSource object describing the new input source, or null to request
|
||||
* that the parser open a regular URI connection to the system identifier.
|
||||
*/
|
||||
@Nullable
|
||||
protected InputSource resolveSchemaEntity(@Nullable String publicId, String systemId) {
|
||||
InputSource source;
|
||||
// External dtd/xsd lookup via https even for canonical http declaration
|
||||
String url = systemId;
|
||||
if (url.startsWith("http:")) {
|
||||
url = "https:" + url.substring(5);
|
||||
}
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("DTD/XSD XML entity [" + systemId + "] not found, falling back to remote https resolution");
|
||||
}
|
||||
try {
|
||||
source = new InputSource(ResourceUtils.toURL(url).openStream());
|
||||
source.setPublicId(publicId);
|
||||
source.setSystemId(systemId);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
|
||||
}
|
||||
// Fall back to the parser's default behavior.
|
||||
source = null;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.springframework.beans.factory.xml;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
class ResourceEntityResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveEntityCallsFallbackWithNullOnDtd() throws IOException, SAXException {
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, null);
|
||||
|
||||
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.dtd"))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEntityCallsFallbackWithNullOnXsd() throws IOException, SAXException {
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, null);
|
||||
|
||||
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.xsd"))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEntityCallsFallbackWithThrowOnDtd() {
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(true, null);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
() -> resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.dtd"))
|
||||
.withMessage("FallingBackEntityResolver that throws");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEntityCallsFallbackWithThrowOnXsd() {
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(true, null);
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
() -> resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.xsd"))
|
||||
.withMessage("FallingBackEntityResolver that throws");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEntityCallsFallbackWithInputSourceOnDtd() throws IOException, SAXException {
|
||||
InputSource expected = Mockito.mock(InputSource.class);
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, expected);
|
||||
|
||||
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.dtd"))
|
||||
.isNotNull()
|
||||
.isSameAs(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEntityCallsFallbackWithInputSourceOnXsd() throws IOException, SAXException {
|
||||
InputSource expected = Mockito.mock(InputSource.class);
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, expected);
|
||||
|
||||
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.xsd"))
|
||||
.isNotNull()
|
||||
.isSameAs(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveEntityDoesntCallFallbackIfNotSchema() throws IOException, SAXException {
|
||||
ResourceEntityResolver resolver = new FallingBackEntityResolver(true, null);
|
||||
|
||||
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/example.xml"))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
private static final class NoOpResourceLoader implements ResourceLoader {
|
||||
@Override
|
||||
public Resource getResource(String location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassLoader getClassLoader() {
|
||||
return ResourceEntityResolverTests.class.getClassLoader();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FallingBackEntityResolver extends ResourceEntityResolver {
|
||||
|
||||
private final boolean shouldThrow;
|
||||
@Nullable
|
||||
private final InputSource returnValue;
|
||||
|
||||
private FallingBackEntityResolver(boolean shouldThrow, @Nullable InputSource returnValue) {
|
||||
super(new NoOpResourceLoader());
|
||||
this.shouldThrow = shouldThrow;
|
||||
this.returnValue = returnValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected InputSource resolveSchemaEntity(String publicId, String systemId) {
|
||||
if (shouldThrow) throw new IllegalStateException("FallingBackEntityResolver that throws");
|
||||
return this.returnValue;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue