[java-shib-shared] branch main updated: JSSH-55: Implement HttpClient support for an overall request timeout
Brent Putman
putmanb at georgetown.edu
Thu Nov 7 23:06:57 UTC 2024
This is an automated email from the git hooks/post-receive script.
putmanb pushed a commit to branch main
in repository java-shib-shared.
View the commit online:
http://git.shibboleth.net/view/?p=java-shib-shared.git;a=commit;h=1008004b231d8159778e747d29bcde09c0016182
The following commit(s) were added to refs/heads/main by this push:
new 1008004b JSSH-55: Implement HttpClient support for an overall request timeout
1008004b is described below
commit 1008004b231d8159778e747d29bcde09c0016182
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Fri Oct 18 11:14:30 2024 -0400
JSSH-55: Implement HttpClient support for an overall request timeout
Initial implementation with tests.
---
shib-networking/pom.xml | 16 ++
.../shared/httpclient/HttpClientBuilder.java | 83 +++++-
.../shared/httpclient/HttpClientSupport.java | 16 ++
.../httpclient/RequestTimeLimitingHttpClient.java | 169 +++++++++++
.../RequestTimeoutExceededException.java | 65 +++++
.../RequestTimeLimitingHttpClientTest.java | 314 +++++++++++++++++++++
.../src/test/resources/logback-test.xml | 17 ++
7 files changed, 679 insertions(+), 1 deletion(-)
diff --git a/shib-networking/pom.xml b/shib-networking/pom.xml
index bd6c7a84..e63b2490 100644
--- a/shib-networking/pom.xml
+++ b/shib-networking/pom.xml
@@ -15,6 +15,7 @@
<packaging>jar</packaging>
<properties>
+ <jetty.version>11.0.9</jetty.version>
<automatic.module.name>net.shibboleth.networking</automatic.module.name>
<checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
</properties>
@@ -66,6 +67,21 @@
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
+
+ <dependency>
+ <groupId>${jetty.groupId}</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${jetty.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${jetty.groupId}</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${jetty.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+
</dependencies>
</project>
diff --git a/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientBuilder.java b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientBuilder.java
index 65d2b11d..a2bb076a 100644
--- a/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientBuilder.java
+++ b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientBuilder.java
@@ -20,6 +20,8 @@ import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -38,6 +40,7 @@ import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.io.ManagedHttpClientConnection;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
import org.apache.hc.core5.http.HttpHost;
@@ -113,6 +116,14 @@ public class HttpClientBuilder {
* Default value: (60 seconds) */
@Nonnull private Duration responseTimeout;
+ /** Maximum allowed length of time for the entire request/response operation to complete.
+ * Default value: null */
+ @Nullable private Duration requestTimeout;
+
+ /** Size of the {@link ScheduledExecutorService} used to implement requestTimeout handling.
+ * Default value: 100 */
+ private int requestTimeoutThreadPoolSize;
+
/**
* Max total simultaneous connections allowed by the pooling connection manager.
*/
@@ -247,6 +258,9 @@ public class HttpClientBuilder {
retryStrategy = null;
schemePortResolver = null;
+ requestTimeout = null;
+ requestTimeoutThreadPoolSize = 100;
+
disableAuthCaching = false;
disableAutomaticRetries = false;
disableConnectionState = false;
@@ -292,6 +306,9 @@ public class HttpClientBuilder {
retryStrategy = null;
schemePortResolver = null;
+ requestTimeout = null;
+ requestTimeoutThreadPoolSize = 100;
+
disableAuthCaching = false;
disableAutomaticRetries = false;
disableConnectionState = false;
@@ -397,6 +414,60 @@ public class HttpClientBuilder {
responseTimeout = timeout;
}
+ /**
+ * Gets the maximum allowed length of time for the entire request/response operation to complete.
+ *
+ * <p>
+ * A value of null means no statically-configured timeout. A timeout may also be supplied on a
+ * per-request basis via {@link HttpClientContext}. See
+ * {@link HttpClientSupport#addRequestTimeout(HttpClientContext, Duration)}.
+ * </p>
+ *
+ * @return the request timeout
+ */
+ @Nullable public Duration getRequestTimeout() {
+ return requestTimeout;
+ }
+
+ /**
+ * Sets the maximum allowed length of time for the entire request/response operation to complete.
+ *
+ * <p>
+ * A value of null means no statically-configured timeout. A timeout may also be supplied on a
+ * per-request basis via {@link HttpClientContext}. See
+ * {@link HttpClientSupport#addRequestTimeout(HttpClientContext, Duration)}.
+ * </p>
+ *
+ * @param timeout the request timeout
+ */
+ public void setRequestTimeout(@Nullable final Duration timeout) {
+ if (timeout != null) {
+ Constraint.isLessThanOrEqual(Integer.MAX_VALUE, timeout.toMillis(), "Timeout too large");
+ }
+
+ requestTimeout = timeout;
+ }
+
+ /**
+ * Gets the size of the {@link ScheduledExecutorService} used to implement requestTimeout handling.
+ *
+ * @return the request timeout
+ */
+ public int getRequestTimeoutThreadPoolSize() {
+ return requestTimeoutThreadPoolSize;
+ }
+
+ /**
+ * Sets the size of the {@link ScheduledExecutorService} used to implement requestTimeout handling.
+ *
+ * @param size the request timeout
+ */
+ public void setRequestTimeoutThreadPoolSize(final int size) {
+ Constraint.isGreaterThanOrEqual(1, size, "Thread pool size is too small");
+
+ requestTimeoutThreadPoolSize = size;
+ }
+
/**
* Gets the maximum period inactivity between two consecutive data packets. A value of less than 1 ms
* indicates no timeout.
@@ -1035,7 +1106,17 @@ public class HttpClientBuilder {
*/
@Nonnull public HttpClient buildClient() throws Exception {
decorateApacheBuilder();
- return new ContextHandlingHttpClient(getApacheBuilder().build(), getStaticContextHandlers());
+
+ final HttpClient apacheClient = getApacheBuilder().build();
+ assert apacheClient != null;
+
+ final HttpClient contextHandlingClient = new ContextHandlingHttpClient(apacheClient,
+ getStaticContextHandlers());
+
+ final ScheduledExecutorService executorService =
+ Executors.newScheduledThreadPool(getRequestTimeoutThreadPoolSize());
+ assert executorService != null;
+ return new RequestTimeLimitingHttpClient(contextHandlingClient, executorService, getRequestTimeout());
}
/**
diff --git a/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientSupport.java b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientSupport.java
index 3660573a..cca70040 100644
--- a/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientSupport.java
+++ b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/HttpClientSupport.java
@@ -23,6 +23,7 @@ import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -58,6 +59,10 @@ public final class HttpClientSupport {
@Nonnull @NotEmpty
public static final String CONTEXT_KEY_DYNAMIC_CONTEXT_HANDLERS = "java-support.DynamicContextHandlers";
+ /** Context key for an overall request timeout. Must be an instance of {@link Duration}. **/
+ @Nonnull @NotEmpty
+ public static final String CONTEXT_KEY_REQUEST_TIMEOUT = "java-support.RequestTimeout";
+
/** Constructor to prevent instantiation. */
private HttpClientSupport() { }
@@ -219,6 +224,17 @@ public final class HttpClientSupport {
}
list.add(handler);
}
+
+ /**
+ * Add overall request timeout, as implemented by {@link RequestTimeLimitingHttpClient}.
+ *
+ * @param context the client context
+ * @param timeout the request timeout
+ */
+ public static void addRequestTimeout(@Nonnull final HttpClientContext context, @Nullable final Duration timeout) {
+ Constraint.isNotNull(context, "HttpClientContext was null");
+ context.setAttribute(CONTEXT_KEY_REQUEST_TIMEOUT, timeout);
+ }
// Checkstyle: CyclomaticComplexity OFF
/**
diff --git a/shib-networking/src/main/java/net/shibboleth/shared/httpclient/RequestTimeLimitingHttpClient.java b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/RequestTimeLimitingHttpClient.java
new file mode 100644
index 00000000..cc06dbad
--- /dev/null
+++ b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/RequestTimeLimitingHttpClient.java
@@ -0,0 +1,169 @@
+/*
+ * 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
+ *
+ * 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 net.shibboleth.shared.httpclient;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.impl.classic.RequestFailedException;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.concurrent.Cancellable;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.io.ModalCloseable;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link HttpClient} implementation which implements an overall request timeout for a wrapped client instance.
+ *
+ * <p>
+ * The request timeout {@link Duration} may be supplied as either a statically-configured value on the client instance,
+ * or via the passed {@link HttpContext} in the attribute {@link HttpClientSupport#CONTEXT_KEY_REQUEST_TIMEOUT}.
+ * A non-null context value will override the static client value.
+ * </p>
+ *
+ * <p>
+ * Requests that exceed the effective timeout will throw {@link RequestTimeoutExceededException}.
+ * </p>
+ */
+public class RequestTimeLimitingHttpClient extends AbstractHttpClient {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(RequestTimeLimitingHttpClient.class);
+
+ /** The wrapped HttpClient instance. */
+ @Nonnull private HttpClient httpClient;
+
+ /** Executor service which implements the timeout handling. */
+ @Nonnull private ScheduledExecutorService executorService;
+
+ /** Client-level request timeout. */
+ @Nullable private Duration timeout;
+
+ /**
+ * Constructor.
+ *
+ * @param client the wrapped HttpClient instance
+ * @param executor executor service which implements the timeout handling
+ * @param requestTimeout client-level request timeout
+ */
+ public RequestTimeLimitingHttpClient(@Nonnull final HttpClient client,
+ @Nonnull final ScheduledExecutorService executor,
+ @Nullable Duration requestTimeout) {
+ super();
+ httpClient = Constraint.isNotNull(client, "HttpClient was null");
+ executorService = Constraint.isNotNull(executor, "ScheduledExecutorService was null");
+ timeout = requestTimeout;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected ClassicHttpResponse doExecute(@Nullable final HttpHost target,
+ @Nonnull final ClassicHttpRequest request,
+ @Nullable final HttpContext context) throws IOException {
+
+ ScheduledFuture<?> future = null;
+ Duration effectiveTimeout = null;
+ try {
+ if (request instanceof Cancellable cancellableRequest) {
+ effectiveTimeout = resolveEffectiveTimeout(context);
+ if (effectiveTimeout != null) {
+ log.debug("Scheduling request timeout of duration: {}", effectiveTimeout);
+ future = executorService.schedule(cancellableRequest::cancel,
+ effectiveTimeout.toMillis(), TimeUnit.MILLISECONDS);
+ }
+ }
+ return httpClient.executeOpen(target, request, context);
+ } catch (RequestFailedException e) {
+ // If we can match on the message, throw a nicer exception. But if it doesn't match, not a big deal,
+ // we'll just get the original RequestFailedException.
+ final String message = e.getMessage();
+ if (message.contains("aborted") || message.contains("cancelled")) {
+ throw new RequestTimeoutExceededException(String.format("Request to '%s'exceeded timeout '%s'",
+ request.getRequestUri(), effectiveTimeout), e);
+ }
+ throw e;
+ } finally {
+ if (future != null) {
+ future.cancel(true);
+ }
+ }
+ }
+
+ /**
+ * Resolve the effective request timeout to use for the current request.
+ *
+ * @param context the client context
+ * @return the effective timeout, may be null
+ */
+ @Nullable protected Duration resolveEffectiveTimeout(@Nullable final HttpContext context) {
+ if (context != null) {
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final Duration contextTimeout = clientContext.getAttribute(HttpClientSupport.CONTEXT_KEY_REQUEST_TIMEOUT,
+ Duration.class);
+ if (contextTimeout != null) {
+ log.debug("Resolved effective request timeout from client context: {}", contextTimeout);
+ return contextTimeout;
+ }
+ }
+ if (timeout != null) {
+ log.debug("Resolved effective request timeout from statically-configured client value: {}", timeout);
+ } else {
+ log.debug("No effective request timeout was resolved");
+ }
+ return timeout;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void close() throws IOException {
+ // Use try/finally here just in case the client #close() throws a RuntimeException or Error
+ try {
+ if (Closeable.class.isInstance(httpClient)) {
+ Closeable.class.cast(httpClient).close();
+ }
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void close(final CloseMode closeMode) {
+ // Use try/finally here just in case the client #close() throws a RuntimeException or Error
+ try {
+ if (ModalCloseable.class.isInstance(httpClient)) {
+ ModalCloseable.class.cast(httpClient).close(closeMode);
+ }
+ } finally {
+ executorService.shutdownNow();
+ }
+ }
+
+}
diff --git a/shib-networking/src/main/java/net/shibboleth/shared/httpclient/RequestTimeoutExceededException.java b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/RequestTimeoutExceededException.java
new file mode 100644
index 00000000..dfc317d5
--- /dev/null
+++ b/shib-networking/src/main/java/net/shibboleth/shared/httpclient/RequestTimeoutExceededException.java
@@ -0,0 +1,65 @@
+/*
+ * 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
+ *
+ * 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 net.shibboleth.shared.httpclient;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Exception that indicates that an HTTP client request exceeded a request timeout.
+ *
+ * @see RequestTimeLimitingHttpClient
+ */
+public class RequestTimeoutExceededException extends IOException {
+
+ private static final long serialVersionUID = -4315409154151655702L;
+
+ /**
+ * Constructor.
+ */
+ public RequestTimeoutExceededException() {
+ super();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param cause exception cause
+ */
+ public RequestTimeoutExceededException(@Nullable final String message, @Nullable final Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public RequestTimeoutExceededException(@Nullable final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause exception cause
+ */
+ public RequestTimeoutExceededException(@Nullable final Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/shib-networking/src/test/java/net/shibboleth/shared/httpclient/RequestTimeLimitingHttpClientTest.java b/shib-networking/src/test/java/net/shibboleth/shared/httpclient/RequestTimeLimitingHttpClientTest.java
new file mode 100644
index 00000000..83edf3fe
--- /dev/null
+++ b/shib-networking/src/test/java/net/shibboleth/shared/httpclient/RequestTimeLimitingHttpClientTest.java
@@ -0,0 +1,314 @@
+/*
+ * 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
+ *
+ * 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 net.shibboleth.shared.httpclient;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.net.URLBuilder;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ *
+ */
+public class RequestTimeLimitingHttpClientTest {
+
+ private static final String REQUEST_BASE = "http://localhost:8080/sleep";
+
+ private HttpClientContext context;
+
+ private HttpClientBuilder clientBuilder;
+
+ private HttpClient client;
+
+ private URLBuilder urlBuilder;
+
+ private List<InetAddress> jettyListenAddrs;
+
+ private Server server;
+
+ @BeforeClass
+ public void setUpClass() throws Exception {
+ resolveListenAddresses();
+ server = startServer(new MappingHandler());
+ }
+
+ @BeforeMethod
+ public void setUpMethod() throws Exception {
+ context = HttpClientContext.create();
+ clientBuilder = new HttpClientBuilder();
+ urlBuilder = new URLBuilder(REQUEST_BASE);
+ }
+
+ @AfterMethod
+ public void tearDownMethod() throws Exception {
+ if (client instanceof Closeable closeableClient) {
+ closeableClient.close();
+ }
+ }
+
+ @AfterClass
+ public void tearDownClass() throws Exception {
+ if (server != null) {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void noTimeout() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "1"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ ClassicHttpResponse response = client.executeOpen(null, request, context);
+ Assert.assertNotNull(response);
+ }
+
+ @Test
+ public void staticTimeoutMeets() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "3"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ clientBuilder.setRequestTimeout(Duration.ofSeconds(5));
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ ClassicHttpResponse response = client.executeOpen(null, request, context);
+ Assert.assertNotNull(response);
+ }
+
+ @Test(expectedExceptions = RequestTimeoutExceededException.class)
+ public void staticTimeoutExceeds() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "3"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ clientBuilder.setRequestTimeout(Duration.ofSeconds(2));
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ client.executeOpen(null, request, context);
+ }
+
+ @Test
+ public void perRequestTimeoutMeets() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "3"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ HttpClientSupport.addRequestTimeout(context, Duration.ofSeconds(5));
+
+ ClassicHttpResponse response = client.executeOpen(null, request, context);
+ Assert.assertNotNull(response);
+ }
+
+ @Test(expectedExceptions = RequestTimeoutExceededException.class)
+ public void perRequestTimeoutExceeds() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "3"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ HttpClientSupport.addRequestTimeout(context, Duration.ofSeconds(2));
+
+ client.executeOpen(null, request, context);
+ }
+
+ @Test
+ public void staticAndPerRequestTimeoutMeets() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "3"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ clientBuilder.setRequestTimeout(Duration.ofSeconds(5));
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ HttpClientSupport.addRequestTimeout(context, Duration.ofSeconds(4));
+
+ ClassicHttpResponse response = client.executeOpen(null, request, context);
+ Assert.assertNotNull(response);
+ }
+
+ @Test(expectedExceptions = RequestTimeoutExceededException.class)
+ public void staticAndPerRequestTimeoutExceeds() throws Exception {
+ urlBuilder.getQueryParams().add(new Pair<>("seconds", "3"));
+ HttpUriRequest request = new HttpGet(urlBuilder.buildURL());
+
+ clientBuilder.setRequestTimeout(Duration.ofSeconds(5));
+ client = clientBuilder.buildClient();
+ Assert.assertTrue(RequestTimeLimitingHttpClient.class.isInstance(client));
+
+ HttpClientSupport.addRequestTimeout(context, Duration.ofSeconds(2));
+
+ client.executeOpen(null, request, context);
+ }
+
+
+ //
+ // Test Helpers
+ //
+
+ @Nonnull private Server startServer(final Handler handler) {
+ final Server server = new Server();
+
+ ArrayList<ServerConnector> connectors = new ArrayList<>();
+ jettyListenAddrs.forEach(addr -> {
+ final ServerConnector connector = new ServerConnector(server);
+ connector.setHost(addr.getHostAddress());
+ connector.setPort(8080);
+ connectors.add(connector);
+ });
+ server.setConnectors(connectors.toArray(new Connector[] {}));
+
+ server.setHandler(handler);
+ try {
+ server.start();
+ } catch (Exception e) {
+ try {
+ server.stop();
+ } catch (Exception e2) {}
+ throw new RuntimeException("Jetty startup failed", e);
+ }
+ final Thread serverRunner = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ server.join();
+ } catch (InterruptedException e) {}
+ }
+ });
+ serverRunner.start();
+ return server;
+ }
+
+ private static class MappingHandler extends AbstractHandler {
+
+ private Map<String, Handler> handlers;
+
+ public MappingHandler() {
+ handlers = new HashMap<>();
+
+ handlers.put("/sleep", new SleepHandler());
+ }
+
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+
+ if (handlers.containsKey(target)) {
+ handlers.get(target).handle(target, baseRequest, request, response);
+ } else {
+ throw new ServletException("Unmapped target: " + target);
+ }
+ }
+
+ }
+
+ private static class SleepHandler extends AbstractHandler {
+ public void handle(
+ final String target,
+ final Request request,
+ final HttpServletRequest servletRequest,
+ final HttpServletResponse servletResponse) throws IOException, ServletException {
+
+ int seconds = 10; // default
+ String secondsStr = StringSupport.trimOrNull(servletRequest.getParameter("seconds"));
+ if (secondsStr != null) {
+ seconds = Integer.parseInt(secondsStr);
+ }
+
+ try {
+ Thread.sleep(seconds * 1000);
+ } catch (InterruptedException e) {
+ throw new ServletException("Error during Thread.sleep()", e);
+ }
+
+ servletResponse.setContentType("text/plain;charset=utf-8");
+ servletResponse.setStatus(200);
+ request.setHandled(true);
+ servletResponse.getWriter().println(String.format("Ok, I just slept for %d seconds", seconds));
+ servletResponse.getWriter().flush();
+ }
+ }
+
+ public void resolveListenAddresses() throws Exception {
+ // As of HttpClient 5.x, we need Jetty to listen on all localhost IPv4 and IPv6 addresses,
+ // b/c on connection failure (e.g. TLS handshake failure) HC will try them all,
+ // so they all have to respond similarly for the tests that expect failure via a specified exception type.
+ // This is an attempt to get them portably depending on whether IPv4 and/or IPv6 is enabled.
+
+ // Resolve the 'localhost' addrs that will be resolved and used by HttpClient
+ final List<InetAddress> localhostAddrs = CollectionSupport.listOf(SystemDefaultDnsResolver.INSTANCE.resolve("localhost"));
+
+ // Resolve available loopback and link-local interfaces
+ // Using ByteBuffer just to get hashcode() and equals() for byte[] for filtering using the Set.
+ final Set<ByteBuffer> interfaceAddrs = Collections.list(NetworkInterface.getNetworkInterfaces()).stream()
+ .map(NetworkInterface::getInterfaceAddresses)
+ .flatMap(Collection::stream)
+ .map(InterfaceAddress::getAddress)
+ .filter(addr -> addr.isLoopbackAddress() || addr.isLinkLocalAddress() )
+ .map(InetAddress::getAddress)
+ .map(ByteBuffer::wrap)
+ .collect(Collectors.toSet());
+
+ // Retain for listening those 'localhost' addrs which correspond to enabled interfaces
+ jettyListenAddrs = localhostAddrs.stream()
+ .filter(addr -> interfaceAddrs.contains(ByteBuffer.wrap(addr.getAddress())))
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/shib-networking/src/test/resources/logback-test.xml b/shib-networking/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..ca9e2a81
--- /dev/null
+++ b/shib-networking/src/test/resources/logback-test.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <logger name="org.eclipse.jetty" level="INFO"/>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <charset>UTF-8</charset>
+ <Pattern>%date{HH:mm:ss.SSS} - %level [%logger:%line] - %msg%n</Pattern>
+ </encoder>
+ </appender>
+
+ <root level="WARN">
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list