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 <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2022-08-19 09:38:34 +02:00 committed by GitHub
parent a87c9fec44
commit a3ba86266a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1697 additions and 164 deletions

View File

@ -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<String> values = getValueList();
if (values == null)
return null;
return values.toArray(String[]::new);
}
public List<String> getValueList()
{
if (_value == null)
return null;
QuotedCSV list = new QuotedCSV(false, _value);
return list.getValues().toArray(new String[list.size()]);
return list.getValues();
}
@Override

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-core</artifactId>
<version>12.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-proxy</artifactId>
<packaging>jar</packaging>
<name>Jetty Core :: Proxy</name>
<properties>
<bundle-symbolic-name>${project.groupId}.proxy</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-java-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-http2-client-transport</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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;
}

View File

@ -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;
/**
* <p>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
* <a href="https://datatracker.ietf.org/doc/html/rfc7230#section-2.3">RFC 7230</a>.</p>
* <p>This class uses {@link HttpClient} to send requests from the proxy to the server.</p>
* <p>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)}.</p>
*
* @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<HttpHeader> 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;
}
/**
* <p>Sets the value to use for the {@code Host} header in proxy-to-server requests.</p>
* <p>If {@code null}, the client-to-proxy value is used.</p>
*
* @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;
}
/**
* <p>Sets the value to use for the {@code Via} header in proxy-to-server requests.</p>
* <p>If {@code null}, the local host name is used.</p>
*
* @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;
}
/**
* <p>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.</p>
*
* @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));
}
/**
* <p>Configures the {@link HttpClient} instance before it is started.</p>
*
* @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);
}
/**
* <p>Rewrites the client-to-proxy request URI to the proxy-to-server request URI.</p>
*
* @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<String> 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<String> findConnectionHeaders(Request clientToProxyRequest)
{
// Any header listed by the Connection header must be removed:
// http://tools.ietf.org/html/rfc7230#section-6.1.
Set<String> hopHeaders = null;
List<String> 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);
}
/**
* <p>A {@code ProxyHandler} that can be used to implement a forward proxy server.</p>
* <p>Forward proxies are configured in client applications that use
* {@link HttpClient} in this way:</p>
* <pre>{@code
* httpClient.getProxyConfiguration().getProxies().add(new HttpProxy(proxyHost, proxyPort));
* }</pre>
*
* @see org.eclipse.jetty.client.ProxyConfiguration
* @see org.eclipse.jetty.client.HttpProxy
* @see Reverse
*/
public static class Forward extends ProxyHandler
{
/**
* {@inheritDoc}
* <p>Applications that use this class should return the client-to-proxy
* request URI, since clients will send the absolute URI of the server.</p>
*
* @param clientToProxyRequest the client-to-proxy request
* @return the client-to-proxy request URI
*/
@Override
protected HttpURI rewriteHttpURI(Request clientToProxyRequest)
{
return clientToProxyRequest.getHttpURI();
}
}
/**
* <p>A {@code ProxyHandler} that can be used to implement a reverse proxy.</p>
* <p>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)}.</p>
*
* @see Forward
*/
public static class Reverse extends ProxyHandler
{
private final Function<Request, HttpURI> httpURIRewriter;
public Reverse(Function<Request, HttpURI> httpURIRewriter)
{
this.httpURIRewriter = httpURIRewriter;
}
public Function<Request, HttpURI> getHttpURIRewriter()
{
return httpURIRewriter;
}
/**
* {@inheritDoc}
* <p>Applications that use this class must provide a rewrite function
* to the constructor.</p>
* <p>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}.</p>
*
* @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;
}
}
}

View File

@ -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<Arguments> 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<Destination> 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<Destination> 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<HTTP2Connection> 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<Destination> 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<String, Object> 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<Session> 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<Stream> 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<String, Object> 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<Session> 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<Stream> 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();
}
}
}

View File

@ -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<HttpVersion> 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());
}
}

View File

@ -0,0 +1,3 @@
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.proxy.LEVEL=DEBUG
#org.eclipse.jetty.server.handler.LEVEL=DEBUG

Binary file not shown.

View File

@ -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?
/**
* <p>The request attribute name to use to obtain the peer certificate
* chain as an array of {@link X509Certificate} objects.</p>
*/
public static final String PEER_CERTIFICATES_ATTRIBUTE = "org.eclipse.jetty.server.certificates";
/**
* <p>The request attribute name to use to obtain the local certificate
* as an {@link X509} object.</p>
*/
public static final String X509_ATTRIBUTE = "org.eclipse.jetty.server.x509";
/**
* <p>The default value of the request attribute name to use to obtain
* the {@link SSLSession} object.</p>
*
* @see #setSslSessionAttribute(String)
*/
public static final String DEFAULT_SSL_SESSION_ATTRIBUTE = "org.eclipse.jetty.server.sslSession";
/**
* <p>The default value of the request attribute name to use to obtain
* the {@link SslSessionData} object.</p>
*
* @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;
}
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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);
}

View File

@ -28,6 +28,7 @@
<module>jetty-jmx</module>
<module>jetty-jndi</module>
<module>jetty-keystore</module>
<module>jetty-proxy</module>
<module>jetty-quic</module>
<module>jetty-rewrite</module>
<module>jetty-server</module>

View File

@ -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
{

View File

@ -4,7 +4,7 @@
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Set name="handler">
<New class="org.eclipse.jetty.server.ConnectHandler">
<New class="org.eclipse.jetty.server.handler.ConnectHandler">
<Set name="handler">
<New class="org.eclipse.jetty.ee10.servlet.ServletHandler">
<Call id="proxyHolder" name="addServletWithMapping">

View File

@ -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;

View File

@ -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;

View File

@ -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;
/**

View File

@ -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

View File

@ -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())

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
{

View File

@ -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

View File

@ -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
{

View File

@ -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
{

View File

@ -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;

View File

@ -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;

View File

@ -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;
/**

View File

@ -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

View File

@ -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())

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
{

View File

@ -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
{