From a3ba86266ab11bed994204caa4cd2d1874ee4491 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 19 Aug 2022 09:38:34 +0200 Subject: [PATCH] Implemented ProxyHandler in new module jetty-core/jetty-proxy. (#8475) Moved ConnectHandler to org.eclipse.jetty.server.handler, where it should have been from the start. Cleaned up and corrected behavior of SecureRequestCustomizer. Now the response is wrapped with isSecure=true if it arrived over a secure transport. The request URI scheme indicates whether the request is secure towards the origin server. Signed-off-by: Simone Bordet --- .../org/eclipse/jetty/http/HttpField.java | 12 +- jetty-core/jetty-proxy/pom.xml | 54 ++ .../src/main/java/module-info.java | 21 + .../org/eclipse/jetty/proxy/ProxyHandler.java | 717 ++++++++++++++++++ .../ForwardProxyWithDynamicTransportTest.java | 591 +++++++++++++++ .../eclipse/jetty/proxy/ReverseProxyTest.java | 151 ++++ .../test/resources/jetty-logging.properties | 3 + .../src/test/resources/keystore.p12 | Bin 0 -> 2597 bytes .../jetty/server/SecureRequestCustomizer.java | 237 +++--- .../server/{ => handler}/ConnectHandler.java | 7 +- .../jetty/server/ssl/SSLEngineTest.java | 4 +- .../ssl/ServerConnectorSslServerTest.java | 8 +- jetty-core/pom.xml | 1 + .../eclipse/jetty/ee10/demos/ProxyServer.java | 2 +- .../src/main/config/etc/jetty-ee10-proxy.xml | 2 +- .../ee10/proxy/AsyncMiddleManServlet.java | 2 +- .../jetty/ee10/proxy/AsyncProxyServlet.java | 2 +- .../jetty/ee10/proxy/ProxyServlet.java | 2 +- .../proxy/AbstractConnectHandlerTest.java | 2 +- .../jetty/ee10/proxy/ClientAuthProxyTest.java | 4 +- .../jetty/ee10/proxy/ConnectHandlerTest.java | 2 +- .../ee10/proxy/ForwardProxyServerTest.java | 2 +- .../ee10/proxy/ForwardProxyTLSServerTest.java | 2 +- .../eclipse/jetty/ee10/proxy/ProxyServer.java | 2 +- .../test/resources/jetty-logging.properties | 2 +- .../SslClientCertAuthenticator.java | 5 +- .../eclipse/jetty/ee9/demos/ProxyServer.java | 2 +- .../ee9/proxy/AsyncMiddleManServlet.java | 2 +- .../jetty/ee9/proxy/AsyncProxyServlet.java | 2 +- .../eclipse/jetty/ee9/proxy/ProxyServlet.java | 2 +- .../ee9/proxy/AbstractConnectHandlerTest.java | 2 +- .../jetty/ee9/proxy/ClientAuthProxyTest.java | 4 +- .../jetty/ee9/proxy/ConnectHandlerTest.java | 2 +- .../ee9/proxy/ForwardProxyServerTest.java | 2 +- .../ee9/proxy/ForwardProxyTLSServerTest.java | 2 +- .../eclipse/jetty/ee9/proxy/ProxyServer.java | 2 +- .../SslClientCertAuthenticator.java | 2 +- 37 files changed, 1697 insertions(+), 164 deletions(-) create mode 100644 jetty-core/jetty-proxy/pom.xml create mode 100644 jetty-core/jetty-proxy/src/main/java/module-info.java create mode 100644 jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java create mode 100644 jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyWithDynamicTransportTest.java create mode 100644 jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java create mode 100644 jetty-core/jetty-proxy/src/test/resources/jetty-logging.properties create mode 100644 jetty-core/jetty-proxy/src/test/resources/keystore.p12 rename jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/{ => handler}/ConnectHandler.java (99%) diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java index 09866c2fd26..b20f312ee82 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.StringTokenizer; @@ -363,12 +364,19 @@ public class HttpField } public String[] getValues() + { + List values = getValueList(); + if (values == null) + return null; + return values.toArray(String[]::new); + } + + public List getValueList() { if (_value == null) return null; - QuotedCSV list = new QuotedCSV(false, _value); - return list.getValues().toArray(new String[list.size()]); + return list.getValues(); } @Override diff --git a/jetty-core/jetty-proxy/pom.xml b/jetty-core/jetty-proxy/pom.xml new file mode 100644 index 00000000000..7daa2d307d8 --- /dev/null +++ b/jetty-core/jetty-proxy/pom.xml @@ -0,0 +1,54 @@ + + + + org.eclipse.jetty + jetty-core + 12.0.0-SNAPSHOT + + + 4.0.0 + jetty-proxy + jar + Jetty Core :: Proxy + + + ${project.groupId}.proxy + + + + + org.slf4j + slf4j-api + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-client + + + + org.eclipse.jetty + jetty-slf4j-impl + test + + + org.eclipse.jetty.http2 + jetty-http2-server + test + + + org.eclipse.jetty + jetty-alpn-java-server + test + + + org.eclipse.jetty.http2 + jetty-http2-client-transport + test + + + + diff --git a/jetty-core/jetty-proxy/src/main/java/module-info.java b/jetty-core/jetty-proxy/src/main/java/module-info.java new file mode 100644 index 00000000000..64b592d8aea --- /dev/null +++ b/jetty-core/jetty-proxy/src/main/java/module-info.java @@ -0,0 +1,21 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +module org.eclipse.jetty.proxy +{ + requires org.slf4j; + requires transitive org.eclipse.jetty.client; + requires transitive org.eclipse.jetty.server; + + exports org.eclipse.jetty.proxy; +} diff --git a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java new file mode 100644 index 00000000000..fc8ec530916 --- /dev/null +++ b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java @@ -0,0 +1,717 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.proxy; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.HttpCookieStore; +import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

A {@link Handler} that can be used to implement a {@link Forward forward + * proxy ("proxy")} or a {@link Reverse reverse proxy ("gateway")} as defined by + * RFC 7230.

+ *

This class uses {@link HttpClient} to send requests from the proxy to the server.

+ *

The {@code HttpClient} instance is either + * {@link #setHttpClient(HttpClient) set explicitly}, or created implicitly. + * To customize the implicit {@code HttpClient} instance, applications can + * override {@link #newHttpClient()} and {@link #configureHttpClient(HttpClient)}.

+ * + * @see Forward + * @see Reverse + */ +public abstract class ProxyHandler extends Handler.Processor +{ + private static final Logger LOG = LoggerFactory.getLogger(ProxyHandler.class); + private static final String CLIENT_TO_PROXY_REQUEST_ATTRIBUTE = ProxyHandler.class.getName() + ".clientToProxyRequest"; + private static final EnumSet HOP_HEADERS = EnumSet.of( + HttpHeader.CONNECTION, + HttpHeader.KEEP_ALIVE, + HttpHeader.PROXY_AUTHORIZATION, + HttpHeader.PROXY_AUTHENTICATE, + HttpHeader.PROXY_CONNECTION, + HttpHeader.TRANSFER_ENCODING, + HttpHeader.TE, + HttpHeader.TRAILER, + HttpHeader.UPGRADE + ); + + private HttpClient httpClient; + private String proxyToServerHost; + private String viaHost; + + public HttpClient getHttpClient() + { + return httpClient; + } + + public void setHttpClient(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + /** + * @return the proxy-to-server {@code Host} header value + */ + public String getProxyToServerHost() + { + return proxyToServerHost; + } + + /** + *

Sets the value to use for the {@code Host} header in proxy-to-server requests.

+ *

If {@code null}, the client-to-proxy value is used.

+ * + * @param host the proxy-to-server {@code Host} header value + */ + public void setProxyToServerHost(String host) + { + this.proxyToServerHost = host; + } + + /** + * @return the value to use for the {@code Via} header + */ + public String getViaHost() + { + return viaHost; + } + + /** + *

Sets the value to use for the {@code Via} header in proxy-to-server requests.

+ *

If {@code null}, the local host name is used.

+ * + * @param viaHost the value to use for the {@code Via} header + */ + public void setViaHost(String viaHost) + { + this.viaHost = viaHost; + } + + private static String viaHost() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException x) + { + return "localhost"; + } + } + + @Override + protected void doStart() throws Exception + { + if (httpClient == null) + setHttpClient(createHttpClient()); + addBean(httpClient, true); + + if (viaHost == null) + setViaHost(viaHost()); + + super.doStart(); + } + + private HttpClient createHttpClient() + { + HttpClient httpClient = newHttpClient(); + configureHttpClient(httpClient); + LifeCycle.start(httpClient); + httpClient.getContentDecoderFactories().clear(); + httpClient.getProtocolHandlers().clear(); + return httpClient; + } + + /** + *

Creates a new {@link HttpClient} instance, by default with a thread + * pool named {@code proxy-client} and with the + * {@link HttpClientTransportDynamic dynamic transport} configured only + * with HTTP/1.1.

+ * + * @return a new {@code HttpClient} instance + */ + protected HttpClient newHttpClient() + { + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool proxyClientThreads = new QueuedThreadPool(); + proxyClientThreads.setName("proxy-client"); + clientConnector.setExecutor(proxyClientThreads); + return new HttpClient(new HttpClientTransportDynamic(clientConnector)); + } + + /** + *

Configures the {@link HttpClient} instance before it is started.

+ * + * @param httpClient the {@code HttpClient} instance to configure + */ + protected void configureHttpClient(HttpClient httpClient) + { + httpClient.setFollowRedirects(false); + httpClient.setCookieStore(new HttpCookieStore.Empty()); + } + + protected static String requestId(Request clientToProxyRequest) + { + return String.valueOf(System.identityHashCode(clientToProxyRequest)); + } + + @Override + public void process(Request clientToProxyRequest, Response proxyToClientResponse, Callback proxyToClientCallback) + { + if (LOG.isDebugEnabled()) + LOG.debug(""" + {} received C2P request + {} + {}""", + requestId(clientToProxyRequest), + clientToProxyRequest, + clientToProxyRequest.getHeaders()); + + HttpURI rewritten = rewriteHttpURI(clientToProxyRequest); + if (LOG.isDebugEnabled()) + LOG.debug("{} URI rewrite {} => {}", requestId(clientToProxyRequest), clientToProxyRequest.getHttpURI(), rewritten); + + var proxyToServerRequest = newProxyToServerRequest(clientToProxyRequest, rewritten); + + copyRequestHeaders(clientToProxyRequest, proxyToServerRequest); + + addProxyHeaders(clientToProxyRequest, proxyToServerRequest); + + if (hasContent(clientToProxyRequest)) + { + if (expects100Continue(clientToProxyRequest)) + { + // TODO + } + else + { + var proxyToServerRequestContent = newProxyToServerRequestContent(clientToProxyRequest, proxyToClientResponse, proxyToServerRequest); + proxyToServerRequest.body(proxyToServerRequestContent); + } + } + + sendProxyToServerRequest(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback); + } + + /** + *

Rewrites the client-to-proxy request URI to the proxy-to-server request URI.

+ * + * @param clientToProxyRequest the client-to-proxy request + * @return an {@code HttpURI} for the proxy-to-server request + */ + protected abstract HttpURI rewriteHttpURI(Request clientToProxyRequest); + + protected org.eclipse.jetty.client.api.Request newProxyToServerRequest(Request clientToProxyRequest, HttpURI newHttpURI) + { + return getHttpClient().newRequest(newHttpURI.toURI()) + .method(clientToProxyRequest.getMethod()) + .attribute(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE, clientToProxyRequest); + } + + protected void copyRequestHeaders(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest) + { + Set headersToRemove = findConnectionHeaders(clientToProxyRequest); + + for (HttpField clientToProxyRequestField : clientToProxyRequest.getHeaders()) + { + HttpHeader clientToProxyRequestHeader = clientToProxyRequestField.getHeader(); + + if (HttpHeader.HOST == clientToProxyRequestHeader) + { + String host = getProxyToServerHost(); + if (host != null) + { + proxyToServerRequest.headers(headers -> headers.put(HttpHeader.HOST, host)); + continue; + } + } + + if (HOP_HEADERS.contains(clientToProxyRequestHeader)) + continue; + if (headersToRemove != null && headersToRemove.contains(clientToProxyRequestField.getLowerCaseName())) + continue; + + proxyToServerRequest.headers(headers -> headers.add(clientToProxyRequestField)); + } + } + + private Set findConnectionHeaders(Request clientToProxyRequest) + { + // Any header listed by the Connection header must be removed: + // http://tools.ietf.org/html/rfc7230#section-6.1. + Set hopHeaders = null; + List connectionHeaders = clientToProxyRequest.getHeaders().getValuesList(HttpHeader.CONNECTION); + for (String value : connectionHeaders) + { + String[] values = value.split(","); + for (String name : values) + { + name = name.trim().toLowerCase(Locale.ENGLISH); + if (hopHeaders == null) + hopHeaders = new HashSet<>(); + hopHeaders.add(name); + } + } + return hopHeaders; + } + + protected void addProxyHeaders(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest) + { + addViaHeader(clientToProxyRequest, proxyToServerRequest); + addForwardedHeader(clientToProxyRequest, proxyToServerRequest); + } + + protected void addViaHeader(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest) + { + String protocol = clientToProxyRequest.getConnectionMetaData().getProtocol(); + String[] parts = protocol.split("/", 2); + // Retain only the version if the protocol is HTTP. + String protocolPart = parts.length == 2 && "HTTP".equalsIgnoreCase(parts[0]) ? parts[1] : protocol; + String viaHeaderValue = protocolPart + " " + getViaHost(); + proxyToServerRequest.headers(headers -> headers.computeField(HttpHeader.VIA, (header, viaFields) -> + { + if (viaFields == null || viaFields.isEmpty()) + return new HttpField(header, viaHeaderValue); + String separator = ", "; + String newValue = viaFields.stream() + .flatMap(field -> Stream.of(field.getValues())) + .filter(value -> !StringUtil.isBlank(value)) + .collect(Collectors.joining(separator)); + if (newValue.length() > 0) + newValue += separator; + newValue += viaHeaderValue; + return new HttpField(HttpHeader.VIA, newValue); + })); + } + + protected void addForwardedHeader(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest) + { + String byAttr = Request.getLocalAddr(clientToProxyRequest); + String forAttr = Request.getRemoteAddr(clientToProxyRequest); + String hostAttr = clientToProxyRequest.getHeaders().get(HttpHeader.HOST); + String scheme = clientToProxyRequest.getHttpURI().getScheme(); + // Even if the request came through a secure channel, look at the original scheme if present. + // For example, a client with a forward proxy may want to communicate in clear-text with the + // server (so the scheme is http), but securely with the forward proxy (so isSecure() is true). + String protoAttr = scheme == null ? (clientToProxyRequest.isSecure() ? "https" : "http") : scheme; + String forwardedValue = "by=%s;for=%s;host=%s;proto=%s".formatted( + QuotedStringTokenizer.quote(byAttr), + QuotedStringTokenizer.quote(forAttr), + QuotedStringTokenizer.quote(hostAttr), + protoAttr + ); + + proxyToServerRequest.headers(headers -> headers.computeField(HttpHeader.FORWARDED, (header, fields) -> + { + String newValue; + if (fields == null || fields.isEmpty()) + { + newValue = forwardedValue; + } + else + { + String separator = ", "; + newValue = fields.stream() + .flatMap(field -> field.getValueList().stream()) + .collect(Collectors.joining(separator)); + newValue += separator + forwardedValue; + } + return new HttpField(HttpHeader.FORWARDED, newValue); + })); + } + + private boolean hasContent(Request clientToProxyRequest) + { + if (clientToProxyRequest.getLength() > 0) + return true; + HttpFields headers = clientToProxyRequest.getHeaders(); + return headers.get(HttpHeader.CONTENT_TYPE) != null || + headers.get(HttpHeader.TRANSFER_ENCODING) != null; + } + + private boolean expects100Continue(Request clientToProxyRequest) + { + return HttpHeaderValue.CONTINUE.is(clientToProxyRequest.getHeaders().get(HttpHeader.EXPECT)); + } + + protected org.eclipse.jetty.client.api.Request.Content newProxyToServerRequestContent(Request clientToProxyRequest, Response proxyToClientResponse, org.eclipse.jetty.client.api.Request proxyToServerRequest) + { + return new ProxyRequestContent(clientToProxyRequest); + } + + protected void sendProxyToServerRequest(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, Response proxyToClientResponse, Callback proxyToClientCallback) + { + if (LOG.isDebugEnabled()) + { + LOG.debug(""" + {} sending P2S request + {} + {}""", + requestId(clientToProxyRequest), + proxyToServerRequest, + proxyToServerRequest.getHeaders()); + } + proxyToServerRequest.send(newServerToProxyResponseListener(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback)); + } + + protected org.eclipse.jetty.client.api.Response.CompleteListener newServerToProxyResponseListener(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, Response proxyToClientResponse, Callback proxyToClientCallback) + { + return new ProxyResponseListener(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback); + } + + protected HttpField filterServerToProxyResponseField(HttpField serverToProxyResponseField) + { + return serverToProxyResponseField; + } + + protected void onServerToProxyResponseFailure(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, org.eclipse.jetty.client.api.Response serverToProxyResponse, Response proxyToClientResponse, Callback proxyToClientCallback, Throwable failure) + { + int status = HttpStatus.BAD_GATEWAY_502; + if (failure instanceof TimeoutException) + status = HttpStatus.GATEWAY_TIMEOUT_504; + Callback callback = new ProxyToClientResponseFailureCallback(clientToProxyRequest, proxyToServerRequest, serverToProxyResponse, proxyToClientResponse, proxyToClientCallback); + Response.writeError(clientToProxyRequest, proxyToClientResponse, callback, status); + } + + protected void onProxyToClientResponseComplete(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, org.eclipse.jetty.client.api.Response serverToProxyResponse, Response proxyToClientResponse, Callback proxyToClientCallback) + { + proxyToClientCallback.succeeded(); + } + + protected void onProxyToClientResponseFailure(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, org.eclipse.jetty.client.api.Response serverToProxyResponse, Response proxyToClientResponse, Callback proxyToClientCallback, Throwable failure) + { + // There is no point trying to write an error, + // we already know we cannot write to the client. + proxyToClientCallback.failed(failure); + } + + /** + *

A {@code ProxyHandler} that can be used to implement a forward proxy server.

+ *

Forward proxies are configured in client applications that use + * {@link HttpClient} in this way:

+ *
{@code
+     * httpClient.getProxyConfiguration().getProxies().add(new HttpProxy(proxyHost, proxyPort));
+     * }
+ * + * @see org.eclipse.jetty.client.ProxyConfiguration + * @see org.eclipse.jetty.client.HttpProxy + * @see Reverse + */ + public static class Forward extends ProxyHandler + { + /** + * {@inheritDoc} + *

Applications that use this class should return the client-to-proxy + * request URI, since clients will send the absolute URI of the server.

+ * + * @param clientToProxyRequest the client-to-proxy request + * @return the client-to-proxy request URI + */ + @Override + protected HttpURI rewriteHttpURI(Request clientToProxyRequest) + { + return clientToProxyRequest.getHttpURI(); + } + } + + /** + *

A {@code ProxyHandler} that can be used to implement a reverse proxy.

+ *

A reverse proxy must rewrite the client-to-proxy request URI into the + * proxy-to-server request URI. + * This can be done by providing a rewrite function to the constructor, + * and/or override {@link #rewriteHttpURI(Request)}.

+ * + * @see Forward + */ + public static class Reverse extends ProxyHandler + { + private final Function httpURIRewriter; + + public Reverse(Function httpURIRewriter) + { + this.httpURIRewriter = httpURIRewriter; + } + + public Function getHttpURIRewriter() + { + return httpURIRewriter; + } + + /** + * {@inheritDoc} + *

Applications that use this class must provide a rewrite function + * to the constructor.

+ *

The rewrite function rewrites the client-to-proxy request URI, + * for example {@code http://example.com/path}, to the proxy-to-server + * request URI, for example {@code http://backend1:8080/path}.

+ * + * @param clientToProxyRequest the client-to-proxy request + * @return the proxy-to-server request URI. + */ + @Override + protected HttpURI rewriteHttpURI(Request clientToProxyRequest) + { + return getHttpURIRewriter().apply(clientToProxyRequest); + } + } + + protected static class ProxyRequestContent implements org.eclipse.jetty.client.api.Request.Content + { + private final Request clientToProxyRequest; + + public ProxyRequestContent(Request clientToProxyRequest) + { + this.clientToProxyRequest = clientToProxyRequest; + } + + @Override + public long getLength() + { + return clientToProxyRequest.getLength(); + } + + @Override + public Content.Chunk read() + { + Content.Chunk chunk = clientToProxyRequest.read(); + if (LOG.isDebugEnabled()) + LOG.debug("{} read C2P content {}", requestId(clientToProxyRequest), chunk); + return chunk; + } + + @Override + public void demand(Runnable demandCallback) + { + clientToProxyRequest.demand(demandCallback); + } + + @Override + public void fail(Throwable failure) + { + clientToProxyRequest.fail(failure); + } + + @Override + public String getContentType() + { + return clientToProxyRequest.getHeaders().get(HttpHeader.CONTENT_TYPE); + } + + @Override + public boolean rewind() + { + // TODO: can this be delegated to the clientToProxyRequest? + return false; + } + } + + protected class ProxyResponseListener extends Callback.Completable implements org.eclipse.jetty.client.api.Response.Listener + { + private final Request clientToProxyRequest; + private final org.eclipse.jetty.client.api.Request proxyToServerRequest; + private final Response proxyToClientResponse; + private final Callback proxyToClientCallback; + + public ProxyResponseListener(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, Response proxyToClientResponse, Callback proxyToClientCallback) + { + this.clientToProxyRequest = clientToProxyRequest; + this.proxyToServerRequest = proxyToServerRequest; + this.proxyToClientResponse = proxyToClientResponse; + this.proxyToClientCallback = proxyToClientCallback; + } + + @Override + public void onBegin(org.eclipse.jetty.client.api.Response serverToProxyResponse) + { + proxyToClientResponse.setStatus(serverToProxyResponse.getStatus()); + } + + @Override + public void onHeaders(org.eclipse.jetty.client.api.Response serverToProxyResponse) + { + if (LOG.isDebugEnabled()) + { + LOG.debug(""" + {} received S2P response + {} + {}""", + requestId(clientToProxyRequest), + serverToProxyResponse, + serverToProxyResponse.getHeaders()); + } + for (HttpField serverToProxyResponseField : serverToProxyResponse.getHeaders()) + { + if (HOP_HEADERS.contains(serverToProxyResponseField.getHeader())) + continue; + HttpField newField = filterServerToProxyResponseField(serverToProxyResponseField); + if (newField == null) + continue; + proxyToClientResponse.getHeaders().add(newField); + } + if (LOG.isDebugEnabled()) + { + LOG.debug(""" + {} sending P2C response + {} + {}""", + requestId(clientToProxyRequest), + proxyToClientResponse, + proxyToClientResponse.getHeaders()); + } + } + + @Override + public void onContent(org.eclipse.jetty.client.api.Response serverToProxyResponse, ByteBuffer serverToProxyContent, Callback serverToProxyContentCallback) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} received S2P content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent)); + Callback callback = new Callback() + { + @Override + public void succeeded() + { + if (LOG.isDebugEnabled()) + LOG.debug("{} succeeded to write P2C content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent)); + serverToProxyContentCallback.succeeded(); + } + + @Override + public void failed(Throwable failure) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} failed to write P2C content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent), failure); + serverToProxyContentCallback.failed(failure); + // Cannot write towards the client, abort towards the server. + serverToProxyResponse.abort(failure); + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + }; + proxyToClientResponse.write(false, serverToProxyContent, callback); + } + + @Override + public void onSuccess(org.eclipse.jetty.client.api.Response serverToProxyResponse) + { + proxyToClientResponse.write(true, BufferUtil.EMPTY_BUFFER, this); + } + + @Override + public void onComplete(Result result) + { + if (result.isSucceeded()) + { + // Wait for the last write to complete. + whenComplete((r, failure) -> + { + if (failure == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} P2C response complete {}", requestId(clientToProxyRequest), proxyToClientResponse); + onProxyToClientResponseComplete(clientToProxyRequest, proxyToServerRequest, result.getResponse(), proxyToClientResponse, proxyToClientCallback); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("{} P2C response failure {}", requestId(clientToProxyRequest), proxyToClientResponse, failure); + onProxyToClientResponseFailure(clientToProxyRequest, proxyToServerRequest, result.getResponse(), proxyToClientResponse, proxyToClientCallback, failure); + } + }); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("{} S2P failure {}", requestId(clientToProxyRequest), result.getResponse(), result.getFailure()); + onServerToProxyResponseFailure(clientToProxyRequest, proxyToServerRequest, result.getResponse(), proxyToClientResponse, proxyToClientCallback, result.getFailure()); + } + } + } + + private class ProxyToClientResponseFailureCallback implements Callback + { + private final Request clientToProxyRequest; + private final org.eclipse.jetty.client.api.Request proxyToServerRequest; + private final org.eclipse.jetty.client.api.Response serverToProxyResponse; + private final Response proxyToClientResponse; + private final Callback proxyToClientCallback; + + private ProxyToClientResponseFailureCallback(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, org.eclipse.jetty.client.api.Response serverToProxyResponse, Response proxyToClientResponse, Callback proxyToClientCallback) + { + this.clientToProxyRequest = clientToProxyRequest; + this.proxyToServerRequest = proxyToServerRequest; + this.serverToProxyResponse = serverToProxyResponse; + this.proxyToClientResponse = proxyToClientResponse; + this.proxyToClientCallback = proxyToClientCallback; + } + + @Override + public void succeeded() + { + if (LOG.isDebugEnabled()) + LOG.debug("{} P2C response complete {}", requestId(clientToProxyRequest), proxyToClientResponse); + onProxyToClientResponseComplete(clientToProxyRequest, proxyToServerRequest, serverToProxyResponse, proxyToClientResponse, proxyToClientCallback); + } + + @Override + public void failed(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} P2C response failure {}", requestId(clientToProxyRequest), proxyToClientResponse, x); + onProxyToClientResponseFailure(clientToProxyRequest, proxyToServerRequest, serverToProxyResponse, proxyToClientResponse, proxyToClientCallback, x); + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + } +} diff --git a/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyWithDynamicTransportTest.java b/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyWithDynamicTransportTest.java new file mode 100644 index 00000000000..f5a80ae3f71 --- /dev/null +++ b/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyWithDynamicTransportTest.java @@ -0,0 +1,591 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.proxy; + +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.AbstractConnectionPool; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.http.HostPortHttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.HTTP2Cipher; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.internal.ErrorCode; +import org.eclipse.jetty.http2.internal.HTTP2Connection; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ConnectHandler; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ForwardProxyWithDynamicTransportTest +{ + private static final Logger LOG = LoggerFactory.getLogger(ForwardProxyWithDynamicTransportTest.class); + + private Server server; + private ServerConnector serverConnector; + private ServerConnector serverTLSConnector; + private Server proxy; + private ServerConnector proxyConnector; + private ServerConnector proxyTLSConnector; + private HTTP2Client http2Client; + private HttpClient client; + + private void start(Handler handler) throws Exception + { + startServer(handler); + startProxy(new ConnectHandler()); + startClient(); + } + + private void startServer(Handler handler) throws Exception + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setUseCipherSuitesOrder(true); + sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + HttpConfiguration httpConfig = new HttpConfiguration(); + HttpConnectionFactory h1c = new HttpConnectionFactory(httpConfig); + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + serverConnector = new ServerConnector(server, 1, 1, h1c, h2c); + server.addConnector(serverConnector); + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpsConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + serverTLSConnector = new ServerConnector(server, 1, 1, ssl, alpn, h2, h1, h2c); + server.addConnector(serverTLSConnector); + server.setHandler(handler); + server.start(); + LOG.info("Started server on :{} and :{}", serverConnector.getLocalPort(), serverTLSConnector.getLocalPort()); + } + + private void startProxy(ConnectHandler connectHandler) throws Exception + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setUseCipherSuitesOrder(true); + sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + + QueuedThreadPool proxyThreads = new QueuedThreadPool(); + proxyThreads.setName("proxy"); + proxy = new Server(proxyThreads); + + HttpConfiguration httpConfig = new HttpConfiguration(); + ConnectionFactory h1c = new HttpConnectionFactory(httpConfig); + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + proxyConnector = new ServerConnector(proxy, 1, 1, h1c, h2c); + proxy.addConnector(proxyConnector); + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpsConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + proxyTLSConnector = new ServerConnector(proxy, 1, 1, ssl, alpn, h2, h1, h2c); + proxy.addConnector(proxyTLSConnector); + proxy.setHandler(connectHandler); + connectHandler.setHandler(new ProxyHandler.Forward() + { + @Override + protected HttpClient newHttpClient() + { + QueuedThreadPool proxyClientThreads = new QueuedThreadPool(); + proxyClientThreads.setName("proxy-client"); + ClientConnector proxyClientConnector = new ClientConnector(); + proxyClientConnector.setSelectors(1); + proxyClientConnector.setExecutor(proxyClientThreads); + proxyClientConnector.setSslContextFactory(new SslContextFactory.Client(true)); + HTTP2Client proxyHTTP2Client = new HTTP2Client(proxyClientConnector); + ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(proxyHTTP2Client); + return new HttpClient(new HttpClientTransportDynamic(proxyClientConnector, h1, http2)); + } + }); + proxy.start(); + LOG.info("Started proxy on :{} and :{}", proxyConnector.getLocalPort(), proxyTLSConnector.getLocalPort()); + } + + private void startClient() throws Exception + { + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setExecutor(clientThreads); + clientConnector.setSslContextFactory(new SslContextFactory.Client(true)); + http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + ClientConnectionFactory.Info http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, http2)); + client.start(); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(client); + LifeCycle.stop(proxy); + LifeCycle.stop(server); + } + + public static java.util.stream.Stream proxyMatrix() + { + var h1 = List.of("http/1.1"); + var h2c = List.of("h2c"); + var h2 = List.of("h2"); + return java.util.stream.Stream.of( + // HTTP/1.1 Proxy with HTTP/1.1 Server. + Arguments.of(new Origin.Protocol(h1, false), false, HttpVersion.HTTP_1_1, false), + Arguments.of(new Origin.Protocol(h1, false), false, HttpVersion.HTTP_1_1, true), + Arguments.of(new Origin.Protocol(h1, false), true, HttpVersion.HTTP_1_1, false), + Arguments.of(new Origin.Protocol(h1, false), true, HttpVersion.HTTP_1_1, true), + // HTTP/1.1 Proxy with HTTP/2 Server. + Arguments.of(new Origin.Protocol(h1, false), false, HttpVersion.HTTP_2, false), + Arguments.of(new Origin.Protocol(h1, false), false, HttpVersion.HTTP_2, true), + Arguments.of(new Origin.Protocol(h1, false), true, HttpVersion.HTTP_2, false), + Arguments.of(new Origin.Protocol(h1, false), true, HttpVersion.HTTP_2, true) + // HTTP/2 Proxy with HTTP/1.1 Server. +// TODO: re-enable when HTTP/2 tunnel support is implemented +// Arguments.of(new Origin.Protocol(h2c, false), false, HttpVersion.HTTP_1_1, false), +// Arguments.of(new Origin.Protocol(h2c, false), false, HttpVersion.HTTP_1_1, true), +// Arguments.of(new Origin.Protocol(h2, false), true, HttpVersion.HTTP_1_1, false), +// Arguments.of(new Origin.Protocol(h2, false), true, HttpVersion.HTTP_1_1, true), +// Arguments.of(new Origin.Protocol(h2, true), true, HttpVersion.HTTP_1_1, false), +// Arguments.of(new Origin.Protocol(h2, true), true, HttpVersion.HTTP_1_1, true), + // HTTP/2 Proxy with HTTP/2 Server. +// TODO: re-enable when HTTP/2 tunnel support is implemented +// Arguments.of(new Origin.Protocol(h2c, false), false, HttpVersion.HTTP_2, false), +// Arguments.of(new Origin.Protocol(h2c, false), false, HttpVersion.HTTP_2, true), +// Arguments.of(new Origin.Protocol(h2, false), true, HttpVersion.HTTP_2, false), +// Arguments.of(new Origin.Protocol(h2, false), true, HttpVersion.HTTP_2, true), +// Arguments.of(new Origin.Protocol(h2, true), true, HttpVersion.HTTP_2, false), +// Arguments.of(new Origin.Protocol(h2, true), true, HttpVersion.HTTP_2, true) + ); + } + + @ParameterizedTest(name = "proxyProtocol={0}, proxySecure={1}, serverProtocol={2}, serverSecure={3}") + @MethodSource("proxyMatrix") + public void testProxy(Origin.Protocol proxyProtocol, boolean proxySecure, HttpVersion serverProtocol, boolean serverSecure) throws Exception + { + int status = HttpStatus.NO_CONTENT_204; + start(new Handler.Processor() + { + @Override + public void process(Request request, Response response, Callback callback) + { + response.setStatus(status); + callback.succeeded(); + } + }); + + int proxyPort = proxySecure ? proxyTLSConnector.getLocalPort() : proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy proxy = new HttpProxy(proxyAddress, proxySecure, proxyProtocol); + client.getProxyConfiguration().getProxies().add(proxy); + + String scheme = serverSecure ? "https" : "http"; + int serverPort = serverSecure ? serverTLSConnector.getLocalPort() : serverConnector.getLocalPort(); + ContentResponse response1 = client.newRequest("localhost", serverPort) + .scheme(scheme) + .version(serverProtocol) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertEquals(status, response1.getStatus()); + + // Make a second request to be sure it went through the same connection. + ContentResponse response2 = client.newRequest("localhost", serverPort) + .scheme(scheme) + .version(serverProtocol) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertEquals(status, response2.getStatus()); + + List destinations = client.getDestinations().stream() + .filter(d -> d.getPort() == serverPort) + .toList(); + assertEquals(1, destinations.size()); + HttpDestination destination = (HttpDestination)destinations.get(0); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(1, connectionPool.getConnectionCount()); + } + + @Test + @Disabled("re-enable when HTTP/2 tunnel support is implemented") + public void testHTTP2TunnelClosedByClient() throws Exception + { + start(new EmptyServerHandler()); + + int proxyPort = proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy proxy = new HttpProxy(proxyAddress, false, new Origin.Protocol(List.of("h2c"), false)); + client.getProxyConfiguration().getProxies().add(proxy); + + long idleTimeout = 1000; + http2Client.setStreamIdleTimeout(idleTimeout); + + String serverScheme = "http"; + int serverPort = serverConnector.getLocalPort(); + ContentResponse response = client.newRequest("localhost", serverPort) + .scheme(serverScheme) + .version(HttpVersion.HTTP_1_1) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + + // Client will close the HTTP2StreamEndPoint. + Thread.sleep(2 * idleTimeout); + + List destinations = client.getDestinations().stream() + .filter(d -> d.getPort() == serverPort) + .toList(); + assertEquals(1, destinations.size()); + HttpDestination destination = (HttpDestination)destinations.get(0); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(0, connectionPool.getConnectionCount()); + + List serverConnections = proxyConnector.getConnectedEndPoints().stream() + .map(EndPoint::getConnection) + .map(HTTP2Connection.class::cast) + .toList(); + assertEquals(1, serverConnections.size()); + assertTrue(serverConnections.get(0).getSession().getStreams().isEmpty()); + } + + @Test + public void testProxyDown() throws Exception + { + start(new EmptyServerHandler()); + + int proxyPort = proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy httpProxy = new HttpProxy(proxyAddress, false, new Origin.Protocol(List.of("h2c"), false)); + client.getProxyConfiguration().getProxies().add(httpProxy); + proxy.stop(); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("localhost", serverConnector.getLocalPort()) + .version(HttpVersion.HTTP_1_1) + .timeout(5, TimeUnit.SECONDS) + .send(result -> + { + assertTrue(result.isFailed()); + assertThat(result.getFailure(), Matchers.instanceOf(ConnectException.class)); + latch.countDown(); + }); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + + @Test + @Disabled("re-enable when HTTP/2 tunnel support is implemented") + public void testHTTP2TunnelHardClosedByProxy() throws Exception + { + startServer(new EmptyServerHandler()); + CountDownLatch closeLatch = new CountDownLatch(1); + startProxy(new ConnectHandler() + { + @Override + protected void handleConnect(Request request, Response response, Callback callback, String serverAddress) + { + request.getConnectionMetaData().getConnection().getEndPoint().close(); + closeLatch.countDown(); + } + }); + startClient(); + + int proxyPort = proxyConnector.getLocalPort(); + Origin.Address proxyAddress = new Origin.Address("localhost", proxyPort); + HttpProxy httpProxy = new HttpProxy(proxyAddress, false, new Origin.Protocol(List.of("h2c"), false)); + client.getProxyConfiguration().getProxies().add(httpProxy); + + CountDownLatch latch = new CountDownLatch(1); + client.newRequest("localhost", serverConnector.getLocalPort()) + .version(HttpVersion.HTTP_1_1) + .timeout(5, TimeUnit.SECONDS) + .send(result -> + { + assertTrue(result.isFailed()); + assertThat(result.getFailure(), Matchers.instanceOf(ClosedChannelException.class)); + latch.countDown(); + }); + assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + + List destinations = client.getDestinations().stream() + .filter(d -> d.getPort() == proxyPort) + .toList(); + assertEquals(1, destinations.size()); + HttpDestination destination = (HttpDestination)destinations.get(0); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool(); + assertEquals(0, connectionPool.getConnectionCount()); + } + + @Test + @Disabled("re-enable when HTTP/2 tunnel support is implemented") + public void testHTTP2TunnelResetByClient() throws Exception + { + startServer(new EmptyServerHandler()); + CountDownLatch closeLatch = new CountDownLatch(2); + startProxy(new ConnectHandler() + { + @Override + protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap context) + { + return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + + @Override + protected UpstreamConnection newUpstreamConnection(EndPoint endPoint, ConnectContext connectContext) + { + return new UpstreamConnection(endPoint, getExecutor(), getByteBufferPool(), connectContext) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + }); + startClient(); + + FuturePromise sessionPromise = new FuturePromise<>(); + http2Client.connect(new InetSocketAddress("localhost", proxyConnector.getLocalPort()), new Session.Listener() {}, sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + String serverAddress = "localhost:" + serverConnector.getLocalPort(); + MetaData.ConnectRequest connect = new MetaData.ConnectRequest(HttpScheme.HTTP, new HostPortHttpField(serverAddress), null, HttpFields.EMPTY, null); + HeadersFrame frame = new HeadersFrame(connect, null, false); + FuturePromise streamPromise = new FuturePromise<>(); + CountDownLatch tunnelLatch = new CountDownLatch(1); + CountDownLatch responseLatch = new CountDownLatch(1); + session.newStream(frame, streamPromise, new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + if (response.getStatus() == HttpStatus.OK_200) + tunnelLatch.countDown(); + stream.demand(); + } + + @Override + public void onDataAvailable(Stream stream) + { + Stream.Data data = stream.readData(); + String response = BufferUtil.toString(data.frame().getData(), StandardCharsets.UTF_8); + data.release(); + if (response.startsWith("HTTP/1.1 200")) + responseLatch.countDown(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + assertTrue(tunnelLatch.await(5, TimeUnit.SECONDS)); + + // Tunnel is established, send a HTTP/1.1 request. + String h1 = "GET / HTTP/1.1\r\n" + + "Host: " + serverAddress + "\r\n" + + "\r\n"; + stream.data(new DataFrame(stream.getId(), ByteBuffer.wrap(h1.getBytes(StandardCharsets.UTF_8)), false), Callback.NOOP); + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + + // Now reset the stream, tunnel must be closed. + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + @Disabled("re-enable when HTTP/2 tunnel support is implemented") + public void testHTTP2TunnelProxyStreamTimeout() throws Exception + { + startServer(new EmptyServerHandler()); + CountDownLatch closeLatch = new CountDownLatch(2); + startProxy(new ConnectHandler() + { + @Override + protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap context) + { + return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + + @Override + protected UpstreamConnection newUpstreamConnection(EndPoint endPoint, ConnectContext connectContext) + { + return new UpstreamConnection(endPoint, getExecutor(), getByteBufferPool(), connectContext) + { + @Override + protected void close(Throwable failure) + { + super.close(failure); + closeLatch.countDown(); + } + }; + } + }); + startClient(); + + long streamIdleTimeout = 1000; + ConnectionFactory h2c = proxyConnector.getConnectionFactory("h2c"); + ((HTTP2CServerConnectionFactory)h2c).setStreamIdleTimeout(streamIdleTimeout); + + FuturePromise sessionPromise = new FuturePromise<>(); + http2Client.connect(new InetSocketAddress("localhost", proxyConnector.getLocalPort()), new Session.Listener() {}, sessionPromise); + Session session = sessionPromise.get(5, TimeUnit.SECONDS); + String serverAddress = "localhost:" + serverConnector.getLocalPort(); + MetaData.ConnectRequest connect = new MetaData.ConnectRequest(HttpScheme.HTTP, new HostPortHttpField(serverAddress), null, HttpFields.EMPTY, null); + HeadersFrame frame = new HeadersFrame(connect, null, false); + FuturePromise streamPromise = new FuturePromise<>(); + CountDownLatch tunnelLatch = new CountDownLatch(1); + CountDownLatch responseLatch = new CountDownLatch(1); + CountDownLatch resetLatch = new CountDownLatch(1); + session.newStream(frame, streamPromise, new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + if (response.getStatus() == HttpStatus.OK_200) + tunnelLatch.countDown(); + stream.demand(); + } + + @Override + public void onDataAvailable(Stream stream) + { + Stream.Data data = stream.readData(); + String response = BufferUtil.toString(data.frame().getData(), StandardCharsets.UTF_8); + data.release(); + if (response.startsWith("HTTP/1.1 200")) + responseLatch.countDown(); + } + + @Override + public void onReset(Stream stream, ResetFrame frame, Callback callback) + { + resetLatch.countDown(); + callback.succeeded(); + } + }); + Stream stream = streamPromise.get(5, TimeUnit.SECONDS); + assertTrue(tunnelLatch.await(5, TimeUnit.SECONDS)); + + // Tunnel is established, send a HTTP/1.1 request. + String h1 = "GET / HTTP/1.1\r\n" + + "Host: " + serverAddress + "\r\n" + + "\r\n"; + stream.data(new DataFrame(stream.getId(), ByteBuffer.wrap(h1.getBytes(StandardCharsets.UTF_8)), false), Callback.NOOP); + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + + // Wait until the proxy stream idle times out. + Thread.sleep(2 * streamIdleTimeout); + + // Client should see a RST_STREAM. + assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); + // Tunnel must be closed. + assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + } + + private static class EmptyServerHandler extends Handler.Processor + { + @Override + public void process(Request request, Response response, Callback callback) + { + callback.succeeded(); + } + } +} diff --git a/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java b/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java new file mode 100644 index 00000000000..0b2edd993db --- /dev/null +++ b/jetty-core/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java @@ -0,0 +1,151 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.proxy; + +import java.util.List; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.client.util.StringRequestContent; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ReverseProxyTest +{ + private Server server; + private ServerConnector serverConnector; + private Server proxy; + private ServerConnector proxyConnector; + private HttpClient client; + + private void startServer(Handler handler) throws Exception + { + QueuedThreadPool serverPool = new QueuedThreadPool(); + serverPool.setName("server"); + server = new Server(serverPool); + HttpConfiguration httpConfig = new HttpConfiguration(); + serverConnector = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig), new HTTP2CServerConnectionFactory(httpConfig)); + server.addConnector(serverConnector); + server.setHandler(handler); + server.start(); + } + + private void startProxy(Handler handler) throws Exception + { + QueuedThreadPool proxyPool = new QueuedThreadPool(); + proxyPool.setName("proxy"); + proxy = new Server(proxyPool); + HttpConfiguration configuration = new HttpConfiguration(); + configuration.setSendDateHeader(false); + configuration.setSendServerVersion(false); + proxyConnector = new ServerConnector(proxy, 1, 1, new HttpConnectionFactory(configuration), new HTTP2CServerConnectionFactory(configuration)); + proxy.addConnector(proxyConnector); + proxy.setHandler(handler); + proxy.start(); + } + + private void startClient() throws Exception + { + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client))); + client.start(); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(client); + LifeCycle.stop(proxy); + LifeCycle.stop(server); + } + + private static List httpVersions() + { + return List.of(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2); + } + + @ParameterizedTest + @MethodSource("httpVersions") + public void testSimple(HttpVersion httpVersion) throws Exception + { + String clientContent = "hello"; + String serverContent = "world"; + startServer(new Handler.Processor() + { + @Override + public void process(Request request, Response response, Callback callback) throws Exception + { + String requestContent = Content.Source.asString(request); + assertEquals(clientContent, requestContent); + Content.Sink.write(response, true, serverContent, callback); + } + }); + + startProxy(new ProxyHandler.Reverse(clientToProxyRequest -> + HttpURI.build(clientToProxyRequest.getHttpURI()).port(serverConnector.getLocalPort())) + { + @Override + protected HttpClient newHttpClient() + { + ClientConnector proxyClientConnector = new ClientConnector(); + QueuedThreadPool proxyClientThreads = new QueuedThreadPool(); + proxyClientThreads.setName("proxy-client"); + proxyClientConnector.setExecutor(proxyClientThreads); + HTTP2Client proxyHTTP2Client = new HTTP2Client(proxyClientConnector); + return new HttpClient(new HttpClientTransportDynamic(proxyClientConnector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(proxyHTTP2Client))); + } + + @Override + protected org.eclipse.jetty.client.api.Request newProxyToServerRequest(Request clientToProxyRequest, HttpURI newHttpURI) + { + return super.newProxyToServerRequest(clientToProxyRequest, newHttpURI) + .version(httpVersion); + } + }); + + startClient(); + + ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()) + .version(httpVersion) + .body(new StringRequestContent(clientContent)) + .send(); + assertEquals(serverContent, response.getContentAsString()); + } +} diff --git a/jetty-core/jetty-proxy/src/test/resources/jetty-logging.properties b/jetty-core/jetty-proxy/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..c7b6c42344c --- /dev/null +++ b/jetty-core/jetty-proxy/src/test/resources/jetty-logging.properties @@ -0,0 +1,3 @@ +#org.eclipse.jetty.LEVEL=DEBUG +#org.eclipse.jetty.proxy.LEVEL=DEBUG +#org.eclipse.jetty.server.handler.LEVEL=DEBUG diff --git a/jetty-core/jetty-proxy/src/test/resources/keystore.p12 b/jetty-core/jetty-proxy/src/test/resources/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..8934437fa14490265dfdb5d9a55bfaa88d039dcd GIT binary patch literal 2597 zcmY+EX*3j!8pmhG7{*v%B9bM0mYIp{l&lkqvL*Yz6GMqCW0#6(7+W)CDGgF~5?Qj0 zDYEZ|LD?H?w(H(=@B5zn;W^JazyJSvKK>9GCSeee4uOG=L7*4o4dQoMfDAx92HFG0 zK);^iRs@FL{r@6*D=>!M>=c`vju8a*-z#P&APA2EA0RN`U4#^b>3{h2`8hBbPN3i( zTIZ9nfF^CY1qqfRwOJQ{=m4!IU<|nPIc2~H*+6q`9a1&On*XiRzGQ=17NxDgml;*4 zZPPJ_{_t=o3-ai9hmGa1tgD;U2aP;>*(fqqC`WD5ES7QKNA(vFWfFF_oFgSI(#A2u zF04~9h&RCkCK5$ARuS|I!S;~(iCJoJ%et^?fnE$>_wv`uaLA??07m^l3O~@2ymL z0~n9)7Jh;&Dny0!ub1F3hf?lBDmgJNdK;0FlSdc4xQ872n^rU8U_X-0*b{R~Zn+E;+e0t53L*MP+fqJt{J6}2Eyk1;nTRxKS)(^%muD)Da>-iOMHG{Ug z7JWCOMDU3HSOg`ecQH9bgx+?~QO2QLz0!5d?!};;kKMxX;xkm8he%^_csE|(5ZImA z_F9E^YESW2N=b?;{+{q*GN>lr-Va5{0f&!^O-YBhm}l{*dsi>AYA*YDnkwHd-|1sI zqe_CDujEEQUpgGnGIg}Dw=OnxMQMF!$OoJJqx=4|4(FK(GJO6~1y zsVmesxl)^a-@f_qrfO`K?d31KgZQ^+<$P#5E-;yK59Qtp=z+LcxeMRZBzA6iu>HgN z$tx8dl{SqazLdv9aeRWW_$LW@W+-y_>_{eSe!6E*&106sh(UWE)n>W*q{wb;d>k+Tv&0tvPSs>OH$pHDr#DVVCc*w0{W(r*lHTwweP98k~8~D-gdZ&aH2+ zoKVNkM6c~MO1;((;$4r{l2NEll@ENx`sZ4J)PC+?eQym}mbPwZ?V896VzZvJ56OuE zc5X6_5Q1Z`{$_ux_i?X^&6LYN7u@>%UO-Wq<*;|zo-;LU*_;P#T+Z>uaNPSkr7|Gu z_Tfwjp8zeXTRlr>WRm1)+Sb=8FZj8Xu&Xb{4&UAB?O^CHWo}HYl~4|fBJ1kBMi%l=LJo5C-eWkorX-CXv!Y-D zG!L$b{6$!P(hGCMy~ayj)Agbfa{Yp&QOo;* z>?`yye<@m#NY3HO77Vcv@Pa}C@kF)CFP4ycUGp2sAAOQc#kmbgQvA;kE6Ax&KOKXP zFIXOynM8Q@P>@!41Bf#Z1Rflh7(RS01pvv%`n6F;)#sc_^Dz1)LfhyM2OvNvDRIRf z`Pm)o8fc5!aGS%AY@mom&%rYn7YXamy!I_pUW)XZ%?N9G*tc1eauFO|k9iF)*mnIQ=UCVjx!iq-3#+sqjvi@<= z31r%}JUgl_v*b`3g?8}8lTAj_*FVy+x=P@&8G$ZI%|rm0P_o`i@FVi(vH?p(mivj{ zBl5};7zKJ6u>r;JUbmT72aRzh=sep)8u6yokMb#N&AcLzN3YIukI*ZnpQ~ZEj(XA~ zJH5p!%qvS)6pmaZP27JcSr%=0f9T75aS`KoSYPc~=|Kh!l8aTXR^EN@TKmggPMs&_ zjCu7;agy=Dl$-!A7(OE0e@rU;D!)%8mgXT4 zL+9ZG(SE~PWW;$vuWPUQ)m$x%G%m~GD0HHwlRVh@(HPPb?VNg|_U+Xl!K)!yiQ6aH zm^-WGKDz|8*Fb2og2)TgZu)HE$8E4f!NNB!96l#59ji8F!|HGF9J~{N{TL|D9#!0l z|CB{Ttp(~eUQ=gQx+0!cy~t~?qUaa1G^$}+3yPDJGZ=b!w{T!&-xlZHFQ63vAt7`O zw`|+zFc7d#ooY)Wu!zSmePToTsC|hNatm7_klWJ}3hHP4)FjxfI>HS!+x3EFq*pf- z7R}n@+SOzf&eUu~HIbQ;o4@58QOdjb+6+Mw*0fFgdk&3rq;lLOe_;~KqA5JLAmbFQ=nl(RhV}5A5}WSHR^-eN#*^ZRnIatR*tbyA91Xt zxRu_L_?g;e-%B4X7cBSJKjqqGKXa7zGHdKWXd;9WPzb#=D;&vlv!aieB=-$21g6O@ruw@%VoMWHs7M literal 0 HcmV?d00001 diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java index 58b908a15fa..81972d693db 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java @@ -27,8 +27,6 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslConnection; @@ -47,12 +45,35 @@ import org.slf4j.LoggerFactory; */ public class SecureRequestCustomizer implements HttpConfiguration.Customizer { - private static final Logger LOG = LoggerFactory.getLogger(SecureRequestCustomizer.class); - public static final String X509_CERT = "org.eclipse.jetty.server.x509_cert"; - public static final String CERTIFICATES = "org.eclipse.jetty.server.certificates"; - private String _sslSessionAttribute = "org.eclipse.jetty.servlet.request.ssl_session"; // TODO better name? - private String _sslSessionDataAttribute = _sslSessionAttribute + "_data"; // TODO better name? + /** + *

The request attribute name to use to obtain the peer certificate + * chain as an array of {@link X509Certificate} objects.

+ */ + public static final String PEER_CERTIFICATES_ATTRIBUTE = "org.eclipse.jetty.server.certificates"; + /** + *

The request attribute name to use to obtain the local certificate + * as an {@link X509} object.

+ */ + public static final String X509_ATTRIBUTE = "org.eclipse.jetty.server.x509"; + /** + *

The default value of the request attribute name to use to obtain + * the {@link SSLSession} object.

+ * + * @see #setSslSessionAttribute(String) + */ + public static final String DEFAULT_SSL_SESSION_ATTRIBUTE = "org.eclipse.jetty.server.sslSession"; + /** + *

The default value of the request attribute name to use to obtain + * the {@link SslSessionData} object.

+ * + * @see #getSslSessionDataAttribute() + */ + public static final String DEFAULT_SSL_SESSION_DATA_ATTRIBUTE = newSslSessionDataAttribute(DEFAULT_SSL_SESSION_ATTRIBUTE); + private static final Logger LOG = LoggerFactory.getLogger(SecureRequestCustomizer.class); + + private String _sslSessionAttribute = DEFAULT_SSL_SESSION_ATTRIBUTE; + private String _sslSessionDataAttribute = DEFAULT_SSL_SESSION_DATA_ATTRIBUTE; private boolean _sniRequired; private boolean _sniHostCheck; private long _stsMaxAge; @@ -144,21 +165,21 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer } /** - * Set the Strict-Transport-Security max age. + * Sets the Strict-Transport-Security max age in seconds. * - * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent. + * @param stsMaxAgeSeconds the max age in seconds for the Strict-Transport-Security response header. + * If less than zero then no Strict-Transport-Security response header is set. */ public void setStsMaxAge(long stsMaxAgeSeconds) { - _stsMaxAge = stsMaxAgeSeconds; - formatSTS(); + setStsMaxAge(stsMaxAgeSeconds, TimeUnit.SECONDS); } /** - * Convenience method to call {@link #setStsMaxAge(long)} + * Sets the Strict-Transport-Security max age in the given time unit. * - * @param period The period in units - * @param units The {@link TimeUnit} of the period + * @param period The max age value + * @param units The {@link TimeUnit} of the max age */ public void setStsMaxAge(long period, TimeUnit units) { @@ -167,7 +188,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer } /** - * @return true if a include subdomain property is sent with any Strict-Transport-Security header + * @return whether the {@code includeSubdomains} attribute is sent with the Strict-Transport-Security response header */ public boolean isStsIncludeSubDomains() { @@ -175,7 +196,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer } /** - * @param stsIncludeSubDomains If true, a include subdomain property is sent with any Strict-Transport-Security header + * @param stsIncludeSubDomains whether the {@code includeSubdomains} attribute is sent with the Strict-Transport-Security response header */ public void setStsIncludeSubDomains(boolean stsIncludeSubDomains) { @@ -185,49 +206,41 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer private void formatSTS() { - if (_stsMaxAge < 0) + long stsMaxAge = getStsMaxAge(); + if (stsMaxAge < 0) _stsField = null; else - _stsField = new PreEncodedHttpField(HttpHeader.STRICT_TRANSPORT_SECURITY, String.format("max-age=%d%s", _stsMaxAge, _stsIncludeSubDomains ? "; includeSubDomains" : "")); + _stsField = new PreEncodedHttpField(HttpHeader.STRICT_TRANSPORT_SECURITY, String.format("max-age=%d%s", stsMaxAge, isStsIncludeSubDomains() ? "; includeSubDomains" : "")); } @Override public Request customize(Request request, HttpFields.Mutable responseHeaders) { - EndPoint endp = request.getConnectionMetaData().getConnection().getEndPoint(); - HttpURI uri = request.getHttpURI(); - SSLEngine sslEngine; - if (endp instanceof DecryptedEndPoint) + EndPoint endPoint = request.getConnectionMetaData().getConnection().getEndPoint(); + if (endPoint instanceof DecryptedEndPoint sslEndPoint) { - DecryptedEndPoint sslEndp = (DecryptedEndPoint)endp; - SslConnection sslConnection = sslEndp.getSslConnection(); - uri = (HttpScheme.HTTPS.is(uri.getScheme())) - ? uri : HttpURI.build(uri).scheme(HttpScheme.HTTPS); - sslEngine = sslConnection.getSSLEngine(); + SslConnection sslConnection = sslEndPoint.getSslConnection(); + SSLEngine sslEngine = sslConnection.getSSLEngine(); + request = newSecureRequest(request, sslEngine); } - else if (endp instanceof ProxyConnectionFactory.ProxyEndPoint) + else if (endPoint instanceof ProxyConnectionFactory.ProxyEndPoint proxyEndPoint) { - ProxyConnectionFactory.ProxyEndPoint proxy = (ProxyConnectionFactory.ProxyEndPoint)endp; - if (proxy.getAttribute(ProxyConnectionFactory.TLS_VERSION) == null) - return request; - uri = (HttpScheme.HTTPS.is(uri.getScheme())) - ? uri : HttpURI.build(uri).scheme(HttpScheme.HTTPS); - sslEngine = null; - } - else - { - return request; + if (proxyEndPoint.getAttribute(ProxyConnectionFactory.TLS_VERSION) != null) + request = newSecureRequest(request, null); } if (_stsField != null) responseHeaders.add(_stsField); - return newSecureRequest(request, uri, sslEngine); + return request; } - protected SecureRequest newSecureRequest(Request request, HttpURI uri, SSLEngine sslEngine) + protected Request newSecureRequest(Request request, SSLEngine sslEngine) { - return new SecureRequest(request, uri, sslEngine); + if (sslEngine != null) + return new SecureRequestWithTLSData(request, sslEngine); + else + return new SecureRequest(request); } private X509Certificate[] getCertChain(Connector connector, SSLSession sslSession) @@ -249,7 +262,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer { Objects.requireNonNull(attribute); _sslSessionAttribute = attribute; - _sslSessionDataAttribute = attribute + "_data"; + _sslSessionDataAttribute = newSslSessionDataAttribute(attribute); } public String getSslSessionAttribute() @@ -258,14 +271,16 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer } /** - * Get data belonging to the {@link SSLSession}. - * - * @return the SslSessionData + * @return {@link #getSslSessionAttribute()} {@code + "Data"} */ - public static SslSessionData getSslSessionData(SSLSession session) + public String getSslSessionDataAttribute() { - String key = SslSessionData.class.getName(); - return (SslSessionData)session.getValue(key); + return _sslSessionDataAttribute; + } + + private static String newSslSessionDataAttribute(String sslSessionAttribute) + { + return sslSessionAttribute + "Data"; } protected void checkSni(Request request, SSLSession session) @@ -291,14 +306,14 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer private X509 getX509(SSLSession session) { - X509 x509 = (X509)session.getValue(X509_CERT); + X509 x509 = (X509)session.getValue(X509_ATTRIBUTE); if (x509 == null) { Certificate[] certificates = session.getLocalCertificates(); if (certificates == null || certificates.length == 0 || !(certificates[0] instanceof X509Certificate)) return null; x509 = new X509(null, (X509Certificate)certificates[0]); - session.putValue(X509_CERT, x509); + session.putValue(X509_ATTRIBUTE, x509); } return x509; } @@ -309,57 +324,11 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer return String.format("%s@%x", this.getClass().getSimpleName(), hashCode()); } - protected class SecureRequest extends Request.Wrapper + protected static class SecureRequest extends Request.Wrapper { - private final HttpURI _uri; - private final SSLSession _sslSession; - private final SslSessionData _sslSessionData; - - private SecureRequest(Request request, HttpURI uri, SSLEngine sslEngine) + public SecureRequest(Request wrapped) { - super(request); - _uri = uri.asImmutable(); - - if (sslEngine == null) - { - _sslSession = null; - _sslSessionData = null; - } - else - { - _sslSession = sslEngine.getSession(); - checkSni(request, _sslSession); - - String key = SslSessionData.class.getName(); - SslSessionData sslSessionData = (SslSessionData)_sslSession.getValue(key); - if (sslSessionData == null) - { - try - { - String cipherSuite = _sslSession.getCipherSuite(); - int keySize = SslContextFactory.deduceKeyLength(cipherSuite); - - X509Certificate[] certs = getCertChain(getConnectionMetaData().getConnector(), _sslSession); - - byte[] bytes = _sslSession.getId(); - String idStr = StringUtil.toHexString(bytes); - - sslSessionData = new SslSessionData(keySize, certs, idStr); - _sslSession.putValue(key, sslSessionData); - } - catch (Exception e) - { - LOG.warn("Unable to get secure details ", e); - } - } - _sslSessionData = sslSessionData; - } - } - - @Override - public HttpURI getHttpURI() - { - return _uri; + super(wrapped); } @Override @@ -367,6 +336,43 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer { return true; } + } + + protected class SecureRequestWithTLSData extends SecureRequest + { + private final SSLSession _sslSession; + private final SslSessionData _sslSessionData; + + public SecureRequestWithTLSData(Request request, SSLEngine sslEngine) + { + super(request); + _sslSession = sslEngine.getSession(); + checkSni(request, _sslSession); + + String key = SslSessionData.class.getName(); + SslSessionData sslSessionData = (SslSessionData)_sslSession.getValue(key); + if (sslSessionData == null) + { + try + { + String cipherSuite = _sslSession.getCipherSuite(); + int keySize = SslContextFactory.deduceKeyLength(cipherSuite); + + X509Certificate[] certs = getCertChain(getConnectionMetaData().getConnector(), _sslSession); + + byte[] bytes = _sslSession.getId(); + String idStr = StringUtil.toHexString(bytes); + + sslSessionData = new SslSessionData(idStr, keySize, certs); + _sslSession.putValue(key, sslSessionData); + } + catch (Exception e) + { + LOG.warn("Unable to get secure details ", e); + } + } + _sslSessionData = sslSessionData; + } @Override public Object getAttribute(String name) @@ -376,14 +382,14 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer { if (name.equals(sessionAttribute)) return _sslSession; - if (name.equals(_sslSessionDataAttribute)) + if (name.equals(getSslSessionDataAttribute())) return _sslSessionData; } return switch (name) { - case X509_CERT -> getX509(_sslSession); - case CERTIFICATES -> _sslSessionData._certs; + case X509_ATTRIBUTE -> getX509(_sslSession); + case PEER_CERTIFICATES_ATTRIBUTE -> _sslSessionData != null ? _sslSessionData.peerCertificates() : null; default -> super.getAttribute(name); }; } @@ -405,32 +411,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer /** * Simple bundle of data that is cached in the SSLSession. */ - public static class SslSessionData + public record SslSessionData(String sessionId, int keySize, X509Certificate[] peerCertificates) { - private final Integer _keySize; - private final X509Certificate[] _certs; - private final String _idStr; - - private SslSessionData(Integer keySize, X509Certificate[] certs, String idStr) - { - this._keySize = keySize; - this._certs = certs; - this._idStr = idStr; - } - - public Integer getKeySize() - { - return _keySize; - } - - public X509Certificate[] getX509Certificates() - { - return _certs; - } - - public String getId() - { - return _idStr; - } } } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java similarity index 99% rename from jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectHandler.java rename to jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java index 6c435cfa798..72979880065 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java @@ -11,7 +11,7 @@ // ======================================================================== // -package org.eclipse.jetty.server; +package org.eclipse.jetty.server.handler; import java.io.Closeable; import java.io.IOException; @@ -39,6 +39,11 @@ import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpStream; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.TunnelSupport; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.IteratingCallback; diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java index e33ad94f1a5..224320eac67 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java @@ -371,8 +371,8 @@ public class SSLEngineTest { // System.err.println("HANDLE "+request.getRequestURI()); SecureRequestCustomizer.SslSessionData sslData = (SecureRequestCustomizer.SslSessionData) - request.getAttribute("org.eclipse.jetty.servlet.request.ssl_session_data"); - String sslId = sslData.getId(); + request.getAttribute(SecureRequestCustomizer.DEFAULT_SSL_SESSION_DATA_ATTRIBUTE); + String sslId = sslData.sessionId(); assertNotNull(sslId); Fields fields = Request.extractQueryParameters(request); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java index a6ec8604298..54fbc3af574 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/ServerConnectorSslServerTest.java @@ -217,15 +217,15 @@ public class ServerConnectorSslServerTest extends HttpServerTestBase StringBuilder out = new StringBuilder(); SSLSession session = (SSLSession)request.getAttribute("SSL_SESSION"); - SecureRequestCustomizer.SslSessionData data = (SecureRequestCustomizer.SslSessionData)request.getAttribute("SSL_SESSION_data"); + SecureRequestCustomizer.SslSessionData data = (SecureRequestCustomizer.SslSessionData)request.getAttribute("SSL_SESSIONData"); out.append("Hello world").append('\n'); out.append("scheme='").append(request.getHttpURI().getScheme()).append("'").append('\n'); out.append("isSecure='").append(request.isSecure()).append("'").append('\n'); - out.append("X509Certificate='").append(data == null ? "" : data.getX509Certificates()).append("'").append('\n'); + out.append("X509Certificate='").append(data == null ? "" : data.peerCertificates()).append("'").append('\n'); out.append("cipher_suite='").append(session == null ? "" : session.getCipherSuite()).append("'").append('\n'); - out.append("key_size='").append(data == null ? "" : data.getKeySize()).append("'").append('\n'); - out.append("ssl_session_id='").append(data == null ? "" : data.getId()).append("'").append('\n'); + out.append("key_size='").append(data == null ? "" : data.keySize()).append("'").append('\n'); + out.append("ssl_session_id='").append(data == null ? "" : data.sessionId()).append("'").append('\n'); out.append("ssl_session='").append(session).append("'").append('\n'); Content.Sink.write(response, true, out.toString(), callback); } diff --git a/jetty-core/pom.xml b/jetty-core/pom.xml index 77ea3653ad5..f7452052683 100644 --- a/jetty-core/pom.xml +++ b/jetty-core/pom.xml @@ -28,6 +28,7 @@ jetty-jmx jetty-jndi jetty-keystore + jetty-proxy jetty-quic jetty-rewrite jetty-server diff --git a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java index 5789dbd90d4..bd0d2c5d506 100644 --- a/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java +++ b/jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-embedded/src/main/java/org/eclipse/jetty/ee10/demos/ProxyServer.java @@ -16,9 +16,9 @@ package org.eclipse.jetty.ee10.demos; import org.eclipse.jetty.ee10.proxy.ProxyServlet; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; public class ProxyServer { diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/config/etc/jetty-ee10-proxy.xml b/jetty-ee10/jetty-ee10-proxy/src/main/config/etc/jetty-ee10-proxy.xml index f086fcdb487..e3502c2b4ca 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/config/etc/jetty-ee10-proxy.xml +++ b/jetty-ee10/jetty-ee10-proxy/src/main/config/etc/jetty-ee10-proxy.xml @@ -4,7 +4,7 @@ - + diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java index a4728af1b41..abbb335815c 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncMiddleManServlet.java @@ -45,7 +45,7 @@ import org.eclipse.jetty.client.util.AsyncRequestContent; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.RuntimeIOException; -import org.eclipse.jetty.server.ConnectHandler; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.CountingCallback; diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncProxyServlet.java index b51af6690f9..dea10891e1c 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AsyncProxyServlet.java @@ -28,7 +28,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.AsyncRequestContent; -import org.eclipse.jetty.server.ConnectHandler; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java index 1989c388943..dcd3aef0c7e 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/ProxyServlet.java @@ -30,7 +30,7 @@ import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.AsyncRequestContent; import org.eclipse.jetty.client.util.InputStreamRequestContent; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.ConnectHandler; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.util.Callback; /** diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AbstractConnectHandlerTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AbstractConnectHandlerTest.java index 655194aae7b..24962304549 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AbstractConnectHandlerTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/AbstractConnectHandlerTest.java @@ -16,9 +16,9 @@ package org.eclipse.jetty.ee10.proxy; import java.io.IOException; import java.net.Socket; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.junit.jupiter.api.AfterEach; public abstract class AbstractConnectHandlerTest diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java index 8bc47a31a3d..5dfedacea32 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ClientAuthProxyTest.java @@ -139,7 +139,7 @@ public class ClientAuthProxyTest @Override public void process(org.eclipse.jetty.server.Request request, Response response, Callback callback) { - X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.CERTIFICATES); + X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE); Assertions.assertNotNull(certificates); X509Certificate certificate = certificates[0]; X500Principal principal = certificate.getSubjectX500Principal(); @@ -211,7 +211,7 @@ public class ClientAuthProxyTest private static String retrieveUser(HttpServletRequest request) { - X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.CERTIFICATES); + X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE); String clientName = certificates[0].getSubjectX500Principal().getName(); Matcher matcher = Pattern.compile("CN=([^,]+)").matcher(clientName); if (matcher.find()) diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ConnectHandlerTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ConnectHandlerTest.java index 1ebcd4cab14..b60b7726d2e 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ConnectHandlerTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ConnectHandlerTest.java @@ -32,12 +32,12 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java index 3e97b4ae45a..22f914a89bc 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyServerTest.java @@ -33,7 +33,6 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.AbstractConnectionFactory; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ForwardedRequestCustomizer; @@ -42,6 +41,7 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.BufferUtil; diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyTLSServerTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyTLSServerTest.java index ea0410ae0a2..3f7b1d8fa7a 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyTLSServerTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ForwardProxyTLSServerTest.java @@ -48,13 +48,13 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.Callback; diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java index 20900fe16a3..bcbadf99855 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServer.java @@ -15,9 +15,9 @@ package org.eclipse.jetty.ee10.proxy; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; public class ProxyServer { diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/resources/jetty-logging.properties b/jetty-ee10/jetty-ee10-proxy/src/test/resources/jetty-logging.properties index e5f9496aab3..dea29d24490 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/resources/jetty-logging.properties +++ b/jetty-ee10/jetty-ee10-proxy/src/test/resources/jetty-logging.properties @@ -2,4 +2,4 @@ #org.eclipse.jetty.client.LEVEL=DEBUG #org.eclipse.jetty.ee10.proxy.LEVEL=DEBUG #org.eclipse.jetty.server.LEVEL=DEBUG -#org.eclipse.jetty.server.ConnectHandler.LEVEL=DEBUG +#org.eclipse.jetty.server.handler.ConnectHandler.LEVEL=DEBUG diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/authentication/SslClientCertAuthenticator.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/authentication/SslClientCertAuthenticator.java index 0949ba5fe18..ee739cbd571 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/authentication/SslClientCertAuthenticator.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/security/authentication/SslClientCertAuthenticator.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.ee10.servlet.security.UserAuthentication; import org.eclipse.jetty.ee10.servlet.security.UserIdentity; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.SecureRequestCustomizer.SslSessionData; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.security.Constraint; @@ -63,14 +64,14 @@ public class SslClientCertAuthenticator extends LoginAuthenticator //TODO this seems fragile, to rely on this name //X509Certificate[] certs = (X509Certificate[])req.getAttribute("jakarta.servlet.request.X509Certificate"); - SslSessionData sslSessionData = (SslSessionData)req.getAttribute("org.eclipse.jetty.servlet.request.ssl_session_data"); + SslSessionData sslSessionData = (SslSessionData)req.getAttribute(SecureRequestCustomizer.DEFAULT_SSL_SESSION_DATA_ATTRIBUTE); if (sslSessionData == null) { Response.writeError(req, res, callback, HttpServletResponse.SC_FORBIDDEN); return Authentication.SEND_FAILURE; } - X509Certificate[] certs = sslSessionData.getX509Certificates(); + X509Certificate[] certs = sslSessionData.peerCertificates(); try { diff --git a/jetty-ee9/jetty-ee9-demos/jetty-ee9-demo-embedded/src/main/java/org/eclipse/jetty/ee9/demos/ProxyServer.java b/jetty-ee9/jetty-ee9-demos/jetty-ee9-demo-embedded/src/main/java/org/eclipse/jetty/ee9/demos/ProxyServer.java index 40ad7f6b33e..7eb5e4e8406 100644 --- a/jetty-ee9/jetty-ee9-demos/jetty-ee9-demo-embedded/src/main/java/org/eclipse/jetty/ee9/demos/ProxyServer.java +++ b/jetty-ee9/jetty-ee9-demos/jetty-ee9-demo-embedded/src/main/java/org/eclipse/jetty/ee9/demos/ProxyServer.java @@ -16,9 +16,9 @@ package org.eclipse.jetty.ee9.demos; import org.eclipse.jetty.ee9.proxy.ProxyServlet; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; import org.eclipse.jetty.ee9.servlet.ServletHolder; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; public class ProxyServer { diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java index e6da92413c0..e1fbac84db3 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncMiddleManServlet.java @@ -45,7 +45,7 @@ import org.eclipse.jetty.client.util.AsyncRequestContent; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.RuntimeIOException; -import org.eclipse.jetty.server.ConnectHandler; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.CountingCallback; diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncProxyServlet.java index bd83741ab91..3b2e959aa60 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AsyncProxyServlet.java @@ -28,7 +28,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.AsyncRequestContent; -import org.eclipse.jetty.server.ConnectHandler; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java index 70fba854c64..5ef09cf8391 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/ProxyServlet.java @@ -30,7 +30,7 @@ import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.AsyncRequestContent; import org.eclipse.jetty.client.util.InputStreamRequestContent; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.ConnectHandler; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.util.Callback; /** diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AbstractConnectHandlerTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AbstractConnectHandlerTest.java index f5e895c6ad1..3821fff1ffa 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AbstractConnectHandlerTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/AbstractConnectHandlerTest.java @@ -16,9 +16,9 @@ package org.eclipse.jetty.ee9.proxy; import java.io.IOException; import java.net.Socket; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.junit.jupiter.api.AfterEach; public abstract class AbstractConnectHandlerTest diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ClientAuthProxyTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ClientAuthProxyTest.java index c6fbb941fe6..c50b8fc09a9 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ClientAuthProxyTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ClientAuthProxyTest.java @@ -139,7 +139,7 @@ public class ClientAuthProxyTest @Override public void process(org.eclipse.jetty.server.Request request, Response response, Callback callback) { - X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.CERTIFICATES); + X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE); Assertions.assertNotNull(certificates); X509Certificate certificate = certificates[0]; X500Principal principal = certificate.getSubjectX500Principal(); @@ -211,7 +211,7 @@ public class ClientAuthProxyTest private static String retrieveUser(HttpServletRequest request) { - X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.CERTIFICATES); + X509Certificate[] certificates = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE); String clientName = certificates[0].getSubjectX500Principal().getName(); Matcher matcher = Pattern.compile("CN=([^,]+)").matcher(clientName); if (matcher.find()) diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ConnectHandlerTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ConnectHandlerTest.java index 23e115c60dd..54194005c9a 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ConnectHandlerTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ConnectHandlerTest.java @@ -32,12 +32,12 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyServerTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyServerTest.java index 3fdee000488..582ce7f2286 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyServerTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyServerTest.java @@ -33,7 +33,6 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.AbstractConnectionFactory; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ForwardedRequestCustomizer; @@ -42,6 +41,7 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.BufferUtil; diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyTLSServerTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyTLSServerTest.java index 0cc8158059f..5e4b0edc453 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyTLSServerTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ForwardProxyTLSServerTest.java @@ -48,13 +48,13 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.FormFields; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.Callback; diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServer.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServer.java index f27a459f6f3..e5fdbc584a4 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServer.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServer.java @@ -15,9 +15,9 @@ package org.eclipse.jetty.ee9.proxy; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; import org.eclipse.jetty.ee9.servlet.ServletHolder; -import org.eclipse.jetty.server.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ConnectHandler; public class ProxyServer { diff --git a/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/authentication/SslClientCertAuthenticator.java b/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/authentication/SslClientCertAuthenticator.java index 6a400a88829..d9394e6971c 100644 --- a/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/authentication/SslClientCertAuthenticator.java +++ b/jetty-ee9/jetty-ee9-security/src/main/java/org/eclipse/jetty/ee9/security/authentication/SslClientCertAuthenticator.java @@ -63,7 +63,7 @@ public class SslClientCertAuthenticator extends LoginAuthenticator HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; - X509Certificate[] certs = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.CERTIFICATES); + X509Certificate[] certs = (X509Certificate[])request.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE); try {