Skip to content

Commit 5965917

Browse files
simonbaslesbrannen
authored andcommitted
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
1 parent 57cfb94 commit 5965917

File tree

2 files changed

+174
-17
lines changed

2 files changed

+174
-17
lines changed

spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -110,27 +110,53 @@ public InputSource resolveEntity(@Nullable String publicId, @Nullable String sys
110110
}
111111
}
112112
else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) {
113-
// External dtd/xsd lookup via https even for canonical http declaration
114-
String url = systemId;
115-
if (url.startsWith("http:")) {
116-
url = "https:" + url.substring(5);
117-
}
118-
try {
119-
source = new InputSource(ResourceUtils.toURL(url).openStream());
120-
source.setPublicId(publicId);
121-
source.setSystemId(systemId);
122-
}
123-
catch (IOException ex) {
124-
if (logger.isDebugEnabled()) {
125-
logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
126-
}
127-
// Fall back to the parser's default behavior.
128-
source = null;
129-
}
113+
source = resolveSchemaEntity(publicId, systemId);
130114
}
131115
}
132116

133117
return source;
134118
}
135119

120+
/**
121+
* A fallback method for {@link #resolveEntity(String, String)} that is used when a
122+
* "schema" entity (DTD or XSD) cannot be resolved as a local resource. The default
123+
* behavior is to perform a remote resolution over HTTPS.
124+
* <p>Subclasses can override this method to change the default behavior.
125+
* <ul>
126+
* <li>Return {@code null} to fall back to the parser's
127+
* {@linkplain org.xml.sax.EntityResolver#resolveEntity(String, String) default behavior}.</li>
128+
* <li>Throw an exception to prevent remote resolution of the XSD or DTD.</li>
129+
* </ul>
130+
* @param publicId the public identifier of the external entity being referenced,
131+
* or null if none was supplied
132+
* @param systemId the system identifier of the external entity being referenced
133+
* @return an InputSource object describing the new input source, or null to request
134+
* that the parser open a regular URI connection to the system identifier.
135+
*/
136+
@Nullable
137+
protected InputSource resolveSchemaEntity(@Nullable String publicId, String systemId) {
138+
InputSource source;
139+
// External dtd/xsd lookup via https even for canonical http declaration
140+
String url = systemId;
141+
if (url.startsWith("http:")) {
142+
url = "https:" + url.substring(5);
143+
}
144+
if (logger.isWarnEnabled()) {
145+
logger.warn("DTD/XSD XML entity [" + systemId + "] not found, falling back to remote https resolution");
146+
}
147+
try {
148+
source = new InputSource(ResourceUtils.toURL(url).openStream());
149+
source.setPublicId(publicId);
150+
source.setSystemId(systemId);
151+
}
152+
catch (IOException ex) {
153+
if (logger.isDebugEnabled()) {
154+
logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
155+
}
156+
// Fall back to the parser's default behavior.
157+
source = null;
158+
}
159+
return source;
160+
}
161+
136162
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans.factory.xml;
18+
19+
import java.io.IOException;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.mockito.Mockito;
23+
import org.xml.sax.InputSource;
24+
import org.xml.sax.SAXException;
25+
26+
import org.springframework.core.io.Resource;
27+
import org.springframework.core.io.ResourceLoader;
28+
import org.springframework.lang.Nullable;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
32+
33+
/**
34+
* @author Simon Baslé
35+
*/
36+
class ResourceEntityResolverTests {
37+
38+
@Test
39+
void resolveEntityCallsFallbackWithNullOnDtd() throws IOException, SAXException {
40+
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, null);
41+
42+
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.dtd"))
43+
.isNull();
44+
}
45+
46+
@Test
47+
void resolveEntityCallsFallbackWithNullOnXsd() throws IOException, SAXException {
48+
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, null);
49+
50+
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.xsd"))
51+
.isNull();
52+
}
53+
54+
@Test
55+
void resolveEntityCallsFallbackWithThrowOnDtd() {
56+
ResourceEntityResolver resolver = new FallingBackEntityResolver(true, null);
57+
58+
assertThatIllegalStateException().isThrownBy(
59+
() -> resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.dtd"))
60+
.withMessage("FallingBackEntityResolver that throws");
61+
}
62+
63+
@Test
64+
void resolveEntityCallsFallbackWithThrowOnXsd() {
65+
ResourceEntityResolver resolver = new FallingBackEntityResolver(true, null);
66+
67+
assertThatIllegalStateException().isThrownBy(
68+
() -> resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.xsd"))
69+
.withMessage("FallingBackEntityResolver that throws");
70+
}
71+
72+
@Test
73+
void resolveEntityCallsFallbackWithInputSourceOnDtd() throws IOException, SAXException {
74+
InputSource expected = Mockito.mock(InputSource.class);
75+
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, expected);
76+
77+
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.dtd"))
78+
.isNotNull()
79+
.isSameAs(expected);
80+
}
81+
82+
@Test
83+
void resolveEntityCallsFallbackWithInputSourceOnXsd() throws IOException, SAXException {
84+
InputSource expected = Mockito.mock(InputSource.class);
85+
ResourceEntityResolver resolver = new FallingBackEntityResolver(false, expected);
86+
87+
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/exampleschema.xsd"))
88+
.isNotNull()
89+
.isSameAs(expected);
90+
}
91+
92+
@Test
93+
void resolveEntityDoesntCallFallbackIfNotSchema() throws IOException, SAXException {
94+
ResourceEntityResolver resolver = new FallingBackEntityResolver(true, null);
95+
96+
assertThat(resolver.resolveEntity("testPublicId", "https://example.org/example.xml"))
97+
.isNull();
98+
}
99+
100+
private static final class NoOpResourceLoader implements ResourceLoader {
101+
@Override
102+
public Resource getResource(String location) {
103+
return null;
104+
}
105+
106+
@Override
107+
public ClassLoader getClassLoader() {
108+
return ResourceEntityResolverTests.class.getClassLoader();
109+
}
110+
}
111+
112+
private static class FallingBackEntityResolver extends ResourceEntityResolver {
113+
114+
private final boolean shouldThrow;
115+
@Nullable
116+
private final InputSource returnValue;
117+
118+
private FallingBackEntityResolver(boolean shouldThrow, @Nullable InputSource returnValue) {
119+
super(new NoOpResourceLoader());
120+
this.shouldThrow = shouldThrow;
121+
this.returnValue = returnValue;
122+
}
123+
124+
@Nullable
125+
@Override
126+
protected InputSource resolveSchemaEntity(String publicId, String systemId) {
127+
if (shouldThrow) throw new IllegalStateException("FallingBackEntityResolver that throws");
128+
return this.returnValue;
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)