diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-transport.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-transport.adoc index 778c0438267f..f66c0a15849e 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-transport.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-transport.adoc @@ -187,9 +187,11 @@ All the transports can be configured with a `ClientConnector`, the component tha By default, `ClientConnector` uses TCP networking to send bytes to the server and receive bytes from the server. -When you are using Java 16 or later, `ClientConnector` also support xref:pg-client-io-arch-unix-domain[Unix-Domain sockets], and every transport can be configured to use Unix-Domain sockets instead of TCP networking. +When you are using Java 16 or later, `ClientConnector` also support xref:pg-client-io-arch-unix-domain[Unix-Domain sockets]. -To configure Unix-Domain sockets, you can create a `ClientConnector` instance in the following way: +Unix-Domain sockets can be used for HTTP/1.1 and HTTP/2 but not for HTTP/3, because HTTP/3 is based on UDP and there is currently no support in Java for UDP over Unix-Domain sockets. + +You can send requests to a Unix-Domain sockets server in the following way: [source,java,indent=0] ---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc index 1b1c0d59092b..e74a42b3fb59 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-connector.adoc @@ -20,12 +20,14 @@ The available implementations are: * `org.eclipse.jetty.server.ServerConnector`, for TCP/IP sockets. * `org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector` for Unix-Domain sockets (requires Java 16 or later). -* `org.eclipse.jetty.quic.server.QuicServerConnector`, for the low-level QUIC protocol. -* `org.eclipse.jetty.http3.server.HTTP3ServerConnector` for the HTTP/3 protocol. +* `org.eclipse.jetty.quic.server.QuicServerConnector`, for the low-level QUIC protocol and HTTP/3. +* `org.eclipse.jetty.server.MemoryConnector`, for memory communication between client and server. -The first two use a `java.nio.channels.ServerSocketChannel` to listen to a socket address and to accept socket connections, while last two use a `java.nio.channels.DatagramChannel`. +`ServerConnector` and `UnixDomainServerConnector` use a `java.nio.channels.ServerSocketChannel` to listen to a socket address and to accept socket connections. +`QuicServerConnector` uses a `java.nio.channels.DatagramChannel` to listen to incoming UDP packets. +`MemoryConnector` uses memory for the communication between client and server, avoiding the use of sockets -Since `ServerConnector` wraps a `ServerSocketChannel`, it can be configured in a similar way, for example the IP port to listen to, the IP address to bind to, etc.: +Since `ServerConnector` wraps a `ServerSocketChannel`, it can be configured in a similar way, for example the TCP port to listen to, the IP address to bind to, etc.: [source,java,indent=0] ---- @@ -44,13 +46,15 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer You can use Unix-Domain sockets support only when you run your server with Java 16 or later. ==== -`QuicServerConnector` and its extension `HTTP3ServerConnector` wrap a `DatagramChannel` and can be configured in a similar way: +`QuicServerConnector` wraps a `DatagramChannel` and can be configured in a similar way: [source,java,indent=0] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorQuic] ---- +// TODO: add a section on MemoryConnector. + [[pg-server-http-connector-acceptors]] ===== Acceptors diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java index fccb713eea71..60dff979f791 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -153,7 +154,7 @@ public void onFillable() CompletableFuture connectionPromise = new Promise.Completable<>(); // Populate the context with the mandatory keys to create and obtain connections. - Map context = new HashMap<>(); + Map context = new ConcurrentHashMap<>(); context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); clientConnector.connect(address, context); diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java index 1c73a3a25405..1d9a6e195b1f 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java @@ -69,6 +69,8 @@ import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -871,8 +873,11 @@ public void http2Transport() throws Exception public void http3Transport() throws Exception { // tag::http3Transport[] + // HTTP/3 requires secure communication. + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); // The HTTP3Client powers the HTTP/3 transport. - HTTP3Client h3Client = new HTTP3Client(); + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client h3Client = new HTTP3Client(clientQuicConfig); h3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); // Create and configure the HTTP/3 transport. @@ -1018,25 +1023,29 @@ public void unixDomain() throws Exception // This is the path where the server "listens" on. Path unixDomainPath = Path.of("/path/to/server.sock"); - // Creates a ClientConnector that uses Unix-Domain - // sockets, not the network, to connect to the server. - ClientConnector unixDomainClientConnector = ClientConnector.forUnixDomain(unixDomainPath); + // Creates a ClientConnector. + ClientConnector clientConnector = new ClientConnector(); - // Use Unix-Domain for HTTP/1.1. - HttpClientTransportOverHTTP http1Transport = new HttpClientTransportOverHTTP(unixDomainClientConnector); + // You can use Unix-Domain for HTTP/1.1. + HttpClientTransportOverHTTP http1Transport = new HttpClientTransportOverHTTP(clientConnector); // You can use Unix-Domain also for HTTP/2. - HTTP2Client http2Client = new HTTP2Client(unixDomainClientConnector); + HTTP2Client http2Client = new HTTP2Client(clientConnector); HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2(http2Client); - // You can also use UnixDomain for the dynamic transport. + // You can use Unix-Domain also for the dynamic transport. ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); - HttpClientTransportDynamic dynamicTransport = new HttpClientTransportDynamic(unixDomainClientConnector, http1, http2); + HttpClientTransportDynamic dynamicTransport = new HttpClientTransportDynamic(clientConnector, http1, http2); // Choose the transport you prefer for HttpClient, for example the dynamic transport. HttpClient httpClient = new HttpClient(dynamicTransport); httpClient.start(); + + // Make a request and specify that you want to send it over Unix-Domain. + ContentResponse response = httpClient.newRequest("jetty.org", 80) + .transportProtocol(new TransportProtocol.TCPUnix(unixDomainPath)) + .send(); // end::unixDomain[] } } diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java index 309f645fe8d1..a18a2b14903c 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java @@ -33,6 +33,8 @@ import org.eclipse.jetty.http3.client.HTTP3Client; import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.util.ssl.SslContextFactory; import static java.lang.System.Logger.Level.INFO; @@ -43,7 +45,8 @@ public void start() throws Exception { // tag::start[] // Instantiate HTTP3Client. - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); // Configure HTTP3Client, for example: http3Client.getHTTP3Configuration().setStreamIdleTimeout(15000); @@ -55,7 +58,8 @@ public void start() throws Exception public void stop() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); // tag::stop[] // Stop HTTP3Client. @@ -65,7 +69,8 @@ public void stop() throws Exception public void connect() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); // tag::connect[] // Address of the server's port. @@ -83,7 +88,8 @@ public void connect() throws Exception public void configure() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); // tag::configure[] @@ -105,7 +111,8 @@ public Map onPreface(Session session) public void newStream() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); // tag::newStream[] SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); @@ -130,7 +137,8 @@ public void newStream() throws Exception public void newStreamWithData() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); // tag::newStreamWithData[] SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); @@ -173,7 +181,8 @@ public void newStreamWithData() throws Exception public void responseListener() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); @@ -231,7 +240,8 @@ private void process(ByteBuffer byteBuffer) public void reset() throws Exception { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); http3Client.start(); SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java index f32612e6827d..f1750d753732 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; @@ -124,8 +125,9 @@ public void connectHTTP2Dynamic() throws Exception { // tag::connectHTTP2Dynamic[] // Use the dynamic HTTP/2 transport for HttpClient. - HTTP2Client http2Client = new HTTP2Client(); - HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client))); + ClientConnector clientConnector = new ClientConnector(); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client))); // Create and start WebSocketClient. WebSocketClient webSocketClient = new WebSocketClient(httpClient); diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index a2eed0eb2850..e1a8e0b621a8 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -46,8 +46,9 @@ import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.rewrite.handler.CompactPathRule; import org.eclipse.jetty.rewrite.handler.RedirectRegexRule; import org.eclipse.jetty.rewrite.handler.RewriteHandler; @@ -223,8 +224,10 @@ public void configureConnectorQuic() throws Exception sslContextFactory.setKeyStorePath("/path/to/keystore"); sslContextFactory.setKeyStorePassword("secret"); - // Create an HTTP3ServerConnector instance. - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory()); + // Create a QuicServerConnector instance. + Path pemWorkDir = Path.of("/path/to/pem/dir"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslContextFactory, pemWorkDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); // The port to listen to. connector.setPort(8080); @@ -289,8 +292,9 @@ public void sameRandomPort() throws Exception server.addConnector(plainConnector); // Third, create the connector for HTTP/3. - HTTP3ServerConnectionFactory http3 = new HTTP3ServerConnectionFactory(secureConfig); - HTTP3ServerConnector http3Connector = new HTTP3ServerConnector(server, sslContextFactory, http3); + Path pemWorkDir = Path.of("/path/to/pem/dir"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslContextFactory, pemWorkDir); + QuicServerConnector http3Connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); server.addConnector(http3Connector); // Set up a listener so that when the secure connector starts, @@ -493,12 +497,12 @@ public void h3() throws Exception httpConfig.addCustomizer(new SecureRequestCustomizer()); // Create and configure the HTTP/3 connector. - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(httpConfig)); + // It is mandatory to configure the PEM directory. + Path pemWorkDir = Path.of("/path/to/pem/dir"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslContextFactory, pemWorkDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); connector.setPort(843); - // It is mandatory to set the PEM directory. - connector.getQuicConfiguration().setPemWorkDirectory(Path.of("/path/to/pem/dir")); - server.addConnector(connector); server.start(); diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java index 0453e2783307..f93598dd2acd 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java @@ -15,6 +15,7 @@ import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.RejectedExecutionException; @@ -29,8 +30,9 @@ import org.eclipse.jetty.http3.api.Stream; import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.HeadersFrame; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -53,14 +55,16 @@ public void setup() throws Exception // The listener for session events. Session.Server.Listener sessionListener = new Session.Server.Listener() {}; + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, Path.of("/path/to/pem/dir")); + // Configure the max number of requests per QUIC connection. + quicConfiguration.setMaxBidirectionalRemoteStreams(1024); + // Create and configure the RawHTTP3ServerConnectionFactory. - RawHTTP3ServerConnectionFactory http3 = new RawHTTP3ServerConnectionFactory(sessionListener); + RawHTTP3ServerConnectionFactory http3 = new RawHTTP3ServerConnectionFactory(quicConfiguration, sessionListener); http3.getHTTP3Configuration().setStreamIdleTimeout(15000); - // Create and configure the HTTP3ServerConnector. - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, http3); - // Configure the max number of requests per QUIC connection. - connector.getQuicConfiguration().setMaxBidirectionalRemoteStreams(1024); + // Create and configure the QuicServerConnector. + QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, http3); // Add the Connector to the Server. server.addConnector(connector); diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java index c390bbd8a3e3..3794710e5520 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectorHttpClientTransport.java @@ -70,7 +70,8 @@ public void connect(SocketAddress address, Map context) @SuppressWarnings("unchecked") Promise promise = (Promise)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed)); - connector.connect(address, context); + context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, connector); + destination.getOrigin().getTransportProtocol().connect(address, context); } @Override diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index c327e73588e1..c7ede005975b 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -50,6 +50,7 @@ import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Jetty; @@ -470,7 +471,16 @@ public Origin createOrigin(Request request, Origin.Protocol protocol) host = host.toLowerCase(Locale.ENGLISH); int port = request.getPort(); port = normalizePort(scheme, port); - return new Origin(scheme, host, port, request.getTag(), protocol); + TransportProtocol transportProtocol = request.getTransportProtocol(); + if (transportProtocol == null) + { + // Ask the ClientConnector for backwards compatibility + // until ClientConnector.Configurator is removed. + transportProtocol = connector.newTransportProtocol(); + if (transportProtocol == null) + transportProtocol = TransportProtocol.TCP_IP; + } + return new Origin(scheme, new Origin.Address(host, port), request.getTag(), protocol, transportProtocol); } /** @@ -528,39 +538,54 @@ public void newConnection(Destination destination, Promise promise) List protocols = protocol != null ? protocol.getProtocols() : List.of("http/1.1"); context.put(ClientConnector.APPLICATION_PROTOCOLS_CONTEXT_KEY, protocols); + Origin origin = destination.getOrigin(); ProxyConfiguration.Proxy proxy = destination.getProxy(); - Origin.Address address = proxy == null ? destination.getOrigin().getAddress() : proxy.getAddress(); - getSocketAddressResolver().resolve(address.getHost(), address.getPort(), new Promise<>() + if (proxy != null) + origin = proxy.getOrigin(); + + TransportProtocol transportProtocol = origin.getTransportProtocol(); + context.put(TransportProtocol.class.getName(), transportProtocol); + + if (transportProtocol.requiresDomainNamesResolution()) { - @Override - public void succeeded(List socketAddresses) + Origin.Address address = origin.getAddress(); + getSocketAddressResolver().resolve(address.getHost(), address.getPort(), new Promise<>() { - connect(socketAddresses, 0, context); - } + @Override + public void succeeded(List socketAddresses) + { + connect(socketAddresses, 0, context); + } - @Override - public void failed(Throwable x) - { - promise.failed(x); - } + @Override + public void failed(Throwable x) + { + promise.failed(x); + } - private void connect(List socketAddresses, int index, Map context) - { - context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, new Promise.Wrapper<>(promise) + private void connect(List socketAddresses, int index, Map context) { - @Override - public void failed(Throwable x) + context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, new Promise.Wrapper<>(promise) { - int nextIndex = index + 1; - if (nextIndex == socketAddresses.size()) - super.failed(x); - else - connect(socketAddresses, nextIndex, context); - } - }); - transport.connect((SocketAddress)socketAddresses.get(index), context); - } - }); + @Override + public void failed(Throwable x) + { + int nextIndex = index + 1; + if (nextIndex == socketAddresses.size()) + super.failed(x); + else + connect(socketAddresses, nextIndex, context); + } + }); + transport.connect((SocketAddress)socketAddresses.get(index), context); + } + }); + } + else + { + context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise); + transport.connect(transportProtocol.getSocketAddress(), context); + } } private HttpConversation newConversation() diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java index 97f98b052985..4de7c7dc60c8 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java @@ -19,7 +19,6 @@ import java.net.URI; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.internal.TunnelRequest; @@ -32,6 +31,7 @@ import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.util.Attachable; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -49,27 +49,27 @@ public HttpProxy(String host, int port) public HttpProxy(Origin.Address address, boolean secure) { - this(address, secure, null, new Origin.Protocol(List.of("http/1.1"), false)); + this(address, secure, new Origin.Protocol(List.of("http/1.1"), false)); } public HttpProxy(Origin.Address address, boolean secure, Origin.Protocol protocol) { - this(address, secure, null, Objects.requireNonNull(protocol)); + this(new Origin(secure ? "https" : "http", address, null, protocol, TransportProtocol.TCP_IP), null); } public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory) { - this(address, true, sslContextFactory, new Origin.Protocol(List.of("http/1.1"), false)); + this(address, sslContextFactory, new Origin.Protocol(List.of("http/1.1"), false)); } public HttpProxy(Origin.Address address, SslContextFactory.Client sslContextFactory, Origin.Protocol protocol) { - this(address, true, sslContextFactory, Objects.requireNonNull(protocol)); + this(new Origin(sslContextFactory == null ? "http" : "https", address, null, protocol, TransportProtocol.TCP_IP), sslContextFactory); } - private HttpProxy(Origin.Address address, boolean secure, SslContextFactory.Client sslContextFactory, Origin.Protocol protocol) + public HttpProxy(Origin origin, SslContextFactory.Client sslContextFactory) { - super(address, secure, sslContextFactory, Objects.requireNonNull(protocol)); + super(origin, sslContextFactory); } @Override diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java index 2f838969d6c1..7d48dd5c6997 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java @@ -22,6 +22,7 @@ import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.URIUtil; @@ -46,6 +47,7 @@ public class Origin private final Address address; private final Object tag; private final Protocol protocol; + private final TransportProtocol transport; public Origin(String scheme, String host, int port) { @@ -73,11 +75,17 @@ public Origin(String scheme, Address address, Object tag) } public Origin(String scheme, Address address, Object tag, Protocol protocol) + { + this(scheme, address, tag, protocol, TransportProtocol.TCP_IP); + } + + public Origin(String scheme, Address address, Object tag, Protocol protocol, TransportProtocol transport) { this.scheme = Objects.requireNonNull(scheme); this.address = address; this.tag = tag; this.protocol = protocol; + this.transport = transport; } public String getScheme() @@ -100,6 +108,17 @@ public Protocol getProtocol() return protocol; } + public TransportProtocol getTransportProtocol() + { + return transport; + } + + @Override + public int hashCode() + { + return Objects.hash(scheme, address, tag, protocol, transport); + } + @Override public boolean equals(Object obj) { @@ -109,15 +128,10 @@ public boolean equals(Object obj) return false; Origin that = (Origin)obj; return scheme.equals(that.scheme) && - address.equals(that.address) && - Objects.equals(tag, that.tag) && - Objects.equals(protocol, that.protocol); - } - - @Override - public int hashCode() - { - return Objects.hash(scheme, address, tag, protocol); + address.equals(that.address) && + Objects.equals(tag, that.tag) && + Objects.equals(protocol, that.protocol) && + Objects.equals(transport, that.transport); } public String asString() @@ -130,12 +144,14 @@ public String asString() @Override public String toString() { - return String.format("%s@%x[%s,tag=%s,protocol=%s]", + return String.format("%s@%x[%s,tag=%s,protocol=%s,transport=%s]", getClass().getSimpleName(), hashCode(), asString(), getTag(), - getProtocol()); + getProtocol(), + getTransportProtocol() + ); } public static class Address @@ -217,7 +233,7 @@ public static class Protocol */ public Protocol(List protocols, boolean negotiate) { - this.protocols = protocols; + this.protocols = List.copyOf(protocols); this.negotiate = negotiate; } diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java index 10a8e75bd035..1bd11eaea2e9 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java @@ -33,6 +33,7 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.util.Fields; /** @@ -103,6 +104,23 @@ default Request port(int port) return this; } + /** + * @param transport the {@link TransportProtocol} of this request + * @return this request object + */ + default Request transportProtocol(TransportProtocol transport) + { + return this; + } + + /** + * @return the {@link TransportProtocol} of this request + */ + default TransportProtocol getTransportProtocol() + { + return null; + } + /** * @return the method of this request, such as GET or POST, as a String */ diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientConnectionFactory.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientConnectionFactory.java index 90c78946f128..e2a1ee43c850 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientConnectionFactory.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientConnectionFactory.java @@ -19,6 +19,7 @@ import org.eclipse.jetty.client.transport.internal.HttpConnectionOverHTTP; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; public class HttpClientConnectionFactory implements ClientConnectionFactory { @@ -49,6 +50,12 @@ public List getProtocols(boolean secure) return protocols; } + @Override + public TransportProtocol newTransportProtocol() + { + return TransportProtocol.TCP_IP; + } + @Override public String toString() { diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java index 77c6f22900d9..14f457d34c95 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java @@ -14,15 +14,12 @@ package org.eclipse.jetty.client.transport; import java.io.IOException; -import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.jetty.alpn.client.ALPNClientConnection; import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; @@ -39,6 +36,7 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,42 +79,42 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans { private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportDynamic.class); - private final List factoryInfos; - private final List protocols; + private final List infos; /** - * Creates a transport that speaks only HTTP/1.1. + * Creates a dynamic transport that speaks only HTTP/1.1. */ public HttpClientTransportDynamic() { - this(HttpClientConnectionFactory.HTTP11); + this(new ClientConnector(), HttpClientConnectionFactory.HTTP11); } - public HttpClientTransportDynamic(ClientConnectionFactory.Info... factoryInfos) + /** + *

Creates a dynamic transport that speaks the given protocols.

+ * + * @param infos the protocols this dynamic transport speaks + * @deprecated use {@link #HttpClientTransportDynamic(ClientConnector, ClientConnectionFactory.Info...)} + */ + @Deprecated(since = "12.0.7", forRemoval = true) + public HttpClientTransportDynamic(ClientConnectionFactory.Info... infos) { - this(findClientConnector(factoryInfos), factoryInfos); + this(findClientConnector(infos), infos); } /** - * Creates a transport with the given {@link ClientConnector} and the given application protocols. + * Creates a dynamic transport with the given {@link ClientConnector} and the given application protocols. * * @param connector the ClientConnector used by this transport - * @param factoryInfos the application protocols that this transport can speak + * @param infos the application protocols that this transport can speak */ - public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... factoryInfos) + public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... infos) { super(connector); - if (factoryInfos.length == 0) - factoryInfos = new Info[]{HttpClientConnectionFactory.HTTP11}; - this.factoryInfos = Arrays.asList(factoryInfos); - this.protocols = Arrays.stream(factoryInfos) - .flatMap(info -> Stream.concat(info.getProtocols(false).stream(), info.getProtocols(true).stream())) - .distinct() - .map(p -> p.toLowerCase(Locale.ENGLISH)) - .collect(Collectors.toList()); - Arrays.stream(factoryInfos).forEach(this::addBean); + this.infos = infos.length == 0 ? List.of(HttpClientConnectionFactory.HTTP11) : List.of(infos); + this.infos.forEach(this::addBean); setConnectionPoolFactory(destination -> - new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), 1)); + new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), 1) + ); } private static ClientConnector findClientConnector(ClientConnectionFactory.Info[] infos) @@ -130,72 +128,128 @@ private static ClientConnector findClientConnector(ClientConnectionFactory.Info[ @Override public Origin newOrigin(Request request) { - String scheme = request.getScheme(); - boolean secure = HttpScheme.isSecure(scheme); - String http1 = "http/1.1"; - String http2 = secure ? "h2" : "h2c"; - List protocols = List.of(); + boolean secure = HttpScheme.isSecure(request.getScheme()); + + List matchingInfos = new ArrayList<>(); + boolean negotiate = false; + if (((HttpRequest)request).isVersionExplicit()) { HttpVersion version = request.getVersion(); - String desired = version == HttpVersion.HTTP_2 ? http2 : http1; - if (this.protocols.contains(desired)) - protocols = List.of(desired); + List wanted = toProtocols(version); + for (Info info : infos) + { + // Find the first protocol that matches the version. + List protocols = info.getProtocols(secure); + for (String p : protocols) + { + if (wanted.contains(p)) + { + matchingInfos.add(info); + break; + } + } + if (matchingInfos.isEmpty()) + continue; + + if (secure && protocols.size() > 1) + negotiate = true; + + break; + } } else { + Info preferredInfo = infos.get(0); if (secure) { - // There may be protocol negotiation, so preserve the order - // of protocols chosen by the application. - // We need to keep multiple protocols in case the protocol - // is negotiated: e.g. [http/1.1, h2] negotiates [h2], but - // here we don't know yet what will be negotiated. - List http = List.of("http/1.1", "h2c", "h2"); - protocols = this.protocols.stream() - .filter(http::contains) - .collect(Collectors.toCollection(ArrayList::new)); + if (preferredInfo.getProtocols(true).contains("h3")) + { + // HTTP/3 is not compatible with HTTP/2 and HTTP/1 + // due to UDP vs TCP, so we can only try HTTP/3. + matchingInfos.add(preferredInfo); + } + else + { + // If the preferred protocol is not HTTP/3, then + // must be excluded since it won't be compatible + // with the other HTTP versions due to UDP vs TCP. + for (Info info : infos) + { + if (info.getProtocols(true).contains("h3")) + continue; + matchingInfos.add(info); + } - // The http/1.1 upgrade to http/2 over TLS implicitly - // "negotiates" [h2c], so we need to remove [h2] - // because we don't want to negotiate using ALPN. - if (request.getHeaders().contains(HttpHeader.UPGRADE, "h2c")) - protocols.remove("h2"); + // We can only have HTTP/1 and HTTP/2 here, + // decide whether negotiation is necessary. + if (!request.getHeaders().contains(HttpHeader.UPGRADE, "h2c")) + { + int matches = matchingInfos.size(); + if (matches > 1) + negotiate = true; + else if (matches == 1) + negotiate = matchingInfos.get(0).getProtocols(true).size() > 1; + } + } } else { - // Pick the first. - protocols = List.of(this.protocols.get(0)); + // Pick the first that allows non-secure. + for (Info info : infos) + { + if (info.getProtocols(false).contains("h3")) + continue; + matchingInfos.add(info); + break; + } } } - Origin.Protocol protocol = null; - if (!protocols.isEmpty()) - protocol = new Origin.Protocol(protocols, secure && protocols.contains(http2)); + + if (matchingInfos.isEmpty()) + return getHttpClient().createOrigin(request, null); + + TransportProtocol transportProtocol = request.getTransportProtocol(); + if (transportProtocol == null) + { + // Ask the ClientConnector for backwards compatibility + // until ClientConnector.Configurator is removed. + transportProtocol = getClientConnector().newTransportProtocol(); + if (transportProtocol == null) + transportProtocol = matchingInfos.get(0).newTransportProtocol(); + request.transportProtocol(transportProtocol); + } + + List protocols = matchingInfos.stream() + .flatMap(info -> info.getProtocols(secure).stream()) + .collect(Collectors.toCollection(ArrayList::new)); + if (negotiate) + protocols.remove("h2c"); + Origin.Protocol protocol = new Origin.Protocol(protocols, negotiate); return getHttpClient().createOrigin(request, protocol); } @Override public Destination newDestination(Origin origin) { - SocketAddress address = origin.getAddress().getSocketAddress(); - return new HttpDestination(getHttpClient(), origin, getClientConnector().isIntrinsicallySecure(address)); + return new HttpDestination(getHttpClient(), origin); } @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); - Origin.Protocol protocol = destination.getOrigin().getProtocol(); + Origin origin = destination.getOrigin(); + Origin.Protocol protocol = origin.getProtocol(); ClientConnectionFactory factory; if (protocol == null) { // Use the default ClientConnectionFactory. - factory = factoryInfos.get(0).getClientConnectionFactory(); + factory = infos.get(0).getClientConnectionFactory(); } else { - SocketAddress address = destination.getOrigin().getAddress().getSocketAddress(); - boolean intrinsicallySecure = getClientConnector().isIntrinsicallySecure(address); + boolean intrinsicallySecure = origin.getTransportProtocol().isIntrinsicallySecure(); if (!intrinsicallySecure && destination.isSecure() && protocol.isNegotiate()) { factory = new ALPNClientConnectionFactory(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); @@ -237,7 +291,7 @@ protected Connection newNegotiatedConnection(EndPoint endPoint, Map findClientConnectionFactoryInfo(List protocols, boolean secure) { - return factoryInfos.stream() - .filter(info -> info.matches(protocols, secure)) - .findFirst(); + return infos.stream() + .filter(info -> info.matches(protocols, secure)) + .findFirst(); + } + + private List toProtocols(HttpVersion version) + { + return switch (version) + { + case HTTP_0_9, HTTP_1_0, HTTP_1_1 -> List.of("http/1.1"); + case HTTP_2 -> List.of("h2c", "h2"); + case HTTP_3 -> List.of("h3"); + }; } } diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportOverHTTP.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportOverHTTP.java index 50153c5db8fe..bab51d75dcaf 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportOverHTTP.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportOverHTTP.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.client.transport; import java.io.IOException; -import java.net.SocketAddress; import java.util.List; import java.util.Map; @@ -68,8 +67,7 @@ public Origin newOrigin(Request request) @Override public Destination newDestination(Origin origin) { - SocketAddress address = origin.getAddress().getSocketAddress(); - return new HttpDestination(getHttpClient(), origin, getClientConnector().isIntrinsicallySecure(address)); + return new HttpDestination(getHttpClient(), origin); } @Override diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java index 4c61e00350b2..6fc8d6fbf2c3 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java @@ -69,7 +69,25 @@ public class HttpDestination extends ContainerLifeCycle implements Destination, private boolean stale; private long activeNanoTime; + /** + * @param client the {@link HttpClient} + * @param origin the {@link Origin} + * @param intrinsicallySecure whether the destination is intrinsically secure + * @deprecated use {@link #HttpDestination(HttpClient, Origin)} instead + */ + @Deprecated public HttpDestination(HttpClient client, Origin origin, boolean intrinsicallySecure) + { + this(client, origin); + } + + /** + *

Creates a new HTTP destination.

+ * + * @param client the {@link HttpClient} + * @param origin the {@link Origin} + */ + public HttpDestination(HttpClient client, Origin origin) { this.client = client; this.origin = origin; @@ -84,9 +102,11 @@ public HttpDestination(HttpClient client, Origin origin, boolean intrinsicallySe host += ":" + port; hostField = new HttpField(HttpHeader.HOST, host); + ClientConnectionFactory connectionFactory = client.getTransport(); + boolean intrinsicallySecure = origin.getTransportProtocol().isIntrinsicallySecure(); + ProxyConfiguration proxyConfig = client.getProxyConfiguration(); proxy = proxyConfig.match(origin); - ClientConnectionFactory connectionFactory = client.getTransport(); if (proxy != null) { connectionFactory = proxy.newClientConnectionFactory(connectionFactory); @@ -383,7 +403,7 @@ private boolean process(Connection connection) if (cause != null) { if (LOG.isDebugEnabled()) - LOG.debug("Aborted before processing {}: {}", exchange, cause); + LOG.debug("Aborted before processing {}", exchange, cause); // Won't use this connection, release it back. boolean released = connectionPool.release(connection); if (!released) diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java index 86cb57949f81..27bff58e7833 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java @@ -56,6 +56,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.Promise; @@ -78,6 +79,7 @@ public class HttpRequest implements Request private String path; private String query; private URI uri; + private TransportProtocol transport; private String method = HttpMethod.GET.asString(); private HttpVersion version = HttpVersion.HTTP_1_1; private boolean versionExplicit; @@ -217,6 +219,19 @@ public Request port(int port) return this; } + @Override + public Request transportProtocol(TransportProtocol transport) + { + this.transport = transport; + return this; + } + + @Override + public TransportProtocol getTransportProtocol() + { + return transport; + } + @Override public String getMethod() { diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index 239a5620d7e5..f4ae856e9d7e 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -675,7 +675,7 @@ public void testCountersSweepToStringThroughLifecycle(ConnectionPoolFactory fact return connectionPool; }); - AbstractConnectionPool connectionPool = (AbstractConnectionPool)factory.factory.newConnectionPool(new HttpDestination(client, new Origin("", "", 0), false)); + AbstractConnectionPool connectionPool = (AbstractConnectionPool)factory.factory.newConnectionPool(new HttpDestination(client, new Origin("", "", 0))); assertThat(connectionPool.getConnectionCount(), is(0)); assertThat(connectionPool.getActiveConnectionCount(), is(0)); assertThat(connectionPool.getIdleConnectionCount(), is(0)); diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/DuplexHttpDestinationTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/DuplexHttpDestinationTest.java index f3f8daf60034..02d0866a690d 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/DuplexHttpDestinationTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/DuplexHttpDestinationTest.java @@ -46,7 +46,7 @@ public void testAcquireWithEmptyQueue(Scenario scenario) throws Exception { start(scenario, new EmptyServerHandler()); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())); try { destination.start(); @@ -71,7 +71,7 @@ public void testAcquireWithOneExchangeQueued(Scenario scenario) throws Exception { start(scenario, new EmptyServerHandler()); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())); try { destination.start(); @@ -100,7 +100,7 @@ public void testSecondAcquireAfterFirstAcquireWithEmptyQueueReturnsSameConnectio { start(scenario, new EmptyServerHandler()); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())); try { destination.start(); @@ -134,7 +134,7 @@ public void testSecondAcquireConcurrentWithFirstAcquireWithEmptyQueueCreatesTwoC CountDownLatch idleLatch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()), false) + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())) { @Override protected ConnectionPool newConnectionPool(HttpClient client) @@ -201,7 +201,7 @@ public void testAcquireProcessReleaseAcquireReturnsSameConnection(Scenario scena { start(scenario, new EmptyServerHandler()); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())); try { destination.start(); @@ -243,7 +243,7 @@ public void testIdleConnectionIdleTimeout(Scenario scenario) throws Exception long idleTimeout = 1000; startClient(scenario, httpClient -> httpClient.setIdleTimeout(idleTimeout)); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())); try { destination.start(); diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java index 73b217a17ce9..447e47091fdc 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAsyncContentTest.java @@ -392,7 +392,7 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon Thread.sleep(500); demandRef.get().accept(1); - assertTrue(resultLatch.await(555, TimeUnit.SECONDS)); + assertTrue(resultLatch.await(5, TimeUnit.SECONDS)); } */ } diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientIdleTimeoutTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientIdleTimeoutTest.java index 67f6fd3be96c..7336e0a514c4 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientIdleTimeoutTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientIdleTimeoutTest.java @@ -98,7 +98,7 @@ protected void service(Request request, Response response) @Override public Destination newDestination(Origin origin) { - return new HttpDestination(getHttpClient(), origin, false) + return new HttpDestination(getHttpClient(), origin) { @Override protected SendFailure send(IConnection connection, HttpExchange exchange) diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java index 4b74ab3f784d..ffa73f332c75 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java @@ -79,7 +79,7 @@ public void init(HttpCompliance compliance) throws Exception client = new HttpClient(); client.setHttpCompliance(compliance); client.start(); - destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); endPoint = new ByteArrayEndPoint(); connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<>()); diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java index 6a723811c4ff..313fd841de41 100644 --- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java +++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java @@ -61,7 +61,7 @@ public void destroy() throws Exception public void testSendNoRequestContent() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -94,7 +94,7 @@ public void onSuccess(Request request) public void testSendNoRequestContentIncompleteFlush() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -124,7 +124,7 @@ public void testSendNoRequestContentException() throws Exception ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); // Shutdown output to trigger the exception on write endPoint.shutdownOutput(); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -154,7 +154,7 @@ public void onComplete(Result result) public void testSendNoRequestContentIncompleteFlushException() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -190,7 +190,7 @@ public void onComplete(Result result) public void testSendSmallRequestContentInOneBuffer() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -225,7 +225,7 @@ public void onSuccess(Request request) public void testSendSmallRequestContentInTwoBuffers() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -261,7 +261,7 @@ public void onSuccess(Request request) public void testSendSmallRequestContentChunkedInTwoChunks() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080), false); + HttpDestination destination = new HttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/HttpClientTransportOverFCGI.java b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/HttpClientTransportOverFCGI.java index ccac893f5d60..422f736260db 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/HttpClientTransportOverFCGI.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/transport/HttpClientTransportOverFCGI.java @@ -13,7 +13,6 @@ package org.eclipse.jetty.fcgi.client.transport; -import java.net.SocketAddress; import java.util.List; import java.util.Map; @@ -82,8 +81,7 @@ public Origin newOrigin(Request request) @Override public Destination newDestination(Origin origin) { - SocketAddress address = origin.getAddress().getSocketAddress(); - return new HttpDestination(getHttpClient(), origin, getClientConnector().isIntrinsicallySecure(address)); + return new HttpDestination(getHttpClient(), origin); } @Override diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java b/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java index e893b54ab144..2fb046668a6c 100644 --- a/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java +++ b/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java @@ -33,6 +33,7 @@ import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.proxy.ProxyHandler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; @@ -267,12 +268,7 @@ protected void doStart() throws Exception @Override protected HttpClient newHttpClient() { - ClientConnector clientConnector; - Path unixDomainPath = getUnixDomainPath(); - if (unixDomainPath != null) - clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - else - clientConnector = new ClientConnector(); + ClientConnector clientConnector = new ClientConnector(); QueuedThreadPool proxyClientThreads = new QueuedThreadPool(); proxyClientThreads.setName("proxy-client"); clientConnector.setExecutor(proxyClientThreads); @@ -333,6 +329,10 @@ protected void sendProxyToServerRequest(Request clientToProxyRequest, org.eclips proxyToServerRequest.headers(headers -> headers.put(HttpHeader.COOKIE, allCookies)); } + Path unixDomain = getUnixDomainPath(); + if (unixDomain != null) + proxyToServerRequest.transportProtocol(new TransportProtocol.TCPUnix(unixDomain)); + super.sendProxyToServerRequest(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback); } diff --git a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java index ca6cb64b10b9..3bfe11785abb 100644 --- a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/ClientConnectionFactoryOverHTTP2.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.http2.client.transport.internal.HttpConnectionOverHTTP2; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.util.Promise; @@ -37,19 +38,19 @@ public class ClientConnectionFactoryOverHTTP2 extends ContainerLifeCycle implements ClientConnectionFactory { private final ClientConnectionFactory factory = new HTTP2ClientConnectionFactory(); - private final HTTP2Client client; + private final HTTP2Client http2Client; - public ClientConnectionFactoryOverHTTP2(HTTP2Client client) + public ClientConnectionFactoryOverHTTP2(HTTP2Client http2Client) { - this.client = client; - addBean(client); + this.http2Client = http2Client; + addBean(http2Client); } @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { HTTPSessionListenerPromise listenerPromise = new HTTPSessionListenerPromise(context); - context.put(HTTP2ClientConnectionFactory.CLIENT_CONTEXT_KEY, client); + context.put(HTTP2ClientConnectionFactory.CLIENT_CONTEXT_KEY, http2Client); context.put(HTTP2ClientConnectionFactory.SESSION_LISTENER_CONTEXT_KEY, listenerPromise); context.put(HTTP2ClientConnectionFactory.SESSION_PROMISE_CONTEXT_KEY, listenerPromise); return factory.newConnection(endPoint, context); @@ -65,9 +66,9 @@ public static class HTTP2 extends Info private static final List protocols = List.of("h2", "h2c"); private static final List h2c = List.of("h2c"); - public HTTP2(HTTP2Client client) + public HTTP2(HTTP2Client http2Client) { - super(new ClientConnectionFactoryOverHTTP2(client)); + super(new ClientConnectionFactoryOverHTTP2(http2Client)); } @Override @@ -76,6 +77,12 @@ public List getProtocols(boolean secure) return secure ? protocols : h2c; } + @Override + public TransportProtocol newTransportProtocol() + { + return TransportProtocol.TCP_IP; + } + @Override public void upgrade(EndPoint endPoint, Map context) { diff --git a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java index f2394adb02f4..0b0e3fc27c67 100644 --- a/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java +++ b/jetty-core/jetty-http2/jetty-http2-client-transport/src/main/java/org/eclipse/jetty/http2/client/transport/HttpClientTransportOverHTTP2.java @@ -46,13 +46,13 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport { private final ClientConnectionFactory connectionFactory = new HTTP2ClientConnectionFactory(); - private final HTTP2Client client; + private final HTTP2Client http2Client; private boolean useALPN = true; - public HttpClientTransportOverHTTP2(HTTP2Client client) + public HttpClientTransportOverHTTP2(HTTP2Client http2Client) { - this.client = client; - addBean(client.getClientConnector(), false); + this.http2Client = http2Client; + addBean(http2Client); setConnectionPoolFactory(destination -> { HttpClient httpClient = getHttpClient(); @@ -62,13 +62,13 @@ public HttpClientTransportOverHTTP2(HTTP2Client client) public HTTP2Client getHTTP2Client() { - return client; + return http2Client; } @ManagedAttribute(value = "The number of selectors", readonly = true) public int getSelectors() { - return client.getSelectors(); + return http2Client.getSelectors(); } @ManagedAttribute(value = "Whether ALPN should be used when establishing connections") @@ -85,31 +85,23 @@ public void setUseALPN(boolean useALPN) @Override protected void doStart() throws Exception { - if (!client.isStarted()) + if (!http2Client.isStarted()) { HttpClient httpClient = getHttpClient(); - client.setExecutor(httpClient.getExecutor()); - client.setScheduler(httpClient.getScheduler()); - client.setByteBufferPool(httpClient.getByteBufferPool()); - client.setConnectTimeout(httpClient.getConnectTimeout()); - client.setIdleTimeout(httpClient.getIdleTimeout()); - client.setInputBufferSize(httpClient.getResponseBufferSize()); - client.setUseInputDirectByteBuffers(httpClient.isUseInputDirectByteBuffers()); - client.setUseOutputDirectByteBuffers(httpClient.isUseOutputDirectByteBuffers()); - client.setConnectBlocking(httpClient.isConnectBlocking()); - client.setBindAddress(httpClient.getBindAddress()); + http2Client.setExecutor(httpClient.getExecutor()); + http2Client.setScheduler(httpClient.getScheduler()); + http2Client.setByteBufferPool(httpClient.getByteBufferPool()); + http2Client.setConnectTimeout(httpClient.getConnectTimeout()); + http2Client.setIdleTimeout(httpClient.getIdleTimeout()); + http2Client.setInputBufferSize(httpClient.getResponseBufferSize()); + http2Client.setUseInputDirectByteBuffers(httpClient.isUseInputDirectByteBuffers()); + http2Client.setUseOutputDirectByteBuffers(httpClient.isUseOutputDirectByteBuffers()); + http2Client.setConnectBlocking(httpClient.isConnectBlocking()); + http2Client.setBindAddress(httpClient.getBindAddress()); } - addBean(client); super.doStart(); } - @Override - protected void doStop() throws Exception - { - super.doStop(); - removeBean(client); - } - @Override public Origin newOrigin(Request request) { @@ -120,20 +112,13 @@ public Origin newOrigin(Request request) @Override public Destination newDestination(Origin origin) { - SocketAddress address = origin.getAddress().getSocketAddress(); - return new HttpDestination(getHttpClient(), origin, getHTTP2Client().getClientConnector().isIntrinsicallySecure(address)); + return new HttpDestination(getHttpClient(), origin); } @Override public void connect(SocketAddress address, Map context) { - HttpClient httpClient = getHttpClient(); - client.setConnectTimeout(httpClient.getConnectTimeout()); - client.setConnectBlocking(httpClient.isConnectBlocking()); - client.setBindAddress(httpClient.getBindAddress()); - SessionListenerPromise listenerPromise = new SessionListenerPromise(context); - HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); connect(address, destination.getClientConnectionFactory(), listenerPromise, listenerPromise, context); } @@ -146,7 +131,8 @@ public void connect(InetSocketAddress address, Map context) protected void connect(SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) { - getHTTP2Client().connect(address, factory, listener, promise, context); + HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); + getHTTP2Client().connect(destination.getOrigin().getTransportProtocol(), address, factory, listener, promise, context); } protected void connect(InetSocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) @@ -164,7 +150,7 @@ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map connect(SocketAddress address, Session.Listener listener) { - return connect(null, address, listener); + return Promise.Completable.with(p -> connect(address, listener, p)); } public void connect(SocketAddress address, Session.Listener listener, Promise promise) @@ -423,16 +424,32 @@ public void connect(SslContextFactory.Client sslContextFactory, SocketAddress ad } public void connect(SslContextFactory.Client sslContextFactory, SocketAddress address, Session.Listener listener, Promise promise, Map context) + { + connect(TransportProtocol.TCP_IP, sslContextFactory, address, listener, promise, context); + } + + public CompletableFuture connect(TransportProtocol transportProtocol, SslContextFactory.Client sslContextFactory, SocketAddress address, Session.Listener listener) + { + return Promise.Completable.with(p -> connect(transportProtocol, sslContextFactory, address, listener, p, null)); + } + + public void connect(TransportProtocol transportProtocol, SslContextFactory.Client sslContextFactory, SocketAddress address, Session.Listener listener, Promise promise, Map context) { ClientConnectionFactory factory = newClientConnectionFactory(sslContextFactory); - connect(address, factory, listener, promise, context); + connect(transportProtocol, address, factory, listener, promise, context); } public void connect(SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) + { + connect(TransportProtocol.TCP_IP, address, factory, listener, promise, context); + } + + public void connect(TransportProtocol transportProtocol, SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) { context = contextFrom(factory, listener, promise, context); + context.put(TransportProtocol.class.getName(), transportProtocol); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed)); - connector.connect(address, context); + transportProtocol.connect(address, context); } public void accept(SslContextFactory.Client sslContextFactory, SocketChannel channel, Session.Listener listener, Promise promise) @@ -442,8 +459,14 @@ public void accept(SslContextFactory.Client sslContextFactory, SocketChannel cha } public void accept(SocketChannel channel, ClientConnectionFactory factory, Session.Listener listener, Promise promise) + { + accept(TransportProtocol.TCP_IP, channel, factory, listener, promise); + } + + public void accept(TransportProtocol transportProtocol, SocketChannel channel, ClientConnectionFactory factory, Session.Listener listener, Promise promise) { Map context = contextFrom(factory, listener, promise, null); + context.put(TransportProtocol.class.getName(), transportProtocol); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed)); connector.accept(channel, context); } @@ -452,6 +475,7 @@ private Map contextFrom(ClientConnectionFactory factory, Session { if (context == null) context = new ConcurrentHashMap<>(); + context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, connector); context.put(HTTP2ClientConnectionFactory.CLIENT_CONTEXT_KEY, this); context.put(HTTP2ClientConnectionFactory.SESSION_LISTENER_CONTEXT_KEY, listener); context.put(HTTP2ClientConnectionFactory.SESSION_PROMISE_CONTEXT_KEY, promise); diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java index e0ee0f48b89b..1a7bab78f2df 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java @@ -44,6 +44,11 @@ public class HTTP2CServerConnectionFactory extends HTTP2ServerConnectionFactory { private static final Logger LOG = LoggerFactory.getLogger(HTTP2CServerConnectionFactory.class); + public HTTP2CServerConnectionFactory() + { + this(new HttpConfiguration()); + } + public HTTP2CServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration) { this(httpConfiguration, "h2c"); diff --git a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java index 0bb7517c9ec7..ba2b27b520b4 100644 --- a/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java +++ b/jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java @@ -44,6 +44,11 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF { private static final Logger LOG = LoggerFactory.getLogger(HTTP2ServerConnectionFactory.class); + public HTTP2ServerConnectionFactory() + { + this(new HttpConfiguration()); + } + public HTTP2ServerConnectionFactory(@Name("config") HttpConfiguration httpConfiguration) { super(httpConfiguration); diff --git a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2ServerTest.java b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2ServerTest.java index e191ac642a41..ec30896c9adf 100644 --- a/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2ServerTest.java +++ b/jetty-core/jetty-http2/jetty-http2-tests/src/test/java/org/eclipse/jetty/http2/tests/HTTP2ServerTest.java @@ -108,7 +108,7 @@ public void onGoAway(GoAwayFrame frame) parseResponse(client, parser); - assertTrue(latch.await(555, TimeUnit.SECONDS)); + assertTrue(latch.await(5, TimeUnit.SECONDS)); } } diff --git a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java index bfcdfc3efc11..a71c5960079e 100644 --- a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java +++ b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/ClientConnectionFactoryOverHTTP3.java @@ -23,6 +23,8 @@ import org.eclipse.jetty.http3.client.transport.internal.SessionClientListener; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; import org.eclipse.jetty.quic.common.ProtocolSession; import org.eclipse.jetty.quic.common.QuicSession; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -30,12 +32,10 @@ public class ClientConnectionFactoryOverHTTP3 extends ContainerLifeCycle implements ClientConnectionFactory { private final HTTP3ClientConnectionFactory factory = new HTTP3ClientConnectionFactory(); - private final HTTP3Client client; - public ClientConnectionFactoryOverHTTP3(HTTP3Client client) + public ClientConnectionFactoryOverHTTP3(HTTP3Client http3Client) { - this.client = client; - addBean(client); + addBean(http3Client); } @Override @@ -51,22 +51,38 @@ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map protocols = List.of("h3"); + + private final HTTP3Client http3Client; + public HTTP3(HTTP3Client client) { super(new ClientConnectionFactoryOverHTTP3(client)); + http3Client = client; + } + + public HTTP3Client getHTTP3Client() + { + return http3Client; } @Override public List getProtocols(boolean secure) { - return List.of("h3"); + return protocols; + } + + @Override + public TransportProtocol newTransportProtocol() + { + return new QuicTransportProtocol(getHTTP3Client().getQuicConfiguration()); } @Override public ProtocolSession newProtocolSession(QuicSession quicSession, Map context) { ClientConnectionFactoryOverHTTP3 http3 = (ClientConnectionFactoryOverHTTP3)getClientConnectionFactory(); - context.put(HTTP3Client.CLIENT_CONTEXT_KEY, http3.client); + context.put(HTTP3Client.CLIENT_CONTEXT_KEY, http3Client); SessionClientListener listener = new SessionClientListener(context); context.put(HTTP3Client.SESSION_LISTENER_CONTEXT_KEY, listener); return http3.factory.newProtocolSession(quicSession, context); diff --git a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java index 0423204ff7ec..1b3390ee144a 100644 --- a/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java +++ b/jetty-core/jetty-http3/jetty-http3-client-transport/src/main/java/org/eclipse/jetty/http3/client/transport/HttpClientTransportOverHTTP3.java @@ -36,18 +36,20 @@ import org.eclipse.jetty.http3.client.transport.internal.SessionClientListener; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; import org.eclipse.jetty.quic.common.ProtocolSession; import org.eclipse.jetty.quic.common.QuicSession; public class HttpClientTransportOverHTTP3 extends AbstractHttpClientTransport implements ProtocolSession.Factory { private final HTTP3ClientConnectionFactory factory = new HTTP3ClientConnectionFactory(); - private final HTTP3Client client; + private final HTTP3Client http3Client; - public HttpClientTransportOverHTTP3(HTTP3Client client) + public HttpClientTransportOverHTTP3(HTTP3Client http3Client) { - this.client = Objects.requireNonNull(client); - addBean(client); + this.http3Client = Objects.requireNonNull(http3Client); + addBean(http3Client); setConnectionPoolFactory(destination -> { HttpClient httpClient = getHttpClient(); @@ -57,16 +59,16 @@ public HttpClientTransportOverHTTP3(HTTP3Client client) public HTTP3Client getHTTP3Client() { - return client; + return http3Client; } @Override protected void doStart() throws Exception { - if (!client.isStarted()) + if (!http3Client.isStarted()) { HttpClient httpClient = getHttpClient(); - ClientConnector clientConnector = this.client.getClientConnector(); + ClientConnector clientConnector = this.http3Client.getClientConnector(); clientConnector.setExecutor(httpClient.getExecutor()); clientConnector.setScheduler(httpClient.getScheduler()); clientConnector.setByteBufferPool(httpClient.getByteBufferPool()); @@ -74,7 +76,7 @@ protected void doStart() throws Exception clientConnector.setConnectBlocking(httpClient.isConnectBlocking()); clientConnector.setBindAddress(httpClient.getBindAddress()); clientConnector.setIdleTimeout(Duration.ofMillis(httpClient.getIdleTimeout())); - HTTP3Configuration configuration = client.getHTTP3Configuration(); + HTTP3Configuration configuration = http3Client.getHTTP3Configuration(); configuration.setInputBufferSize(httpClient.getResponseBufferSize()); configuration.setUseInputDirectByteBuffers(httpClient.isUseInputDirectByteBuffers()); configuration.setUseOutputDirectByteBuffers(httpClient.isUseOutputDirectByteBuffers()); @@ -85,14 +87,16 @@ protected void doStart() throws Exception @Override public Origin newOrigin(Request request) { + TransportProtocol transportProtocol = request.getTransportProtocol(); + if (transportProtocol == null) + request.transportProtocol(new QuicTransportProtocol(http3Client.getQuicConfiguration())); return getHttpClient().createOrigin(request, new Origin.Protocol(List.of("h3"), false)); } @Override public Destination newDestination(Origin origin) { - SocketAddress address = origin.getAddress().getSocketAddress(); - return new HttpDestination(getHttpClient(), origin, getHTTP3Client().getClientConnector().isIntrinsicallySecure(address)); + return new HttpDestination(getHttpClient(), origin); } @Override @@ -104,17 +108,11 @@ public void connect(InetSocketAddress address, Map context) @Override public void connect(SocketAddress address, Map context) { - HttpClient httpClient = getHttpClient(); - ClientConnector clientConnector = client.getClientConnector(); - clientConnector.setConnectTimeout(Duration.ofMillis(httpClient.getConnectTimeout())); - clientConnector.setConnectBlocking(httpClient.isConnectBlocking()); - clientConnector.setBindAddress(httpClient.getBindAddress()); - HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, destination.getClientConnectionFactory()); SessionClientListener listener = new TransportSessionClientListener(context); - getHTTP3Client().connect(address, listener, context) + getHTTP3Client().connect(destination.getOrigin().getTransportProtocol(), address, listener, context) .whenComplete(listener::onConnect); } diff --git a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java index e0debf261133..76ed05eb81d1 100644 --- a/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java +++ b/jetty-core/jetty-http3/jetty-http3-client/src/main/java/org/eclipse/jetty/http3/client/HTTP3Client.java @@ -22,13 +22,12 @@ import org.eclipse.jetty.http3.HTTP3Configuration; import org.eclipse.jetty.http3.api.Session; import org.eclipse.jetty.io.ClientConnector; -import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.DatagramChannelEndPoint; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.quic.client.ClientQuicConnection; import org.eclipse.jetty.quic.client.ClientQuicSession; -import org.eclipse.jetty.quic.client.QuicClientConnectorConfigurator; -import org.eclipse.jetty.quic.common.QuicConfiguration; -import org.eclipse.jetty.quic.common.QuicConnection; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; import org.eclipse.jetty.quic.common.QuicSessionContainer; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -134,20 +133,22 @@ public class HTTP3Client extends ContainerLifeCycle public static final String SESSION_PROMISE_CONTEXT_KEY = CLIENT_CONTEXT_KEY + ".promise"; private static final Logger LOG = LoggerFactory.getLogger(HTTP3Client.class); - private final HTTP3Configuration http3Configuration = new HTTP3Configuration(); private final QuicSessionContainer container = new QuicSessionContainer(); + private final HTTP3Configuration http3Configuration = new HTTP3Configuration(); + private final ClientQuicConfiguration quicConfiguration; private final ClientConnector connector; - private final QuicConfiguration quicConfiguration; - public HTTP3Client() + public HTTP3Client(ClientQuicConfiguration quicConfiguration) + { + this(quicConfiguration, new ClientConnector()); + } + + public HTTP3Client(ClientQuicConfiguration quicConfiguration, ClientConnector connector) { - QuicClientConnectorConfigurator configurator = new QuicClientConnectorConfigurator(this::configureConnection); - this.connector = new ClientConnector(configurator); - this.quicConfiguration = configurator.getQuicConfiguration(); + this.quicConfiguration = quicConfiguration; + this.connector = connector; addBean(connector); - addBean(quicConfiguration); - addBean(http3Configuration); - addBean(container); + connector.setSslContextFactory(quicConfiguration.getSslContextFactory()); // Allow the mandatory unidirectional streams, plus pushed streams. quicConfiguration.setMaxUnidirectionalRemoteStreams(48); quicConfiguration.setUnidirectionalStreamRecvWindow(4 * 1024 * 1024); @@ -159,7 +160,7 @@ public ClientConnector getClientConnector() return connector; } - public QuicConfiguration getQuicConfiguration() + public ClientQuicConfiguration getQuicConfiguration() { return quicConfiguration; } @@ -173,45 +174,46 @@ public HTTP3Configuration getHTTP3Configuration() protected void doStart() throws Exception { LOG.info("HTTP/3+QUIC support is experimental and not suited for production use."); + addBean(quicConfiguration); + addBean(container); + addBean(http3Configuration); + quicConfiguration.addEventListener(container); super.doStart(); } - public CompletableFuture connect(SocketAddress address, Session.Client.Listener listener) + public CompletableFuture connect(SocketAddress socketAddress, Session.Client.Listener listener) { Map context = new ConcurrentHashMap<>(); - return connect(address, listener, context); + return connect(socketAddress, listener, context); + } + + public CompletableFuture connect(SocketAddress socketAddress, Session.Client.Listener listener, Map context) + { + if (context == null) + context = new ConcurrentHashMap<>(); + return connect(new QuicTransportProtocol(getQuicConfiguration()), socketAddress, listener, context); } - public CompletableFuture connect(SocketAddress address, Session.Client.Listener listener, Map context) + public CompletableFuture connect(TransportProtocol transportProtocol, SocketAddress socketAddress, Session.Client.Listener listener, Map context) { + if (context == null) + context = new ConcurrentHashMap<>(); Promise.Completable completable = new Promise.Completable<>(); context.put(CLIENT_CONTEXT_KEY, this); context.put(SESSION_LISTENER_CONTEXT_KEY, listener); context.put(SESSION_PROMISE_CONTEXT_KEY, completable); + context.putIfAbsent(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, connector); context.computeIfAbsent(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, key -> new HTTP3ClientConnectionFactory()); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, completable::failed)); + context.put(TransportProtocol.class.getName(), transportProtocol); if (LOG.isDebugEnabled()) - LOG.debug("connecting to {}", address); + LOG.debug("connecting to {}", socketAddress); - connector.connect(address, context); + transportProtocol.connect(socketAddress, context); return completable; } - private Connection configureConnection(Connection connection) - { - if (connection instanceof QuicConnection) - { - QuicConnection quicConnection = (QuicConnection)connection; - quicConnection.addEventListener(container); - quicConnection.setInputBufferSize(getHTTP3Configuration().getInputBufferSize()); - quicConnection.setOutputBufferSize(getHTTP3Configuration().getOutputBufferSize()); - quicConnection.setUseInputDirectByteBuffers(getHTTP3Configuration().isUseInputDirectByteBuffers()); - quicConnection.setUseOutputDirectByteBuffers(getHTTP3Configuration().isUseOutputDirectByteBuffers()); - } - return connection; - } - public CompletableFuture shutdown() { return container.shutdown(); diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/AbstractHTTP3ServerConnectionFactory.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/AbstractHTTP3ServerConnectionFactory.java index 7e1882a03bce..ff27435a561b 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/AbstractHTTP3ServerConnectionFactory.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/AbstractHTTP3ServerConnectionFactory.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.quic.common.ProtocolSession; import org.eclipse.jetty.quic.common.QuicSession; import org.eclipse.jetty.quic.common.QuicStreamEndPoint; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.quic.server.ServerQuicSession; import org.eclipse.jetty.server.AbstractConnectionFactory; import org.eclipse.jetty.server.Connector; @@ -33,21 +34,31 @@ public abstract class AbstractHTTP3ServerConnectionFactory extends AbstractConnectionFactory implements ProtocolSession.Factory { - private final HTTP3Configuration configuration = new HTTP3Configuration(); + private final HTTP3Configuration http3Configuration = new HTTP3Configuration(); + private final ServerQuicConfiguration quicConfiguration; private final HttpConfiguration httpConfiguration; private final Session.Server.Listener listener; - public AbstractHTTP3ServerConnectionFactory(HttpConfiguration httpConfiguration, Session.Server.Listener listener) + public AbstractHTTP3ServerConnectionFactory(ServerQuicConfiguration quicConfiguration, HttpConfiguration httpConfiguration, Session.Server.Listener listener) { super("h3"); - addBean(configuration); + this.quicConfiguration = Objects.requireNonNull(quicConfiguration); this.httpConfiguration = Objects.requireNonNull(httpConfiguration); - addBean(httpConfiguration); this.listener = listener; - configuration.setUseInputDirectByteBuffers(httpConfiguration.isUseInputDirectByteBuffers()); - configuration.setUseOutputDirectByteBuffers(httpConfiguration.isUseOutputDirectByteBuffers()); - configuration.setMaxRequestHeadersSize(httpConfiguration.getRequestHeaderSize()); - configuration.setMaxResponseHeadersSize(httpConfiguration.getResponseHeaderSize()); + // Max concurrent streams that a client can open. + quicConfiguration.setMaxBidirectionalRemoteStreams(128); + // HTTP/3 requires a few mandatory unidirectional streams. + quicConfiguration.setMaxUnidirectionalRemoteStreams(8); + quicConfiguration.setUnidirectionalStreamRecvWindow(1024 * 1024); + http3Configuration.setUseInputDirectByteBuffers(httpConfiguration.isUseInputDirectByteBuffers()); + http3Configuration.setUseOutputDirectByteBuffers(httpConfiguration.isUseOutputDirectByteBuffers()); + http3Configuration.setMaxRequestHeadersSize(httpConfiguration.getRequestHeaderSize()); + http3Configuration.setMaxResponseHeadersSize(httpConfiguration.getResponseHeaderSize()); + } + + public ServerQuicConfiguration getQuicConfiguration() + { + return quicConfiguration; } public HttpConfiguration getHttpConfiguration() @@ -57,7 +68,16 @@ public HttpConfiguration getHttpConfiguration() public HTTP3Configuration getHTTP3Configuration() { - return configuration; + return http3Configuration; + } + + @Override + protected void doStart() throws Exception + { + addBean(quicConfiguration); + addBean(http3Configuration); + addBean(httpConfiguration); + super.doStart(); } @Override diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnectionFactory.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnectionFactory.java index 42256031e64f..fde8bb5dbfbe 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnectionFactory.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnectionFactory.java @@ -16,7 +16,8 @@ import java.util.Objects; import java.util.concurrent.TimeoutException; -import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http3.HTTP3Stream; import org.eclipse.jetty.http3.api.Session; @@ -27,34 +28,40 @@ import org.eclipse.jetty.http3.server.internal.ServerHTTP3Session; import org.eclipse.jetty.http3.server.internal.ServerHTTP3StreamConnection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.ConnectionMetaData; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.Promise; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HTTP3ServerConnectionFactory extends AbstractHTTP3ServerConnectionFactory { - public HTTP3ServerConnectionFactory() + public HTTP3ServerConnectionFactory(ServerQuicConfiguration quicConfiguration) { - this(new HttpConfiguration()); + this(quicConfiguration, new HttpConfiguration()); } - public HTTP3ServerConnectionFactory(HttpConfiguration configuration) + public HTTP3ServerConnectionFactory(ServerQuicConfiguration quicConfiguration, HttpConfiguration configuration) { - super(configuration, new HTTP3SessionListener()); - configuration.addCustomizer((request, responseHeaders) -> + super(quicConfiguration, configuration, new HTTP3SessionListener()); + configuration.addCustomizer(new AltSvcCustomizer()); + } + + private static class AltSvcCustomizer implements HttpConfiguration.Customizer + { + @Override + public Request customize(Request request, HttpFields.Mutable responseHeaders) { ConnectionMetaData connectionMetaData = request.getConnectionMetaData(); - HTTP3ServerConnector http3Connector = connectionMetaData.getConnector().getServer().getBean(HTTP3ServerConnector.class); - if (http3Connector != null && HttpVersion.HTTP_2 == connectionMetaData.getHttpVersion()) - { - HttpField altSvc = http3Connector.getAltSvcHttpField(); - if (altSvc != null) - responseHeaders.add(altSvc); - } + Connector connector = connectionMetaData.getConnector(); + if (connector instanceof NetworkConnector networkConnector && HttpVersion.HTTP_2 == connectionMetaData.getHttpVersion()) + responseHeaders.add(HttpHeader.ALT_SVC, String.format("h3=\":%d\"", networkConnector.getLocalPort())); return request; - }); + } } private static class HTTP3SessionListener implements Session.Server.Listener diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnector.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnector.java index 0b50ef95a841..96076ceae5db 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnector.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/HTTP3ServerConnector.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.http3.server; +import java.util.Arrays; import java.util.concurrent.Executor; import org.eclipse.jetty.http.HttpField; @@ -20,6 +21,7 @@ import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -32,7 +34,10 @@ * *

HTTP/3+QUIC support is experimental and not suited for production use. * APIs may change incompatibly between releases.

+ * + * @deprecated use {@link QuicServerConnector} instead */ +@Deprecated(since = "12.0.7", forRemoval = true) public class HTTP3ServerConnector extends QuicServerConnector { private static final Logger LOG = LoggerFactory.getLogger(HTTP3ServerConnector.class); @@ -46,12 +51,17 @@ public HTTP3ServerConnector(Server server, SslContextFactory.Server sslContextFa public HTTP3ServerConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool bufferPool, SslContextFactory.Server sslContextFactory, ConnectionFactory... factories) { - super(server, executor, scheduler, bufferPool, sslContextFactory, factories); - // Max concurrent streams that a client can open. - getQuicConfiguration().setMaxBidirectionalRemoteStreams(128); - // HTTP/3 requires a few mandatory unidirectional streams. - getQuicConfiguration().setMaxUnidirectionalRemoteStreams(8); - getQuicConfiguration().setUnidirectionalStreamRecvWindow(1024 * 1024); + super(server, executor, scheduler, bufferPool, extractServerQuicConfiguration(factories), factories); + } + + private static ServerQuicConfiguration extractServerQuicConfiguration(ConnectionFactory[] factories) + { + return Arrays.stream(factories) + .filter(factory -> factory instanceof AbstractHTTP3ServerConnectionFactory) + .map(AbstractHTTP3ServerConnectionFactory.class::cast) + .map(AbstractHTTP3ServerConnectionFactory::getQuicConfiguration) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Missing HTTP/3 ConnectionFactory")); } @Override diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/RawHTTP3ServerConnectionFactory.java b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/RawHTTP3ServerConnectionFactory.java index 5774814ac3ad..d6dc4cb40b47 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/RawHTTP3ServerConnectionFactory.java +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/RawHTTP3ServerConnectionFactory.java @@ -14,17 +14,18 @@ package org.eclipse.jetty.http3.server; import org.eclipse.jetty.http3.api.Session; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.HttpConfiguration; public class RawHTTP3ServerConnectionFactory extends AbstractHTTP3ServerConnectionFactory { - public RawHTTP3ServerConnectionFactory(Session.Server.Listener listener) + public RawHTTP3ServerConnectionFactory(ServerQuicConfiguration quicConfiguration, Session.Server.Listener listener) { - this(new HttpConfiguration(), listener); + this(quicConfiguration, new HttpConfiguration(), listener); } - public RawHTTP3ServerConnectionFactory(HttpConfiguration httpConfiguration, Session.Server.Listener listener) + public RawHTTP3ServerConnectionFactory(ServerQuicConfiguration quicConfiguration, HttpConfiguration httpConfiguration, Session.Server.Listener listener) { - super(httpConfiguration, listener); + super(quicConfiguration, httpConfiguration, listener); } } diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java index 188169ba5730..05c45c4f3c82 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java @@ -13,10 +13,8 @@ package org.eclipse.jetty.http3.tests; -import java.io.InputStream; import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; -import java.security.KeyStore; import java.util.concurrent.TimeUnit; import javax.management.MBeanServer; @@ -31,9 +29,12 @@ import org.eclipse.jetty.http3.client.HTTP3Client; import org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3; import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -53,21 +54,30 @@ public class AbstractClientServerTest public WorkDir workDir; @RegisterExtension - final BeforeTestExecutionCallback printMethodName = context -> + public final BeforeTestExecutionCallback printMethodName = context -> System.err.printf("Running %s.%s() %s%n", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), context.getDisplayName()); protected Server server; - protected HTTP3ServerConnector connector; + protected QuicServerConnector connector; protected HTTP3Client http3Client; protected HttpClient httpClient; protected void start(Handler handler) throws Exception { - prepareServer(new HTTP3ServerConnectionFactory()); + ServerQuicConfiguration quicConfiguration = newServerQuicConfiguration(); + prepareServer(quicConfiguration, new HTTP3ServerConnectionFactory(quicConfiguration)); server.setHandler(handler); server.start(); startClient(); } + private ServerQuicConfiguration newServerQuicConfiguration() + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath("src/test/resources/keystore.p12"); + sslServer.setKeyStorePassword("storepwd"); + return new ServerQuicConfiguration(sslServer, workDir.getEmptyPathDir()); + } + protected void start(Session.Server.Listener listener) throws Exception { startServer(listener); @@ -76,20 +86,17 @@ protected void start(Session.Server.Listener listener) throws Exception protected void startServer(Session.Server.Listener listener) throws Exception { - prepareServer(new RawHTTP3ServerConnectionFactory(listener)); + ServerQuicConfiguration quicConfiguration = newServerQuicConfiguration(); + prepareServer(quicConfiguration, new RawHTTP3ServerConnectionFactory(quicConfiguration, listener)); server.start(); } - private void prepareServer(ConnectionFactory serverConnectionFactory) + private void prepareServer(ServerQuicConfiguration quicConfiguration, ConnectionFactory serverConnectionFactory) { - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); - sslContextFactory.setKeyStorePassword("storepwd"); QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); server = new Server(serverThreads); - connector = new HTTP3ServerConnector(server, sslContextFactory, serverConnectionFactory); - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); + connector = new QuicServerConnector(server, quicConfiguration, serverConnectionFactory); server.addConnector(connector); MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); server.addBean(mbeanContainer); @@ -97,20 +104,15 @@ private void prepareServer(ConnectionFactory serverConnectionFactory) protected void startClient() throws Exception { - KeyStore trustStore = KeyStore.getInstance("PKCS12"); - try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) - { - trustStore.load(is, "storepwd".toCharArray()); - } - - http3Client = new HTTP3Client(); - SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); - clientSslContextFactory.setTrustStore(trustStore); - http3Client.getClientConnector().setSslContextFactory(clientSslContextFactory); - httpClient = new HttpClient(new HttpClientTransportDynamic(new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client))); + ClientConnector clientConnector = new ClientConnector(); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - httpClient.setExecutor(clientThreads); + clientConnector.setExecutor(clientThreads); + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + clientConnector.setSslContextFactory(sslClient); + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslClient, null); + http3Client = new HTTP3Client(quicConfiguration, clientConnector); + httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client))); MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); MBeanContainer mbeanContainer = new MBeanContainer(mbeanServer); httpClient.addBean(mbeanContainer); diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/ExternalServerTest.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/ExternalServerTest.java index 894f9a1ddfe9..1c7cd9048bef 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/ExternalServerTest.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/ExternalServerTest.java @@ -30,7 +30,9 @@ import org.eclipse.jetty.http3.client.HTTP3Client; import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.util.HostPort; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -49,7 +51,9 @@ public class ExternalServerTest @Tag("external") public void testExternalServerWithHttpClient() throws Exception { - HTTP3Client client = new HTTP3Client(); + SslContextFactory.Client sslClient = new SslContextFactory.Client(); + ClientQuicConfiguration quicConfig = new ClientQuicConfiguration(sslClient, null); + HTTP3Client client = new HTTP3Client(quicConfig); HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(client); HttpClient httpClient = new HttpClient(transport); httpClient.start(); @@ -69,7 +73,9 @@ public void testExternalServerWithHttpClient() throws Exception @Tag("external") public void testExternalServerWithHTTP3Client() throws Exception { - HTTP3Client client = new HTTP3Client(); + SslContextFactory.Client sslClient = new SslContextFactory.Client(); + ClientQuicConfiguration quicConfig = new ClientQuicConfiguration(sslClient, null); + HTTP3Client client = new HTTP3Client(quicConfig); client.start(); try { diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/GoAwayTest.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/GoAwayTest.java index f62c616bd318..75b42dab098c 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/GoAwayTest.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/GoAwayTest.java @@ -1049,7 +1049,7 @@ public void onFailure(Stream.Client stream, long error, Throwable failure) // Client sends a graceful GOAWAY. clientSession.goAway(true); - assertTrue(serverGracefulGoAwayLatch.await(555, TimeUnit.SECONDS)); + assertTrue(serverGracefulGoAwayLatch.await(5, TimeUnit.SECONDS)); assertTrue(streamFailureLatch.await(5, TimeUnit.SECONDS)); assertTrue(clientGoAwayLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); assertTrue(serverDisconnectLatch.await(5, TimeUnit.SECONDS)); diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ServerConnectorTest.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ServerConnectorTest.java index 6c3e4d9a6863..ef4e695f045c 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ServerConnectorTest.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ServerConnectorTest.java @@ -19,7 +19,7 @@ import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; import org.eclipse.jetty.http3.server.HTTP3ServerConnector; -import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; @@ -40,8 +40,8 @@ public void testStartHTTP3ServerConnectorWithoutKeyStore() { Server server = new Server(); SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(new HttpConfiguration())); - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, workDir.getEmptyPathDir()); + HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(quicConfiguration)); server.addConnector(connector); assertThrows(IllegalStateException.class, server::start); } @@ -52,8 +52,8 @@ public void testStartHTTP3ServerConnectorWithoutKeyStoreWithSSLContext() throws Server server = new Server(); SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setSslContext(SSLContext.getDefault()); - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(new HttpConfiguration())); - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, workDir.getEmptyPathDir()); + HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(quicConfiguration)); server.addConnector(connector); assertThrows(IllegalStateException.class, server::start); } @@ -66,8 +66,8 @@ public void testStartHTTP3ServerConnectorWithEmptyKeyStoreInstance() throws Exce KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(null, null); sslContextFactory.setKeyStore(keyStore); - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(new HttpConfiguration())); - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, workDir.getEmptyPathDir()); + HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(quicConfiguration)); server.addConnector(connector); assertThrows(IllegalStateException.class, server::start); } @@ -84,7 +84,8 @@ public void testStartHTTP3ServerConnectorWithValidKeyStoreInstanceWithoutPemWork } sslContextFactory.setKeyStore(keyStore); sslContextFactory.setKeyManagerPassword("storepwd"); - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(new HttpConfiguration())); + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, null); + HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(quicConfiguration)); server.addConnector(connector); assertThrows(IllegalStateException.class, server::start); } @@ -101,8 +102,8 @@ public void testStartHTTP3ServerConnectorWithValidKeyStoreInstance() throws Exce } sslContextFactory.setKeyStore(keyStore); sslContextFactory.setKeyManagerPassword("storepwd"); - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(new HttpConfiguration())); - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, workDir.getEmptyPathDir()); + HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, new HTTP3ServerConnectionFactory(quicConfiguration)); server.addConnector(connector); try { diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HandlerClientServerTest.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HandlerClientServerTest.java index d4f00d4a3779..aa4c1b64f943 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HandlerClientServerTest.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HandlerClientServerTest.java @@ -142,10 +142,10 @@ public void onDataAvailable(Stream.Client stream) new Random().nextBytes(bytes); stream.data(new DataFrame(ByteBuffer.wrap(bytes, 0, bytes.length / 2), false)) .thenCompose(s -> s.data(new DataFrame(ByteBuffer.wrap(bytes, bytes.length / 2, bytes.length / 2), true))) - .get(555, TimeUnit.SECONDS); + .get(5, TimeUnit.SECONDS); - assertTrue(serverLatch.await(555, TimeUnit.SECONDS)); - assertTrue(clientResponseLatch.await(555, TimeUnit.SECONDS)); + assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); + assertTrue(clientResponseLatch.await(5, TimeUnit.SECONDS)); int sum = clientReceivedBuffers.stream().mapToInt(Buffer::remaining).sum(); assertThat(sum, is(bytes.length)); diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HttpClientTransportOverHTTP3Test.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HttpClientTransportOverHTTP3Test.java index 475a13563b12..b6802bb159a3 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HttpClientTransportOverHTTP3Test.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HttpClientTransportOverHTTP3Test.java @@ -21,6 +21,7 @@ import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.Content; @@ -51,6 +52,7 @@ public boolean handle(Request request, org.eclipse.jetty.server.Response respons }); ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) .onRequestBegin(request -> { if (request.getVersion() != HttpVersion.HTTP_3) @@ -164,6 +166,7 @@ public boolean handle(Request request, org.eclipse.jetty.server.Response respons AtomicInteger contentCount = new AtomicInteger(); CountDownLatch latch = new CountDownLatch(1); httpClient.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) .onResponseContentSource((response, contentSource) -> { // Do not demand. @@ -208,6 +211,7 @@ public boolean handle(Request request, org.eclipse.jetty.server.Response respons CountDownLatch contentLatch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1); httpClient.newRequest("localhost", connector.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) .onResponseContentSource((response, contentSource) -> { // Do not demand. diff --git a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/IdleTimeoutTest.java b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/IdleTimeoutTest.java index 3d450df6da93..99727ffc307d 100644 --- a/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/IdleTimeoutTest.java +++ b/jetty-core/jetty-http3/jetty-http3-tests/src/test/java/org/eclipse/jetty/http3/tests/IdleTimeoutTest.java @@ -29,13 +29,14 @@ import org.eclipse.jetty.http3.api.Stream; import org.eclipse.jetty.http3.client.HTTP3Client; import org.eclipse.jetty.http3.frames.HeadersFrame; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.quic.quiche.QuicheConnection; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.quic.server.ServerQuicConnection; import org.eclipse.jetty.quic.server.ServerQuicSession; -import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; @@ -74,9 +75,15 @@ public void dispose() public void testIdleTimeoutWhenCongested(WorkDir workDir) throws Exception { long idleTimeout = 1000; + + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath("src/test/resources/keystore.p12"); + sslServer.setKeyStorePassword("storepwd"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslServer, workDir.getEmptyPathDir()); + AtomicBoolean established = new AtomicBoolean(); CountDownLatch disconnectLatch = new CountDownLatch(1); - RawHTTP3ServerConnectionFactory h3 = new RawHTTP3ServerConnectionFactory(new HttpConfiguration(), new Session.Server.Listener() + RawHTTP3ServerConnectionFactory h3 = new RawHTTP3ServerConnectionFactory(serverQuicConfig, new Session.Server.Listener() { @Override public void onAccept(Session session) @@ -92,15 +99,12 @@ public void onDisconnect(Session session, long error, String reason) }); CountDownLatch closeLatch = new CountDownLatch(1); - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); - sslContextFactory.setKeyStorePassword("storepwd"); - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactory, h3) + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, h3) { @Override protected ServerQuicConnection newConnection(EndPoint endpoint) { - return new ServerQuicConnection(this, endpoint) + return new ServerQuicConnection(this, getQuicConfiguration(), endpoint) { @Override protected ServerQuicSession newQuicSession(SocketAddress remoteAddress, QuicheConnection quicheConnection) @@ -126,13 +130,13 @@ public void outwardClose(long error, String reason) }; } }; - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); connector.setIdleTimeout(idleTimeout); server.addConnector(connector); server.start(); - http3Client = new HTTP3Client(); - http3Client.getClientConnector().setSslContextFactory(new SslContextFactory.Client(true)); + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + http3Client = new HTTP3Client(new ClientQuicConfiguration(sslClient, null)); + http3Client.getClientConnector().setSslContextFactory(sslClient); http3Client.start(); Session.Client session = http3Client.connect(new InetSocketAddress("localhost", connector.getLocalPort()), new Session.Client.Listener() {}) diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java index a4bbc7e5ebef..9fbeb7d14ef4 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java @@ -61,9 +61,10 @@ interface Decorator } /** - *

A holder for a list of protocol strings identifying an application protocol - * (for example {@code ["h2", "h2-17", "h2-16"]}) and a {@link ClientConnectionFactory} - * that creates connections that speak that network protocol.

+ *

A holder for a list of protocol strings identifiers + * (for example {@code ["h2", "h2-17", "h2-16"]}) and a + * {@link ClientConnectionFactory} that creates connections + * that speak an application protocol such as HTTP.

*/ public abstract static class Info extends ContainerLifeCycle { @@ -75,15 +76,29 @@ public Info(ClientConnectionFactory factory) addBean(factory); } + /** + * @param secure {@code true} for the secure protocol identifiers, + * {@code false} for the clear-text protocol identifiers + * @return a list of protocol string identifiers + */ public abstract List getProtocols(boolean secure); + /** + * @return the {@link ClientConnectionFactory} that speaks the protocol + */ public ClientConnectionFactory getClientConnectionFactory() { return factory; } /** - * Tests whether one of the protocols of this class is also present in the given candidates list. + * @return the default {@link TransportProtocol} used by the protocol + */ + public abstract TransportProtocol newTransportProtocol(); + + /** + *

Tests whether one of the protocol identifiers of this + * class is also present in the given candidates list.

* * @param candidates the candidates to match against * @param secure whether the protocol should be a secure one @@ -94,6 +109,12 @@ public boolean matches(List candidates, boolean secure) return getProtocols(secure).stream().anyMatch(p -> candidates.stream().anyMatch(c -> c.equalsIgnoreCase(p))); } + /** + *

Upgrades the given {@link EndPoint} to the protocol represented by this class.

+ * + * @param endPoint the {@link EndPoint} to upgrade + * @param context the context information to perform the upgrade + */ public void upgrade(EndPoint endPoint, Map context) { throw new UnsupportedOperationException(this + " does not support upgrade to another protocol"); diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index f94fab824678..0049e6e56ca8 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -21,6 +21,7 @@ import java.net.SocketOption; import java.net.StandardProtocolFamily; import java.net.StandardSocketOptions; +import java.nio.channels.DatagramChannel; import java.nio.channels.NetworkChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; @@ -29,7 +30,6 @@ import java.time.Duration; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import org.eclipse.jetty.util.IO; @@ -85,7 +85,9 @@ public class ClientConnector extends ContainerLifeCycle * * @param path the Unix-Domain path to connect to * @return a ClientConnector that connects to the given Unix-Domain path + * @deprecated replaced by {@link TransportProtocol.TCPUnix} */ + @Deprecated(since = "12.0.7", forRemoval = true) public static ClientConnector forUnixDomain(Path path) { return new ClientConnector(Configurator.forUnixDomain(path)); @@ -113,6 +115,11 @@ public ClientConnector() this(new Configurator()); } + /** + * @param configurator the {@link Configurator} + * @deprecated replaced by {@link TransportProtocol} + */ + @Deprecated(since = "12.0.7", forRemoval = true) public ClientConnector(Configurator configurator) { this.configurator = Objects.requireNonNull(configurator); @@ -124,17 +131,40 @@ public ClientConnector(Configurator configurator) * @param address the SocketAddress to connect to * @return whether the connection to the given SocketAddress is intrinsically secure * @see Configurator#isIntrinsicallySecure(ClientConnector, SocketAddress) + * + * @deprecated replaced by {@link TransportProtocol#isIntrinsicallySecure()} */ + @Deprecated(since = "12.0.7", forRemoval = true) public boolean isIntrinsicallySecure(SocketAddress address) { return configurator.isIntrinsicallySecure(this, address); } + public SelectorManager getSelectorManager() + { + return selectorManager; + } + public Executor getExecutor() { return executor; } + /** + *

Returns the default {@link TransportProtocol} for this connector.

+ *

This method only exists for backwards compatibility, when + * {@link Configurator} was used, and should be removed when + * {@link Configurator} is removed.

+ * + * @return the default {@link TransportProtocol} for this connector + * @deprecated use {@link TransportProtocol} instead + */ + @Deprecated(since = "12.0.7", forRemoval = true) + public TransportProtocol newTransportProtocol() + { + return configurator.newTransportProtocol(); + } + public void setExecutor(Executor executor) { if (isStarted()) @@ -401,25 +431,29 @@ public void connect(SocketAddress address, Map context) SelectableChannel channel = null; try { - if (context == null) - context = new ConcurrentHashMap<>(); context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this); - context.putIfAbsent(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address); - Configurator.ChannelWithAddress channelWithAddress = configurator.newChannelWithAddress(this, address, context); - channel = channelWithAddress.getSelectableChannel(); - address = channelWithAddress.getSocketAddress(); + TransportProtocol transport = (TransportProtocol)context.get(TransportProtocol.class.getName()); + + if (address == null) + address = transport.getSocketAddress(); + context.putIfAbsent(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address); + channel = transport.newSelectableChannel(); configure(channel); - SocketAddress bindAddress = getBindAddress(); - if (bindAddress != null && channel instanceof NetworkChannel) - bind((NetworkChannel)channel, bindAddress); + if (channel instanceof NetworkChannel networkChannel) + { + SocketAddress bindAddress = getBindAddress(); + if (bindAddress != null) + bind(networkChannel, bindAddress); + else if (networkChannel instanceof DatagramChannel) + bind(networkChannel, null); + } boolean connected = true; - if (channel instanceof SocketChannel) + if (channel instanceof SocketChannel socketChannel) { - SocketChannel socketChannel = (SocketChannel)channel; boolean blocking = isConnectBlocking() && address instanceof InetSocketAddress; if (LOG.isDebugEnabled()) LOG.debug("Connecting {} to {}", blocking ? "blocking" : "non-blocking", address); @@ -462,9 +496,11 @@ public void accept(SelectableChannel selectable, Map context) try { SocketChannel channel = (SocketChannel)selectable; - context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this); if (!channel.isConnected()) throw new IllegalStateException("SocketChannel must be connected"); + + context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this); + configure(channel); channel.configureBlocking(false); selectorManager.accept(channel, context); @@ -474,9 +510,7 @@ public void accept(SelectableChannel selectable, Map context) if (LOG.isDebugEnabled()) LOG.debug("Could not accept {}", selectable); IO.close(selectable); - Promise promise = (Promise)context.get(CONNECTION_PROMISE_CONTEXT_KEY); - if (promise != null) - promise.failed(failure); + acceptFailed(failure, selectable, context); } } @@ -489,9 +523,8 @@ private void bind(NetworkChannel channel, SocketAddress bindAddress) throws IOEx protected void configure(SelectableChannel selectable) throws IOException { - if (selectable instanceof NetworkChannel) + if (selectable instanceof NetworkChannel channel) { - NetworkChannel channel = (NetworkChannel)selectable; setSocketOption(channel, StandardSocketOptions.TCP_NODELAY, isTCPNoDelay()); setSocketOption(channel, StandardSocketOptions.SO_REUSEADDR, getReuseAddress()); setSocketOption(channel, StandardSocketOptions.SO_REUSEPORT, isReusePort()); @@ -521,14 +554,23 @@ protected EndPoint newEndPoint(SelectableChannel selectable, ManagedSelector sel { @SuppressWarnings("unchecked") Map context = (Map)selectionKey.attachment(); - SocketAddress address = (SocketAddress)context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY); - return configurator.newEndPoint(this, address, selectable, selector, selectionKey); + TransportProtocol transportProtocol = (TransportProtocol)context.get(TransportProtocol.class.getName()); + return transportProtocol.newEndPoint(getScheduler(), selector, selectable, selectionKey); } protected Connection newConnection(EndPoint endPoint, Map context) throws IOException { - SocketAddress address = (SocketAddress)context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY); - return configurator.newConnection(this, address, endPoint, context); + TransportProtocol transportProtocol = (TransportProtocol)context.get(TransportProtocol.class.getName()); + return transportProtocol.newConnection(endPoint, context); + } + + protected void acceptFailed(Throwable failure, SelectableChannel channel, Map context) + { + if (LOG.isDebugEnabled()) + LOG.debug("Could not accept {}", channel); + Promise promise = (Promise)context.get(CONNECTION_PROMISE_CONTEXT_KEY); + if (promise != null) + promise.failed(failure); } protected void connectFailed(Throwable failure, Map context) @@ -566,15 +608,21 @@ public Connection newConnection(SelectableChannel channel, EndPoint endPoint, Ob @Override public void connectionOpened(Connection connection, Object context) { - super.connectionOpened(connection, context); - // TODO: the block below should be moved to Connection.onOpen() in each implementation, - // so that each implementation can decide when to notify the promise, possibly not in onOpen(). @SuppressWarnings("unchecked") Map contextMap = (Map)context; @SuppressWarnings("unchecked") Promise promise = (Promise)contextMap.get(CONNECTION_PROMISE_CONTEXT_KEY); - if (promise != null) + try + { + super.connectionOpened(connection, context); + // TODO: the block below should be moved to Connection.onOpen() in each implementation, + // so that each implementation can decide when to notify the promise, possibly not in onOpen(). promise.succeeded(connection); + } + catch (Throwable x) + { + promise.failed(x); + } } @Override @@ -588,9 +636,20 @@ protected void connectionFailed(SelectableChannel channel, Throwable failure, Ob /** *

Configures a {@link ClientConnector}.

+ * + * @deprecated replaced by {@link TransportProtocol} */ + @Deprecated(since = "12.0.7", forRemoval = true) public static class Configurator extends ContainerLifeCycle { + /** + * @return the default {@link TransportProtocol} for this configurator + */ + public TransportProtocol newTransportProtocol() + { + return null; + } + /** *

Returns whether the connection to a given {@link SocketAddress} is intrinsically secure.

*

A protocol such as HTTP/1.1 can be transported by TCP; however, TCP is not secure because @@ -649,7 +708,10 @@ public Connection newConnection(ClientConnector clientConnector, SocketAddress a /** *

A pair/record holding a {@link SelectableChannel} and a {@link SocketAddress} to connect to.

+ * + * @deprecated replaced by {@link TransportProtocol} */ + @Deprecated(since = "12.0.7", forRemoval = true) public static class ChannelWithAddress { private final SelectableChannel channel; @@ -676,6 +738,12 @@ private static Configurator forUnixDomain(Path path) { return new Configurator() { + @Override + public TransportProtocol newTransportProtocol() + { + return new TransportProtocol.TCPUnix(path); + } + @Override public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map context) { diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/DatagramChannelEndPoint.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/DatagramChannelEndPoint.java index 4bdced9d9b32..52a28cb58bb8 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/DatagramChannelEndPoint.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/DatagramChannelEndPoint.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.io; import java.io.IOException; -import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; @@ -32,7 +31,6 @@ */ public class DatagramChannelEndPoint extends SelectableChannelEndPoint { - public static final SocketAddress EOF = InetSocketAddress.createUnresolved("", 0); private static final Logger LOG = LoggerFactory.getLogger(DatagramChannelEndPoint.class); public DatagramChannelEndPoint(DatagramChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) @@ -61,14 +59,7 @@ public SocketAddress getRemoteSocketAddress() return null; } - /** - *

Receives data into the given buffer from the returned address.

- *

This method should be used to receive UDP data.

- * - * @param buffer the buffer to fill with data - * @return the peer address that sent the data - * @throws IOException if the receive fails - */ + @Override public SocketAddress receive(ByteBuffer buffer) throws IOException { if (isInputShutdown()) @@ -88,16 +79,7 @@ public SocketAddress receive(ByteBuffer buffer) throws IOException return peer; } - /** - *

Sends to the given address the data in the given buffers.

- *

This methods should be used to send UDP data.

- * - * @param address the peer address to send data to - * @param buffers the buffers containing the data to send - * @return true if all the buffers have been consumed - * @throws IOException if the send fails - * @see #write(Callback, SocketAddress, ByteBuffer...) - */ + @Override public boolean send(SocketAddress address, ByteBuffer... buffers) throws IOException { boolean flushedAll = true; @@ -130,16 +112,7 @@ public boolean send(SocketAddress address, ByteBuffer... buffers) throws IOExcep return flushedAll; } - /** - *

Writes to the given address the data contained in the given buffers, and invokes - * the given callback when either all the data has been sent, or a failure occurs.

- * - * @param callback the callback to notify of the success or failure of the write operation - * @param address the peer address to send data to - * @param buffers the buffers containing the data to send - * @throws WritePendingException if a previous write was initiated but was not yet completed - * @see #send(SocketAddress, ByteBuffer...) - */ + @Override public void write(Callback callback, SocketAddress address, ByteBuffer... buffers) throws WritePendingException { getWriteFlusher().write(callback, address, buffers); diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java index 5779cd6509ca..e06fdce80afa 100644 --- a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/EndPoint.java @@ -92,6 +92,11 @@ */ public interface EndPoint extends Closeable { + /** + *

Constant returned by {@link #receive(ByteBuffer)} to indicate the end-of-file.

+ */ + SocketAddress EOF = InetSocketAddress.createUnresolved("", 0); + /** * Marks an {@code EndPoint} that wraps another {@code EndPoint}. */ @@ -211,6 +216,24 @@ default int fill(ByteBuffer buffer) throws IOException throw new UnsupportedOperationException(); } + /** + *

Receives data into the given buffer from the returned address.

+ *

This method should be used to receive UDP data.

+ * + * @param buffer the buffer to fill with data + * @return the peer address that sent the data, or {@link #EOF} + * @throws IOException if the receive fails + */ + default SocketAddress receive(ByteBuffer buffer) throws IOException + { + int filled = fill(buffer); + if (filled < 0) + return EndPoint.EOF; + if (filled == 0) + return null; + return getRemoteSocketAddress(); + } + /** * Flush data from the passed header/buffer to this endpoint. As many bytes as can be consumed * are taken from the header/buffer position up until the buffer limit. The header/buffers position @@ -226,6 +249,21 @@ default boolean flush(ByteBuffer... buffer) throws IOException throw new UnsupportedOperationException(); } + /** + *

Sends to the given address the data in the given buffers.

+ *

This methods should be used to send UDP data.

+ * + * @param address the peer address to send data to + * @param buffers the buffers containing the data to send + * @return true if all the buffers have been consumed + * @throws IOException if the send fails + * @see #write(Callback, SocketAddress, ByteBuffer...) + */ + default boolean send(SocketAddress address, ByteBuffer... buffers) throws IOException + { + return flush(buffers); + } + /** * @return The underlying transport object (socket, channel, etc.) */ @@ -285,6 +323,21 @@ default void write(Callback callback, ByteBuffer... buffers) throws WritePending throw new UnsupportedOperationException(); } + /** + *

Writes to the given address the data contained in the given buffers, and invokes + * the given callback when either all the data has been sent, or a failure occurs.

+ * + * @param callback the callback to notify of the success or failure of the write operation + * @param address the peer address to send data to + * @param buffers the buffers containing the data to send + * @throws WritePendingException if a previous write was initiated but was not yet completed + * @see #send(SocketAddress, ByteBuffer...) + */ + default void write(Callback callback, SocketAddress address, ByteBuffer... buffers) throws WritePendingException + { + write(callback, buffers); + } + /** * @return the {@link Connection} associated with this EndPoint * @see #setConnection(Connection) @@ -439,4 +492,20 @@ static SslSessionData withSslSessionId(SslSessionData baseData, String sslSessio baseData.peerCertificates()); } } + + /** + *

A communication conduit between two peers.

+ */ + interface Pipe + { + /** + * @return the {@link EndPoint} of the local peer + */ + EndPoint getLocalEndPoint(); + + /** + * @return the {@link EndPoint} of the remote peer + */ + EndPoint getRemoteEndPoint(); + } } diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/MemoryEndPointPipe.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/MemoryEndPointPipe.java new file mode 100644 index 000000000000..b7b1786e8ca7 --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/MemoryEndPointPipe.java @@ -0,0 +1,344 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.io; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HexFormat; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.thread.AutoLock; +import org.eclipse.jetty.util.thread.Invocable; +import org.eclipse.jetty.util.thread.Scheduler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

Memory-based implementation of {@link EndPoint.Pipe}.

+ */ +public class MemoryEndPointPipe implements EndPoint.Pipe +{ + private static final Logger LOG = LoggerFactory.getLogger(MemoryEndPointPipe.class); + + private final LocalEndPoint localEndPoint; + private final RemoteEndPoint remoteEndPoint; + private final Consumer taskConsumer; + + public MemoryEndPointPipe(Scheduler scheduler, Consumer consumer, SocketAddress socketAddress) + { + localEndPoint = new LocalEndPoint(scheduler, socketAddress); + remoteEndPoint = new RemoteEndPoint(scheduler, new MemorySocketAddress()); + localEndPoint.setPeerEndPoint(remoteEndPoint); + remoteEndPoint.setPeerEndPoint(localEndPoint); + taskConsumer = consumer; + } + + @Override + public EndPoint getLocalEndPoint() + { + return localEndPoint; + } + + @Override + public EndPoint getRemoteEndPoint() + { + return remoteEndPoint; + } + + private class MemoryEndPoint extends AbstractEndPoint + { + private static final ByteBuffer EOF = ByteBuffer.allocate(0); + + private final AutoLock lock = new AutoLock(); + private final Deque byteBuffers = new ArrayDeque<>(); + private final SocketAddress localAddress; + private MemoryEndPoint peerEndPoint; + private Invocable.Task fillableTask; + private Invocable.Task completeWriteTask; + private long maxCapacity; + private long capacity; + + private MemoryEndPoint(Scheduler scheduler, SocketAddress localAddress) + { + super(scheduler); + this.localAddress = localAddress; + } + + void setPeerEndPoint(MemoryEndPoint peerEndPoint) + { + this.peerEndPoint = peerEndPoint; + this.fillableTask = new FillableTask(peerEndPoint.getFillInterest()); + this.completeWriteTask = new CompleteWriteTask(peerEndPoint.getWriteFlusher()); + } + + public long getMaxCapacity() + { + return maxCapacity; + } + + public void setMaxCapacity(long maxCapacity) + { + this.maxCapacity = maxCapacity; + } + + @Override + public Object getTransport() + { + return null; + } + + @Override + public SocketAddress getLocalSocketAddress() + { + return localAddress; + } + + @Override + public SocketAddress getRemoteSocketAddress() + { + return peerEndPoint.getLocalSocketAddress(); + } + + @Override + protected void onIncompleteFlush() + { + } + + @Override + protected void needsFillInterest() + { + } + + @Override + public int fill(ByteBuffer buffer) throws IOException + { + if (!isOpen()) + throw new IOException("closed"); + if (isInputShutdown()) + return -1; + + int filled; + ByteBuffer data; + try (AutoLock ignored = peerEndPoint.lock.lock()) + { + Queue byteBuffers = peerEndPoint.byteBuffers; + data = byteBuffers.peek(); + + if (data == null) + { + filled = 0; + } + else if (data == EOF) + { + filled = -1; + } + else + { + int length = data.remaining(); + int space = BufferUtil.space(buffer); + if (length <= space) + byteBuffers.poll(); + + filled = Math.min(length, space); + peerEndPoint.capacity -= filled; + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("filled {} from {}", filled, this); + + if (data == null) + return 0; + + if (data == EOF) + { + shutdownInput(); + return -1; + } + + int copied = BufferUtil.append(buffer, data); + assert copied == filled; + + if (filled > 0) + { + notIdle(); + onFilled(); + } + + return filled; + } + + private void onFilled() + { + taskConsumer.accept(completeWriteTask); + } + + @Override + public boolean flush(ByteBuffer... buffers) throws IOException + { + if (!isOpen()) + throw new IOException("closed"); + if (isOutputShutdown()) + throw new IOException("shutdown"); + + long flushed = 0; + boolean result = true; + try (AutoLock ignored = lock.lock()) + { + for (ByteBuffer buffer : buffers) + { + int remaining = buffer.remaining(); + if (remaining == 0) + continue; + + long newCapacity = capacity + remaining; + long maxCapacity = getMaxCapacity(); + if (maxCapacity > 0 && newCapacity > maxCapacity) + { + result = false; + break; + } + + byteBuffers.offer(BufferUtil.copy(buffer)); + buffer.position(buffer.limit()); + capacity = newCapacity; + flushed += remaining; + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("flushed {} to {}", flushed, this); + + if (flushed > 0) + { + notIdle(); + onFlushed(); + } + + return result; + } + + @Override + protected void doShutdownOutput() + { + super.doShutdownOutput(); + try (AutoLock ignored = lock.lock()) + { + byteBuffers.offer(EOF); + } + onFlushed(); + } + + @Override + protected void doClose() + { + super.doClose(); + try (AutoLock ignored = lock.lock()) + { + ByteBuffer last = byteBuffers.peekLast(); + if (last != EOF) + byteBuffers.offer(EOF); + } + onFlushed(); + } + + private void onFlushed() + { + taskConsumer.accept(fillableTask); + } + } + + private class LocalEndPoint extends MemoryEndPoint + { + private LocalEndPoint(Scheduler scheduler, SocketAddress socketAddress) + { + super(scheduler, socketAddress); + } + } + + private class RemoteEndPoint extends MemoryEndPoint + { + private RemoteEndPoint(Scheduler scheduler, SocketAddress socketAddress) + { + super(scheduler, socketAddress); + } + } + + private record FillableTask(FillInterest fillInterest) implements Invocable.Task + { + @Override + public void run() + { + fillInterest.fillable(); + } + + @Override + public InvocationType getInvocationType() + { + return fillInterest.getCallbackInvocationType(); + } + } + + private record CompleteWriteTask(WriteFlusher writeFlusher) implements Invocable.Task + { + @Override + public void run() + { + writeFlusher.completeWrite(); + } + + @Override + public InvocationType getInvocationType() + { + return writeFlusher.getCallbackInvocationType(); + } + } + + private static class MemorySocketAddress extends SocketAddress + { + private static final AtomicLong ID = new AtomicLong(); + + private final long id = ID.incrementAndGet(); + private final String address = "[memory:/%s]".formatted(HexFormat.of().formatHex(ByteBuffer.allocate(8).putLong(id).array())); + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj instanceof MemorySocketAddress that) + return id == that.id; + return false; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public String toString() + { + return address; + } + } +} diff --git a/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/TransportProtocol.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/TransportProtocol.java new file mode 100644 index 000000000000..487264320676 --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/TransportProtocol.java @@ -0,0 +1,402 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.io; + +import java.io.IOException; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jetty.util.thread.Scheduler; + +/** + *

The low-level transport protocol used by clients.

+ *

A high-level protocol such as HTTP/1.1 can be transported over a low-level + * protocol such as TCP/IP, Unix-Domain sockets, QUIC, shared memory, etc.

+ *

This class defines the programming interface to implement low-level + * protocols, and useful implementations for commonly used low-level + * protocols such as TCP/IP or Unix-Domain sockets.

+ *

Low-level transports may be layered; some of them maybe considered + * lower-level than others, but from the point of view of the high-level + * protocols they are all considered low-level.

+ *

For example, QUIC is typically layered on top of the UDP/IP low-level + * transport protocol, but it may be layered on top Unix-Domain sockets, + * or on top of shared memory. + * As QUIC provides a reliable, ordered, stream-based transport, it may + * be seen as a replacement for TCP, and high-level protocols that need + * a reliable, ordered, stream-based transport may use either the non-layered + * TCP/IP or the layered QUIC over UDP/IP without noticing the difference. + * This makes possible to transport HTTP/1.1 over QUIC over Unix-Domain + * sockets, or HTTP/2 over QUIC over shared memory, etc.

+ */ +public interface TransportProtocol +{ + /** + *

The transport protocol TCP/IP.

+ */ + TransportProtocol TCP_IP = new TCPIP(); + + /** + *

The transport protocol UDP/IP.

+ */ + TransportProtocol UDP_IP = new UDPIP(); + + /** + * @return whether this transport protocol is intrinsically secure. + */ + default boolean isIntrinsicallySecure() + { + return false; + } + + /** + *

Returns whether this transport protocol requires resolution of domain + * names.

+ *

When domain name resolution is required, it must be performed by + * an external service, and the value returned by {@link #getSocketAddress()} + * is ignored, while the resolved socket address is eventually passed to + * {@link #connect(SocketAddress, Map)}. + * Otherwise, domain name resolution is not required, and the value returned + * by {@link #getSocketAddress()} is eventually passed to + * {@link #connect(SocketAddress, Map)}.

+ * + * @return whether this transport protocol requires domain names resolution + */ + default boolean requiresDomainNamesResolution() + { + return false; + } + + /** + *

Establishes a connection to the given socket address.

+ * + * @param socketAddress the socket address to connect to + * @param context the context information to establish the connection + */ + default void connect(SocketAddress socketAddress, Map context) + { + } + + /** + * @return the socket address to use in case domain name resolution is not required + */ + default SocketAddress getSocketAddress() + { + return null; + } + + /** + *

For transport protocols that are based on sockets, or for transport protocols + * that are layered on top of another transport protocol that is based on sockets, + * this method is invoked to create a new {@link SelectableChannel} used for the + * socket communication.

+ * + * @return a new {@link SelectableChannel} used for the socket communication, + * or {@code null} if the communication does not use sockets. + * @throws IOException if the {@link SelectableChannel} cannot be created + */ + default SelectableChannel newSelectableChannel() throws IOException + { + return null; + } + + /** + *

For transport protocols that are based on sockets, or for transport protocols + * that are layered on top of another transport protocol that is based on sockets, + * this method is invoked to create a new {@link EndPoint} that wraps the + * {@link SelectableChannel} created by {@link #newSelectableChannel()}.

+ * + * @param scheduler the {@link Scheduler} + * @param selector the {@link ManagedSelector} + * @param selectable the {@link SelectableChannel} + * @param selectionKey the {@link SelectionKey} + * @return a new {@link EndPoint} + */ + default EndPoint newEndPoint(Scheduler scheduler, ManagedSelector selector, SelectableChannel selectable, SelectionKey selectionKey) + { + return null; + } + + /** + *

Creates a new {@link Connection} to be associated with the given low-level {@link EndPoint}.

+ *

For non-layered transport protocols such as TCP/IP, the {@link Connection} is typically + * that of the high-level protocol. + * For layered transport protocols such as QUIC, the {@link Connection} is typically that of the + * layered transport protocol.

+ * + * @param endPoint the {@link EndPoint} to associate the {@link Connection} to + * @param context the context information to create the connection + * @return a new {@link Connection} + * @throws IOException if the {@link Connection} cannot be created + */ + default Connection newConnection(EndPoint endPoint, Map context) throws IOException + { + ClientConnectionFactory factory = (ClientConnectionFactory)context.get(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY); + return factory.newConnection(endPoint, context); + } + + int hashCode(); + + boolean equals(Object obj); + + /** + *

Abstract implementation of transport protocols based on sockets.

+ */ + abstract class Socket implements TransportProtocol + { + @Override + public void connect(SocketAddress socketAddress, Map context) + { + ClientConnector connector = (ClientConnector)context.get(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY); + connector.connect(socketAddress, context); + } + + @Override + public String toString() + { + return "%s@%x".formatted(getClass().getSimpleName(), hashCode()); + } + } + + /** + *

Abstract implementation of transport protocols based on IP.

+ */ + abstract class IP extends Socket + { + @Override + public boolean requiresDomainNamesResolution() + { + return true; + } + } + + /** + *

The TCP/IP transport protocol.

+ */ + class TCPIP extends IP + { + protected TCPIP() + { + // Do not instantiate, use the singleton. + } + + @Override + public SelectableChannel newSelectableChannel() throws IOException + { + return SocketChannel.open(); + } + + @Override + public EndPoint newEndPoint(Scheduler scheduler, ManagedSelector selector, SelectableChannel selectable, SelectionKey selectionKey) + { + return new SocketChannelEndPoint((SocketChannel)selectable, selector, selectionKey, scheduler); + } + } + + /** + *

The UDP/IP transport protocol.

+ */ + class UDPIP extends TransportProtocol.IP + { + protected UDPIP() + { + // Do not instantiate, use the singleton. + } + + @Override + public SelectableChannel newSelectableChannel() throws IOException + { + return DatagramChannel.open(); + } + + @Override + public EndPoint newEndPoint(Scheduler scheduler, ManagedSelector selector, SelectableChannel selectable, SelectionKey selectionKey) + { + return new DatagramChannelEndPoint((DatagramChannel)selectable, selector, selectionKey, scheduler); + } + } + + /** + *

Abstract implementation of transport protocols based on Unix-Domain sockets.

+ */ + abstract class Unix extends Socket + { + private final UnixDomainSocketAddress socketAddress; + + protected Unix(Path path) + { + this.socketAddress = UnixDomainSocketAddress.of(path); + } + + @Override + public SocketAddress getSocketAddress() + { + return socketAddress; + } + + @Override + public int hashCode() + { + return Objects.hash(socketAddress); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj instanceof Unix unix) + return Objects.equals(socketAddress, unix.socketAddress); + return false; + } + + @Override + public String toString() + { + return "%s[%s]".formatted(super.toString(), socketAddress.getPath()); + } + } + + /** + *

The stream Unix-Domain socket transport protocol.

+ */ + class TCPUnix extends Unix + { + public TCPUnix(Path path) + { + super(path); + } + + @Override + public SelectableChannel newSelectableChannel() throws IOException + { + return SocketChannel.open(StandardProtocolFamily.UNIX); + } + + @Override + public EndPoint newEndPoint(Scheduler scheduler, ManagedSelector selector, SelectableChannel selectable, SelectionKey selectionKey) + { + return new SocketChannelEndPoint((SocketChannel)selectable, selector, selectionKey, scheduler); + } + } + + /** + *

The datagram Unix-Domain socket transport protocol.

+ */ + class UDPUnix extends Unix + { + public UDPUnix(Path path) + { + super(path); + } + + @Override + public SelectableChannel newSelectableChannel() throws IOException + { + return DatagramChannel.open(StandardProtocolFamily.UNIX); + } + + @Override + public EndPoint newEndPoint(Scheduler scheduler, ManagedSelector selector, SelectableChannel selectable, SelectionKey selectionKey) + { + return new DatagramChannelEndPoint((DatagramChannel)selectable, selector, selectionKey, scheduler); + } + } + + /** + *

A wrapper for {@link TransportProtocol} instances to allow layering of transport protocols.

+ */ + class Wrapper implements TransportProtocol + { + private final TransportProtocol wrapped; + + public Wrapper(TransportProtocol wrapped) + { + this.wrapped = Objects.requireNonNull(wrapped); + } + + public TransportProtocol getWrapped() + { + return wrapped; + } + + public TransportProtocol unwrap() + { + TransportProtocol result = getWrapped(); + while (true) + { + if (result instanceof Wrapper wrapper) + result = wrapper.getWrapped(); + else + break; + } + return result; + } + + @Override + public boolean isIntrinsicallySecure() + { + return wrapped.isIntrinsicallySecure(); + } + + @Override + public boolean requiresDomainNamesResolution() + { + return wrapped.requiresDomainNamesResolution(); + } + + @Override + public void connect(SocketAddress socketAddress, Map context) + { + wrapped.connect(socketAddress, context); + } + + @Override + public SocketAddress getSocketAddress() + { + return wrapped.getSocketAddress(); + } + + @Override + public SelectableChannel newSelectableChannel() throws IOException + { + return wrapped.newSelectableChannel(); + } + + @Override + public EndPoint newEndPoint(Scheduler scheduler, ManagedSelector selector, SelectableChannel selectable, SelectionKey selectionKey) + { + return wrapped.newEndPoint(scheduler, selector, selectable, selectionKey); + } + + @Override + public Connection newConnection(EndPoint endPoint, Map context) throws IOException + { + return wrapped.newConnection(endPoint, context); + } + + @Override + public String toString() + { + return "%s@%x[%s]".formatted(getClass().getSimpleName(), hashCode(), getWrapped()); + } + } +} diff --git a/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConfiguration.java b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConfiguration.java new file mode 100644 index 000000000000..669c5cc0531e --- /dev/null +++ b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConfiguration.java @@ -0,0 +1,106 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.quic.client; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; + +import org.eclipse.jetty.quic.common.QuicConfiguration; +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

Client-side {@link QuicConfiguration} with client-specific settings.

+ *

The PEM working directory constructor argument is only necessary + * when the client-side needs to send certificates to the server, or + * when it needs a TrustStore, otherwise it may be null.

+ */ +public class ClientQuicConfiguration extends QuicConfiguration +{ + private static final Logger LOG = LoggerFactory.getLogger(ClientQuicConfiguration.class); + + private final SslContextFactory.Client sslContextFactory; + + public ClientQuicConfiguration(SslContextFactory.Client sslContextFactory, Path pemWorkDirectory) + { + this.sslContextFactory = sslContextFactory; + setPemWorkDirectory(pemWorkDirectory); + setSessionRecvWindow(16 * 1024 * 1024); + setBidirectionalStreamRecvWindow(8 * 1024 * 1024); + } + + public SslContextFactory.Client getSslContextFactory() + { + return sslContextFactory; + } + + @Override + protected void doStart() throws Exception + { + addBean(sslContextFactory); + + super.doStart(); + + Path pemWorkDirectory = getPemWorkDirectory(); + KeyStore trustStore = sslContextFactory.getTrustStore(); + if (trustStore != null) + { + Path trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, pemWorkDirectory); + getImplementationConfiguration().put(TRUSTED_CERTIFICATES_PEM_PATH_KEY, trustedCertificatesPemPath); + } + + String certAlias = sslContextFactory.getCertAlias(); + if (certAlias != null) + { + KeyStore keyStore = sslContextFactory.getKeyStore(); + String keyManagerPassword = sslContextFactory.getKeyManagerPassword(); + char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray(); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, certAlias, password, pemWorkDirectory); + Path privateKeyPemPath = keyPair[0]; + getImplementationConfiguration().put(PRIVATE_KEY_PEM_PATH_KEY, privateKeyPemPath); + Path certificateChainPemPath = keyPair[1]; + getImplementationConfiguration().put(CERTIFICATE_CHAIN_PEM_PATH_KEY, certificateChainPemPath); + } + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + + Path certificateChainPemPath = (Path)getImplementationConfiguration().remove(CERTIFICATE_CHAIN_PEM_PATH_KEY); + deleteFile(certificateChainPemPath); + Path privateKeyPemPath = (Path)getImplementationConfiguration().remove(PRIVATE_KEY_PEM_PATH_KEY); + deleteFile(privateKeyPemPath); + Path trustedCertificatesPemPath = (Path)getImplementationConfiguration().remove(TRUSTED_CERTIFICATES_PEM_PATH_KEY); + deleteFile(trustedCertificatesPemPath); + } + + private void deleteFile(Path path) + { + try + { + if (path != null) + Files.delete(path); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("could not delete {}", path, x); + } + } +} diff --git a/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java index bd0e307cb788..76452046bda2 100644 --- a/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java @@ -14,14 +14,15 @@ package org.eclipse.jetty.quic.client; import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -49,16 +50,19 @@ public class ClientQuicConnection extends QuicConnection { private static final Logger LOG = LoggerFactory.getLogger(ClientQuicConnection.class); - private final Map pendingSessions = new ConcurrentHashMap<>(); private final ClientConnector connector; private final Map context; + private final InetSocketAddress inetLocalAddress; private Scheduler.Task connectTask; + private ClientQuicSession pendingSession; + private InetSocketAddress inetRemoteAddress; public ClientQuicConnection(ClientConnector connector, EndPoint endPoint, Map context) { super(connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), endPoint); this.connector = connector; this.context = context; + this.inetLocalAddress = getEndPoint().getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : new InetSocketAddress(InetAddress.getLoopbackAddress(), 0xFA93); } @Override @@ -83,9 +87,15 @@ public void onOpen() quicheConfig.setDisableActiveMigration(quicConfiguration.isDisableActiveMigration()); quicheConfig.setVerifyPeer(!connector.getSslContextFactory().isTrustAll()); Map implCtx = quicConfiguration.getImplementationConfiguration(); - quicheConfig.setTrustedCertsPemPath((String)implCtx.get(QuicClientConnectorConfigurator.TRUSTED_CERTIFICATES_PEM_PATH_KEY)); - quicheConfig.setPrivKeyPemPath((String)implCtx.get(QuicClientConnectorConfigurator.PRIVATE_KEY_PEM_PATH_KEY)); - quicheConfig.setCertChainPemPath((String)implCtx.get(QuicClientConnectorConfigurator.CERTIFICATE_CHAIN_PEM_PATH_KEY)); + Path trustedCertificatesPath = (Path)implCtx.get(QuicConfiguration.TRUSTED_CERTIFICATES_PEM_PATH_KEY); + if (trustedCertificatesPath != null) + quicheConfig.setTrustedCertsPemPath(trustedCertificatesPath.toString()); + Path privateKeyPath = (Path)implCtx.get(QuicConfiguration.PRIVATE_KEY_PEM_PATH_KEY); + if (privateKeyPath != null) + quicheConfig.setPrivKeyPemPath(privateKeyPath.toString()); + Path certificatesPath = (Path)implCtx.get(QuicConfiguration.CERTIFICATE_CHAIN_PEM_PATH_KEY); + if (certificatesPath != null) + quicheConfig.setCertChainPemPath(certificatesPath.toString()); // Idle timeouts must not be managed by Quiche. quicheConfig.setMaxIdleTimeout(0L); quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow()); @@ -96,14 +106,15 @@ public void onOpen() quicheConfig.setInitialMaxStreamsBidi((long)quicConfiguration.getMaxBidirectionalRemoteStreams()); quicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); - InetSocketAddress remoteAddress = (InetSocketAddress)context.get(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY); + SocketAddress remoteAddress = (SocketAddress)context.get(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY); + inetRemoteAddress = remoteAddress instanceof InetSocketAddress inet ? inet : new InetSocketAddress(InetAddress.getLoopbackAddress(), 443); if (LOG.isDebugEnabled()) LOG.debug("connecting to {} with protocols {}", remoteAddress, protocols); - QuicheConnection quicheConnection = QuicheConnection.connect(quicheConfig, getEndPoint().getLocalAddress(), remoteAddress); - ClientQuicSession session = new ClientQuicSession(getExecutor(), getScheduler(), getByteBufferPool(), quicheConnection, this, remoteAddress, context); - pendingSessions.put(remoteAddress, session); + QuicheConnection quicheConnection = QuicheConnection.connect(quicheConfig, inetLocalAddress, inetRemoteAddress); + ClientQuicSession session = new ClientQuicSession(getExecutor(), getScheduler(), getByteBufferPool(), quicheConnection, this, inetRemoteAddress, context); + pendingSession = session; if (LOG.isDebugEnabled()) LOG.debug("created {}", session); @@ -132,30 +143,41 @@ private void connectTimeout(SocketAddress remoteAddress) if (LOG.isDebugEnabled()) LOG.debug("connect timeout {} ms to {} on {}", connector.getConnectTimeout(), remoteAddress, this); close(); - outwardClose(remoteAddress, new SocketTimeoutException("connect timeout")); + outwardClose(new SocketTimeoutException("connect timeout")); } @Override protected QuicSession createSession(SocketAddress remoteAddress, ByteBuffer cipherBuffer) throws IOException { - ClientQuicSession session = pendingSessions.get(remoteAddress); - if (session != null) + InetSocketAddress inetRemote = remoteAddress instanceof InetSocketAddress inet ? inet : inetRemoteAddress; + Runnable task = pendingSession.process(inetRemote, cipherBuffer); + pendingSession.offerTask(task); + if (pendingSession.isConnectionEstablished()) { - Runnable task = session.process(remoteAddress, cipherBuffer); - session.offerTask(task); - if (session.isConnectionEstablished()) - { - pendingSessions.remove(remoteAddress); - return session; - } + ClientQuicSession session = pendingSession; + pendingSession = null; + return session; } return null; } + @Override + public InetSocketAddress getLocalInetSocketAddress() + { + return inetLocalAddress; + } + + @Override + protected Runnable process(QuicSession session, SocketAddress remoteAddress, ByteBuffer cipherBuffer) + { + InetSocketAddress inetRemote = remoteAddress instanceof InetSocketAddress inet ? inet : inetRemoteAddress; + return super.process(session, inetRemote, cipherBuffer); + } + @Override protected void onFailure(Throwable failure) { - pendingSessions.values().forEach(session -> outwardClose(session, failure)); + outwardClose(pendingSession, failure); super.onFailure(failure); } @@ -178,21 +200,15 @@ public boolean onIdleExpired(TimeoutException timeoutException) public void outwardClose(QuicSession session, Throwable failure) { super.outwardClose(session, failure); - SocketAddress remoteAddress = session.getRemoteAddress(); - outwardClose(remoteAddress, failure); + outwardClose(failure); } - private void outwardClose(SocketAddress remoteAddress, Throwable failure) + private void outwardClose(Throwable failure) { - if (remoteAddress != null) - { - if (pendingSessions.remove(remoteAddress) != null) - { - Promise promise = (Promise)context.get(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY); - if (promise != null) - promise.failed(failure); - } - } + pendingSession = null; + Promise promise = (Promise)context.get(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY); + if (promise != null) + promise.failed(failure); getEndPoint().close(failure); } } diff --git a/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java index 2d471a85be0f..6a4bcd01a2d8 100644 --- a/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java +++ b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java @@ -19,9 +19,6 @@ import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStore; import java.util.Map; import java.util.Objects; import java.util.function.UnaryOperator; @@ -32,11 +29,9 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.quic.common.QuicConfiguration; -import org.eclipse.jetty.quic.quiche.PemExporter; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** *

A QUIC specific {@link ClientConnector.Configurator}.

@@ -45,20 +40,14 @@ * {@link SocketChannelEndPoint}s.

* * @see QuicConfiguration + * @deprecated replaced by {@link TransportProtocol} */ +@Deprecated(since = "12.0.7", forRemoval = true) public class QuicClientConnectorConfigurator extends ClientConnector.Configurator { - private static final Logger LOG = LoggerFactory.getLogger(QuicClientConnectorConfigurator.class); - - static final String PRIVATE_KEY_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".privateKeyPemPath"; - static final String CERTIFICATE_CHAIN_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".certificateChainPemPath"; - static final String TRUSTED_CERTIFICATES_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".trustedCertificatesPemPath"; - - private final QuicConfiguration configuration = new QuicConfiguration(); + private final QuicConfiguration initQuicConfig = new QuicConfiguration(); private final UnaryOperator configurator; - private Path privateKeyPemPath; - private Path certificateChainPemPath; - private Path trustedCertificatesPemPath; + private ClientQuicConfiguration quicConfig; public QuicClientConnectorConfigurator() { @@ -69,72 +58,48 @@ public QuicClientConnectorConfigurator(UnaryOperator configurator) { this.configurator = Objects.requireNonNull(configurator); // Initialize to sane defaults for a client. - configuration.setSessionRecvWindow(16 * 1024 * 1024); - configuration.setBidirectionalStreamRecvWindow(8 * 1024 * 1024); - configuration.setDisableActiveMigration(true); + initQuicConfig.setSessionRecvWindow(16 * 1024 * 1024); + initQuicConfig.setBidirectionalStreamRecvWindow(8 * 1024 * 1024); + initQuicConfig.setDisableActiveMigration(true); } public QuicConfiguration getQuicConfiguration() { - return configuration; + if (!isStarted()) + return initQuicConfig; + else + return quicConfig; } @Override protected void doStart() throws Exception { - Path pemWorkDirectory = configuration.getPemWorkDirectory(); ClientConnector clientConnector = getBean(ClientConnector.class); SslContextFactory.Client sslContextFactory = clientConnector.getSslContextFactory(); - KeyStore trustStore = sslContextFactory.getTrustStore(); - if (trustStore != null) - { - trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, pemWorkDirectory != null ? pemWorkDirectory : Path.of(System.getProperty("java.io.tmpdir"))); - configuration.getImplementationConfiguration().put(TRUSTED_CERTIFICATES_PEM_PATH_KEY, trustedCertificatesPemPath.toString()); - } - String certAlias = sslContextFactory.getCertAlias(); - if (certAlias != null) - { - if (pemWorkDirectory == null) - throw new IllegalStateException("No PEM work directory configured"); - KeyStore keyStore = sslContextFactory.getKeyStore(); - String keyManagerPassword = sslContextFactory.getKeyManagerPassword(); - char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray(); - Path[] keyPair = PemExporter.exportKeyPair(keyStore, certAlias, password, pemWorkDirectory); - privateKeyPemPath = keyPair[0]; - certificateChainPemPath = keyPair[1]; - configuration.getImplementationConfiguration().put(PRIVATE_KEY_PEM_PATH_KEY, privateKeyPemPath.toString()); - configuration.getImplementationConfiguration().put(CERTIFICATE_CHAIN_PEM_PATH_KEY, certificateChainPemPath.toString()); - } + + quicConfig = new ClientQuicConfiguration(sslContextFactory, initQuicConfig.getPemWorkDirectory()); + addBean(quicConfig); + + quicConfig.setInputBufferSize(initQuicConfig.getInputBufferSize()); + quicConfig.setOutputBufferSize(initQuicConfig.getOutputBufferSize()); + quicConfig.setUseInputDirectByteBuffers(initQuicConfig.isUseInputDirectByteBuffers()); + quicConfig.setUseOutputDirectByteBuffers(initQuicConfig.isUseOutputDirectByteBuffers()); + quicConfig.setProtocols(initQuicConfig.getProtocols()); + quicConfig.setDisableActiveMigration(initQuicConfig.isDisableActiveMigration()); + quicConfig.setMaxBidirectionalRemoteStreams(initQuicConfig.getMaxBidirectionalRemoteStreams()); + quicConfig.setMaxUnidirectionalRemoteStreams(initQuicConfig.getMaxUnidirectionalRemoteStreams()); + quicConfig.setSessionRecvWindow(initQuicConfig.getSessionRecvWindow()); + quicConfig.setBidirectionalStreamRecvWindow(initQuicConfig.getBidirectionalStreamRecvWindow()); + quicConfig.setUnidirectionalStreamRecvWindow(initQuicConfig.getUnidirectionalStreamRecvWindow()); + quicConfig.getImplementationConfiguration().putAll(initQuicConfig.getImplementationConfiguration()); + super.doStart(); } @Override - protected void doStop() throws Exception + public TransportProtocol newTransportProtocol() { - super.doStop(); - deleteFile(privateKeyPemPath); - privateKeyPemPath = null; - configuration.getImplementationConfiguration().remove(PRIVATE_KEY_PEM_PATH_KEY); - deleteFile(certificateChainPemPath); - certificateChainPemPath = null; - configuration.getImplementationConfiguration().remove(CERTIFICATE_CHAIN_PEM_PATH_KEY); - deleteFile(trustedCertificatesPemPath); - trustedCertificatesPemPath = null; - configuration.getImplementationConfiguration().remove(TRUSTED_CERTIFICATES_PEM_PATH_KEY); - } - - private void deleteFile(Path file) - { - try - { - if (file != null) - Files.delete(file); - } - catch (IOException x) - { - if (LOG.isDebugEnabled()) - LOG.debug("could not delete {}", file, x); - } + return new QuicTransportProtocol(quicConfig); } @Override @@ -146,7 +111,7 @@ public boolean isIntrinsicallySecure(ClientConnector clientConnector, SocketAddr @Override public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map context) throws IOException { - context.put(QuicConfiguration.CONTEXT_KEY, configuration); + context.put(QuicConfiguration.CONTEXT_KEY, initQuicConfig); DatagramChannel channel = DatagramChannel.open(); if (clientConnector.getBindAddress() == null) diff --git a/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicTransportProtocol.java b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicTransportProtocol.java new file mode 100644 index 000000000000..6ae1d5da8ea5 --- /dev/null +++ b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicTransportProtocol.java @@ -0,0 +1,81 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.quic.client; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.common.QuicConfiguration; + +/** + *

A {@link TransportProtocol} for QUIC that delegates to another {@code TransportProtocol}.

+ *

By default, the delegate is {@link TransportProtocol#UDP_IP}, but it may be a different + * implementation.

+ */ +public class QuicTransportProtocol extends TransportProtocol.Wrapper +{ + private final ClientQuicConfiguration quicConfiguration; + + public QuicTransportProtocol(ClientQuicConfiguration quicConfiguration) + { + this(UDP_IP, quicConfiguration); + } + + public QuicTransportProtocol(TransportProtocol wrapped, ClientQuicConfiguration quicConfiguration) + { + super(wrapped); + this.quicConfiguration = quicConfiguration; + } + + @Override + public boolean isIntrinsicallySecure() + { + return true; + } + + @Override + public Connection newConnection(EndPoint endPoint, Map context) throws IOException + { + context.put(QuicConfiguration.CONTEXT_KEY, quicConfiguration); + ClientConnector clientConnector = (ClientConnector)context.get(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY); + ClientQuicConnection connection = new ClientQuicConnection(clientConnector, endPoint, context); + connection.setInputBufferSize(quicConfiguration.getInputBufferSize()); + connection.setOutputBufferSize(quicConfiguration.getOutputBufferSize()); + connection.setUseInputDirectByteBuffers(quicConfiguration.isUseInputDirectByteBuffers()); + connection.setUseOutputDirectByteBuffers(quicConfiguration.isUseOutputDirectByteBuffers()); + quicConfiguration.getEventListeners().forEach(connection::addEventListener); + return connection; + } + + @Override + public int hashCode() + { + return getWrapped().hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj instanceof QuicTransportProtocol that) + return Objects.equals(getWrapped(), that.getWrapped()); + return false; + } +} diff --git a/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java b/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java index a9a35e429990..526b7d2d3358 100644 --- a/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java +++ b/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java @@ -74,7 +74,6 @@ public void setUp() throws Exception { keyStore.load(is, "storepwd".toCharArray()); } - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStore(keyStore); sslContextFactory.setKeyStorePassword("storepwd"); @@ -84,6 +83,7 @@ public void setUp() throws Exception HttpConfiguration httpConfiguration = new HttpConfiguration(); HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration); HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration); + // Use the deprecated APIs for backwards compatibility testing. connector = new QuicServerConnector(server, sslContextFactory, http1, http2); connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); server.addConnector(connector); @@ -100,9 +100,9 @@ public boolean handle(Request request, Response response, Callback callback) server.start(); + // Use the deprecated APIs for backwards compatibility testing. ClientConnector clientConnector = new ClientConnector(new QuicClientConnectorConfigurator()); - SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); - clientSslContextFactory.setTrustStore(keyStore); + SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(true); clientConnector.setSslContextFactory(clientSslContextFactory); ClientConnectionFactory.Info http1Info = HttpClientConnectionFactory.HTTP11; ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector)); diff --git a/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java b/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java index 50a757cf516b..7ff11ef82d98 100644 --- a/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java +++ b/jetty-core/jetty-quic/jetty-quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java @@ -95,6 +95,7 @@ public void setUp() throws Exception httpConfiguration.addCustomizer(new SecureRequestCustomizer()); HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration); HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration); + // Use the deprecated APIs for backwards compatibility testing. connector = new QuicServerConnector(server, serverSslContextFactory, http1, http2); connector.getQuicConfiguration().setPemWorkDirectory(serverWorkPath); server.addConnector(connector); @@ -111,6 +112,7 @@ public boolean handle(Request request, Response response, Callback callback) server.start(); + // Use the deprecated APIs for backwards compatibility testing. QuicClientConnectorConfigurator configurator = new QuicClientConnectorConfigurator(); configurator.getQuicConfiguration().setPemWorkDirectory(clientWorkPath); ClientConnector clientConnector = new ClientConnector(configurator); @@ -154,7 +156,7 @@ public void testServerRejectsClientInvalidCert() throws Exception server.start(); assertThrows(TimeoutException.class, () -> client.newRequest("https://localhost:" + connector.getLocalPort()) - .timeout(5, TimeUnit.SECONDS) + .timeout(1, TimeUnit.SECONDS) .send()); } } diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java index ba74b7648d4b..aeeb9e0a92fb 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java @@ -18,13 +18,22 @@ import java.util.List; import java.util.Map; +import org.eclipse.jetty.util.component.ContainerLifeCycle; + /** *

A record that captures QUIC configuration parameters.

*/ -public class QuicConfiguration +public class QuicConfiguration extends ContainerLifeCycle { public static final String CONTEXT_KEY = QuicConfiguration.class.getName(); - + public static final String PRIVATE_KEY_PEM_PATH_KEY = CONTEXT_KEY + ".privateKeyPemPath"; + public static final String CERTIFICATE_CHAIN_PEM_PATH_KEY = CONTEXT_KEY + ".certificateChainPemPath"; + public static final String TRUSTED_CERTIFICATES_PEM_PATH_KEY = CONTEXT_KEY + ".trustedCertificatesPemPath"; + + private int inputBufferSize = 2048; + private int outputBufferSize = 2048; + private boolean useInputDirectByteBuffers = true; + private boolean useOutputDirectByteBuffers = true; private List protocols = List.of(); private boolean disableActiveMigration; private int maxBidirectionalRemoteStreams; @@ -35,6 +44,46 @@ public class QuicConfiguration private Path pemWorkDirectory; private final Map implementationConfiguration = new HashMap<>(); + public int getInputBufferSize() + { + return inputBufferSize; + } + + public void setInputBufferSize(int inputBufferSize) + { + this.inputBufferSize = inputBufferSize; + } + + public int getOutputBufferSize() + { + return outputBufferSize; + } + + public void setOutputBufferSize(int outputBufferSize) + { + this.outputBufferSize = outputBufferSize; + } + + public boolean isUseInputDirectByteBuffers() + { + return useInputDirectByteBuffers; + } + + public void setUseInputDirectByteBuffers(boolean useInputDirectByteBuffers) + { + this.useInputDirectByteBuffers = useInputDirectByteBuffers; + } + + public boolean isUseOutputDirectByteBuffers() + { + return useOutputDirectByteBuffers; + } + + public void setUseOutputDirectByteBuffers(boolean useOutputDirectByteBuffers) + { + this.useOutputDirectByteBuffers = useOutputDirectByteBuffers; + } + public List getProtocols() { return protocols; @@ -112,6 +161,8 @@ public Path getPemWorkDirectory() public void setPemWorkDirectory(Path pemWorkDirectory) { + if (isStarted()) + throw new IllegalStateException("cannot change PEM working directory after start"); this.pemWorkDirectory = pemWorkDirectory; } diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConnection.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConnection.java index 0b324ec8a870..c5ff003db1b0 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConnection.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.quic.common; import java.io.IOException; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.ArrayDeque; @@ -71,19 +72,11 @@ public abstract class QuicConnection extends AbstractConnection protected QuicConnection(Executor executor, Scheduler scheduler, ByteBufferPool bufferPool, EndPoint endPoint) { super(endPoint, executor); - if (!(endPoint instanceof DatagramChannelEndPoint)) - throw new IllegalArgumentException("EndPoint must be a " + DatagramChannelEndPoint.class.getSimpleName()); this.scheduler = scheduler; this.bufferPool = bufferPool; this.strategy = new AdaptiveExecutionStrategy(new QuicProducer(), getExecutor()); } - @Override - public DatagramChannelEndPoint getEndPoint() - { - return (DatagramChannelEndPoint)super.getEndPoint(); - } - public Scheduler getScheduler() { return scheduler; @@ -211,6 +204,8 @@ public void outwardClose(QuicSession session, Throwable failure) protected abstract QuicSession createSession(SocketAddress remoteAddress, ByteBuffer cipherBuffer) throws IOException; + public abstract InetSocketAddress getLocalInetSocketAddress(); + public void write(Callback callback, SocketAddress remoteAddress, ByteBuffer... buffers) { flusher.offer(callback, remoteAddress, buffers); @@ -232,10 +227,9 @@ private Runnable receiveAndProcess() { BufferUtil.clear(cipherBuffer); SocketAddress remoteAddress = getEndPoint().receive(cipherBuffer); - int fill = remoteAddress == DatagramChannelEndPoint.EOF ? -1 : cipherBuffer.remaining(); + int fill = remoteAddress == EndPoint.EOF ? -1 : cipherBuffer.remaining(); if (LOG.isDebugEnabled()) LOG.debug("filled cipher buffer with {} byte(s)", fill); - // DatagramChannelEndPoint will only return -1 if input is shut down. if (fill < 0) { buffer.release(); @@ -314,7 +308,7 @@ private Runnable receiveAndProcess() } } - private Runnable process(QuicSession session, SocketAddress remoteAddress, ByteBuffer cipherBuffer) + protected Runnable process(QuicSession session, SocketAddress remoteAddress, ByteBuffer cipherBuffer) { try { diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java index 606f444d9dcc..664522383b34 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java @@ -306,7 +306,7 @@ public Runnable process(SocketAddress remoteAddress, ByteBuffer cipherBufferIn) int remaining = cipherBufferIn.remaining(); if (LOG.isDebugEnabled()) LOG.debug("feeding {} cipher bytes to {}", remaining, this); - int accepted = quicheConnection.feedCipherBytes(cipherBufferIn, getLocalAddress(), remoteAddress); + int accepted = quicheConnection.feedCipherBytes(cipherBufferIn, connection.getLocalInetSocketAddress(), remoteAddress); if (accepted != remaining) throw new IllegalStateException(); diff --git a/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnectionFactory.java b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnectionFactory.java new file mode 100644 index 000000000000..a93bde24664c --- /dev/null +++ b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnectionFactory.java @@ -0,0 +1,74 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.quic.server; + +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.AbstractConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

A {@link ConnectionFactory} for QUIC that can be used by + * {@link Connector}s that are not {@link QuicServerConnector}.

+ */ +public class QuicServerConnectionFactory extends AbstractConnectionFactory +{ + private static final Logger LOG = LoggerFactory.getLogger(QuicServerConnectionFactory.class); + + private final ServerQuicConfiguration quicConfiguration; + + public QuicServerConnectionFactory(ServerQuicConfiguration quicConfiguration) + { + super("quic"); + this.quicConfiguration = quicConfiguration; + } + + public ServerQuicConfiguration getQuicConfiguration() + { + return quicConfiguration; + } + + @Override + public int getInputBufferSize() + { + return quicConfiguration.getInputBufferSize(); + } + + @Override + public void setInputBufferSize(int size) + { + quicConfiguration.setInputBufferSize(size); + } + + @Override + protected void doStart() throws Exception + { + LOG.info("HTTP/3+QUIC support is experimental and not suited for production use."); + addBean(quicConfiguration); + super.doStart(); + } + + @Override + public ServerQuicConnection newConnection(Connector connector, EndPoint endPoint) + { + ServerQuicConnection connection = new ServerQuicConnection(connector, quicConfiguration, endPoint); + connection = configure(connection, connector, endPoint); + connection.setOutputBufferSize(quicConfiguration.getOutputBufferSize()); + connection.setUseInputDirectByteBuffers(quicConfiguration.isUseInputDirectByteBuffers()); + connection.setUseOutputDirectByteBuffers(quicConfiguration.isUseOutputDirectByteBuffers()); + return connection; + } +} diff --git a/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java index a5726e261ef3..789bda3ee286 100644 --- a/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java +++ b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java @@ -20,10 +20,7 @@ import java.nio.channels.SelectionKey; import java.nio.file.Files; import java.nio.file.Path; -import java.security.KeyStore; import java.util.EventListener; -import java.util.List; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -33,12 +30,9 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SelectorManager; -import org.eclipse.jetty.quic.common.QuicConfiguration; import org.eclipse.jetty.quic.common.QuicSession; import org.eclipse.jetty.quic.common.QuicSessionContainer; import org.eclipse.jetty.quic.common.QuicStreamEndPoint; -import org.eclipse.jetty.quic.quiche.PemExporter; -import org.eclipse.jetty.quic.quiche.QuicheConfig; import org.eclipse.jetty.server.AbstractNetworkConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Server; @@ -48,54 +42,63 @@ /** *

A server side network connector that uses a {@link DatagramChannel} to listen on a network port for QUIC traffic.

- *

This connector uses {@link ConnectionFactory}s to configure the protocols to support. + *

This connector uses {@link ConnectionFactory}s to configure the protocols to be transported by QUIC. * The protocol is negotiated during the connection establishment by {@link QuicSession}, and for each QUIC stream * managed by a {@link QuicSession} a {@link ConnectionFactory} is used to create a {@link Connection} for the * correspondent {@link QuicStreamEndPoint}.

* - * @see QuicConfiguration + * @see ServerQuicConfiguration */ public class QuicServerConnector extends AbstractNetworkConnector { - private final QuicConfiguration quicConfiguration = new QuicConfiguration(); private final QuicSessionContainer container = new QuicSessionContainer(); private final ServerDatagramSelectorManager selectorManager; - private final SslContextFactory.Server sslContextFactory; - private Path privateKeyPemPath; - private Path certificateChainPemPath; - private Path trustedCertificatesPemPath; + private final QuicServerConnectionFactory connectionFactory; private volatile DatagramChannel datagramChannel; private volatile int localPort = -1; - private int inputBufferSize = 2048; - private int outputBufferSize = 2048; - private boolean useInputDirectByteBuffers = true; - private boolean useOutputDirectByteBuffers = true; + /** + * @param server the {@link Server} + * @param sslContextFactory the {@link SslContextFactory.Server} + * @param factories the {@link ConnectionFactory}s of the protocols transported by QUIC + * @deprecated use {@link #QuicServerConnector(Server, ServerQuicConfiguration, ConnectionFactory...)} instead + */ + @Deprecated(since = "12.0.7", forRemoval = true) public QuicServerConnector(Server server, SslContextFactory.Server sslContextFactory, ConnectionFactory... factories) { - this(server, null, null, null, sslContextFactory, factories); + this(server, new ServerQuicConfiguration(sslContextFactory, null), factories); } + public QuicServerConnector(Server server, ServerQuicConfiguration quicConfiguration, ConnectionFactory... factories) + { + this(server, null, null, null, quicConfiguration, factories); + } + + /** + * @param server the {@link Server} + * @param executor the {@link Executor} + * @param scheduler the {@link Scheduler} + * @param bufferPool the {@link ByteBufferPool} + * @param sslContextFactory the {@link SslContextFactory.Server} + * @param factories the {@link ConnectionFactory}s of the protocols transported by QUIC + * @deprecated use {@link #QuicServerConnector(Server, Executor, Scheduler, ByteBufferPool, ServerQuicConfiguration, ConnectionFactory...)} instead + */ + @Deprecated(since = "12.0.7", forRemoval = true) public QuicServerConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool bufferPool, SslContextFactory.Server sslContextFactory, ConnectionFactory... factories) + { + this(server, executor, scheduler, bufferPool, new ServerQuicConfiguration(sslContextFactory, null), factories); + } + + public QuicServerConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool bufferPool, ServerQuicConfiguration quicConfiguration, ConnectionFactory... factories) { super(server, executor, scheduler, bufferPool, 0, factories); this.selectorManager = new ServerDatagramSelectorManager(getExecutor(), getScheduler(), 1); - addBean(this.selectorManager); - this.sslContextFactory = sslContextFactory; - addBean(this.sslContextFactory); - addBean(quicConfiguration); - addBean(container); - // Initialize to sane defaults for a server. - quicConfiguration.setSessionRecvWindow(4 * 1024 * 1024); - quicConfiguration.setBidirectionalStreamRecvWindow(2 * 1024 * 1024); - // One bidirectional stream to simulate the TCP stream, and no unidirectional streams. - quicConfiguration.setMaxBidirectionalRemoteStreams(1); - quicConfiguration.setMaxUnidirectionalRemoteStreams(0); + this.connectionFactory = new QuicServerConnectionFactory(quicConfiguration); } - public QuicConfiguration getQuicConfiguration() + public ServerQuicConfiguration getQuicConfiguration() { - return quicConfiguration; + return connectionFactory.getQuicConfiguration(); } @Override @@ -106,42 +109,42 @@ public int getLocalPort() public int getInputBufferSize() { - return inputBufferSize; + return getQuicConfiguration().getInputBufferSize(); } public void setInputBufferSize(int inputBufferSize) { - this.inputBufferSize = inputBufferSize; + getQuicConfiguration().setInputBufferSize(inputBufferSize); } public int getOutputBufferSize() { - return outputBufferSize; + return getQuicConfiguration().getOutputBufferSize(); } public void setOutputBufferSize(int outputBufferSize) { - this.outputBufferSize = outputBufferSize; + getQuicConfiguration().setOutputBufferSize(outputBufferSize); } public boolean isUseInputDirectByteBuffers() { - return useInputDirectByteBuffers; + return getQuicConfiguration().isUseInputDirectByteBuffers(); } public void setUseInputDirectByteBuffers(boolean useInputDirectByteBuffers) { - this.useInputDirectByteBuffers = useInputDirectByteBuffers; + getQuicConfiguration().setUseInputDirectByteBuffers(useInputDirectByteBuffers); } public boolean isUseOutputDirectByteBuffers() { - return useOutputDirectByteBuffers; + return getQuicConfiguration().isUseOutputDirectByteBuffers(); } public void setUseOutputDirectByteBuffers(boolean useOutputDirectByteBuffers) { - this.useOutputDirectByteBuffers = useOutputDirectByteBuffers; + getQuicConfiguration().setUseOutputDirectByteBuffers(useOutputDirectByteBuffers); } @Override @@ -154,27 +157,18 @@ public boolean isOpen() @Override protected void doStart() throws Exception { + addBean(container); + addBean(selectorManager); + addBean(connectionFactory); + for (EventListener l : getBeans(SelectorManager.SelectorManagerListener.class)) selectorManager.addEventListener(l); + + connectionFactory.getQuicConfiguration().setPemWorkDirectory(findPemWorkDirectory()); + super.doStart(); - selectorManager.accept(datagramChannel); - Set aliases = sslContextFactory.getAliases(); - if (aliases.isEmpty()) - throw new IllegalStateException("Missing or invalid KeyStore: a SslContextFactory configured with a valid, non-empty KeyStore is required"); - String alias = sslContextFactory.getCertAlias(); - if (alias == null) - alias = aliases.stream().findFirst().orElseThrow(); - String keyManagerPassword = sslContextFactory.getKeyManagerPassword(); - char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray(); - KeyStore keyStore = sslContextFactory.getKeyStore(); - Path certificateWorkPath = findPemWorkDirectory(); - Path[] keyPair = PemExporter.exportKeyPair(keyStore, alias, password, certificateWorkPath); - privateKeyPemPath = keyPair[0]; - certificateChainPemPath = keyPair[1]; - KeyStore trustStore = sslContextFactory.getTrustStore(); - if (trustStore != null) - trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, certificateWorkPath); + selectorManager.accept(datagramChannel); } private Path findPemWorkDirectory() @@ -222,29 +216,6 @@ protected DatagramChannel openDatagramChannel() throws IOException } } - QuicheConfig newQuicheConfig() - { - QuicheConfig quicheConfig = new QuicheConfig(); - quicheConfig.setPrivKeyPemPath(privateKeyPemPath.toString()); - quicheConfig.setCertChainPemPath(certificateChainPemPath.toString()); - quicheConfig.setTrustedCertsPemPath(trustedCertificatesPemPath == null ? null : trustedCertificatesPemPath.toString()); - quicheConfig.setVerifyPeer(sslContextFactory.getNeedClientAuth() || sslContextFactory.getWantClientAuth()); - // Idle timeouts must not be managed by Quiche. - quicheConfig.setMaxIdleTimeout(0L); - quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow()); - quicheConfig.setInitialMaxStreamDataBidiLocal((long)quicConfiguration.getBidirectionalStreamRecvWindow()); - quicheConfig.setInitialMaxStreamDataBidiRemote((long)quicConfiguration.getBidirectionalStreamRecvWindow()); - quicheConfig.setInitialMaxStreamDataUni((long)quicConfiguration.getUnidirectionalStreamRecvWindow()); - quicheConfig.setInitialMaxStreamsUni((long)quicConfiguration.getMaxUnidirectionalRemoteStreams()); - quicheConfig.setInitialMaxStreamsBidi((long)quicConfiguration.getMaxBidirectionalRemoteStreams()); - quicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); - List protocols = getProtocols(); - // This is only needed for Quiche example clients. - protocols.add(0, "http/0.9"); - quicheConfig.setApplicationProtos(protocols.toArray(String[]::new)); - return quicheConfig; - } - @Override public void setIdleTimeout(long idleTimeout) { @@ -255,13 +226,6 @@ public void setIdleTimeout(long idleTimeout) @Override protected void doStop() throws Exception { - deleteFile(privateKeyPemPath); - privateKeyPemPath = null; - deleteFile(certificateChainPemPath); - certificateChainPemPath = null; - deleteFile(trustedCertificatesPemPath); - trustedCertificatesPemPath = null; - // We want the DatagramChannel to be stopped by the SelectorManager. super.doStop(); @@ -269,24 +233,12 @@ protected void doStop() throws Exception datagramChannel = null; localPort = -2; + removeBean(connectionFactory); + for (EventListener l : getBeans(EventListener.class)) selectorManager.removeEventListener(l); } - private void deleteFile(Path file) - { - try - { - if (file != null) - Files.delete(file); - } - catch (IOException x) - { - if (LOG.isDebugEnabled()) - LOG.debug("could not delete {}", file, x); - } - } - @Override public CompletableFuture shutdown() { @@ -312,7 +264,7 @@ protected EndPoint newEndPoint(DatagramChannel channel, ManagedSelector selector protected ServerQuicConnection newConnection(EndPoint endpoint) { - return new ServerQuicConnection(QuicServerConnector.this, endpoint); + return connectionFactory.newConnection(QuicServerConnector.this, endpoint); } private class ServerDatagramSelectorManager extends SelectorManager @@ -333,13 +285,7 @@ protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector select @Override public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) { - ServerQuicConnection connection = QuicServerConnector.this.newConnection(endpoint); - connection.addEventListener(container); - connection.setInputBufferSize(getInputBufferSize()); - connection.setOutputBufferSize(getOutputBufferSize()); - connection.setUseInputDirectByteBuffers(isUseInputDirectByteBuffers()); - connection.setUseOutputDirectByteBuffers(isUseOutputDirectByteBuffers()); - return connection; + return QuicServerConnector.this.newConnection(endpoint); } @Override diff --git a/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConfiguration.java b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConfiguration.java new file mode 100644 index 000000000000..00d959cda03a --- /dev/null +++ b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConfiguration.java @@ -0,0 +1,111 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.quic.server; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.util.Set; + +import org.eclipse.jetty.quic.common.QuicConfiguration; +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

Server-side {@link QuicConfiguration} with server-specific settings.

+ *

The PEM working directory constructor argument is mandatory, although + * it may be set after construction via {@link #setPemWorkDirectory(Path)} + * before starting this instance.

+ */ +public class ServerQuicConfiguration extends QuicConfiguration +{ + private static final Logger LOG = LoggerFactory.getLogger(ServerQuicConfiguration.class); + + private final SslContextFactory.Server sslContextFactory; + + public ServerQuicConfiguration(SslContextFactory.Server sslContextFactory, Path pemWorkDirectory) + { + this.sslContextFactory = sslContextFactory; + setPemWorkDirectory(pemWorkDirectory); + setSessionRecvWindow(4 * 1024 * 1024); + setBidirectionalStreamRecvWindow(2 * 1024 * 1024); + // One bidirectional stream to simulate the TCP stream, and no unidirectional streams. + setMaxBidirectionalRemoteStreams(1); + setMaxUnidirectionalRemoteStreams(0); + } + + public SslContextFactory.Server getSslContextFactory() + { + return sslContextFactory; + } + + @Override + protected void doStart() throws Exception + { + addBean(sslContextFactory); + + super.doStart(); + + Path pemWorkDirectory = getPemWorkDirectory(); + Set aliases = sslContextFactory.getAliases(); + if (aliases.isEmpty()) + throw new IllegalStateException("Missing or invalid KeyStore: a SslContextFactory configured with a valid, non-empty KeyStore is required"); + String alias = sslContextFactory.getCertAlias(); + if (alias == null) + alias = aliases.stream().findFirst().orElseThrow(); + String keyManagerPassword = sslContextFactory.getKeyManagerPassword(); + char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray(); + KeyStore keyStore = sslContextFactory.getKeyStore(); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, alias, password, pemWorkDirectory); + Path privateKeyPemPath = keyPair[0]; + getImplementationConfiguration().put(PRIVATE_KEY_PEM_PATH_KEY, privateKeyPemPath); + Path certificateChainPemPath = keyPair[1]; + getImplementationConfiguration().put(CERTIFICATE_CHAIN_PEM_PATH_KEY, certificateChainPemPath); + KeyStore trustStore = sslContextFactory.getTrustStore(); + if (trustStore != null) + { + Path trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, pemWorkDirectory); + getImplementationConfiguration().put(TRUSTED_CERTIFICATES_PEM_PATH_KEY, trustedCertificatesPemPath); + } + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + + Path trustedCertificatesPemPath = (Path)getImplementationConfiguration().remove(TRUSTED_CERTIFICATES_PEM_PATH_KEY); + deleteFile(trustedCertificatesPemPath); + Path certificateChainPemPath = (Path)getImplementationConfiguration().remove(CERTIFICATE_CHAIN_PEM_PATH_KEY); + deleteFile(certificateChainPemPath); + Path privateKeyPemPath = (Path)getImplementationConfiguration().remove(PRIVATE_KEY_PEM_PATH_KEY); + deleteFile(privateKeyPemPath); + } + + private void deleteFile(Path path) + { + try + { + if (path != null) + Files.delete(path); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("could not delete {}", path, x); + } + } +} diff --git a/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConnection.java b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConnection.java index 08a212857fef..ce9d77f44844 100644 --- a/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-server/src/main/java/org/eclipse/jetty/quic/server/ServerQuicConnection.java @@ -14,23 +14,32 @@ package org.eclipse.jetty.quic.server; import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.CyclicTimeouts; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.RetainableByteBuffer; +import org.eclipse.jetty.quic.common.QuicConfiguration; import org.eclipse.jetty.quic.common.QuicConnection; import org.eclipse.jetty.quic.common.QuicSession; +import org.eclipse.jetty.quic.quiche.QuicheConfig; import org.eclipse.jetty.quic.quiche.QuicheConnection; import org.eclipse.jetty.quic.server.internal.SimpleTokenMinter; import org.eclipse.jetty.quic.server.internal.SimpleTokenValidator; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,17 +51,22 @@ public class ServerQuicConnection extends QuicConnection { private static final Logger LOG = LoggerFactory.getLogger(ServerQuicConnection.class); - private final QuicServerConnector connector; + private final Map remoteSocketAddresses = new ConcurrentHashMap<>(); + private final Connector connector; + private final ServerQuicConfiguration quicConfiguration; private final SessionTimeouts sessionTimeouts; + private final InetSocketAddress inetLocalAddress; - public ServerQuicConnection(QuicServerConnector connector, EndPoint endPoint) + public ServerQuicConnection(Connector connector, ServerQuicConfiguration quicConfiguration, EndPoint endPoint) { super(connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), endPoint); this.connector = connector; + this.quicConfiguration = quicConfiguration; this.sessionTimeouts = new SessionTimeouts(connector.getScheduler()); + this.inetLocalAddress = endPoint.getLocalSocketAddress() instanceof InetSocketAddress inet ? inet : new InetSocketAddress(InetAddress.getLoopbackAddress(), 443); } - public QuicServerConnector getQuicServerConnector() + public Connector getQuicServerConnector() { return connector; } @@ -64,19 +78,27 @@ public void onOpen() fillInterested(); } + private InetSocketAddress toInetSocketAddress(SocketAddress socketAddress) + { + if (socketAddress instanceof InetSocketAddress inet) + return inet; + return remoteSocketAddresses.computeIfAbsent(socketAddress, key -> new InetSocketAddress(InetAddress.getLoopbackAddress(), 0xFA93)); + } + @Override protected QuicSession createSession(SocketAddress remoteAddress, ByteBuffer cipherBuffer) throws IOException { + InetSocketAddress inetRemote = toInetSocketAddress(remoteAddress); ByteBufferPool bufferPool = getByteBufferPool(); // TODO make the token validator configurable - QuicheConnection quicheConnection = QuicheConnection.tryAccept(connector.newQuicheConfig(), new SimpleTokenValidator((InetSocketAddress)remoteAddress), cipherBuffer, getEndPoint().getLocalAddress(), remoteAddress); + QuicheConnection quicheConnection = QuicheConnection.tryAccept(newQuicheConfig(), new SimpleTokenValidator(inetRemote), cipherBuffer, inetLocalAddress, inetRemote); if (quicheConnection == null) { RetainableByteBuffer negotiationBuffer = bufferPool.acquire(getOutputBufferSize(), true); ByteBuffer byteBuffer = negotiationBuffer.getByteBuffer(); int pos = BufferUtil.flipToFill(byteBuffer); // TODO make the token minter configurable - if (!QuicheConnection.negotiate(new SimpleTokenMinter((InetSocketAddress)remoteAddress), cipherBuffer, byteBuffer)) + if (!QuicheConnection.negotiate(new SimpleTokenMinter(inetRemote), cipherBuffer, byteBuffer)) { if (LOG.isDebugEnabled()) LOG.debug("QUIC connection negotiation failed, dropping packet"); @@ -104,6 +126,48 @@ protected ServerQuicSession newQuicSession(SocketAddress remoteAddress, QuicheCo return new ServerQuicSession(getExecutor(), getScheduler(), getByteBufferPool(), quicheConnection, this, remoteAddress, getQuicServerConnector()); } + @Override + public InetSocketAddress getLocalInetSocketAddress() + { + return inetLocalAddress; + } + + @Override + protected Runnable process(QuicSession session, SocketAddress remoteAddress, ByteBuffer cipherBuffer) + { + InetSocketAddress inetRemote = toInetSocketAddress(remoteAddress); + return super.process(session, inetRemote, cipherBuffer); + } + + private QuicheConfig newQuicheConfig() + { + QuicheConfig quicheConfig = new QuicheConfig(); + Map implConfig = quicConfiguration.getImplementationConfiguration(); + Path privateKeyPath = (Path)implConfig.get(QuicConfiguration.PRIVATE_KEY_PEM_PATH_KEY); + quicheConfig.setPrivKeyPemPath(privateKeyPath.toString()); + Path certificatesPath = (Path)implConfig.get(QuicConfiguration.CERTIFICATE_CHAIN_PEM_PATH_KEY); + quicheConfig.setCertChainPemPath(certificatesPath.toString()); + Path trustedCertificatesPath = (Path)implConfig.get(QuicConfiguration.TRUSTED_CERTIFICATES_PEM_PATH_KEY); + if (trustedCertificatesPath != null) + quicheConfig.setTrustedCertsPemPath(trustedCertificatesPath.toString()); + SslContextFactory.Server sslContextFactory = quicConfiguration.getSslContextFactory(); + quicheConfig.setVerifyPeer(sslContextFactory.getNeedClientAuth() || sslContextFactory.getWantClientAuth()); + // Idle timeouts must not be managed by Quiche. + quicheConfig.setMaxIdleTimeout(0L); + quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow()); + quicheConfig.setInitialMaxStreamDataBidiLocal((long)quicConfiguration.getBidirectionalStreamRecvWindow()); + quicheConfig.setInitialMaxStreamDataBidiRemote((long)quicConfiguration.getBidirectionalStreamRecvWindow()); + quicheConfig.setInitialMaxStreamDataUni((long)quicConfiguration.getUnidirectionalStreamRecvWindow()); + quicheConfig.setInitialMaxStreamsUni((long)quicConfiguration.getMaxUnidirectionalRemoteStreams()); + quicheConfig.setInitialMaxStreamsBidi((long)quicConfiguration.getMaxBidirectionalRemoteStreams()); + quicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); + List protocols = connector.getProtocols(); + // This is only needed for Quiche example clients. + protocols.add(0, "http/0.9"); + quicheConfig.setApplicationProtos(protocols.toArray(String[]::new)); + return quicheConfig; + } + public void schedule(ServerQuicSession session) { sessionTimeouts.schedule(session); diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java index 019da630d8ef..130324f1ef89 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java @@ -90,16 +90,16 @@ protected static String findNextProtocol(Connector connector, String currentProt return nextProtocol; } - protected AbstractConnection configure(AbstractConnection connection, Connector connector, EndPoint endPoint) + protected T configure(T connection, Connector connector, EndPoint endPoint) { - connection.setInputBufferSize(getInputBufferSize()); - - // Add Connection.Listeners from Connector + // Add Connection.Listeners from Connector. connector.getEventListeners().forEach(connection::addEventListener); - // Add Connection.Listeners from this factory + // Add Connection.Listeners from this factory. getEventListeners().forEach(connection::addEventListener); + connection.setInputBufferSize(getInputBufferSize()); + return connection; } diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryConnector.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryConnector.java new file mode 100644 index 000000000000..2df1a2c6dabc --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryConnector.java @@ -0,0 +1,203 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.server; + +import java.io.IOException; +import java.net.SocketAddress; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; + +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.MemoryEndPointPipe; +import org.eclipse.jetty.util.thread.ExecutionStrategy; +import org.eclipse.jetty.util.thread.Invocable; +import org.eclipse.jetty.util.thread.Scheduler; +import org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

A server {@link Connector} that allows clients to communicate via memory.

+ *

Typical usage on the server-side:

+ *
{@code
+ * Server server = new Server();
+ * MemoryConnector memoryConnector = new MemoryConnector(server, new HttpConnectionFactory());
+ * server.addConnector(memoryConnector);
+ * server.start();
+ * }
+ *

Typical usage on the client-side:

+ *
 {@code
+ * // Connect to the server and get the local, client-side, EndPoint.
+ * EndPoint clientEndPoint = memoryConnector.connect().getLocalEndPoint();
+ *
+ * // Be ready to read responses.
+ * Callback readCallback = ...;
+ * clientEndPoint.fillInterested(readCallback);
+ *
+ * // Write a request to the server.
+ * ByteBuffer request = StandardCharsets.UTF_8.encode("""
+ *     GET / HTTP/1.1
+ *     Host: localhost
+ *
+ *     """);
+ * Callback.Completable writeCallback = new Callback.Completable();
+ * clientEndPoint.write(writeCallback, request);
+ * }
+ */ +public class MemoryConnector extends AbstractConnector +{ + private static final Logger LOG = LoggerFactory.getLogger(MemoryConnector.class); + + private final SocketAddress socketAddress = new MemorySocketAddress(); + private final TaskProducer producer = new TaskProducer(); + private ExecutionStrategy strategy; + + public MemoryConnector(Server server, ConnectionFactory... factories) + { + this(server, null, null, null, factories); + } + + public MemoryConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool bufferPool, ConnectionFactory... factories) + { + super(server, executor, scheduler, bufferPool, 0, factories); + } + + @Override + protected void doStart() throws Exception + { + strategy = new AdaptiveExecutionStrategy(producer, getExecutor()); + addBean(strategy); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + removeBean(strategy); + } + + @Override + public Object getTransport() + { + return null; + } + + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException + { + // Nothing to do here. + } + + /** + *

Client-side applications use this method to connect to the server and obtain a {@link EndPoint.Pipe}.

+ *

Client-side applications should then use {@link EndPoint.Pipe#getLocalEndPoint()} to access the + * client-side {@link EndPoint} to write requests bytes to the server and read response bytes.

+ * + * @return a {@link EndPoint.Pipe} representing the connection between client and server + */ + public EndPoint.Pipe connect() + { + MemoryEndPointPipe pipe = new MemoryEndPointPipe(getScheduler(), producer::offer, socketAddress); + accept(pipe.getRemoteEndPoint()); + + if (LOG.isDebugEnabled()) + LOG.debug("connected {} to {}", pipe, this); + + return pipe; + } + + private void accept(EndPoint endPoint) + { + endPoint.setIdleTimeout(getIdleTimeout()); + + AbstractConnection connection = (AbstractConnection)getDefaultConnectionFactory().newConnection(this, endPoint); + endPoint.setConnection(connection); + + endPoint.onOpen(); + onEndPointOpened(endPoint); + + connection.addEventListener(new Connection.Listener() + { + @Override + public void onClosed(Connection connection) + { + onEndPointClosed(endPoint); + } + }); + + connection.onOpen(); + + if (LOG.isDebugEnabled()) + LOG.debug("accepted {} in {}", endPoint, this); + } + + /** + * @return the local {@link SocketAddress} of this connector + */ + public SocketAddress getLocalSocketAddress() + { + return socketAddress; + } + + private class TaskProducer implements ExecutionStrategy.Producer + { + private final Queue tasks = new ConcurrentLinkedQueue<>(); + + @Override + public Runnable produce() + { + return tasks.poll(); + } + + private void offer(Invocable.Task task) + { + if (LOG.isDebugEnabled()) + LOG.debug("offer {} to {}", task, MemoryConnector.this); + tasks.offer(task); + strategy.produce(); + } + } + + private class MemorySocketAddress extends SocketAddress + { + private final String address = "[memory:@%x]".formatted(System.identityHashCode(MemoryConnector.this)); + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj instanceof MemorySocketAddress that) + return address.equals(that.address); + return false; + } + + @Override + public int hashCode() + { + return address.hashCode(); + } + + @Override + public String toString() + { + return address; + } + } +} diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryTransportProtocol.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryTransportProtocol.java new file mode 100644 index 000000000000..9ad145357a45 --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryTransportProtocol.java @@ -0,0 +1,89 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.server; + +import java.net.SocketAddress; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.util.Promise; + +/** + *

A {@link TransportProtocol} suitable to be used when using a {@link MemoryConnector}.

+ */ +public class MemoryTransportProtocol implements TransportProtocol +{ + private final MemoryConnector connector; + + public MemoryTransportProtocol(MemoryConnector connector) + { + this.connector = connector; + } + + @Override + public void connect(SocketAddress socketAddress, Map context) + { + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY); + try + { + EndPoint endPoint = connector.connect().getLocalEndPoint(); + ClientConnector clientConnector = (ClientConnector)context.get(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY); + endPoint.setIdleTimeout(clientConnector.getIdleTimeout().toMillis()); + + // This instance may be nested inside other TransportProtocol instances. + // Retrieve the outermost instance to call newConnection(). + TransportProtocol transportProtocol = (TransportProtocol)context.get(TransportProtocol.class.getName()); + Connection connection = transportProtocol.newConnection(endPoint, context); + endPoint.setConnection(connection); + + endPoint.onOpen(); + connection.onOpen(); + + // TODO: move this to Connection.onOpen(), see + // ClientSelectorManager.connectionOpened() + promise.succeeded(connection); + } + catch (Throwable x) + { + promise.failed(x); + } + } + + @Override + public SocketAddress getSocketAddress() + { + return connector.getLocalSocketAddress(); + } + + @Override + public int hashCode() + { + return Objects.hash(connector); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj instanceof MemoryTransportProtocol that) + return Objects.equals(connector, that.connector); + return false; + } +} diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java index 9246f05b3677..aa894af0c3a5 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java @@ -169,7 +169,7 @@ protected SslConnection newSslConnection(Connector connector, EndPoint endPoint, } @Override - protected AbstractConnection configure(AbstractConnection connection, Connector connector, EndPoint endPoint) + protected T configure(T connection, Connector connector, EndPoint endPoint) { if (connection instanceof SslConnection sslConnection) { diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java index eee7f0a138b4..b51f938ea72f 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTest.java @@ -13,13 +13,11 @@ package org.eclipse.jetty.test.client.transport; -import java.io.InputStream; import java.lang.management.ManagementFactory; import java.lang.reflect.AnnotatedElement; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.security.KeyStore; import java.util.Collection; import java.util.Comparator; import java.util.EnumSet; @@ -45,10 +43,11 @@ import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; import org.eclipse.jetty.http3.server.AbstractHTTP3ServerConnectionFactory; import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; @@ -60,9 +59,9 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.toolchain.test.MavenPaths; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -76,7 +75,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @ExtendWith(WorkDirExtension.class) @@ -89,10 +87,10 @@ public class AbstractTest System.err.printf("Running %s.%s() %s%n", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), context.getDisplayName()); protected final HttpConfiguration httpConfig = new HttpConfiguration(); protected SslContextFactory.Server sslContextFactoryServer; + protected ServerQuicConfiguration serverQuicConfig; protected Server server; protected AbstractConnector connector; protected HttpClient client; - protected Path unixDomainPath; protected ArrayByteBufferPool.Tracking serverBufferPool; protected ArrayByteBufferPool.Tracking clientBufferPool; @@ -111,18 +109,10 @@ public static Collection transportsNoFCGI() return transports; } - public static Collection transportsNoUnixDomain() - { - Collection transports = transports(); - transports.remove(Transport.UNIX_DOMAIN); - return transports; - } - public static Collection transportsTCP() { Collection transports = transports(); transports.remove(Transport.H3); - transports.remove(Transport.UNIX_DOMAIN); return transports; } @@ -177,7 +167,7 @@ private static boolean isLeakTrackingDisabled(TestInfo testInfo, String tagSubVa String[] transportNames = transports.split("\\|"); boolean disabled = isAnnotatedWithTagValue(testInfo.getTestMethod().orElseThrow(), disableLeakTrackingTagValue) || - isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue); + isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue); if (disabled) { System.err.println("Not tracking " + tagSubValue + " leaks"); @@ -187,7 +177,7 @@ private static boolean isLeakTrackingDisabled(TestInfo testInfo, String tagSubVa for (String transportName : transportNames) { disabled = isAnnotatedWithTagValue(testInfo.getTestMethod().orElseThrow(), disableLeakTrackingTagValue + ":" + transportName) || - isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue + ":" + transportName); + isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue + ":" + transportName); if (disabled) { System.err.println("Not tracking " + tagSubValue + " leaks for transport " + transportName); @@ -196,7 +186,7 @@ private static boolean isLeakTrackingDisabled(TestInfo testInfo, String tagSubVa } disabled = isAnnotatedWithTagValue(testInfo.getTestMethod().orElseThrow(), disableLeakTrackingTagValue + ":" + tagSubValue) || - isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue + ":" + tagSubValue); + isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue + ":" + tagSubValue); if (disabled) { System.err.println("Not tracking " + tagSubValue + " leaks"); @@ -206,7 +196,7 @@ private static boolean isLeakTrackingDisabled(TestInfo testInfo, String tagSubVa for (String transportName : transportNames) { disabled = isAnnotatedWithTagValue(testInfo.getTestMethod().orElseThrow(), disableLeakTrackingTagValue + ":" + tagSubValue + ":" + transportName) || - isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue + ":" + tagSubValue + ":" + transportName); + isAnnotatedWithTagValue(testInfo.getTestClass().orElseThrow(), disableLeakTrackingTagValue + ":" + tagSubValue + ":" + transportName); if (disabled) { System.err.println("Not tracking " + tagSubValue + " leaks for transport " + transportName); @@ -272,14 +262,8 @@ protected void startServer(Transport transport, Handler handler) throws Exceptio protected void prepareServer(Transport transport, Handler handler) throws Exception { - if (transport == Transport.UNIX_DOMAIN) - { - String unixDomainDir = System.getProperty("jetty.unixdomain.dir", System.getProperty("java.io.tmpdir")); - unixDomainPath = Files.createTempFile(Path.of(unixDomainDir), "unix_", ".sock"); - assertTrue(unixDomainPath.toAbsolutePath().toString().length() < UnixDomainServerConnector.MAX_UNIX_DOMAIN_PATH_LENGTH, "Unix-Domain path too long"); - Files.delete(unixDomainPath); - } sslContextFactoryServer = newSslContextFactoryServer(); + serverQuicConfig = new ServerQuicConfiguration(sslContextFactoryServer, workDir.getEmptyPathDir()); if (server == null) server = newServer(); connector = newConnector(transport, server); @@ -295,27 +279,16 @@ protected Server newServer() return new Server(serverThreads, null, serverBufferPool); } - protected SslContextFactory.Server newSslContextFactoryServer() throws Exception + protected SslContextFactory.Server newSslContextFactoryServer() { SslContextFactory.Server ssl = new SslContextFactory.Server(); - configureSslContextFactory(ssl); + ssl.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + ssl.setKeyStorePassword("storepwd"); + ssl.setUseCipherSuitesOrder(true); + ssl.setCipherComparator(HTTP2Cipher.COMPARATOR); return ssl; } - private void configureSslContextFactory(SslContextFactory sslContextFactory) throws Exception - { - KeyStore keystore = KeyStore.getInstance("PKCS12"); - try (InputStream is = Files.newInputStream(Path.of("src/test/resources/keystore.p12"))) - { - keystore.load(is, "storepwd".toCharArray()); - } - sslContextFactory.setTrustStore(keystore); - sslContextFactory.setKeyStore(keystore); - sslContextFactory.setKeyStorePassword("storepwd"); - sslContextFactory.setUseCipherSuitesOrder(true); - sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); - } - protected void startClient(Transport transport) throws Exception { QueuedThreadPool clientThreads = new QueuedThreadPool(); @@ -339,13 +312,7 @@ public AbstractConnector newConnector(Transport transport, Server server) case FCGI: yield new ServerConnector(server, 1, 1, newServerConnectionFactory(transport)); case H3: - HTTP3ServerConnector h3Connector = new HTTP3ServerConnector(server, sslContextFactoryServer, newServerConnectionFactory(transport)); - h3Connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); - yield h3Connector; - case UNIX_DOMAIN: - UnixDomainServerConnector unixConnector = new UnixDomainServerConnector(server, 1, 1, newServerConnectionFactory(transport)); - unixConnector.setUnixDomainPath(unixDomainPath); - yield unixConnector; + yield new QuicServerConnector(server, serverQuicConfig, newServerConnectionFactory(transport)); }; } @@ -353,7 +320,7 @@ protected ConnectionFactory[] newServerConnectionFactory(Transport transport) { List list = switch (transport) { - case HTTP, UNIX_DOMAIN -> List.of(new HttpConnectionFactory(httpConfig)); + case HTTP -> List.of(new HttpConnectionFactory(httpConfig)); case HTTPS -> { httpConfig.addCustomizer(new SecureRequestCustomizer()); @@ -379,57 +346,47 @@ protected ConnectionFactory[] newServerConnectionFactory(Transport transport) { httpConfig.addCustomizer(new SecureRequestCustomizer()); httpConfig.addCustomizer(new HostHeaderCustomizer()); - yield List.of(new HTTP3ServerConnectionFactory(httpConfig)); + yield List.of(new HTTP3ServerConnectionFactory(serverQuicConfig, httpConfig)); } case FCGI -> List.of(new ServerFCGIConnectionFactory(httpConfig)); }; return list.toArray(ConnectionFactory[]::new); } - protected SslContextFactory.Client newSslContextFactoryClient() throws Exception + protected SslContextFactory.Client newSslContextFactoryClient() { - SslContextFactory.Client ssl = new SslContextFactory.Client(); - configureSslContextFactory(ssl); - ssl.setEndpointIdentificationAlgorithm(null); - return ssl; + return new SslContextFactory.Client(true); } protected HttpClientTransport newHttpClientTransport(Transport transport) throws Exception { return switch (transport) + { + case HTTP, HTTPS -> { - case HTTP, HTTPS -> - { - ClientConnector clientConnector = new ClientConnector(); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - yield new HttpClientTransportOverHTTP(clientConnector); - } - case H2C, H2 -> - { - ClientConnector clientConnector = new ClientConnector(); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - HTTP2Client http2Client = new HTTP2Client(clientConnector); - yield new HttpClientTransportOverHTTP2(http2Client); - } - case H3 -> - { - HTTP3Client http3Client = new HTTP3Client(); - ClientConnector clientConnector = http3Client.getClientConnector(); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - yield new HttpClientTransportOverHTTP3(http3Client); - } - case FCGI -> new HttpClientTransportOverFCGI(1, ""); - case UNIX_DOMAIN -> - { - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - yield new HttpClientTransportOverHTTP(clientConnector); - } - }; + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(newSslContextFactoryClient()); + yield new HttpClientTransportOverHTTP(clientConnector); + } + case H2C, H2 -> + { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(newSslContextFactoryClient()); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + yield new HttpClientTransportOverHTTP2(http2Client); + } + case H3 -> + { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + SslContextFactory.Client sslClient = newSslContextFactoryClient(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslClient, null), clientConnector); + yield new HttpClientTransportOverHTTP3(http3Client); + } + case FCGI -> new HttpClientTransportOverFCGI(1, ""); + }; } protected URI newURI(Transport transport) @@ -474,13 +431,13 @@ protected void setMaxRequestsPerConnection(int maxRequestsPerConnection) public enum Transport { - HTTP, HTTPS, H2C, H2, H3, FCGI, UNIX_DOMAIN; + HTTP, HTTPS, H2C, H2, H3, FCGI; public boolean isSecure() { return switch (this) { - case HTTP, H2C, FCGI, UNIX_DOMAIN -> false; + case HTTP, H2C, FCGI -> false; case HTTPS, H2, H3 -> true; }; } @@ -489,7 +446,7 @@ public boolean isMultiplexed() { return switch (this) { - case HTTP, HTTPS, FCGI, UNIX_DOMAIN -> false; + case HTTP, HTTPS, FCGI -> false; case H2C, H2, H3 -> true; }; } diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java index 90b0c8b095ba..026fa5702e80 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/EventsHandlerTest.java @@ -220,7 +220,6 @@ public void testUsingEventsResponseAsContentSourceFails(Transport transport) thr case HTTP: case HTTPS: case FCGI: - case UNIX_DOMAIN: await().atMost(5, TimeUnit.SECONDS).until(() -> eventsHandler.exceptions.size() / 4, allOf(greaterThanOrEqualTo(10), lessThanOrEqualTo(11))); break; // One read, maybe one null read, one write, one write complete. diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP1TransportProtocolTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP1TransportProtocolTest.java new file mode 100644 index 000000000000..911be28a5afe --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP1TransportProtocolTest.java @@ -0,0 +1,189 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.test.client.transport; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.Destination; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransportProtocol; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +@ExtendWith(WorkDirExtension.class) +public class HTTP1TransportProtocolTest +{ + private Server server; + private HttpClient httpClient; + + @BeforeEach + public void prepare() + { + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + serverThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + httpClient = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); + server.addBean(httpClient); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + @Test + public void testDefaultTransportProtocol() throws Exception + { + ServerConnector connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + List destinations = httpClient.getDestinations(); + assertThat(destinations.size(), is(1)); + Destination destination = destinations.get(0); + assertThat(destination.getOrigin().getTransportProtocol(), sameInstance(TransportProtocol.TCP_IP)); + + HttpClientTransportOverHTTP httpClientTransport = (HttpClientTransportOverHTTP)httpClient.getTransport(); + int networkConnections = httpClientTransport.getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(1)); + } + + @Test + public void testExplicitTransportProtocol() throws Exception + { + ServerConnector connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(TransportProtocol.TCP_IP) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testMemoryTransportProtocol() throws Exception + { + MemoryConnector connector = new MemoryConnector(server, new HttpConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transportProtocol(new MemoryTransportProtocol(connector)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + HttpClientTransportOverHTTP httpClientTransport = (HttpClientTransportOverHTTP)httpClient.getTransport(); + int networkConnections = httpClientTransport.getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(0)); + } + + @Test + public void testUnixDomainTransportProtocol() throws Exception + { + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, new HttpConnectionFactory()); + connector.setUnixDomainPath(Path.of(System.getProperty("java.io.tmpdir"), "jetty.sock")); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transportProtocol(new TransportProtocol.TCPUnix(connector.getUnixDomainPath())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testQUICTransportProtocol(WorkDir workDir) throws Exception + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + + Path pemServerDir = workDir.getEmptyPathDir().resolve("server"); + Files.createDirectories(pemServerDir); + + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, new HttpConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + httpClient.setSslContextFactory(sslClient); + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslClient, null); + httpClient.addBean(clientQuicConfig); + + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(new QuicTransportProtocol(clientQuicConfig)) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } +} diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP2TransportProtocolTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP2TransportProtocolTest.java new file mode 100644 index 000000000000..36d4c36500e0 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP2TransportProtocolTest.java @@ -0,0 +1,359 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.test.client.transport; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.Destination; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +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.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransportProtocol; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(WorkDirExtension.class) +public class HTTP2TransportProtocolTest +{ + private Server server; + private HttpClient httpClient; + private HTTP2Client http2Client; + + @BeforeEach + public void prepare() + { + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + serverThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + http2Client = new HTTP2Client(clientConnector); + httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)); + server.addBean(httpClient); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + @Test + public void testDefaultTransportProtocol() throws Exception + { + ServerConnector connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + List destinations = httpClient.getDestinations(); + assertThat(destinations.size(), is(1)); + Destination destination = destinations.get(0); + assertThat(destination.getOrigin().getTransportProtocol(), sameInstance(TransportProtocol.TCP_IP)); + + HttpClientTransportOverHTTP2 httpClientTransport = (HttpClientTransportOverHTTP2)httpClient.getTransport(); + int networkConnections = httpClientTransport.getHTTP2Client().getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(1)); + } + + @Test + public void testExplicitTransportProtocol() throws Exception + { + ServerConnector connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(TransportProtocol.TCP_IP) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testMemoryTransportProtocol() throws Exception + { + MemoryConnector connector = new MemoryConnector(server, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transportProtocol(new MemoryTransportProtocol(connector)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + HttpClientTransportOverHTTP2 httpClientTransport = (HttpClientTransportOverHTTP2)httpClient.getTransport(); + int networkConnections = httpClientTransport.getHTTP2Client().getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(0)); + } + + @Test + public void testUnixDomainTransportProtocol() throws Exception + { + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory()); + connector.setUnixDomainPath(Path.of(System.getProperty("java.io.tmpdir"), "jetty.sock")); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transportProtocol(new TransportProtocol.TCPUnix(connector.getUnixDomainPath())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testQUICTransportProtocolWithH2C(WorkDir workDir) throws Exception + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + + Path pemServerDir = workDir.getEmptyPathDir().resolve("server"); + Files.createDirectories(pemServerDir); + + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + httpClient.setSslContextFactory(sslClient); + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslClient, null); + httpClient.addBean(clientQuicConfig); + + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(new QuicTransportProtocol(clientQuicConfig)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testQUICTransportProtocolWithH2(WorkDir workDir) throws Exception + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + + Path pemServerDir = workDir.getEmptyPathDir().resolve("server"); + Files.createDirectories(pemServerDir); + + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, new HTTP2ServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + httpClient.setSslContextFactory(sslClient); + HttpClientTransportOverHTTP2 httpClientTransport = (HttpClientTransportOverHTTP2)httpClient.getTransport(); + // ALPN is negotiated by QUIC. + httpClientTransport.setUseALPN(false); + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslClient, null); + httpClient.addBean(clientQuicConfig); + + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(new QuicTransportProtocol(clientQuicConfig)) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testLowLevelH2COverTCPIP() throws Exception + { + ServerConnector connector = new ServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + InetSocketAddress socketAddress = new InetSocketAddress("localhost", connector.getLocalPort()); + Session session = http2Client.connect(socketAddress, new Session.Listener() {}).get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + session.newStream(new HeadersFrame(request, null, true), new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testLowLevelH2COverMemory() throws Exception + { + MemoryConnector connector = new MemoryConnector(server, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + Session session = http2Client.connect(new MemoryTransportProtocol(connector), null, connector.getLocalSocketAddress(), new Session.Listener() {}).get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + session.newStream(new HeadersFrame(request, null, true), new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testLowLevelH2COverUnixDomain() throws Exception + { + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, new HTTP2CServerConnectionFactory()); + connector.setUnixDomainPath(Path.of(System.getProperty("java.io.tmpdir"), "jetty.sock")); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + Session session = http2Client.connect(new TransportProtocol.TCPUnix(connector.getUnixDomainPath()), null, connector.getLocalSocketAddress(), new Session.Listener() {}).get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + session.newStream(new HeadersFrame(request, null, true), new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testLowLevelH2COverQUIC(WorkDir workDir) throws Exception + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + + Path pemServerDir = workDir.getEmptyPathDir().resolve("server"); + Files.createDirectories(pemServerDir); + + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + http2Client.getClientConnector().setSslContextFactory(sslClient); + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslClient, null); + clientQuicConfig.setProtocols(List.of("h2c")); + http2Client.addBean(clientQuicConfig); + + server.start(); + + SocketAddress socketAddress = new InetSocketAddress("localhost", connector.getLocalPort()); + Session session = http2Client.connect(new QuicTransportProtocol(clientQuicConfig), null, socketAddress, new Session.Listener() {}).get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/"), HttpVersion.HTTP_2, HttpFields.EMPTY); + session.newStream(new HeadersFrame(request, null, true), new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP3TransportProtocolTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP3TransportProtocolTest.java new file mode 100644 index 000000000000..f4d7367bdfc9 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP3TransportProtocolTest.java @@ -0,0 +1,248 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.test.client.transport; + +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.Destination; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.api.Session; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; +import org.eclipse.jetty.quic.server.QuicServerConnectionFactory; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransportProtocol; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@ExtendWith(WorkDirExtension.class) +public class HTTP3TransportProtocolTest +{ + private SslContextFactory.Server sslServer; + private Path pemServerDir; + private Server server; + private SslContextFactory.Client sslClient; + private HttpClient httpClient; + private HTTP3Client http3Client; + + @BeforeEach + public void prepare(WorkDir workDir) throws Exception + { + sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + pemServerDir = workDir.getEmptyPathDir().resolve("server"); + Files.createDirectories(pemServerDir); + + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + sslClient = new SslContextFactory.Client(true); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslClient, null); + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + serverThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + http3Client = new HTTP3Client(quicConfiguration, clientConnector); + httpClient = new HttpClient(new HttpClientTransportOverHTTP3(http3Client)); + server.addBean(httpClient); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + @Test + public void testDefaultTransportProtocol() throws Exception + { + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + List destinations = httpClient.getDestinations(); + assertThat(destinations.size(), is(1)); + Destination destination = destinations.get(0); + TransportProtocol transportProtocol = destination.getOrigin().getTransportProtocol(); + if (transportProtocol instanceof TransportProtocol.Wrapper wrapper) + transportProtocol = wrapper.unwrap(); + assertThat(transportProtocol, sameInstance(TransportProtocol.UDP_IP)); + + HttpClientTransportOverHTTP3 httpClientTransport = (HttpClientTransportOverHTTP3)httpClient.getTransport(); + int networkConnections = httpClientTransport.getHTTP3Client().getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(1)); + } + + @Test + public void testExplicitTransportProtocol() throws Exception + { + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(new QuicTransportProtocol(http3Client.getQuicConfiguration())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testMemoryTransportProtocol() throws Exception + { + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnectionFactory quic = new QuicServerConnectionFactory(quicConfiguration); + HTTP3ServerConnectionFactory h3 = new HTTP3ServerConnectionFactory(quic.getQuicConfiguration()); + MemoryConnector connector = new MemoryConnector(server, quic, h3); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transportProtocol(new QuicTransportProtocol(new MemoryTransportProtocol(connector), http3Client.getQuicConfiguration())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + + HttpClientTransportOverHTTP3 httpClientTransport = (HttpClientTransportOverHTTP3)httpClient.getTransport(); + int networkConnections = httpClientTransport.getHTTP3Client().getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(0)); + } + + @Test + public void testUnixDomainTransportProtocol() + { + noUnixDomainForDatagramChannel(); + } + + @Test + public void testLowLevelH3OverUDPIP() throws Exception + { + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + InetSocketAddress socketAddress = new InetSocketAddress("localhost", connector.getLocalPort()); + Session.Client session = http3Client.connect(socketAddress, new Session.Client.Listener() {}).get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/"), HttpVersion.HTTP_3, HttpFields.EMPTY); + session.newRequest(new HeadersFrame(request, true), new Stream.Client.Listener() + { + @Override + public void onResponse(Stream.Client stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testLowLevelH3OverMemory() throws Exception + { + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + QuicServerConnectionFactory quic = new QuicServerConnectionFactory(serverQuicConfig); + HTTP3ServerConnectionFactory h3 = new HTTP3ServerConnectionFactory(quic.getQuicConfiguration()); + MemoryConnector connector = new MemoryConnector(server, quic, h3); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + TransportProtocol transportProtocol = new QuicTransportProtocol(new MemoryTransportProtocol(connector), http3Client.getQuicConfiguration()); + Session.Client session = http3Client.connect(transportProtocol, connector.getLocalSocketAddress(), new Session.Client.Listener() {}, null).get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/"), HttpVersion.HTTP_3, HttpFields.EMPTY); + session.newRequest(new HeadersFrame(request, true), new Stream.Client.Listener() + { + @Override + public void onResponse(Stream.Client stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testLowLevelH3OverUnixDomain() + { + noUnixDomainForDatagramChannel(); + } + + private static void noUnixDomainForDatagramChannel() + { + assumeTrue(false, "DatagramChannel over Unix-Domain is not supported yet by Java"); + } +} diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTPDynamicTransportProtocolTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTPDynamicTransportProtocolTest.java new file mode 100644 index 000000000000..9dacb381c567 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTPDynamicTransportProtocolTest.java @@ -0,0 +1,561 @@ +// +// ======================================================================== +// Copyright (c) 1995 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.test.client.transport; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.transport.HttpClientConnectionFactory; +import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +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.HTTP2ClientConnectionFactory; +import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.TransportProtocol; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransportProtocol; +import org.eclipse.jetty.quic.server.QuicServerConnectionFactory; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +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.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransportProtocol; +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.SslConnectionFactory; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.HostPort; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@ExtendWith(WorkDirExtension.class) +public class HTTPDynamicTransportProtocolTest +{ + private SslContextFactory.Server sslServer; + private Path pemServerDir; + private Server server; + private ClientConnector clientConnector; + private HTTP2Client http2Client; + private HTTP3Client http3Client; + + @BeforeEach + public void prepare(WorkDir workDir) throws Exception + { + sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + pemServerDir = workDir.getEmptyPathDir().resolve("server"); + Files.createDirectories(pemServerDir); + + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + + clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + serverThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + + http2Client = new HTTP2Client(clientConnector); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslClient, null); + http3Client = new HTTP3Client(quicConfiguration, clientConnector); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + @Test + public void testExplicitHTTPVersionWithSameHttpClientForAllHTTPVersions() throws Exception + { + int port = freePort(); + ConnectionFactory h1 = new HttpConnectionFactory(); + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(); + ServerConnector tcp = new ServerConnector(server, 1, 1, h1, h2c); + tcp.setPort(port); + server.addConnector(tcp); + + ServerQuicConfiguration quicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + ConnectionFactory h3 = new HTTP3ServerConnectionFactory(quicConfig); + QuicServerConnector quic = new QuicServerConnector(server, quicConfig, h3); + quic.setPort(port); + server.addConnector(quic); + + server.setHandler(new EmptyServerHandler()); + + HttpClientTransportDynamic httpClientTransport = new HttpClientTransportDynamic( + clientConnector, + HttpClientConnectionFactory.HTTP11, + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client), + new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client) + ); + HttpClient httpClient = new HttpClient(httpClientTransport); + server.addBean(httpClient); + + server.start(); + + for (HttpVersion httpVersion : List.of(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2, HttpVersion.HTTP_3)) + { + ContentResponse response = httpClient.newRequest("localhost", port) + .version(httpVersion) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(httpVersion.toString(), response.getStatus(), is(HttpStatus.OK_200)); + } + } + + @Test + public void testNonExplicitHTTPVersionH3H2H1() throws Exception + { + int port = freePort(); + ConnectionFactory h1 = new HttpConnectionFactory(); + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(); + ServerConnector tcp = new ServerConnector(server, 1, 1, h1, h2c); + tcp.setPort(port); + server.addConnector(tcp); + + ServerQuicConfiguration quicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + ConnectionFactory h3 = new HTTP3ServerConnectionFactory(quicConfig); + QuicServerConnector quic = new QuicServerConnector(server, quicConfig, h3); + quic.setPort(port); + server.addConnector(quic); + + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, true, request.getConnectionMetaData().getProtocol(), callback); + return true; + } + }); + + HttpClientTransportDynamic httpClientTransport = new HttpClientTransportDynamic( + clientConnector, + new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client), + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client), + HttpClientConnectionFactory.HTTP11 + ); + HttpClient httpClient = new HttpClient(httpClientTransport); + server.addBean(httpClient); + + server.start(); + + // No explicit version, HttpClientTransport preference wins. + ContentResponse response = httpClient.newRequest("localhost", port) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("/3")); + + // Non-secure scheme, must not be HTTP/3. + response = httpClient.newRequest("localhost", port) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("/2")); + } + + @Test + public void testNonExplicitHTTPVersionH2H3H1() throws Exception + { + int port = freePort(); + ConnectionFactory h1 = new HttpConnectionFactory(); + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(); + ServerConnector tcp = new ServerConnector(server, 1, 1, h1, h2c); + tcp.setPort(port); + server.addConnector(tcp); + + int securePort = freePort(); + ConnectionFactory h2 = new HTTP2ServerConnectionFactory(); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + ConnectionFactory ssl = new SslConnectionFactory(sslServer, alpn.getProtocol()); + ServerConnector tcpSecure = new ServerConnector(server, 1, 1, ssl, alpn, h2, h1); + tcpSecure.setPort(securePort); + server.addConnector(tcpSecure); + + ServerQuicConfiguration quicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + ConnectionFactory h3 = new HTTP3ServerConnectionFactory(quicConfig); + QuicServerConnector quic = new QuicServerConnector(server, quicConfig, h3); + quic.setPort(securePort); + server.addConnector(quic); + + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, true, request.getConnectionMetaData().getProtocol(), callback); + return true; + } + }); + + HttpClientTransportDynamic httpClientTransport = new HttpClientTransportDynamic( + clientConnector, + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client), + new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client), + HttpClientConnectionFactory.HTTP11 + ); + HttpClient httpClient = new HttpClient(httpClientTransport); + server.addBean(httpClient); + + server.start(); + + // No explicit version, non-secure, HttpClientTransport preference wins. + ContentResponse response = httpClient.newRequest("localhost", port) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("/2")); + + // Secure scheme, but must not be HTTP/3. + response = httpClient.newRequest("localhost", securePort) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("/2")); + } + + @Test + public void testClientH2H3H1ServerALPNH1() throws Exception + { + int securePort = freePort(); + + ConnectionFactory h1 = new HttpConnectionFactory(); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + ConnectionFactory ssl = new SslConnectionFactory(sslServer, alpn.getProtocol()); + ServerConnector tcpSecure = new ServerConnector(server, 1, 1, ssl, alpn, h1); + tcpSecure.setPort(securePort); + server.addConnector(tcpSecure); + + ServerQuicConfiguration quicConfig = new ServerQuicConfiguration(sslServer, pemServerDir); + ConnectionFactory h3 = new HTTP3ServerConnectionFactory(quicConfig); + QuicServerConnector quic = new QuicServerConnector(server, quicConfig, h3); + quic.setPort(securePort); + server.addConnector(quic); + + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Content.Sink.write(response, true, request.getConnectionMetaData().getProtocol(), callback); + return true; + } + }); + + HttpClientTransportDynamic httpClientTransport = new HttpClientTransportDynamic( + clientConnector, + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client), + new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client), + HttpClientConnectionFactory.HTTP11 + ); + HttpClient httpClient = new HttpClient(httpClientTransport); + server.addBean(httpClient); + + server.start(); + + // Secure scheme, must negotiate HTTP/1. + ContentResponse response = httpClient.newRequest("localhost", securePort) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), containsString("/1")); + } + + @Test + public void testClientSendH3ServerDoesNotSupportH3() throws Exception + { + ConnectionFactory h2 = new HTTP2ServerConnectionFactory(); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h2.getProtocol()); + ConnectionFactory ssl = new SslConnectionFactory(sslServer, alpn.getProtocol()); + ServerConnector tcpSecure = new ServerConnector(server, 1, 1, ssl, alpn, h2); + server.addConnector(tcpSecure); + + server.setHandler(new EmptyServerHandler()); + + HttpClientTransportDynamic httpClientTransport = new HttpClientTransportDynamic( + clientConnector, + new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client), + new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client) + ); + HttpClient httpClient = new HttpClient(httpClientTransport); + server.addBean(httpClient); + + server.start(); + + // The client will attempt a request with H3 due to client preference. + // The attempt to connect via QUIC/UDP will time out (there is no immediate + // failure like would happen with TCP not listening on the connector port). + assertThrows(TimeoutException.class, () -> httpClient.newRequest("localhost", tcpSecure.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .timeout(1, TimeUnit.SECONDS) + .send() + ); + + // Make sure the client can speak H2. + ContentResponse response = httpClient.newRequest("localhost", tcpSecure.getLocalPort()) + .scheme(HttpScheme.HTTPS.asString()) + .version(HttpVersion.HTTP_2) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testHighLevelH1OverUNIX() throws Exception + { + ConnectionFactory h1 = new HttpConnectionFactory(); + ServerConnector tcp = new ServerConnector(server, 1, 1, h1); + server.addConnector(tcp); + + Path unixDomainPath = newUnixDomainPath(); + UnixDomainServerConnector unix = new UnixDomainServerConnector(server, 1, 1, h1); + unix.setUnixDomainPath(unixDomainPath); + server.addConnector(unix); + + server.setHandler(new EmptyServerHandler()); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(new ClientConnector(), HttpClientConnectionFactory.HTTP11)); + server.addBean(httpClient); + + server.start(); + + ContentResponse response = httpClient.newRequest("localhost", tcp.getLocalPort()) + .transportProtocol(new TransportProtocol.TCPUnix(unixDomainPath)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(tcp.getConnectedEndPoints().size(), is(0)); + assertThat(unix.getConnectedEndPoints().size(), is(1)); + } + + @Test + public void testLowLevelH2OverUNIX() throws Exception + { + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setServerAuthority(new HostPort("localhost")); + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + ServerConnector tcp = new ServerConnector(server, 1, 1, h2c); + server.addConnector(tcp); + + Path unixDomainPath = newUnixDomainPath(); + UnixDomainServerConnector unix = new UnixDomainServerConnector(server, 1, 1, h2c); + unix.setUnixDomainPath(unixDomainPath); + server.addConnector(unix); + + server.setHandler(new EmptyServerHandler()); + + server.addBean(http2Client); + + server.start(); + + TransportProtocol.TCPUnix transportProtocol = new TransportProtocol.TCPUnix(unixDomainPath); + Promise.Completable promise = new Promise.Completable<>(); + http2Client.connect(transportProtocol, null, new HTTP2ClientConnectionFactory(), new Session.Listener() {}, promise, null); + Session session = promise.get(5, TimeUnit.SECONDS); + + CountDownLatch responseLatch = new CountDownLatch(1); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost/path"), HttpVersion.HTTP_2, HttpFields.EMPTY); + session.newStream(new HeadersFrame(request, null, true), new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + responseLatch.countDown(); + } + }); + + assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testHighLevelH1OverMemory() throws Exception + { + ConnectionFactory h1 = new HttpConnectionFactory(); + MemoryConnector local = new MemoryConnector(server, h1); + server.addConnector(local); + + server.setHandler(new EmptyServerHandler()); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic()); + server.addBean(httpClient); + + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transportProtocol(new MemoryTransportProtocol(local)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testHighLevelH2OverQUIC(WorkDir workDir) throws Exception + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + + ConnectionFactory h2c = new HTTP2CServerConnectionFactory(new HttpConfiguration()); + ServerQuicConfiguration serverQuicConfiguration = new ServerQuicConfiguration(sslServer, null); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfiguration, h2c); + connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); + server.addConnector(connector); + + server.setHandler(new EmptyServerHandler()); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client))); + server.addBean(httpClient); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + httpClient.addBean(sslClient); + + server.start(); + + ClientQuicConfiguration clientQuicConfiguration = new ClientQuicConfiguration(sslClient, null); + QuicTransportProtocol transportProtocol = new QuicTransportProtocol(clientQuicConfiguration); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transportProtocol(transportProtocol) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testHighLevelH3OverMemory(WorkDir workDir) throws Exception + { + SslContextFactory.Server sslServer = new SslContextFactory.Server(); + sslServer.setKeyStorePath(MavenPaths.findTestResourceFile("keystore.p12").toString()); + sslServer.setKeyStorePassword("storepwd"); + + HttpConnectionFactory h1 = new HttpConnectionFactory(); + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslServer, workDir.getEmptyPathDir()); + QuicServerConnectionFactory quic = new QuicServerConnectionFactory(quicConfiguration); + HTTP3ServerConnectionFactory h3 = new HTTP3ServerConnectionFactory(quicConfiguration); + + MemoryConnector connector = new MemoryConnector(server, quic, h1, h3); + server.addConnector(connector); + + server.setHandler(new EmptyServerHandler()); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client))); + server.addBean(httpClient); + + server.start(); + + TransportProtocol transportProtocol = new QuicTransportProtocol(new MemoryTransportProtocol(connector), http3Client.getQuicConfiguration()); + + ContentResponse response = httpClient.newRequest("https://localhost/") + .transportProtocol(transportProtocol) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testHighLevelH1OverProxyProtocolOverQUICOverMemory() + { + // TODO: UGH! :) + assumeTrue(false); + } + + private static int freePort() throws IOException + { + try (ServerSocket server = new ServerSocket()) + { + server.setReuseAddress(true); + server.bind(new InetSocketAddress("localhost", 0)); + return server.getLocalPort(); + } + } + + private static Path newUnixDomainPath() + { + String unixDomainDir = System.getProperty("jetty.unixdomain.dir", System.getProperty("java.io.tmpdir")); + return Path.of(unixDomainDir, "jetty.sock"); + } +} diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java index 676f68bb0a7f..95c2d27bbf53 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpChannelAssociationTest.java @@ -41,7 +41,9 @@ import org.eclipse.jetty.http3.client.transport.internal.HttpConnectionOverHTTP3; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -169,9 +171,9 @@ public boolean associate(HttpExchange exchange) } case H3: { - HTTP3Client http3Client = new HTTP3Client(); + SslContextFactory.Client sslClient = newSslContextFactoryClient(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslClient, null)); http3Client.getClientConnector().setSelectors(1); - http3Client.getClientConnector().setSslContextFactory(newSslContextFactoryClient()); yield new HttpClientTransportOverHTTP3(http3Client) { @Override @@ -223,34 +225,6 @@ public boolean associate(HttpExchange exchange) } }; } - case UNIX_DOMAIN: - { - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - yield new HttpClientTransportOverHTTP(clientConnector) - { - @Override - public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) - { - return new HttpConnectionOverHTTP(endPoint, context) - { - @Override - protected HttpChannelOverHTTP newHttpChannel() - { - return new HttpChannelOverHTTP(this) - { - @Override - public boolean associate(HttpExchange exchange) - { - return code.test(exchange) && super.associate(exchange); - } - }; - } - }; - } - }; - } }; } diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientStreamTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientStreamTest.java index 08ca24c6e2df..de15afea3ec3 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientStreamTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientStreamTest.java @@ -879,9 +879,6 @@ public void onComplete(Result result) @MethodSource("transports") public void testUploadWithOutputStreamFailureToConnect(Transport transport) throws Exception { - // Failure to connect is based on InetSocketAddress failure, which Unix-Domain does not use. - Assumptions.assumeTrue(transport != Transport.UNIX_DOMAIN); - long connectTimeout = 1000; start(transport, new EmptyServerHandler()); client.setConnectTimeout(connectTimeout); @@ -960,9 +957,6 @@ public void failed(Throwable x) @MethodSource("transports") public void testUploadWithConnectFailureClosesStream(Transport transport) throws Exception { - // Failure to connect is based on InetSocket address failure, which Unix-Domain does not use. - Assumptions.assumeTrue(transport != Transport.UNIX_DOMAIN); - long connectTimeout = 1000; start(transport, new EmptyServerHandler()); client.setConnectTimeout(connectTimeout); diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java index 8d4bea61158b..abe2c3f80e5a 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTest.java @@ -347,7 +347,7 @@ public void testClientCannotValidateServerCertificate(Transport transport) throw // Use a SslContextFactory.Client that verifies server certificates, // requests should fail because the server certificate is unknown. - SslContextFactory.Client clientTLS = newSslContextFactoryClient(); + SslContextFactory.Client clientTLS = new SslContextFactory.Client(); clientTLS.setEndpointIdentificationAlgorithm("HTTPS"); client.stop(); client.setSslContextFactory(clientTLS); @@ -671,7 +671,6 @@ public void testOneDestinationPerUser(Transport transport) throws Exception public void testIPv6Host(Transport transport) throws Exception { assumeTrue(Net.isIpv6InterfaceAvailable()); - assumeTrue(transport != Transport.UNIX_DOMAIN); assumeTrue(transport != Transport.H3); start(transport, new Handler.Abstract() @@ -726,8 +725,6 @@ public boolean handle(Request request, org.eclipse.jetty.server.Response respons @MethodSource("transports") public void testRequestWithDifferentDestination(Transport transport) throws Exception { - assumeTrue(transport != Transport.UNIX_DOMAIN); - String requestScheme = newURI(transport).getScheme(); String requestHost = "otherHost.com"; int requestPort = 8888; diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTimeoutTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTimeoutTest.java index bdc6e3a33c66..a7db5a19b982 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTimeoutTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTimeoutTest.java @@ -47,7 +47,6 @@ import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -86,7 +85,7 @@ public void testTimeoutOnListener(Transport transport) throws Exception long timeout = 1000; start(transport, new TimeoutHandler(2 * timeout)); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); Request request = client.newRequest(newURI(transport)) .timeout(timeout, TimeUnit.MILLISECONDS); request.send(result -> @@ -108,7 +107,7 @@ public void testTimeoutOnQueuedRequest(Transport transport) throws Exception client.setMaxConnectionsPerDestination(1); // The first request has a long timeout - final CountDownLatch firstLatch = new CountDownLatch(1); + CountDownLatch firstLatch = new CountDownLatch(1); Request request = client.newRequest(newURI(transport)) .timeout(4 * timeout, TimeUnit.MILLISECONDS); request.send(result -> @@ -118,7 +117,7 @@ public void testTimeoutOnQueuedRequest(Transport transport) throws Exception }); // Second request has a short timeout and should fail in the queue - final CountDownLatch secondLatch = new CountDownLatch(1); + CountDownLatch secondLatch = new CountDownLatch(1); request = client.newRequest(newURI(transport)) .timeout(timeout, TimeUnit.MILLISECONDS); request.send(result -> @@ -140,8 +139,8 @@ public void testTimeoutIsCancelledOnSuccess(Transport transport) throws Exceptio long timeout = 1000; start(transport, new TimeoutHandler(timeout)); - final CountDownLatch latch = new CountDownLatch(1); - final byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + CountDownLatch latch = new CountDownLatch(1); + byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; Request request = client.newRequest(newURI(transport)) .body(new InputStreamRequestContent(new ByteArrayInputStream(content))) .timeout(2 * timeout, TimeUnit.MILLISECONDS); @@ -192,7 +191,6 @@ public void testTimeoutOnListenerWithExplicitConnection(Transport transport) thr @MethodSource("transports") public void testTimeoutIsCancelledOnSuccessWithExplicitConnection(Transport transport) throws Exception { - long timeout = 1000; start(transport, new TimeoutHandler(timeout)); @@ -287,8 +285,8 @@ public void testNonBlockingConnectTimeoutFailsRequest(Transport transport) throw private void testConnectTimeoutFailsRequest(Transport transport, boolean blocking) throws Exception { // Using IANA hosted example.com:81 to reliably produce a Connect Timeout. - final String host = "example.com"; - final int port = 81; + String host = "example.com"; + int port = 81; int connectTimeout = 1000; assumeConnectTimeout(host, port, connectTimeout); @@ -296,7 +294,7 @@ private void testConnectTimeoutFailsRequest(Transport transport, boolean blockin client.setConnectTimeout(connectTimeout); client.setConnectBlocking(blocking); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); Request request = client.newRequest(host, port); request.scheme(newURI(transport).getScheme()) .send(result -> @@ -314,9 +312,6 @@ private void testConnectTimeoutFailsRequest(Transport transport, boolean blockin @Tag("external") public void testConnectTimeoutIsCancelledByShorterRequestTimeout(Transport transport) throws Exception { - // Failure to connect is based on InetSocket address failure, which Unix-Domain does not use. - Assumptions.assumeTrue(transport != Transport.UNIX_DOMAIN); - // Using IANA hosted example.com:81 to reliably produce a Connect Timeout. String host = "example.com"; int port = 81; @@ -326,8 +321,8 @@ public void testConnectTimeoutIsCancelledByShorterRequestTimeout(Transport trans start(transport, new EmptyServerHandler()); client.setConnectTimeout(connectTimeout); - final AtomicInteger completes = new AtomicInteger(); - final CountDownLatch latch = new CountDownLatch(2); + AtomicInteger completes = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(2); Request request = client.newRequest(host, port); request.scheme(newURI(transport).getScheme()) .timeout(connectTimeout / 2, TimeUnit.MILLISECONDS) @@ -347,9 +342,6 @@ public void testConnectTimeoutIsCancelledByShorterRequestTimeout(Transport trans @Tag("external") public void testRetryAfterConnectTimeout(Transport transport) throws Exception { - // Failure to connect is based on InetSocket address failure, which Unix-Domain does not use. - Assumptions.assumeTrue(transport != Transport.UNIX_DOMAIN); - // Using IANA hosted example.com:81 to reliably produce a Connect Timeout. String host = "example.com"; int port = 81; @@ -359,7 +351,7 @@ public void testRetryAfterConnectTimeout(Transport transport) throws Exception start(transport, new EmptyServerHandler()); client.setConnectTimeout(connectTimeout); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); Request request = client.newRequest(host, port); String scheme = newURI(transport).getScheme(); request.scheme(scheme) @@ -392,7 +384,7 @@ public void testVeryShortTimeout(Transport transport) throws Exception { start(transport, new EmptyServerHandler()); - final CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); client.newRequest(newURI(transport)) .timeout(1, TimeUnit.MILLISECONDS) // Very short timeout .send(result -> latch.countDown()); diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/RoundRobinConnectionPoolTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/RoundRobinConnectionPoolTest.java index 7447f21276a9..bb6ff64bd448 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/RoundRobinConnectionPoolTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/RoundRobinConnectionPoolTest.java @@ -99,7 +99,7 @@ public boolean handle(Request request, Response response, Callback callback) int expected = remotePorts.get(base); int candidate = remotePorts.get(i); assertThat(client.dump() + System.lineSeparator() + remotePorts, expected, Matchers.equalTo(candidate)); - if (transport != Transport.UNIX_DOMAIN && i > 0) + if (i > 0) assertThat(remotePorts.get(i - 1), Matchers.not(Matchers.equalTo(candidate))); } } @@ -195,7 +195,7 @@ public boolean handle(Request request, Response response, Callback callback) int expected = remotePorts.get(base); int candidate = remotePorts.get(i); assertThat(client.dump() + System.lineSeparator() + remotePorts, expected, Matchers.equalTo(candidate)); - if (transport != Transport.UNIX_DOMAIN && i > 0) + if (i > 0) assertThat(remotePorts.get(i - 1), Matchers.not(Matchers.equalTo(candidate))); } } @@ -248,10 +248,6 @@ public boolean handle(Request request, Response response, Callback callback) assertTrue(clientLatch.await(count, TimeUnit.SECONDS)); assertEquals(count, remotePorts.size()); - // Unix Domain does not have ports. - if (transport == Transport.UNIX_DOMAIN) - return; - // UDP does not have TIME_WAIT so ports may be reused by different connections. if (transport == Transport.H3) return; diff --git a/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java b/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java index b1665e433f31..f9aa9ad4003a 100644 --- a/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java +++ b/jetty-core/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java @@ -145,6 +145,18 @@ public void setAcceptedSendBufferSize(int acceptedSendBufferSize) this.acceptedSendBufferSize = acceptedSendBufferSize; } + public SocketAddress getLocalSocketAddress() + { + try + { + return serverChannel == null ? null : serverChannel.getLocalAddress(); + } + catch (Throwable x) + { + return null; + } + } + @Override protected void doStart() throws Exception { diff --git a/jetty-core/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java b/jetty-core/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java index a4fd8b4bfb96..ee172437e6f2 100644 --- a/jetty-core/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java +++ b/jetty-core/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java @@ -15,18 +15,21 @@ import java.io.IOException; import java.net.SocketAddress; +import java.net.UnixDomainSocketAddress; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -40,11 +43,7 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.JRE; import static org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V1; import static org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V2; @@ -56,33 +55,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -@EnabledForJreRange(min = JRE.JAVA_16) public class UnixDomainTest { - private static final Class unixDomainSocketAddressClass = probe(); - - private static Class probe() - { - try - { - return ClassLoader.getPlatformClassLoader().loadClass("java.net.UnixDomainSocketAddress"); - } - catch (Throwable x) - { - return null; - } - } - private ConnectionFactory[] factories = new ConnectionFactory[]{new HttpConnectionFactory()}; private Server server; private Path unixDomainPath; - @BeforeEach - public void prepare() - { - Assumptions.assumeTrue(unixDomainSocketAddressClass != null); - } - private void start(Handler handler) throws Exception { server = new Server(); @@ -120,9 +98,9 @@ public boolean handle(Request request, Response response, Callback callback) // Verify the SocketAddresses. SocketAddress local = endPoint.getLocalSocketAddress(); - assertThat(local, Matchers.instanceOf(unixDomainSocketAddressClass)); + assertThat(local, Matchers.instanceOf(UnixDomainSocketAddress.class)); SocketAddress remote = endPoint.getRemoteSocketAddress(); - assertThat(remote, Matchers.instanceOf(unixDomainSocketAddressClass)); + assertThat(remote, Matchers.instanceOf(UnixDomainSocketAddress.class)); // Verify that other address methods don't throw. local = assertDoesNotThrow(endPoint::getLocalAddress); @@ -137,12 +115,12 @@ public boolean handle(Request request, Response response, Callback callback) } }); - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic()); httpClient.start(); try { ContentResponse response = httpClient.newRequest(uri) + .transportProtocol(new TransportProtocol.TCPUnix(unixDomainPath)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -173,14 +151,20 @@ public boolean handle(Request request, Response response, Callback callback) } }); - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - - HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); - httpClient.getProxyConfiguration().addProxy(new HttpProxy("localhost", fakeProxyPort)); + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic()); + Origin proxyOrigin = new Origin( + "http", + new Origin.Address("localhost", fakeProxyPort), + null, + new Origin.Protocol(List.of("http/1.1"), false), + new TransportProtocol.TCPUnix(unixDomainPath) + ); + httpClient.getProxyConfiguration().addProxy(new HttpProxy(proxyOrigin, null)); httpClient.start(); try { ContentResponse response = httpClient.newRequest("localhost", fakeServerPort) + .transportProtocol(new TransportProtocol.TCPUnix(unixDomainPath)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -205,19 +189,20 @@ public boolean handle(Request request, Response response, Callback callback) { EndPoint endPoint = request.getConnectionMetaData().getConnection().getEndPoint(); assertThat(endPoint, Matchers.instanceOf(ProxyConnectionFactory.ProxyEndPoint.class)); - assertThat(endPoint.getLocalSocketAddress(), Matchers.instanceOf(unixDomainSocketAddressClass)); - assertThat(endPoint.getRemoteSocketAddress(), Matchers.instanceOf(unixDomainSocketAddressClass)); + assertThat(endPoint.getLocalSocketAddress(), Matchers.instanceOf(UnixDomainSocketAddress.class)); + assertThat(endPoint.getRemoteSocketAddress(), Matchers.instanceOf(UnixDomainSocketAddress.class)); String target = Request.getPathInContext(request); + Path localPath = ((UnixDomainSocketAddress)endPoint.getLocalSocketAddress()).getPath(); if ("/v1".equals(target)) { // As PROXYv1 does not support UNIX, the wrapped EndPoint data is used. - Path localPath = toUnixDomainPath(endPoint.getLocalSocketAddress()); assertThat(localPath, Matchers.equalTo(unixDomainPath)); } else if ("/v2".equals(target)) { - assertThat(toUnixDomainPath(endPoint.getLocalSocketAddress()).toString(), Matchers.equalTo(FS.separators(dstAddr))); - assertThat(toUnixDomainPath(endPoint.getRemoteSocketAddress()).toString(), Matchers.equalTo(FS.separators(srcAddr))); + assertThat(localPath.toString(), Matchers.equalTo(FS.separators(dstAddr))); + Path remotePath = ((UnixDomainSocketAddress)endPoint.getRemoteSocketAddress()).getPath(); + assertThat(remotePath.toString(), Matchers.equalTo(FS.separators(srcAddr))); } else { @@ -228,16 +213,14 @@ else if ("/v2".equals(target)) } }); - // Java 11+ portable way to implement SocketChannelWithAddress.Factory. - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - - HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic()); httpClient.start(); try { // Try PROXYv1 with the PROXY information retrieved from the EndPoint. // PROXYv1 does not support the UNIX family. ContentResponse response1 = httpClient.newRequest("localhost", 0) + .transportProtocol(new TransportProtocol.TCPUnix(unixDomainPath)) .path("/v1") .tag(new V1.Tag()) .timeout(5, TimeUnit.SECONDS) @@ -248,6 +231,7 @@ else if ("/v2".equals(target)) // Try PROXYv2 with explicit PROXY information. var tag = new V2.Tag(V2.Tag.Command.PROXY, V2.Tag.Family.UNIX, V2.Tag.Protocol.STREAM, srcAddr, 0, dstAddr, 0, null); ContentResponse response2 = httpClient.newRequest("localhost", 0) + .transportProtocol(new TransportProtocol.TCPUnix(unixDomainPath)) .path("/v2") .tag(tag) .timeout(5, TimeUnit.SECONDS) @@ -270,18 +254,4 @@ public void testInvalidUnixDomainPath() server.addConnector(connector); assertThrows(IOException.class, () -> server.start()); } - - private static Path toUnixDomainPath(SocketAddress address) - { - try - { - Assertions.assertNotNull(unixDomainSocketAddressClass); - return (Path)unixDomainSocketAddressClass.getMethod("getPath").invoke(address); - } - catch (Throwable x) - { - Assertions.fail(x); - throw new AssertionError(); - } - } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java index 143e43c45c01..2b8f639b271a 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/component/ContainerLifeCycle.java @@ -49,7 +49,7 @@ *
  • If the added bean is !running and the container is started, it will be added as an unmanaged bean.
  • * * When the container is started, then all contained managed beans will also be started. - * Any contained AUTO beans will be check for their status and if already started will be switched unmanaged beans, + * Any contained AUTO beans will be checked for their status and if already started will be switched unmanaged beans, * else they will be started and switched to managed beans. * Beans added after a container is started are not started and their state needs to be explicitly managed. *

    @@ -104,9 +104,8 @@ protected void doStart() throws Exception { if (!isStarting()) break; - if (b._bean instanceof LifeCycle) + if (b._bean instanceof LifeCycle l) { - LifeCycle l = (LifeCycle)b._bean; switch (b._managed) { case MANAGED: @@ -139,9 +138,8 @@ protected void doStart() throws Exception Collections.reverse(reverse); for (Bean b : reverse) { - if (b._bean instanceof LifeCycle && b._managed == Managed.MANAGED) + if (b._bean instanceof LifeCycle l && b._managed == Managed.MANAGED) { - LifeCycle l = (LifeCycle)b._bean; if (l.isRunning()) { try @@ -197,9 +195,8 @@ protected void doStop() throws Exception { if (!isStopping()) break; - if (b._managed == Managed.MANAGED && b._bean instanceof LifeCycle) + if (b._managed == Managed.MANAGED && b._bean instanceof LifeCycle l) { - LifeCycle l = (LifeCycle)b._bean; try { stop(l); @@ -224,9 +221,8 @@ public void destroy() Collections.reverse(reverse); for (Bean b : reverse) { - if (b._bean instanceof Destroyable && (b._managed == Managed.MANAGED || b._managed == Managed.POJO)) + if (b._bean instanceof Destroyable d && (b._managed == Managed.MANAGED || b._managed == Managed.POJO)) { - Destroyable d = (Destroyable)b._bean; try { d.destroy(); @@ -311,11 +307,8 @@ public boolean isUnmanaged(Object bean) @Override public boolean addBean(Object o) { - if (o instanceof LifeCycle) - { - LifeCycle l = (LifeCycle)o; + if (o instanceof LifeCycle l) return addBean(o, l.isRunning() ? Managed.UNMANAGED : Managed.AUTO); - } return addBean(o, Managed.POJO); } @@ -324,7 +317,7 @@ public boolean addBean(Object o) * Adds the given bean, explicitly managing it or not. * * @param o The bean object to add - * @param managed whether to managed the lifecycle of the bean + * @param managed whether to manage the lifecycle of the bean * @return true if the bean was added, false if it was already present */ @Override @@ -374,9 +367,8 @@ private boolean addBean(Object o, Managed managed) break; case AUTO: - if (o instanceof LifeCycle) + if (o instanceof LifeCycle l) { - LifeCycle l = (LifeCycle)o; if (isStarting()) { if (l.isRunning()) @@ -460,9 +452,8 @@ public boolean addEventListener(EventListener listener) // already been added, so we will not enter this branch. addBean(listener); - if (listener instanceof Container.Listener) + if (listener instanceof Container.Listener cl) { - Container.Listener cl = (Container.Listener)listener; _listeners.add(cl); // tell it about existing beans @@ -491,9 +482,8 @@ public boolean removeEventListener(EventListener listener) if (super.removeEventListener(listener)) { removeBean(listener); - if (listener instanceof Container.Listener && _listeners.remove(listener)) + if (listener instanceof Container.Listener cl && _listeners.remove(listener)) { - Container.Listener cl = (Container.Listener)listener; // remove existing beans for (Bean b : _beans) { @@ -774,15 +764,12 @@ public boolean isManaged() public boolean isManageable() { - switch (_managed) + return switch (_managed) { - case MANAGED: - return true; - case AUTO: - return _bean instanceof LifeCycle && ((LifeCycle)_bean).isStopped(); - default: - return false; - } + case MANAGED -> true; + case AUTO -> _bean instanceof LifeCycle && ((LifeCycle)_bean).isStopped(); + default -> false; + }; } @Override diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java index 05a1b755a023..c280e2ae4164 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java @@ -42,8 +42,10 @@ import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; import org.eclipse.jetty.http3.server.AbstractHTTP3ServerConnectionFactory; import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.HostHeaderCustomizer; @@ -56,7 +58,6 @@ import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -64,8 +65,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; -import static org.junit.jupiter.api.Assertions.assertTrue; - @ExtendWith(WorkDirExtension.class) public class AbstractTest { @@ -73,11 +72,11 @@ public class AbstractTest protected final HttpConfiguration httpConfig = new HttpConfiguration(); protected SslContextFactory.Server sslContextFactoryServer; + protected ServerQuicConfiguration serverQuicConfig; protected Server server; protected AbstractConnector connector; protected ServletContextHandler servletContextHandler; protected HttpClient client; - protected Path unixDomainPath; public static Collection transports() { @@ -122,14 +121,8 @@ protected void startServer(Transport transport, HttpServlet servlet) throws Exce protected void prepareServer(Transport transport, HttpServlet servlet) throws Exception { - if (transport == Transport.UNIX_DOMAIN) - { - String unixDomainDir = System.getProperty("jetty.unixdomain.dir", System.getProperty("java.io.tmpdir")); - unixDomainPath = Files.createTempFile(Path.of(unixDomainDir), "unix_", ".sock"); - assertTrue(unixDomainPath.toAbsolutePath().toString().length() < UnixDomainServerConnector.MAX_UNIX_DOMAIN_PATH_LENGTH, "Unix-Domain path too long"); - Files.delete(unixDomainPath); - } sslContextFactoryServer = newSslContextFactoryServer(); + serverQuicConfig = new ServerQuicConfiguration(sslContextFactoryServer, workDir.getEmptyPathDir()); if (server == null) server = newServer(); connector = newConnector(transport, server); @@ -194,17 +187,7 @@ public AbstractConnector newConnector(Transport transport, Server server) case HTTP, HTTPS, H2C, H2, FCGI -> new ServerConnector(server, 1, 1, newServerConnectionFactory(transport)); case H3 -> - { - HTTP3ServerConnector connector = new HTTP3ServerConnector(server, sslContextFactoryServer, newServerConnectionFactory(transport)); - connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); - yield connector; - } - case UNIX_DOMAIN -> - { - UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, newServerConnectionFactory(transport)); - connector.setUnixDomainPath(unixDomainPath); - yield connector; - } + new QuicServerConnector(server, serverQuicConfig, newServerConnectionFactory(transport)); }; } @@ -212,7 +195,7 @@ protected ConnectionFactory[] newServerConnectionFactory(Transport transport) { List list = switch (transport) { - case HTTP, UNIX_DOMAIN -> + case HTTP -> List.of(new HttpConnectionFactory(httpConfig)); case HTTPS -> { @@ -239,7 +222,7 @@ protected ConnectionFactory[] newServerConnectionFactory(Transport transport) { httpConfig.addCustomizer(new SecureRequestCustomizer()); httpConfig.addCustomizer(new HostHeaderCustomizer()); - yield List.of(new HTTP3ServerConnectionFactory(httpConfig)); + yield List.of(new HTTP3ServerConnectionFactory(serverQuicConfig, httpConfig)); } case FCGI -> List.of(new ServerFCGIConnectionFactory(httpConfig)); }; @@ -275,20 +258,14 @@ protected HttpClientTransport newHttpClientTransport(Transport transport) throws } case H3 -> { - HTTP3Client http3Client = new HTTP3Client(); - ClientConnector clientConnector = http3Client.getClientConnector(); + ClientConnector clientConnector = new ClientConnector(); clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); + SslContextFactory.Client sslContextFactory = newSslContextFactoryClient(); + clientConnector.setSslContextFactory(sslContextFactory); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); yield new HttpClientTransportOverHTTP3(http3Client); } case FCGI -> new HttpClientTransportOverFCGI(1, ""); - case UNIX_DOMAIN -> - { - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - yield new HttpClientTransportOverHTTP(clientConnector); - } }; } @@ -320,13 +297,13 @@ protected void setStreamIdleTimeout(long idleTimeout) public enum Transport { - HTTP, HTTPS, H2C, H2, H3, FCGI, UNIX_DOMAIN; + HTTP, HTTPS, H2C, H2, H3, FCGI; public boolean isSecure() { return switch (this) { - case HTTP, H2C, FCGI, UNIX_DOMAIN -> false; + case HTTP, H2C, FCGI -> false; case HTTPS, H2, H3 -> true; }; } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AsyncIOServletTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AsyncIOServletTest.java index 9f20da747fd0..41472d1fd7c0 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AsyncIOServletTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AsyncIOServletTest.java @@ -1136,7 +1136,7 @@ public void onError(Throwable x) .body(requestContent) .onResponseSuccess(response -> { - if (transport == Transport.HTTP || transport == Transport.UNIX_DOMAIN) + if (transport == Transport.HTTP) responseLatch.countDown(); }) .onResponseFailure((response, failure) -> @@ -1155,7 +1155,6 @@ public void onError(Throwable x) switch (transport) { case HTTP: - case UNIX_DOMAIN: assertThat(result.getResponse().getStatus(), Matchers.equalTo(responseCode)); break; case H2C: @@ -1173,7 +1172,6 @@ public void onError(Throwable x) switch (transport) { case HTTP: - case UNIX_DOMAIN: ((HttpConnectionOverHTTP)connection).getEndPoint().shutdownOutput(); break; case H2C: diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java index ac535ce011f3..11b33f6cc8b8 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/HttpClientContinueTest.java @@ -214,7 +214,7 @@ public long getLength() assertNotNull(response); assertEquals(200, response.getStatus()); - if (EnumSet.of(Transport.HTTP, Transport.HTTPS, Transport.UNIX_DOMAIN).contains(transport)) + if (EnumSet.of(Transport.HTTP, Transport.HTTPS).contains(transport)) assertTrue(response.getHeaders().contains(HttpHeader.TRANSFER_ENCODING, "chunked")); int index = 0; diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AbstractTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AbstractTest.java index e73d3ca6676d..216a11d5c7b0 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AbstractTest.java +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AbstractTest.java @@ -46,8 +46,10 @@ import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; import org.eclipse.jetty.http3.server.AbstractHTTP3ServerConnectionFactory; import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; -import org.eclipse.jetty.http3.server.HTTP3ServerConnector; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.HostHeaderCustomizer; @@ -60,7 +62,6 @@ import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -68,8 +69,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.ExtendWith; -import static org.junit.jupiter.api.Assertions.assertTrue; - @ExtendWith(WorkDirExtension.class) public class AbstractTest { @@ -77,11 +76,11 @@ public class AbstractTest protected final HttpConfiguration httpConfig = new HttpConfiguration(); protected SslContextFactory.Server sslContextFactoryServer; + protected ServerQuicConfiguration serverQuicConfig; protected Server server; protected AbstractConnector connector; protected ServletContextHandler servletContextHandler; protected HttpClient client; - protected Path unixDomainPath; public static Collection transports() { @@ -131,14 +130,8 @@ protected void prepareServer(Transport transport, HttpServlet servlet) throws Ex protected void prepareServer(Transport transport, HttpServlet servlet, String path) throws Exception { - if (transport == Transport.UNIX_DOMAIN) - { - String unixDomainDir = System.getProperty("jetty.unixdomain.dir", System.getProperty("java.io.tmpdir")); - unixDomainPath = Files.createTempFile(Path.of(unixDomainDir), "unix_", ".sock"); - assertTrue(unixDomainPath.toAbsolutePath().toString().length() < UnixDomainServerConnector.MAX_UNIX_DOMAIN_PATH_LENGTH, "Unix-Domain path too long"); - Files.delete(unixDomainPath); - } sslContextFactoryServer = newSslContextFactoryServer(); + serverQuicConfig = new ServerQuicConfiguration(sslContextFactoryServer, workDir.getEmptyPathDir()); if (server == null) server = newServer(); connector = newConnector(transport, server); @@ -148,7 +141,7 @@ protected void prepareServer(Transport transport, HttpServlet servlet, String pa server.setHandler(servletContextHandler); } - protected void addServlet(HttpServlet servlet, String path) throws Exception + protected void addServlet(HttpServlet servlet, String path) { Objects.requireNonNull(servletContextHandler); ServletHolder holder = new ServletHolder(servlet); @@ -201,17 +194,7 @@ public AbstractConnector newConnector(Transport transport, Server server) case HTTP, HTTPS, H2C, H2, FCGI -> new ServerConnector(server, 1, 1, newServerConnectionFactory(transport)); case H3 -> - { - HTTP3ServerConnector http3ServerConnector = new HTTP3ServerConnector(server, sslContextFactoryServer, newServerConnectionFactory(transport)); - http3ServerConnector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); - yield http3ServerConnector; - } - case UNIX_DOMAIN -> - { - UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, newServerConnectionFactory(transport)); - connector.setUnixDomainPath(unixDomainPath); - yield connector; - } + new QuicServerConnector(server, serverQuicConfig, newServerConnectionFactory(transport)); }; } @@ -219,7 +202,7 @@ protected ConnectionFactory[] newServerConnectionFactory(Transport transport) { List list = switch (transport) { - case HTTP, UNIX_DOMAIN -> List.of(new HttpConnectionFactory(httpConfig)); + case HTTP -> List.of(new HttpConnectionFactory(httpConfig)); case HTTPS -> { httpConfig.addCustomizer(new SecureRequestCustomizer()); @@ -245,7 +228,7 @@ protected ConnectionFactory[] newServerConnectionFactory(Transport transport) { httpConfig.addCustomizer(new SecureRequestCustomizer()); httpConfig.addCustomizer(new HostHeaderCustomizer()); - yield List.of(new HTTP3ServerConnectionFactory(httpConfig)); + yield List.of(new HTTP3ServerConnectionFactory(serverQuicConfig, httpConfig)); } case FCGI -> List.of(new ServerFCGIConnectionFactory(httpConfig)); }; @@ -281,20 +264,14 @@ protected HttpClientTransport newHttpClientTransport(Transport transport) throws } case H3 -> { - HTTP3Client http3Client = new HTTP3Client(); - ClientConnector clientConnector = http3Client.getClientConnector(); + ClientConnector clientConnector = new ClientConnector(); clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); + SslContextFactory.Client sslContextFactory = newSslContextFactoryClient(); + clientConnector.setSslContextFactory(sslContextFactory); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); yield new HttpClientTransportOverHTTP3(http3Client); } case FCGI -> new HttpClientTransportOverFCGI(1, ""); - case UNIX_DOMAIN -> - { - ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - clientConnector.setSelectors(1); - clientConnector.setSslContextFactory(newSslContextFactoryClient()); - yield new HttpClientTransportOverHTTP(clientConnector); - } }; } @@ -326,13 +303,13 @@ protected void setStreamIdleTimeout(long idleTimeout) public enum Transport { - HTTP, HTTPS, H2C, H2, H3, FCGI, UNIX_DOMAIN; + HTTP, HTTPS, H2C, H2, H3, FCGI; public boolean isSecure() { return switch (this) { - case HTTP, H2C, FCGI, UNIX_DOMAIN -> false; + case HTTP, H2C, FCGI -> false; case HTTPS, H2, H3 -> true; }; } diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AsyncIOServletTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AsyncIOServletTest.java index 3c455e8d2038..4a10ca2b01fe 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AsyncIOServletTest.java +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-client-transports/src/test/java/org/eclipse/jetty/ee9/test/client/transport/AsyncIOServletTest.java @@ -1138,7 +1138,7 @@ public void onError(Throwable x) .body(requestContent) .onResponseSuccess(response -> { - if (transport == Transport.HTTP || transport == Transport.UNIX_DOMAIN) + if (transport == Transport.HTTP) responseLatch.countDown(); }) .onResponseFailure((response, failure) -> @@ -1157,7 +1157,6 @@ public void onError(Throwable x) switch (transport) { case HTTP: - case UNIX_DOMAIN: assertThat(result.getResponse().getStatus(), Matchers.equalTo(responseCode)); break; case H2C: @@ -1175,7 +1174,6 @@ public void onError(Throwable x) switch (transport) { case HTTP: - case UNIX_DOMAIN: ((HttpConnectionOverHTTP)connection).getEndPoint().shutdownOutput(); break; case H2C: diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 607cc2fc1820..8d1a0dca7e14 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.transport.HttpClientConnectionFactory; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpHeader; @@ -53,7 +54,9 @@ import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.TransportProtocol; import org.eclipse.jetty.io.content.ByteBufferContentSource; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; import org.eclipse.jetty.tests.testers.JettyHomeTester; import org.eclipse.jetty.tests.testers.Tester; import org.eclipse.jetty.toolchain.test.FS; @@ -968,7 +971,6 @@ public void testDefaultLoggingProviderNotActiveWhenExplicitProviderIsPresent() t } @Test - @EnabledForJreRange(min = JRE.JAVA_16) public void testUnixDomain() throws Exception { Path jettyBase = newTestJettyBaseDirectory(); @@ -992,10 +994,12 @@ public void testUnixDomain() throws Exception { assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); - ClientConnector connector = ClientConnector.forUnixDomain(path); - client = new HttpClient(new HttpClientTransportDynamic(connector)); + ClientConnector connector = new ClientConnector(); + client = new HttpClient(new HttpClientTransportDynamic(connector, HttpClientConnectionFactory.HTTP11)); client.start(); - ContentResponse response = client.GET("http://localhost/path"); + ContentResponse response = client.newRequest("http://localhost/path") + .transportProtocol(new TransportProtocol.TCPUnix(path)) + .send(); assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); } } @@ -1188,8 +1192,8 @@ public void testH3() throws Exception { assertTrue(run2.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); - HTTP3Client http3Client = new HTTP3Client(); - http3Client.getClientConnector().setSslContextFactory(new SslContextFactory.Client(true)); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(true); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); this.client = new HttpClient(new HttpClientTransportOverHTTP3(http3Client)); this.client.start(); ContentResponse response = this.client.newRequest("localhost", h3Port)