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:
parent
a87c9fec44
commit
a3ba86266a
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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.
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue