blob: 41bc91d871f8577f7c1816f0363b1bbfaed32239 [file]
/**
* 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 tests;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.XmlSchemaException;
import org.apache.ws.commons.schema.resolver.DefaultURIResolver;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.xml.sax.InputSource;
/**
* A schema location naming a hostile host must not be able to hold the parsing thread or its
* heap. The import depth and resolution limits bound the shape of the import graph, not the
* cost of one fetch within it, so a single import reaches none of them.
*/
public class RemoteFetchBoundsTest extends Assert {
private ServerSocket server;
private volatile boolean running;
/** Accepts, then sends nothing and never closes. */
private static final int MODE_SILENT = 0;
/** Sends a byte at a time forever, resetting the per-read timeout on each one. */
private static final int MODE_TRICKLE = 1;
/** Sends a well-formed but endless body. */
private static final int MODE_FLOOD = 2;
/** Redirects to a sibling path on the same server, once per connection. */
private static final int MODE_REDIRECT = 3;
/** Serves a small valid schema for namespace urn:b. */
private static final int MODE_SCHEMA = 4;
/** Redirects for ever, so the hop cap is what stops it. */
private static final int MODE_REDIRECT_LOOP = 5;
/** Redirects to a file: URL, changing scheme. */
private static final int MODE_REDIRECT_TO_FILE = 6;
private static final String SCHEMA_BODY =
"<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" targetNamespace=\"urn:b\"/>";
private void startServer(final int mode) throws IOException {
server = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"));
running = true;
Thread thread = new Thread(new Runnable() {
public void run() {
while (running) {
try {
Socket socket = server.accept();
serve(socket, mode);
} catch (IOException e) {
return;
}
}
}
});
thread.setDaemon(true);
thread.start();
}
private void serve(Socket socket, int mode) throws IOException {
if (mode == MODE_SILENT) {
return;
}
OutputStream out = socket.getOutputStream();
if (mode == MODE_REDIRECT || mode == MODE_SCHEMA || mode == MODE_REDIRECT_LOOP
|| mode == MODE_REDIRECT_TO_FILE) {
out.write(oneShotResponse(mode).getBytes(StandardCharsets.UTF_8));
out.flush();
socket.close();
return;
}
out.write("HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\n\r\n".getBytes(StandardCharsets.UTF_8));
out.flush();
byte[] chunk = mode == MODE_TRICKLE
? new byte[] {' '}
: new byte[64 * 1024];
while (running) {
try {
out.write(chunk);
out.flush();
if (mode == MODE_TRICKLE) {
Thread.sleep(50);
}
} catch (Exception e) {
return;
}
}
}
private String oneShotResponse(int mode) {
if (mode == MODE_SCHEMA) {
return "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nContent-Length: "
+ SCHEMA_BODY.getBytes(StandardCharsets.UTF_8).length
+ "\r\nConnection: close\r\n\r\n" + SCHEMA_BODY;
}
String location = mode == MODE_REDIRECT_TO_FILE
? "file:///etc/passwd"
: "http://127.0.0.1:" + server.getLocalPort() + "/next.xsd";
return "HTTP/1.1 302 Found\r\nLocation: " + location
+ "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
}
/** First connection answers with {@code first}, every later one with {@code rest}. */
private void startServer(final int first, final int rest) throws IOException {
server = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"));
running = true;
Thread thread = new Thread(new Runnable() {
public void run() {
boolean isFirst = true;
while (running) {
try {
Socket socket = server.accept();
serve(socket, isFirst ? first : rest);
isFirst = false;
} catch (IOException e) {
return;
}
}
}
});
thread.setDaemon(true);
thread.start();
}
private String importing() {
return "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" targetNamespace=\"urn:a\">"
+ "<xs:import namespace=\"urn:b\" schemaLocation=\"http://127.0.0.1:"
+ server.getLocalPort() + "/b.xsd\"/>"
+ "</xs:schema>";
}
@After
public void stopServer() throws IOException {
running = false;
if (server != null) {
server.close();
}
}
@Before
public void shortenBounds() {
System.setProperty(DefaultURIResolver.CONNECT_TIMEOUT_PROPERTY, "2000");
System.setProperty(DefaultURIResolver.READ_TIMEOUT_PROPERTY, "1000");
System.setProperty(DefaultURIResolver.MAX_FETCH_MILLIS_PROPERTY, "2000");
System.setProperty(DefaultURIResolver.MAX_BYTES_PROPERTY, "1048576");
}
@After
public void restoreBounds() {
System.clearProperty(DefaultURIResolver.CONNECT_TIMEOUT_PROPERTY);
System.clearProperty(DefaultURIResolver.READ_TIMEOUT_PROPERTY);
System.clearProperty(DefaultURIResolver.MAX_FETCH_MILLIS_PROPERTY);
System.clearProperty(DefaultURIResolver.MAX_BYTES_PROPERTY);
}
private void assertRefusedWithin(long millis) throws IOException {
assertRefusedWithin(millis, null);
}
/**
* @param messageFragment when given, the reason the fetch was refused must mention it, so a
* test for one bound cannot pass because a different bound happened to fire first.
*/
private void assertRefusedWithin(long millis, String messageFragment) throws IOException {
long start = System.currentTimeMillis();
try {
new XmlSchemaCollection().read(new StringReader(importing()));
fail("expected the fetch to be refused");
} catch (XmlSchemaException expected) {
long elapsed = System.currentTimeMillis() - start;
assertTrue("refused, but only after " + elapsed + "ms", elapsed < millis);
if (messageFragment != null) {
assertTrue("refused for the wrong reason: " + expected.getMessage(),
expected.getMessage().contains(messageFragment));
}
}
}
@Test(timeout = 60000)
public void testSilentHostDoesNotHoldTheThread() throws IOException {
startServer(MODE_SILENT);
assertRefusedWithin(30000);
}
/** The case a per-read timeout alone does not catch. */
@Test(timeout = 60000)
public void testTricklingHostIsCutOffAtTheTotalDeadline() throws IOException {
startServer(MODE_TRICKLE);
assertRefusedWithin(30000);
}
@Test(timeout = 60000)
public void testOversizedResponseIsRefused() throws IOException {
startServer(MODE_FLOOD);
assertRefusedWithin(30000);
}
/**
* A schema that has simply moved must still resolve. Bounding a fetch is not a reason to stop
* following a redirect, and schemas do get reorganised behind one.
*/
@Test(timeout = 60000)
public void testMovedSchemaIsStillFollowed() throws Exception {
startServer(MODE_REDIRECT, MODE_SCHEMA);
XmlSchemaCollection collection = new XmlSchemaCollection();
collection.read(new StringReader(importing()));
assertNotNull("a schema behind a redirect must still resolve",
collection.schemaForNamespace("urn:b"));
}
@Test(timeout = 60000)
public void testEndlessRedirectChainIsCutOffAtTheHopCap() throws Exception {
System.setProperty(DefaultURIResolver.MAX_REDIRECTS_PROPERTY, "3");
try {
startServer(MODE_REDIRECT_LOOP, MODE_REDIRECT_LOOP);
assertRefusedWithin(30000, "redirected more than 3 times");
} finally {
System.clearProperty(DefaultURIResolver.MAX_REDIRECTS_PROPERTY);
}
}
/** Zero hops is meaningful: it refuses a redirected location outright. */
@Test(timeout = 60000)
public void testZeroHopsRefusesARedirect() throws Exception {
System.setProperty(DefaultURIResolver.MAX_REDIRECTS_PROPERTY, "0");
try {
startServer(MODE_REDIRECT, MODE_SCHEMA);
assertRefusedWithin(30000, "redirected more than 0 times");
} finally {
System.clearProperty(DefaultURIResolver.MAX_REDIRECTS_PROPERTY);
}
}
/** A redirect may not turn a network fetch into a local read. */
@Test(timeout = 60000)
public void testRedirectChangingSchemeIsRefused() throws Exception {
startServer(MODE_REDIRECT_TO_FILE, MODE_SCHEMA);
assertRefusedWithin(30000, "redirected from the scheme");
}
/** Local schemas keep the system-id-only path: no buffering, no behaviour change. */
@Test
public void testLocalImportStillResolves() throws Exception {
XmlSchemaCollection collection = new XmlSchemaCollection();
collection.setBaseUri(Resources.TEST_RESOURCES);
XmlSchema schema = collection.read(new InputSource(Resources.asURI("importBase.xsd")));
assertNotNull(schema);
assertNotNull(collection.schemaForNamespace("http://soapinterop.org/xsd2"));
}
}