diff --git a/Jenkinsfile b/Jenkinsfile index c65f3fd2c543..d3905c5555d4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -20,7 +20,7 @@ pipeline { mavenBuild( "jdk21", "clean install -Dspotbugs.skip=true -Djacoco.skip=true", "maven3") recordIssues id: "jdk21", name: "Static Analysis jdk21", aggregatingResults: true, enabledForFailure: true, tools: [mavenConsole(), java(), checkStyle(), javaDoc()], - skipPublishingChecks: true, blameDisabled: true + skipPublishingChecks: true, skipBlames: true } } } @@ -33,7 +33,7 @@ pipeline { mavenBuild( "jdk17", "clean install -Perrorprone", "maven3") // javadoc:javadoc recordIssues id: "analysis-jdk17", name: "Static Analysis jdk17", aggregatingResults: true, enabledForFailure: true, tools: [mavenConsole(), java(), checkStyle(), errorProne(), spotBugs(), javaDoc()], - skipPublishingChecks: true, blameDisabled: true + skipPublishingChecks: true, skipBlames: true recordCoverage id: "coverage-jdk17", name: "Coverage jdk17", tools: [[parser: 'JACOCO']], sourceCodeRetention: 'MODIFIED', sourceDirectories: [[path: 'src/main/java'], [path: 'target/generated-sources/ee8']] } diff --git a/VERSION.txt b/VERSION.txt index 1f75af3b197b..da3048896c94 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -21,6 +21,7 @@ jetty-12.0.6 - 29 January 2024 + 11253 Jetty 12 ComplianceViolation.Listener not notified for URI, Cookie, and Multipart violations. + 11259 HTTP/2 connection not closed after idle timeout when TCP congested + (CVE-2024-22201) + 11260 QuickStartConfiguration cannot be mixed with contexts that do not have a `WEB-INF/quickstart-web.xml` + 11263 Using `jetty.version` override from jetty-start does not use version @@ -198,6 +199,7 @@ jetty-12.0.1 - 29 August 2023 jetty-9.4.54.v20240208 - 08 February 2024 + 1256 DoSFilter leaks USER_AUTH entries + 11259 HTTP/2 connection not closed after idle timeout when TCP congested + (CVE-2024-22201) + 11389 Strip default ports on ws/wss scheme uris too jetty-11.0.20 - 29 January 2024 diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/client-io-arch.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/client-io-arch.adoc index 504ab9606e6e..9b3405a84400 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/client-io-arch.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/client-io-arch.adoc @@ -14,22 +14,44 @@ [[pg-client-io-arch]] === I/O Architecture -The Jetty client libraries provide the basic components and APIs to implement a network client. +The Jetty client libraries provide the basic components and APIs to implement a client application. They build on the common xref:pg-arch-io[Jetty I/O Architecture] and provide client specific concepts (such as establishing a connection to a server). There are conceptually two layers that compose the Jetty client libraries: -. xref:pg-client-io-arch-network[The network layer], that handles the low level I/O and deals with buffers, threads, etc. -. xref:pg-client-io-arch-protocol[The protocol layer], that handles the parsing of bytes read from the network and the generation of bytes to write to the network. +. xref:pg-client-io-arch-transport[The transport layer], that handles the low-level communication with the server, and deals with buffers, threads, etc. +. xref:pg-client-io-arch-protocol[The protocol layer], that handles the high-level protocol by parsing the bytes read from the transport layer and by generating the bytes to write to the transport layer. -[[pg-client-io-arch-network]] -==== Network Layer +[[pg-client-io-arch-transport]] +==== Transport Layer + +The transport layer is the low-level layer that communicates with the server. + +Protocols such as HTTP/1.1 and HTTP/2 are typically transported over TCP, while the newer HTTP/3 is transported over QUIC, which is itself transported over UDP. + +However, there are other means of communication supported by the Jetty client libraries, in particular over xref:pg-client-io-arch-unix-domain[Unix-Domain sockets] (for inter-process communication), and over xref:pg-client-io-arch-memory[memory] (for intra-process communication). + +The same high-level protocol can be carried by different low-level transports. +For example, the high-level HTTP/1.1 protocol can be transported over either TCP (the default), or QUIC, or Unix-Domain sockets, or memory, because all these low-level transport provide reliable and ordered communication between client and server. + +Similarly, the high-level HTTP/3 protocol can be transported over either QUIC (the default) or memory. +It would be possible to transport HTTP/3 also over Unix-Domain sockets, but the current version of Java only supports Unix-Domain sockets for ``SocketChannel``s and not for ``DatagramChannel``s. The Jetty client libraries use the common I/O design described in xref:pg-arch-io[this section]. -The main client-side component is the link:{javadoc-url}/org/eclipse/jetty/io/ClientConnector.html[`ClientConnector`]. -The `ClientConnector` primarily wraps the link:{javadoc-url}/org/eclipse/jetty/io/SelectorManager.html[`SelectorManager`] and aggregates other four components: +The common I/O components and concepts are used for all low-level transports. +The only partial exception is the xref:pg-client-io-arch-memory[memory transport], which is not based on network components; as such it does not need a `SelectorManager`, but it exposes `EndPoint` so that high-level protocols have a common interface to interact with the low-level transport. + +The client-side abstraction for the low-level transport is `org.eclipse.jetty.io.Transport`. + +`Transport` represents how high-level protocols can be transported; there is `Transport.TCP_IP` that represents communication over TCP, but also `Transport.TCPUnix` for Unix-Domain sockets, `QuicTransport` for QUIC and `MemoryTransport` for memory. + +Applications can specify the `Transport` to use for each request as described in xref:pg-client-http-api-transport[this section]. + +When the `Transport` implementation uses the network, it delegates to `org.eclipse.jetty.io.ClientConnector`. + +`ClientConnector` primarily wraps `org.eclipse.jetty.io.SelectorManager` to provide network functionalities, and aggregates other four components: * a thread pool (in form of an `java.util.concurrent.Executor`) * a scheduler (in form of `org.eclipse.jetty.util.thread.Scheduler`) @@ -39,6 +61,8 @@ The `ClientConnector` primarily wraps the link:{javadoc-url}/org/eclipse/jetty/i The `ClientConnector` is where you want to set those components after you have configured them. If you don't explicitly set those components on the `ClientConnector`, then appropriate defaults will be chosen when the `ClientConnector` starts. +`ClientConnector` manages all network-related components, and therefore it is used for TCP, UDP, QUIC and xref:pg-client-io-arch-unix-domain[Unix-Domain sockets]. + The simplest example that creates and starts a `ClientConnector` is the following: [source,java,indent=0] @@ -60,11 +84,11 @@ A more advanced example that customizes the `ClientConnector` by overriding some include::../{doc_code}/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=advanced] ---- -Since `ClientConnector` is the component that handles the low-level network, it is also the component where you want to configure the low-level network configuration. +Since `ClientConnector` is the component that handles the low-level network transport, it is also the component where you want to configure the low-level network configuration. The most common parameters are: -* `ClientConnector.selectors`: the number of ``java.nio.Selector``s components (defaults to `1`) that are present to handle the ``SocketChannel``s opened by the `ClientConnector`. +* `ClientConnector.selectors`: the number of ``java.nio.Selector``s components (defaults to `1`) that are present to handle the ``SocketChannel``s and ``DatagramChannel``s opened by the `ClientConnector`. You typically want to increase the number of selectors only for those use cases where each selector should handle more than few hundreds _concurrent_ socket events. For example, one selector typically runs well for `250` _concurrent_ socket events; as a rule of thumb, you can multiply that number by `10` to obtain the number of opened sockets a selector can handle (`2500`), based on the assumption that not all the `2500` sockets will be active _at the same time_. * `ClientConnector.idleTimeout`: the duration of time after which `ClientConnector` closes a socket due to inactivity (defaults to `30` seconds). @@ -81,32 +105,38 @@ Please refer to the `ClientConnector` link:{javadoc-url}/org/eclipse/jetty/io/Cl [[pg-client-io-arch-unix-domain]] ===== Unix-Domain Support -link:https://openjdk.java.net/jeps/380[JEP 380] introduced Unix-Domain sockets support in Java 16, on all operative systems. +link:https://openjdk.java.net/jeps/380[JEP 380] introduced Unix-Domain sockets support in Java 16, on all operative systems, but only for ``SocketChannel``s (not for ``DatagramChannel``s). -`ClientConnector` can be configured to support Unix-Domain sockets in the following way: +`ClientConnector` handles Unix-Domain sockets exactly like it handles regular TCP sockets, so there is no additional configuration necessary -- Unix-Domain sockets are supported out-of-the-box. -[source,java,indent=0] ----- -include::../{doc_code}/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=unixDomain] ----- +Applications can specify the `Transport` to use for each request as described in xref:pg-client-http-api-transport[this section]. -[IMPORTANT] -==== -You can use Unix-Domain sockets support only when you run your client application with Java 16 or later. -==== +[[pg-client-io-arch-memory]] +===== Memory Support + +In addition to support communication between client and server via network or Unix-Domain, the Jetty client libraries also support communication between client and server via memory for intra-process communication. +This means that the client and server must be in the same JVM process. + +This functionality is provided by `org.eclipse.jetty.server.MemoryTransport`, which does not delegate to `ClientConnector`, but instead delegates to the server-side `MemoryConnector` and its related classes. + +Applications can specify the `Transport` to use for each request as described in xref:pg-client-http-api-transport[this section]. [[pg-client-io-arch-protocol]] ==== Protocol Layer -The protocol layer builds on top of the network layer to generate the bytes to be written to the network and to parse the bytes read from the network. +The protocol layer builds on top of the transport layer to generate the bytes to be written to the low-level transport and to parse the bytes read from the low-level transport. -Recall from xref:pg-arch-io-connection[this section] that Jetty uses the `Connection` abstraction to produce and interpret the network bytes. +Recall from xref:pg-arch-io-connection[this section] that Jetty uses the `Connection` abstraction to produce and interpret the low-level transport bytes. On the client side, a `ClientConnectionFactory` implementation is the component that creates `Connection` instances based on the protocol that the client wants to "speak" with the server. -Applications use `ClientConnector.connect(SocketAddress, Map)` to establish a TCP connection to the server, and must tell `ClientConnector` how to create the `Connection` for that particular TCP connection, and how to notify back the application when the connection creation succeeds or fails. +Applications may use `ClientConnector.connect(SocketAddress, Map)` to establish a TCP connection to the server, and must provide `ClientConnector` with the following information in the context map: + +* A `Transport` instance that specifies the low-level transport to use. +* A `ClientConnectionFactory` that creates `Connection` instances for the high-level protocol. +* A `Promise` that is notified when the connection creation succeeds or fails. -This is done by passing a link:{javadoc-url}/org/eclipse/jetty/io/ClientConnectionFactory.html[`ClientConnectionFactory`] (that creates `Connection` instances) and a link:{javadoc-url}/org/eclipse/jetty/util/Promise.html[`Promise`] (that is notified of connection creation success or failure) in the context `Map` as follows: +For example: [source,java,indent=0] ---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc index 1f4d763b8972..6339e011cd2a 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-api.adoc @@ -270,3 +270,33 @@ An application that implements a forwarder between two servers can be implemente ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=forwardContent] ---- + +[[pg-client-http-api-transport]] +===== Request `Transport` + +The communication between client and server happens over a xref:pg-client-io-arch-transport[low-level transport], and applications can specify the low-level transport to use for each request. + +This gives client applications great flexibility, because they can use the same `HttpClient` instance to communicate, for example, with an external third party web application via TCP, to a different process via Unix-Domain sockets, and efficiently to the same process via memory. + +Client application can also choose more esoteric configurations such as using QUIC, typically used to transport HTTP/3, to transport HTTP/1.1 or HTTP/2, because QUIC provides reliable and ordered communication like TCP does. + +Provided you have configured a xref:pg-server-http-connector[`UnixDomainServerConnector`] on the server, this is how you can configure a request to use Unix-Domain sockets: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=unixDomain] +---- + +In the same way, if you have configured a xref:pg-server-http-connector[`MemoryConnector`] on the server, this is how you can configure a request to use memory for communication: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=memory] +---- + +This is a fancy example of how to mix HTTP versions and low-level transports: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=mixedTransports] +---- diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-configuration.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-configuration.adoc index 9fbce705fa00..ed1caca31e4a 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-configuration.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-configuration.adoc @@ -19,9 +19,9 @@ Please refer to the `HttpClient` link:{javadoc-url}/org/eclipse/jetty/client/Htt The most common parameters are: -* `HttpClient.idleTimeout`: same as `ClientConnector.idleTimeout` described in xref:pg-client-io-arch-network[this section]. -* `HttpClient.connectBlocking`: same as `ClientConnector.connectBlocking` described in xref:pg-client-io-arch-network[this section]. -* `HttpClient.connectTimeout`: same as `ClientConnector.connectTimeout` described in xref:pg-client-io-arch-network[this section]. +* `HttpClient.idleTimeout`: same as `ClientConnector.idleTimeout` described in xref:pg-client-io-arch-transport[this section]. +* `HttpClient.connectBlocking`: same as `ClientConnector.connectBlocking` described in xref:pg-client-io-arch-transport[this section]. +* `HttpClient.connectTimeout`: same as `ClientConnector.connectTimeout` described in xref:pg-client-io-arch-transport[this section]. * `HttpClient.maxConnectionsPerDestination`: the max number of TCP connections that are opened for a particular destination (defaults to 64). * `HttpClient.maxRequestsQueuedPerDestination`: the max number of requests queued (defaults to 1024). diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc index eb9e737f5236..fb4840dff7a6 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/client/http/client-http-intro.adoc @@ -21,17 +21,21 @@ It offers an asynchronous API that never blocks for I/O, making it very efficien However, when all you need to do is to perform a `GET` request to a resource, Jetty's HTTP client offers also a synchronous API; a programming interface where the thread that issued the request blocks until the request/response conversation is complete. -Jetty's HTTP client supports different xref:pg-client-http-transport[transports protocols]: HTTP/1.1, HTTP/2, HTTP/3 and FastCGI. This means that the semantic of an HTTP request such as: " ``GET`` the resource ``/index.html`` " can be carried over the network in different formats. -The most common and default format is HTTP/1.1. That said, Jetty's HTTP client can carry the same request using the HTTP/2 format, the HTTP/3 format, or the FastCGI format. +Jetty's HTTP client supports different xref:pg-client-http-transport[HTTP formats]: HTTP/1.1, HTTP/2, HTTP/3 and FastCGI. +Each format has a different `HttpClientTransport` implementation, that in turn use a xref:pg-client-io-arch-transport[low-level transport] to communicate with the server. -Furthermore, every transport protocol can be sent either over the network or via Unix-Domain sockets. +This means that the semantic of an HTTP request such as: " ``GET`` the resource ``/index.html`` " can be carried over the low-level transport in different formats. +The most common and default format is HTTP/1.1. +That said, Jetty's HTTP client can carry the same request using the HTTP/2 format, the HTTP/3 format, or the FastCGI format. + +Furthermore, every format can be transported over different low-level transport, such as TCP, Unix-Domain sockets, QUIC or memory. Supports for Unix-Domain sockets requires Java 16 or later, since Unix-Domain sockets support has been introduced in OpenJDK with link:https://openjdk.java.net/jeps/380[JEP 380]. -The xref:pg-client-http-transport-fcgi[FastCGI transport] is heavily used in Jetty's xref:pg-server-fastcgi[FastCGI support] that allows Jetty to work as a reverse proxy to PHP (exactly like Apache or Nginx do) and therefore be able to serve, for example, WordPress websites, often in conjunction with Unix-Domain sockets (although it's possible to use FastCGI via network too). +The xref:pg-client-http-transport-fcgi[FastCGI format] is used in Jetty's xref:pg-server-fastcgi[FastCGI support] that allows Jetty to work as a reverse proxy to PHP (exactly like Apache or Nginx do) and therefore be able to serve, for example, WordPress websites, often in conjunction with Unix-Domain sockets (although it is possible to use FastCGI via network too). -The HTTP/2 transport allows Jetty's HTTP client to perform requests using HTTP/2 to HTTP/2 enabled web sites, see also Jetty's xref:pg-client-http2[HTTP/2 support]. +The HTTP/2 format allows Jetty's HTTP client to perform requests using HTTP/2 to HTTP/2 enabled websites, see also Jetty's xref:pg-client-http2[HTTP/2 support]. -The HTTP/3 transport allows Jetty's HTTP client to perform requests using HTTP/3 to HTTP/3 enabled web sites, see also Jetty's xref:pg-client-http3[HTTP/3 support]. +The HTTP/3 format allows Jetty's HTTP client to perform requests using HTTP/3 to HTTP/3 enabled websites, see also Jetty's xref:pg-client-http3[HTTP/3 support]. Out of the box features that you get with the Jetty HTTP client include: @@ -72,7 +76,7 @@ There are several reasons for having multiple `HttpClient` instances including, * You want to specify different configuration parameters (for example, one instance is configured with a forward proxy while another is not). * You want the two instances to behave like two different browsers and hence have different cookies, different authentication credentials, etc. -* You want to use xref:pg-client-http-transport[different transports]. +* You want to use xref:pg-client-http-transport[different ``HttpClientTransport``s]. Like browsers, HTTPS requests are supported out-of-the-box (see xref:pg-client-http-configuration-tls[this section] for the TLS configuration), as long as the server provides a valid certificate. In case the server does not provide a valid certificate (or in case it is self-signed) you want to customize ``HttpClient``'s TLS configuration as described in xref:pg-client-http-configuration-tls[this section]. @@ -105,18 +109,18 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPCli A `HttpClient` instance can be thought as a browser instance, and it manages the following components: -* a `CookieStore` (see xref:pg-client-http-cookie[this section]). -* a `AuthenticationStore` (see xref:pg-client-http-authentication[this section]). -* a `ProxyConfiguration` (see xref:pg-client-http-proxy[this section]). -* a set of ``Destination``s +* A `CookieStore` (see xref:pg-client-http-cookie[this section]). +* A `AuthenticationStore` (see xref:pg-client-http-authentication[this section]). +* A `ProxyConfiguration` (see xref:pg-client-http-proxy[this section]). +* A set of ``Destination``s -A `Destination` is the client-side component that represents an _origin_ server, and manages a queue of requests for that origin, and a xref:pg-client-http-connection-pool[pool of TCP connections] to that origin. +A `Destination` is the client-side component that represents an _origin_ server, and manages a queue of requests for that origin, and a xref:pg-client-http-connection-pool[pool of connections] to that origin. An _origin_ may be simply thought as the tuple `(scheme, host, port)` and it is where the client connects to in order to communicate with the server. However, this is not enough. If you use `HttpClient` to write a proxy you may have different clients that want to contact the same server. -In this case, you may not want to use the same proxy-to-server connection to proxy requests for both clients, for example for authentication reasons: the server may associate the connection with authentication credentials and you do not want to use the same connection for two different users that have different credentials. +In this case, you may not want to use the same proxy-to-server connection to proxy requests for both clients, for example for authentication reasons: the server may associate the connection with authentication credentials, and you do not want to use the same connection for two different users that have different credentials. Instead, you want to use different connections for different clients and this can be achieved by "tagging" a destination with a tag object that represents the remote client (for example, it could be the remote client IP address). Two origins with the same `(scheme, host, port)` but different `tag` create two different destinations and therefore two different connection pools. @@ -125,17 +129,20 @@ However, also this is not enough. It is possible for a server to speak different protocols on the same `port`. A connection may start by speaking one protocol, for example HTTP/1.1, but then be upgraded to speak a different protocol, for example HTTP/2. After a connection has been upgraded to a second protocol, it cannot speak the first protocol anymore, so it can only be used to communicate using the second protocol. -Two origins with the same `(scheme, host, port)` but different `protocol` create two different destinations and therefore two different connection pools. +Two origins with the same `(scheme, host, port, tag)` but different `protocol` create two different destinations and therefore two different connection pools. + +Finally, it is possible for a server to speak the same protocol over different xref:pg-client-io-arch-transport[low-level transports] (represented by `Transport`), for example TCP and Unix-Domain. + +Two origins with the same `(scheme, host, port, tag, protocol)` but different low-level transports create two different destinations and therefore two different connection pools. -Therefore an origin is identified by the tuple `(scheme, host, port, tag, protocol)`. +Therefore, an origin is identified by the tuple `(scheme, host, port, tag, protocol, transport)`. [[pg-client-http-connection-pool]] ==== HttpClient Connection Pooling -A `Destination` manages a `org.eclipse.jetty.client.ConnectionPool`, where connections to a particular origin are pooled for performance reasons: -opening a connection is a costly operation and it's better to reuse them for multiple requests. +A `Destination` manages a `org.eclipse.jetty.client.ConnectionPool`, where connections to a particular origin are pooled for performance reasons: opening a connection is a costly operation, and it's better to reuse them for multiple requests. -NOTE: Remember that to select a specific `Destination` you must select a specific origin, and that an origin is identified by the tuple `(scheme, host, port, tag, protocol)`, so you can have multiple ``Destination``s for the same `host` and `port`, and therefore multiple ``ConnectionPool``s +NOTE: Remember that to select a specific `Destination` you must select a specific origin, and that an origin is identified by the tuple `(scheme, host, port, tag, protocol, transport)`, so you can have multiple ``Destination``s for the same `host` and `port`, and therefore multiple ``ConnectionPool``s You can access the `ConnectionPool` in this way: @@ -147,7 +154,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPCli Jetty's client library provides the following `ConnectionPool` implementations: * `DuplexConnectionPool`, historically the first implementation, only used by the HTTP/1.1 transport. -* `MultiplexConnectionPool`, the generic implementation valid for any transport where connections are reused with a MRU (most recently used) algorithm (that is, the connections most recently returned to the connection pool are the more likely to be used again). +* `MultiplexConnectionPool`, the generic implementation valid for any transport where connections are reused with a most recently used algorithm (that is, the connections most recently returned to the connection pool are the more likely to be used again). * `RoundRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with a round-robin algorithm. * `RandomRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly. 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..a06a69284d85 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 @@ -14,11 +14,11 @@ [[pg-client-http-transport]] ==== HttpClient Pluggable Transports -Jetty's `HttpClient` can be configured to use different transport protocols to carry the semantic of HTTP requests and responses. +Jetty's `HttpClient` can be configured to use different HTTP formats to carry the semantic of HTTP requests and responses, by specifying different `HttpClientTransport` implementations. -This means that the intention of a client to request resource `/index.html` using the `GET` method can be carried over the network in different formats. +This means that the intention of a client to request resource `/index.html` using the `GET` method can be carried over a xref:pg-client-http-api-transport[low-level transport] in different formats. -An `HttpClient` transport is the component that is in charge of converting a high-level, semantic, HTTP requests such as " ``GET`` resource ``/index.html`` " into the specific format understood by the server (for example, HTTP/2 or HTTP/3), and to convert the server response from the specific format (HTTP/2 or HTTP/3) into high-level, semantic objects that can be used by applications. +An `HttpClientTransport` is the component that is in charge of converting a high-level, semantic, HTTP requests such as " ``GET`` resource ``/index.html`` " into the specific format understood by the server (for example, HTTP/2 or HTTP/3), and to convert the server response from the specific format (HTTP/2 or HTTP/3) into high-level, semantic objects that can be used by applications. The most common protocol format is HTTP/1.1, a textual protocol with lines separated by `\r\n`: @@ -44,25 +44,25 @@ x0C x0B D O C U M E ... ---- -Similarly, HTTP/2 is a binary protocol that transports the same information in a yet different format via TCP, while HTTP/3 is a binary protocol that transports the same information in yet another format via UDP. +Similarly, HTTP/2 is a binary protocol that transports the same information in a yet different format via TCP, while HTTP/3 is a binary protocol that transports the same information in yet another format via QUIC. -A protocol may be _negotiated_ between client and server. +The HTTP protocol version may be _negotiated_ between client and server. A request for a resource may be sent using one protocol (for example, HTTP/1.1), but the response may arrive in a different protocol (for example, HTTP/2). -`HttpClient` supports these static transports, each speaking only one protocol: +`HttpClient` supports these `HttpClientTransport` implementations, each speaking only one protocol: -* xref:pg-client-http-transport-http11[HTTP/1.1] (both clear-text and TLS encrypted) -* xref:pg-client-http-transport-http2[HTTP/2] (both clear-text and TLS encrypted) -* xref:pg-client-http-transport-http3[HTTP/3] (only encrypted via QUIC+TLS) -* xref:pg-client-http-transport-fcgi[FastCGI] (both clear-text and TLS encrypted) +* `HttpClientTransportOverHTTP`, for xref:pg-client-http-transport-http11[HTTP/1.1] (both clear-text and TLS encrypted) +* `HttpClientTransportOverHTTP2`, for xref:pg-client-http-transport-http2[HTTP/2] (both clear-text and TLS encrypted) +* `HttpClientTransportOverHTTP3`, for xref:pg-client-http-transport-http3[HTTP/3] (only encrypted via QUIC) +* `HttpClientTransportOverFCGI`, for xref:pg-client-http-transport-fcgi[FastCGI] (both clear-text and TLS encrypted) -`HttpClient` also supports one xref:pg-client-http-transport-dynamic[dynamic transport], that can speak different protocols and can select the right protocol by negotiating it with the server or by explicit indication from applications. +`HttpClient` also supports `HttpClientTransportDynamic`, a xref:pg-client-http-transport-dynamic[dynamic transport] that can speak different HTTP formats and can select the right protocol by negotiating it with the server or by explicit indication from applications. -Furthermore, every transport protocol can be sent either over the network or via Unix-Domain sockets. +Furthermore, every HTTP format can be sent over different xref:pg-client-http-api-transport[low-level transports] such as TCP, Unix-Domain, QUIC or memory. Supports for Unix-Domain sockets requires Java 16 or later, since Unix-Domain sockets support has been introduced in OpenJDK with link:https://openjdk.java.net/jeps/380[JEP 380]. -Applications are typically not aware of the actual protocol being used. -This allows them to write their logic against a high-level API that hides the details of the specific protocol being used over the network. +Applications are typically not aware of the actual HTTP format or low-level transport being used. +This allows them to write their logic against a high-level API that hides the details of the specific HTTP format and low-level transport being used. [[pg-client-http-transport-http11]] ===== HTTP/1.1 Transport @@ -93,7 +93,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPCli `HTTP2Client` is the lower-level client that provides an API based on HTTP/2 concepts such as _sessions_, _streams_ and _frames_ that are specific to HTTP/2. See xref:pg-client-http2[the HTTP/2 client section] for more information. -`HttpClientTransportOverHTTP2` uses `HTTP2Client` to format high-level semantic HTTP requests (like "GET resource /index.html") into the HTTP/2 specific format. +`HttpClientTransportOverHTTP2` uses `HTTP2Client` to format high-level semantic HTTP requests into the HTTP/2 specific format. [[pg-client-http-transport-http3]] ===== HTTP/3 Transport @@ -107,7 +107,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPCli `HTTP3Client` is the lower-level client that provides an API based on HTTP/3 concepts such as _sessions_, _streams_ and _frames_ that are specific to HTTP/3. See xref:pg-client-http3[the HTTP/3 client section] for more information. -`HttpClientTransportOverHTTP3` uses `HTTP3Client` to format high-level semantic HTTP requests (like "GET resource /index.html") into the HTTP/3 specific format. +`HttpClientTransportOverHTTP3` uses `HTTP3Client` to format high-level semantic HTTP requests into the HTTP/3 specific format. [[pg-client-http-transport-fcgi]] ===== FastCGI Transport @@ -126,14 +126,15 @@ The FastCGI transport is primarily used by Jetty's xref:pg-server-fastcgi[FastCG [[pg-client-http-transport-dynamic]] ===== Dynamic Transport -The static transports work well if you know in advance the protocol you want to speak with the server, or if the server only supports one protocol (such as FastCGI). +The static `HttpClientTransport` implementations work well if you know in advance the protocol you want to speak with the server, or if the server only supports one protocol (such as FastCGI). -With the advent of HTTP/2 and HTTP/3, however, servers are now able to support multiple protocols, at least both HTTP/1.1 and HTTP/2. +With the advent of HTTP/2 and HTTP/3, however, servers are now able to support multiple protocols. The HTTP/2 protocol is typically negotiated between client and server. This negotiation can happen via ALPN, a TLS extension that allows the client to tell the server the list of protocol that the client supports, so that the server can pick one of the client supported protocols that also the server supports; or via HTTP/1.1 upgrade by means of the `Upgrade` header. -Applications can configure the dynamic transport with one or more _application_ protocols such as HTTP/1.1 or HTTP/2. The implementation will take care of using TLS for HTTPS URIs, using ALPN if necessary, negotiating protocols, upgrading from one protocol to another, etc. +Applications can configure the dynamic transport with one or more HTTP versions such as HTTP/1.1, HTTP/2 or HTTP/3. +The implementation will take care of using TLS for HTTPS URIs, using ALPN if necessary, negotiating protocols, upgrading from one protocol to another, etc. By default, the dynamic transport only speaks HTTP/1.1: @@ -149,56 +150,52 @@ The dynamic transport can be configured with just one protocol, making it equiva include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicOneProtocol] ---- -The dynamic transport, however, has been implemented to support multiple transports, in particular both HTTP/1.1 and HTTP/2: +The dynamic transport, however, has been implemented to support multiple transports, in particular HTTP/1.1, HTTP/2 and HTTP/3: [source,java,indent=0] ---- -include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicH1H2] +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicH1H2H3] ---- -NOTE: The order in which the protocols are specified to `HttpClientTransportDynamic` indicates what is the client preference. +The order in which the protocols are specified to `HttpClientTransportDynamic` indicates what is the client preference (first the most preferred). -IMPORTANT: When using TLS (i.e. URIs with the `https` scheme), the application protocol is _negotiated_ between client and server via ALPN, and it is the server that decides what is the application protocol to use for the communication, regardless of the client preference. +When clear-text communication is used (i.e. URIs with the `http` scheme) there is no HTTP protocol version negotiation, and therefore the application must know _a priori_ whether the server supports the HTTP version or not. +For example, if the server only supports clear-text HTTP/2, and `HttpClientTransportDynamic` is configured as in the example above, where HTTP/1.1 has precedence over HTTP/2, the client will send, by default, a clear-text HTTP/1.1 request to a clear-text HTTP/2 only server, which will result in a communication failure. -When clear-text communication is used (i.e. URIs with the `http` scheme) there is no application protocol negotiation, and therefore the application must know _a priori_ whether the server supports the protocol or not. -For example, if the server only supports clear-text HTTP/2, and `HttpClientTransportDynamic` is configured as in the example above, the client will send, by default, a clear-text HTTP/1.1 request to a clear-text HTTP/2 only server, which will result in a communication failure. +When using TLS (i.e. URIs with the `https` scheme), the HTTP protocol version is _negotiated_ between client and server via ALPN, and it is the server that decides what is the application protocol to use for the communication, regardless of the client preference. -Provided that the server supports both HTTP/1.1 and HTTP/2 clear-text, client applications can explicitly hint the version they want to use: +[IMPORTANT] +==== +HTTP/1.1 and HTTP/2 are _compatible_ because they both use TCP, while HTTP/3 is incompatible with previous HTTP versions because it uses QUIC. -[source,java,indent=0] +Only compatible HTTP versions can negotiate the HTTP protocol version to use via ALPN, and only compatible HTTP versions can be upgraded from an older version to a newer version. +==== + +Provided that the server supports HTTP/1.1, HTTP/2 and HTTP/3, client applications can explicitly hint the version they want to use: + +[source,java,indent=0,options=nowrap] ---- -include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicClearText] +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicExplicitVersion] ---- -In case of TLS encrypted communication using the `https` scheme, things are a little more complicated. - If the client application explicitly specifies the HTTP version, then ALPN is not used by the client. By specifying the HTTP version explicitly, the client application has prior-knowledge of what HTTP version the server supports, and therefore ALPN is not needed. If the server does not support the HTTP version chosen by the client, then the communication will fail. -If the client application does not explicitly specify the HTTP version, then ALPN will be used by the client. +If the client application does not explicitly specify the HTTP version, then ALPN will be used by the client, but only for compatible protocols. If the server also supports ALPN, then the protocol will be negotiated via ALPN and the server will choose the protocol to use. If the server does not support ALPN, the client will try to use the first protocol configured in `HttpClientTransportDynamic`, and the communication may succeed or fail depending on whether the server supports the protocol chosen by the client. -[[pg-client-http-transport-unix-domain]] -===== Unix-Domain Configuration - -All the transports can be configured with a `ClientConnector`, the component that is responsible for the transmission of the bytes generated by the transport to the server. - -By default, `ClientConnector` uses TCP networking to send bytes to the server and receive bytes from the server. +For example, HTTP/3 is not compatible with previous HTTP version; if `HttpClientTransportDynamic` is configured to prefer HTTP/3, it will be the only protocol attempted by the client: -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. - -To configure Unix-Domain sockets, you can create a `ClientConnector` instance in the following way: - -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- -include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=unixDomain] +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicPreferH3] ---- -[IMPORTANT] -==== -You can use Unix-Domain sockets support only when you run your client application with Java 16 or later. -==== +When the client application configures `HttpClientTransportDynamic` to prefer HTTP/2, there could be ALPN negotiation between HTTP/2 and HTTP/1.1 (but not HTTP/3 because it is incompatible); HTTP/3 will only be possible by specifying the HTTP version explicitly: -You can configure a Jetty server to use Unix-Domain sockets, as explained in xref:pg-server-http-connector[this section]. +[source,java,indent=0,options=nowrap] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicPreferH2] +---- 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 71c9abd11b04..1591b7c579db 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,37 +20,49 @@ 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] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnector] ---- `UnixDomainServerConnector` also wraps a `ServerSocketChannel` and can be configured with the Unix-Domain path to listen to: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorUnix] ---- [IMPORTANT] ==== -You can use Unix-Domain sockets support only when you run your server with Java 16 or later. +You can use Unix-Domain sockets 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, as shown in the example below. +Since the communication via UDP does not require to "accept" connections like TCP does, the number of xref:pg-server-http-connector-acceptors[acceptors] is set to `0` and there is no API to configure their number. -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorQuic] ---- +`MemoryConnector` uses in-process memory, not sockets, for the communication between client and server, that therefore must be in the same process. + +Typical usage of `MemoryConnector` is the following: + +[source,java,indent=0,options=nowrap] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=memoryConnector] +---- + [[pg-server-http-connector-acceptors]] ===== Acceptors @@ -61,13 +73,13 @@ When a TCP connection is accepted, `ServerConnector` wraps the accepted `SocketC Therefore, there is a little moment where the acceptor thread is not accepting new connections because it is busy wrapping the just accepted connection to pass it to the `SelectorManager`. Connections that are ready to be accepted but are not accepted yet are queued in a bounded queue (at the OS level) whose capacity can be configured with the `acceptQueueSize` parameter. -If your application must withstand a very high rate of connections opened, configuring more than one acceptor thread may be beneficial: when one acceptor thread accepts one connection, another acceptor thread can take over accepting connections. +If your application must withstand a very high rate of connection opening, configuring more than one acceptor thread may be beneficial: when one acceptor thread accepts one connection, another acceptor thread can take over accepting connections. [[pg-server-http-connector-selectors]] ===== Selectors The _selectors_ are components that manage a set of accepted TCP sockets, implemented by xref:pg-arch-io-selector-manager[`ManagedSelector`]. -For QUIC or HTTP/3, there are no accepted TCP sockets, but only one `DatagramChannel` and therefore there is only 1 selector. +For QUIC or HTTP/3, there are no accepted TCP sockets, but only one `DatagramChannel` and therefore there is only one selector. Each selector requires one thread and uses the Java NIO mechanism to efficiently handle a set of registered channels. @@ -83,25 +95,26 @@ In this case a single selector may be able to manage less TCP sockets because ch It is possible to configure more than one `Connector` per `Server`. Typical cases are a `ServerConnector` for clear-text HTTP, and another `ServerConnector` for secure HTTP. -Another case could be a publicly exposed `ServerConnector` for secure HTTP, and an internally exposed `UnixDomainServerConnector` for clear-text HTTP. -Yet another example could be a `ServerConnector` for clear-text HTTP, a `ServerConnector` for secure HTTP/2, and an `HTTP3ServerConnector` for QUIC+HTTP/3. +Another case could be a publicly exposed `ServerConnector` for secure HTTP, and an internally exposed `UnixDomainServerConnector` or `MemoryConnector` for clear-text HTTP. +Yet another example could be a `ServerConnector` for clear-text HTTP, a `ServerConnector` for secure HTTP/2, and an `QuicServerConnector` for QUIC+HTTP/3. For example: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectors] ---- If you do not specify the port the connector listens to explicitly, the OS will allocate one randomly when the connector starts. -You may need to use the randomly allocated port to configure other components, for example the port to use for secure redirects (when redirecting from a URI with the `http` scheme to the `https` scheme). -Another example is to bind both the HTTP/2 connector and the HTTP/3 connector to the same port. -This is possible since the HTTP/2 connector uses TCP, while the HTTP/3 connector uses UDP. +You may need to use the randomly allocated port to configure other components. +One example is to use the randomly allocated port to configure secure redirects (when redirecting from a URI with the `http` scheme to the `https` scheme). +Another example is to bind both the HTTP/2 connector and the HTTP/3 connector to the same randomly allocated port. +It is possible that the HTTP/2 connector and the HTTP/3 connector share the same port, because one uses TCP, while the other uses UDP. For example: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=sameRandomPort] ---- @@ -134,7 +147,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer This is how you configure Jetty to support clear-text HTTP/1.1: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=http11] ---- @@ -144,7 +157,7 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer Supporting encrypted HTTP/1.1 (that is, requests with the `https` scheme) is supported by configuring an `SslContextFactory` that has access to the KeyStore containing the private server key and public server certificate, in this way: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=tlsHttp11] ---- @@ -163,7 +176,7 @@ Therefore, with HTTP/2, clients that connect to port `80` (or to a specific Unix Jetty can support both HTTP/1.1 and HTTP/2 on the same clear-text port by configuring both the HTTP/1.1 and the HTTP/2 ``ConnectionFactory``s: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=http11H2C] ---- @@ -181,7 +194,7 @@ When using encrypted HTTP/2, the unencrypted protocol is negotiated by client an Jetty supports ALPN and encrypted HTTP/2 with this configuration: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=tlsALPNHTTP] ---- @@ -244,7 +257,7 @@ The code necessary to configure HTTP/2 is described in xref:pg-server-http-conne To setup HTTP/3, for example on port `843`, you need the following code (some of which could be shared with other connectors such as HTTP/2's): -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=h3] ---- @@ -256,7 +269,7 @@ The use of the Quiche native library requires the private key and public certifi It is therefore mandatory to configure the PEM directory as shown above. The PEM directory must also be adequately protected using file system permissions, because it stores the private key PEM file. -You want to grant as few permissions as possible, typically only the equivalent of POSIX `rwx` to the user that runs the Jetty process. Using `/tmp` or any other directory accessible by any user is not a secure choice. +You want to grant as few permissions as possible, typically the equivalent of POSIX `rwx` only to the user that runs the Jetty process. Using `/tmp` or any other directory accessible by any user is not a secure choice. ==== [[pg-server-http-connector-protocol-tls-conscrypt]] @@ -276,7 +289,7 @@ To use Conscrypt as TLS provider, you must have the Conscrypt jar and the Jetty Then, you must configure the JDK with the Conscrypt provider, and configure Jetty to use the Conscrypt provider, in this way: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=conscrypt] ---- @@ -292,7 +305,7 @@ NOTE: The PROXY protocol is widely supported by load balancers such as link:http To support this case, Jetty can be configured in this way: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=proxyHTTP] ---- @@ -306,7 +319,7 @@ Therefore the `ProxyConnection` will handle the PROXY protocol bytes and `HttpCo The load balancer may be configured to communicate with Jetty backend servers via Unix-Domain sockets (requires Java 16 or later). For example: -[source,java,indent=0] +[source,java,indent=0,options=nowrap] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=proxyHTTPUnix] ---- 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..d621cc99f6eb 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 @@ -18,10 +18,10 @@ import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; 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; @@ -30,6 +30,7 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.Transport; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.util.BufferUtil; @@ -142,6 +143,9 @@ public void onFillable() int port = 8080; SocketAddress address = new InetSocketAddress(host, port); + // The Transport instance. + Transport transport = Transport.TCP_IP; + // The ClientConnectionFactory that creates CustomConnection instances. ClientConnectionFactory connectionFactory = (endPoint, context) -> { @@ -153,7 +157,8 @@ 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(Transport.class.getName(), transport); context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); clientConnector.connect(address, context); @@ -257,7 +262,7 @@ public void writeLine(String line, Callback callback) ClientConnector clientConnector = new ClientConnector(); clientConnector.start(); - String host = "wikipedia.org"; + String host = "example.org"; int port = 80; SocketAddress address = new InetSocketAddress(host, port); @@ -267,6 +272,7 @@ public void writeLine(String line, Callback callback) CompletableFuture connectionPromise = new Promise.Completable<>(); Map context = new HashMap<>(); + context.put(Transport.class.getName(), Transport.TCP_IP); context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); clientConnector.connect(address, context); @@ -279,9 +285,7 @@ public void writeLine(String line, Callback callback) connection.onLine(line -> System.getLogger("app").log(INFO, "line: {0}", line)); // Write a line. - connection.writeLine("" + - "GET / HTTP/1.0\r\n" + - "", Callback.NOOP); + connection.writeLine("GET / HTTP/1.0\r\n", Callback.NOOP); } else { @@ -378,7 +382,7 @@ public void writeLine(String line, Callback callback) clientConnector.start(); // Use port 443 to contact the server using encrypted HTTP. - String host = "wikipedia.org"; + String host = "example.org"; int port = 443; SocketAddress address = new InetSocketAddress(host, port); @@ -392,7 +396,8 @@ public void writeLine(String line, Callback callback) // We will obtain a SslConnection now. CompletableFuture connectionPromise = new Promise.Completable<>(); - Map context = new HashMap<>(); + Map context = new ConcurrentHashMap<>(); + context.put(Transport.class.getName(), Transport.TCP_IP); context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); clientConnector.connect(address, context); @@ -407,9 +412,7 @@ public void writeLine(String line, Callback callback) connection.onLine(line -> System.getLogger("app").log(INFO, "line: {0}", line)); // Write a line. - connection.writeLine("" + - "GET / HTTP/1.0\r\n" + - "", Callback.NOOP); + connection.writeLine("GET / HTTP/1.0\r\n", Callback.NOOP); } else { @@ -419,19 +422,6 @@ public void writeLine(String line, Callback callback) // end::tlsTelnet[] } - public void unixDomain() throws Exception - { - // tag::unixDomain[] - // 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 clientConnector = ClientConnector.forUnixDomain(unixDomainPath); - clientConnector.start(); - // end::unixDomain[] - } - public static void main(String[] args) throws Exception { new ClientConnectorDocs().tlsTelnet(); 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 9aa2b2040f2f..816c4f3427f1 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 @@ -67,11 +67,18 @@ import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3; import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.Transport; import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransport; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -888,11 +895,11 @@ public void http2Transport() throws Exception { // tag::http2Transport[] // The HTTP2Client powers the HTTP/2 transport. - HTTP2Client h2Client = new HTTP2Client(); - h2Client.setInitialSessionRecvWindow(64 * 1024 * 1024); + HTTP2Client http2Client = new HTTP2Client(); + http2Client.setInitialSessionRecvWindow(64 * 1024 * 1024); // Create and configure the HTTP/2 transport. - HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(h2Client); + HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(http2Client); transport.setUseALPN(true); HttpClient client = new HttpClient(transport); @@ -903,12 +910,15 @@ 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(); - h3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(clientQuicConfig); + http3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); // Create and configure the HTTP/3 transport. - HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(h3Client); + HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(http3Client); HttpClient client = new HttpClient(transport); client.start(); @@ -951,57 +961,136 @@ public void dynamicOneProtocol() // end::dynamicOneProtocol[] } - public void dynamicH1H2() throws Exception + public void dynamicH1H2H3() throws Exception { - // tag::dynamicH1H2[] + // tag::dynamicH1H2H3[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; HTTP2Client http2Client = new HTTP2Client(connector); ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); - HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2); + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + + // The order of the protocols indicates the client's preference. + // The first is the most preferred, the last is the least preferred, but + // the protocol version to use can be explicitly specified in the request. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2, http3); HttpClient client = new HttpClient(transport); client.start(); - // end::dynamicH1H2[] + // end::dynamicH1H2H3[] } - public void dynamicClearText() throws Exception + public void dynamicExplicitVersion() throws Exception { - // tag::dynamicClearText[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(connector); ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); - HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + // tag::dynamicExplicitVersion[] + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2, http3); HttpClient client = new HttpClient(transport); client.start(); - // The server supports both HTTP/1.1 and HTTP/2 clear-text on port 8080. + // The server supports HTTP/1.1, HTTP/2 and HTTP/3. - // Make a clear-text request without explicit version. - // The first protocol specified to HttpClientTransportDynamic - // is picked, in this example will be HTTP/1.1. - ContentResponse http1Response = client.newRequest("host", 8080).send(); + ContentResponse http1Response = client.newRequest("https://host/") + // Specify the version explicitly. + .version(HttpVersion.HTTP_1_1) + .send(); - // Make a clear-text request with explicit version. - // Clear-text HTTP/2 is used for this request. - ContentResponse http2Response = client.newRequest("host", 8080) + ContentResponse http2Response = client.newRequest("https://host/") // Specify the version explicitly. .version(HttpVersion.HTTP_2) .send(); + ContentResponse http3Response = client.newRequest("https://host/") + // Specify the version explicitly. + .version(HttpVersion.HTTP_3) + .send(); + // Make a clear-text upgrade request from HTTP/1.1 to HTTP/2. // The request will start as HTTP/1.1, but the response will be HTTP/2. - ContentResponse upgradedResponse = client.newRequest("host", 8080) + ContentResponse upgradedResponse = client.newRequest("https://host/") .headers(headers -> headers .put(HttpHeader.UPGRADE, "h2c") .put(HttpHeader.HTTP2_SETTINGS, "") .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .send(); - // end::dynamicClearText[] + // end::dynamicExplicitVersion[] + } + + public void dynamicPreferH3() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + // tag::dynamicPreferH3[] + // Client prefers HTTP/3. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http3, http2, http1); + HttpClient client = new HttpClient(transport); + client.start(); + + // No explicit HTTP version specified. + // Either HTTP/3 succeeds, or communication failure. + ContentResponse httpResponse = client.newRequest("https://host/") + .send(); + // end::dynamicPreferH3[] + } + + public void dynamicPreferH2() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + // tag::dynamicPreferH2[] + // Client prefers HTTP/2. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http2, http1, http3); + HttpClient client = new HttpClient(transport); + client.start(); + + // No explicit HTTP version specified. + // Either HTTP/1.1 or HTTP/2 will be negotiated via ALPN. + // HTTP/3 only possible by specifying the version explicitly. + ContentResponse httpResponse = client.newRequest("https://host/") + .send(); + // end::dynamicPreferH2[] } public void getConnectionPool() throws Exception @@ -1050,25 +1139,104 @@ 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(); + + ContentResponse response = httpClient.newRequest("jetty.org", 80) + // Specify that the request must be sent over Unix-Domain. + .transport(new Transport.TCPUnix(unixDomainPath)) + .send(); // end::unixDomain[] } + + public void memory() throws Exception + { + // tag::memory[] + // The server-side MemoryConnector speaking HTTP/1.1. + Server server = new Server(); + MemoryConnector memoryConnector = new MemoryConnector(server, new HttpConnectionFactory()); + server.addConnector(memoryConnector); + // ... + + // The code above is the server-side. + // ---- + // The code below is the client-side. + + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // Use the MemoryTransport to communicate with the server-side. + Transport transport = new MemoryTransport(memoryConnector); + + httpClient.newRequest("http://localhost/") + // Specify the Transport to use. + .transport(transport) + .send(); + // end::memory[] + } + + public void mixedTransports() throws Exception + { + Path unixDomainPath = Path.of("/path/to/server.sock"); + + Server server = new Server(); + MemoryConnector memoryConnector = new MemoryConnector(server, new HttpConnectionFactory()); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, clientConnector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + + // tag::mixedTransports[] + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, http2, http1, http3)); + httpClient.start(); + + // Make a TCP request to a 3rd party web application. + ContentResponse thirdPartyResponse = httpClient.newRequest("https://third-party.com/api") + // No need to specify the Transport, TCP will be used by default. + .send(); + + // Upload the third party response content to a validation process. + ContentResponse validatedResponse = httpClient.newRequest("http://localhost/validate") + // The validation process is available via Unix-Domain. + .transport(new Transport.TCPUnix(unixDomainPath)) + .method(HttpMethod.POST) + .body(new BytesRequestContent(thirdPartyResponse.getContent())) + .send(); + + // Process the validated response intra-process by sending + // it to another web application in the same Jetty server. + ContentResponse response = httpClient.newRequest("http://localhost/process") + // The processing is in-memory. + .transport(new MemoryTransport(memoryConnector)) + .method(HttpMethod.POST) + .body(new BytesRequestContent(validatedResponse.getContent())) + .send(); + // end::mixedTransports[] + } } 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 48c0c3930657..1572e0ceb686 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 @@ -31,6 +31,8 @@ import jakarta.servlet.http.HttpServletResponse; import org.conscrypt.OpenSSLProvider; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.ee10.servlet.DefaultServlet; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; @@ -48,9 +50,10 @@ 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.io.ssl.SslHandshakeListener; +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; @@ -62,6 +65,8 @@ 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.MemoryTransport; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.ProxyConnectionFactory; import org.eclipse.jetty.server.Request; @@ -227,8 +232,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); @@ -240,6 +247,31 @@ public void configureConnectorQuic() throws Exception // end::configureConnectorQuic[] } + public void memoryConnector() throws Exception + { + // tag::memoryConnector[] + Server server = new Server(); + + // Create a MemoryConnector instance that speaks HTTP/1.1. + MemoryConnector connector = new MemoryConnector(server, new HttpConnectionFactory()); + + server.addConnector(connector); + server.start(); + + // The code above is the server-side. + // ---- + // The code below is the client-side. + + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + // Use the memory Transport to communicate with the server-side. + .transport(new MemoryTransport(connector)) + .send(); + // end::memoryConnector[] + } + public void configureConnectors() throws Exception { // tag::configureConnectors[] @@ -293,8 +325,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, @@ -528,12 +561,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..2845f1fbf929 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().getTransport().connect(address, context); } @Override diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentSourceRequestContent.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentSourceRequestContent.java new file mode 100644 index 000000000000..0765aaec8fae --- /dev/null +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/ContentSourceRequestContent.java @@ -0,0 +1,85 @@ +// +// ======================================================================== +// 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.client; + +import java.util.Objects; + +import org.eclipse.jetty.io.Content; + +/** + * A {@link Request.Content} that wraps a {@link Content.Source}. + */ +public class ContentSourceRequestContent implements Request.Content +{ + private final Content.Source source; + private final String contentType; + + public ContentSourceRequestContent(Content.Source source) + { + this(source, "application/octet-stream"); + } + + public ContentSourceRequestContent(Content.Source source, String contentType) + { + this.source = Objects.requireNonNull(source); + this.contentType = contentType; + } + + public Content.Source getContentSource() + { + return source; + } + + @Override + public String getContentType() + { + return contentType; + } + + @Override + public long getLength() + { + return getContentSource().getLength(); + } + + @Override + public Content.Chunk read() + { + return getContentSource().read(); + } + + @Override + public void demand(Runnable demandCallback) + { + getContentSource().demand(demandCallback); + } + + @Override + public void fail(Throwable failure) + { + fail(failure, true); + } + + @Override + public void fail(Throwable failure, boolean last) + { + getContentSource().fail(failure, last); + } + + @Override + public boolean rewind() + { + return getContentSource().rewind(); + } +} 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 7f619611dd96..100376aff51d 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.Transport; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.Jetty; @@ -81,7 +82,7 @@ * for cases where this is needed.

*

HttpClient also acts as a central configuration point for cookies, via {@link #getHttpCookieStore()}.

*

Typical usage:

- *
+ * 
{@code
  * HttpClient httpClient = new HttpClient();
  * httpClient.start();
  *
@@ -95,15 +96,12 @@
  * int status = response.status();
  *
  * // Asynchronously
- * httpClient.newRequest("http://localhost:8080").send(new Response.CompleteListener()
+ * httpClient.newRequest("http://localhost:8080").send(result ->
  * {
- *     @Override
- *     public void onComplete(Result result)
- *     {
- *         ...
- *     }
+ *     Response response = result.getResponse();
+ *     ...
  * });
- * 
+ * }
*/ @ManagedObject("The HTTP client") public class HttpClient extends ContainerLifeCycle @@ -471,7 +469,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); + Transport transport = request.getTransport(); + if (transport == null) + { + // Ask the ClientConnector for backwards compatibility + // until ClientConnector.Configurator is removed. + transport = connector.newTransport(); + if (transport == null) + transport = Transport.TCP_IP; + } + return new Origin(scheme, new Origin.Address(host, port), request.getTag(), protocol, transport); } /** @@ -529,39 +536,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(); + + Transport transport = origin.getTransport(); + context.put(Transport.class.getName(), transport); + + if (transport.requiresDomainNameResolution()) { - @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); + } + }); + HttpClient.this.transport.connect((SocketAddress)socketAddresses.get(index), context); + } + }); + } + else + { + context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise); + this.transport.connect(transport.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..830f6425ea33 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.Transport; 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, Transport.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, Transport.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/MultiPartRequestContent.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/MultiPartRequestContent.java index 3992f03f33db..388a9610a21a 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/MultiPartRequestContent.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/MultiPartRequestContent.java @@ -34,12 +34,12 @@ * .send(); * *

The above example would be the equivalent of submitting this form:

- *
- * <form method="POST" enctype="multipart/form-data"  accept-charset="UTF-8">
- *     <input type="text" name="field" value="foo" />
- *     <input type="file" name="icon" />
- * </form>
- * 
+ *
{@code
+ * 
+ * + * + *
+ * }
*/ public class MultiPartRequestContent extends MultiPartFormData.ContentSource implements Request.Content { 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 73b56b8a921f..9b0781ffd970 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 @@ -23,23 +23,29 @@ import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.Transport; import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.URIUtil; /** *

Class that groups the elements that uniquely identify a destination.

*

The elements are {@code scheme}, {@code host}, {@code port}, a - * {@link Origin.Protocol} and a tag object that further distinguishes - * destinations that have the same origin and protocol.

- *

In general it is possible that, for the same origin, the server can - * speak different protocols (for example, clear-text HTTP/1.1 and clear-text - * HTTP/2), so the {@link Origin.Protocol} makes that distinction.

+ * {@link Origin.Protocol}, a tag object that further distinguishes + * destinations that have the same scheme, host, port and protocol, + * and a {@link Transport}.

+ *

In general it is possible that, for the same scheme, host and port, + * the server can speak different protocols (for example, clear-text HTTP/1.1 + * and clear-text HTTP/2), so the {@link Origin.Protocol} makes that distinction.

*

Furthermore, it may be desirable to have different destinations for - * the same origin and protocol (for example, when using the PROXY protocol - * in a reverse proxy server, you want to be able to map the client ip:port - * to the destination {@code tag}, so that all the connections to the server - * associated to that destination can specify the PROXY protocol bytes for - * that particular client connection.

+ * the same scheme, host, port and protocol. + * For example, when using the PROXY protocol in a reverse proxy server, you + * may want to be able to map the client ip:port to the destination {@code tag}, + * so that all the connections to the server associated to that destination can + * specify the PROXY protocol bytes for that particular client connection.

+ *

Finally, it is necessary to have different destinations for the same + * scheme, host, port, and protocol, but having different {@link Transport}, + * for example when the same server may be reached via TCP/IP but also via + * Unix-Domain sockets.

*/ public class Origin { @@ -47,6 +53,7 @@ public class Origin private final Address address; private final Object tag; private final Protocol protocol; + private final Transport transport; public Origin(String scheme, String host, int port) { @@ -74,11 +81,17 @@ public Origin(String scheme, Address address, Object tag) } public Origin(String scheme, Address address, Object tag, Protocol protocol) + { + this(scheme, address, tag, protocol, Transport.TCP_IP); + } + + public Origin(String scheme, Address address, Object tag, Protocol protocol, Transport transport) { this.scheme = URIUtil.normalizeScheme(Objects.requireNonNull(scheme)); this.address = address; this.tag = tag; this.protocol = protocol; + this.transport = transport; } public String getScheme() @@ -101,6 +114,17 @@ public Protocol getProtocol() return protocol; } + public Transport getTransport() + { + return transport; + } + + @Override + public int hashCode() + { + return Objects.hash(scheme, address, tag, protocol, transport); + } + @Override public boolean equals(Object obj) { @@ -110,15 +134,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() @@ -129,12 +148,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(), + getTransport() + ); } public static class Address @@ -216,7 +237,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..4a7982d801a1 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.Transport; import org.eclipse.jetty.util.Fields; /** @@ -103,6 +104,23 @@ default Request port(int port) return this; } + /** + * @param transport the {@link Transport} of this request + * @return this request object + */ + default Request transport(Transport transport) + { + return this; + } + + /** + * @return the {@link Transport} of this request + */ + default Transport getTransport() + { + 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..c5e9f081a857 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.Transport; public class HttpClientConnectionFactory implements ClientConnectionFactory { @@ -49,6 +50,12 @@ public List getProtocols(boolean secure) return protocols; } + @Override + public Transport newTransport() + { + return Transport.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..8f7da5848174 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,8 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.Transport; +import org.eclipse.jetty.util.annotation.ManagedObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +47,7 @@ * it supports, in order of preference. The typical case is when the server supports both HTTP/1.1 and * HTTP/2, but the client does not know that. In this case, the application will create a * HttpClientTransportDynamic in this way:

- *
+ * 
{@code
  * ClientConnector clientConnector = new ClientConnector();
  * // Configure the clientConnector.
  *
@@ -62,7 +61,7 @@
  *
  * // Create the HttpClient.
  * client = new HttpClient(transport);
- * 
+ * }
*

Note how in the code above the HttpClientTransportDynamic has been created with the application * protocols {@code h2} and {@code h1}, without the need to specify TLS (which is implied by the request * scheme) or ALPN (which is implied by HTTP/2 over TLS).

@@ -77,46 +76,49 @@ * version, or request headers, or request attributes, or even request path) by overriding * {@link HttpClientTransport#newOrigin(Request)}.

*/ +@ManagedObject("The HTTP client transport that supports many HTTP versions") public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport { 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, in order of preference + * (first the most preferred).

+ * + * @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 protocols, + * in order of preference (first the most preferred).

* * @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 +132,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); + + Transport transport = request.getTransport(); + if (transport == null) + { + // Ask the ClientConnector for backwards compatibility + // until ClientConnector.Configurator is removed. + transport = getClientConnector().newTransport(); + if (transport == null) + transport = matchingInfos.get(0).newTransport(); + request.transport(transport); + } + + 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.getTransport().isIntrinsicallySecure(); if (!intrinsicallySecure && destination.isSecure() && protocol.isNegotiate()) { factory = new ALPNClientConnectionFactory(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); @@ -237,7 +295,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 ae55652baf07..8ba6035f2e51 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 @@ -70,7 +70,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(since = "12.0.7", forRemoval = true) 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; @@ -86,9 +104,11 @@ public HttpDestination(HttpClient client, Origin origin, boolean intrinsicallySe host += ":" + port; hostField = new HttpField(HttpHeader.HOST, host); + ClientConnectionFactory connectionFactory = client.getTransport(); + boolean intrinsicallySecure = origin.getTransport().isIntrinsicallySecure(); + ProxyConfiguration proxyConfig = client.getProxyConfiguration(); proxy = proxyConfig.match(origin); - ClientConnectionFactory connectionFactory = client.getTransport(); if (proxy != null) { connectionFactory = proxy.newClientConnectionFactory(connectionFactory); @@ -385,7 +405,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 8471655ef6f7..b804e2a0d217 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 @@ -57,6 +57,7 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.Transport; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.NanoTime; import org.eclipse.jetty.util.Promise; @@ -79,6 +80,7 @@ public class HttpRequest implements Request private String path; private String query; private URI uri; + private Transport transport; private String method = HttpMethod.GET.asString(); private HttpVersion version = HttpVersion.HTTP_1_1; private boolean versionExplicit; @@ -216,6 +218,19 @@ public Request port(int port) return this; } + @Override + public Request transport(Transport transport) + { + this.transport = transport; + return this; + } + + @Override + public Transport getTransport() + { + 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 5c5d2f88ce7a..dd977d2d2b36 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.Transport; import org.eclipse.jetty.proxy.ProxyHandler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; @@ -268,12 +269,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); @@ -334,6 +330,10 @@ protected void sendProxyToServerRequest(Request clientToProxyRequest, org.eclips proxyToServerRequest.headers(headers -> headers.put(HttpHeader.COOKIE, allCookies)); } + Path unixDomain = getUnixDomainPath(); + if (unixDomain != null) + proxyToServerRequest.transport(new Transport.TCPUnix(unixDomain)); + super.sendProxyToServerRequest(clientToProxyRequest, proxyToServerRequest, proxyToClientResponse, proxyToClientCallback); } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java index fffb1cd8bdc7..e1d16ed11e85 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java @@ -357,12 +357,25 @@ private static Set copyOf(Set violations) public static String checkUriCompliance(UriCompliance compliance, HttpURI uri, ComplianceViolation.Listener listener) { - for (UriCompliance.Violation violation : UriCompliance.Violation.values()) + if (uri.hasViolations()) { - if (uri.hasViolation(violation) && (compliance == null || !compliance.allows(violation))) - return violation.getDescription(); - else if (listener != null) - listener.onComplianceViolation(new ComplianceViolation.Event(compliance, violation, uri.toString())); + StringBuilder violations = null; + for (UriCompliance.Violation violation : uri.getViolations()) + { + if (compliance == null || !compliance.allows(violation)) + { + if (listener != null) + listener.onComplianceViolation(new ComplianceViolation.Event(compliance, violation, uri.toString())); + + if (violations == null) + violations = new StringBuilder(); + else + violations.append(", "); + violations.append(violation.getDescription()); + } + } + if (violations != null) + return violations.toString(); } return null; } 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..567f27db9392 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.Transport; 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 Transport newTransport() + { + return Transport.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 26e1125ec096..cd56a974030e 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 @@ -47,13 +47,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(); @@ -63,13 +63,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") @@ -86,31 +86,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) { @@ -121,20 +113,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); } @@ -147,7 +132,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().getTransport(), address, factory, listener, promise, context); } protected void connect(InetSocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) @@ -165,7 +151,7 @@ public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, MapHTTP2Client provides an asynchronous, non-blocking implementation * to send HTTP/2 frames to a server.

*

Typical usage:

- *
+ * 
 {@code
  * // Create and start HTTP2Client.
- * HTTP2Client client = new HTTP2Client();
- * client.start();
- * SslContextFactory sslContextFactory = client.getClientConnector().getSslContextFactory();
+ * HTTP2Client http2Client = new HTTP2Client();
+ * http2Client.start();
+ * SslContextFactory sslContextFactory = http2Client.getClientConnector().getSslContextFactory();
  *
  * // Connect to host.
  * String host = "webtide.com";
  * int port = 443;
  *
- * FuturePromise<Session> sessionPromise = new FuturePromise<>();
- * client.connect(sslContextFactory, new InetSocketAddress(host, port), new ServerSessionListener() {}, sessionPromise);
+ * CompletableFuture sessionPromise = http2Client.connect(sslContextFactory, new InetSocketAddress(host, port), new ServerSessionListener() {});
  *
- * // Obtain the client Session object.
+ * // Obtain the client-side Session object.
  * Session session = sessionPromise.get(5, TimeUnit.SECONDS);
  *
  * // Prepare the HTTP request headers.
- * HttpFields requestFields = HttpFields.build();
- * requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION);
+ * HttpFields.Mutable requestFields = HttpFields.build();
+ * requestFields.put("User-Agent", http2Client.getClass().getName() + "/" + Jetty.VERSION);
  * // Prepare the HTTP request object.
  * MetaData.Request request = new MetaData.Request("PUT", HttpURI.from("https://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields);
  * // Create the HTTP/2 HEADERS frame representing the HTTP request.
  * HeadersFrame headersFrame = new HeadersFrame(request, null, false);
  *
  * // Prepare the listener to receive the HTTP response frames.
- * Stream.Listener responseListener = new new Stream.Listener()
+ * Stream.Listener responseListener = new Stream.Listener()
  * {
- *      @Override
+ *      @Override
  *      public void onHeaders(Stream stream, HeadersFrame frame)
  *      {
  *          System.err.println(frame);
  *      }
  *
- *      @Override
+ *      @Override
  *      public void onData(Stream stream, DataFrame frame, Callback callback)
  *      {
  *          System.err.println(frame);
@@ -86,18 +86,17 @@
  * };
  *
  * // Send the HEADERS frame to create a stream.
- * FuturePromise<Stream> streamPromise = new FuturePromise<>();
- * session.newStream(headersFrame, streamPromise, responseListener);
+ * CompletableFuture streamPromise = session.newStream(headersFrame, responseListener);
  * Stream stream = streamPromise.get(5, TimeUnit.SECONDS);
  *
  * // Use the Stream object to send request content, if any, using a DATA frame.
- * ByteBuffer content = ...;
+ * ByteBuffer content = UTF_8.encode("hello");
  * DataFrame requestContent = new DataFrame(stream.getId(), content, true);
  * stream.data(requestContent, Callback.NOOP);
  *
- * // When done, stop the client.
- * client.stop();
- * 
+ * // When done, stop the HTTP2Client. + * http2Client.stop(); + *}
*/ @ManagedObject public class HTTP2Client extends ContainerLifeCycle @@ -403,7 +402,7 @@ public void setUseALPN(boolean useALPN) public CompletableFuture 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 +422,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(Transport.TCP_IP, sslContextFactory, address, listener, promise, context); + } + + public CompletableFuture connect(Transport transport, SslContextFactory.Client sslContextFactory, SocketAddress address, Session.Listener listener) + { + return Promise.Completable.with(p -> connect(transport, sslContextFactory, address, listener, p, null)); + } + + public void connect(Transport transport, SslContextFactory.Client sslContextFactory, SocketAddress address, Session.Listener listener, Promise promise, Map context) { ClientConnectionFactory factory = newClientConnectionFactory(sslContextFactory); - connect(address, factory, listener, promise, context); + connect(transport, address, factory, listener, promise, context); } public void connect(SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) + { + connect(Transport.TCP_IP, address, factory, listener, promise, context); + } + + public void connect(Transport transport, SocketAddress address, ClientConnectionFactory factory, Session.Listener listener, Promise promise, Map context) { context = contextFrom(factory, listener, promise, context); + context.put(Transport.class.getName(), transport); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed)); - connector.connect(address, context); + transport.connect(address, context); } public void accept(SslContextFactory.Client sslContextFactory, SocketChannel channel, Session.Listener listener, Promise promise) @@ -442,8 +457,14 @@ public void accept(SslContextFactory.Client sslContextFactory, SocketChannel cha } public void accept(SocketChannel channel, ClientConnectionFactory factory, Session.Listener listener, Promise promise) + { + accept(Transport.TCP_IP, channel, factory, listener, promise); + } + + public void accept(Transport transport, SocketChannel channel, ClientConnectionFactory factory, Session.Listener listener, Promise promise) { Map context = contextFrom(factory, listener, promise, null); + context.put(Transport.class.getName(), transport); context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, Promise.from(ioConnection -> {}, promise::failed)); connector.accept(channel, context); } @@ -452,6 +473,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..f6fa9a5a6dcd 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.Transport; +import org.eclipse.jetty.quic.client.QuicTransport; 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 Transport newTransport() + { + return new QuicTransport(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..ef2b3bc65234 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.Transport; +import org.eclipse.jetty.quic.client.QuicTransport; 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) { + Transport transport = request.getTransport(); + if (transport == null) + request.transport(new QuicTransport(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().getTransport(), 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..b8bfe7f32010 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.Transport; +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.QuicTransport; import org.eclipse.jetty.quic.common.QuicSessionContainer; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -39,31 +38,30 @@ *

HTTP3Client provides an asynchronous, non-blocking implementation to send * HTTP/3 frames to a server.

*

Typical usage:

- *
- * // HTTP3Client setup.
+ * 
 {@code
+ * // Client-side QUIC configuration to configure QUIC properties.
+ * ClientQuicConfiguration quicConfig = new ClientQuicConfiguration(sslClient, null);
  *
- * HTTP3Client client = new HTTP3Client();
- *
- * // To configure QUIC properties.
- * QuicConfiguration quicConfig = client.getQuicConfiguration();
+ * // Create the HTTP3Client instance.
+ * HTTP3Client http3Client = new HTTP3Client(quicConfig);
  *
  * // To configure HTTP/3 properties.
- * HTTP3Configuration h3Config = client.getHTTP3Configuration();
+ * HTTP3Configuration h3Config = http3Client.getHTTP3Configuration();
  *
- * client.start();
+ * http3Client.start();
  *
  * // HTTP3Client request/response usage.
  *
  * // Connect to host.
  * String host = "webtide.com";
  * int port = 443;
- * Session.Client session = client
+ * Session.Client session = http3Client
  *     .connect(new InetSocketAddress(host, port), new Session.Client.Listener() {})
  *     .get(5, TimeUnit.SECONDS);
  *
  * // Prepare the HTTP request headers.
- * HttpFields requestFields = new HttpFields();
- * requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION);
+ * HttpFields.Mutable requestFields = HttpFields.build();
+ * requestFields.put("User-Agent", http3Client.getClass().getName() + "/" + Jetty.VERSION);
  *
  * // Prepare the HTTP request object.
  * MetaData.Request request = new MetaData.Request("PUT", HttpURI.from("https://" + host + ":" + port + "/"), HttpVersion.HTTP_3, requestFields);
@@ -74,7 +72,7 @@
  * // Send the HEADERS frame to create a request stream.
  * Stream stream = session.newRequest(headersFrame, new Stream.Listener()
  * {
- *     @Override
+ *     @Override
  *     public void onResponse(Stream stream, HeadersFrame frame)
  *     {
  *         // Inspect the response status and headers.
@@ -84,7 +82,7 @@
  *         stream.demand();
  *     }
  *
- *     @Override
+ *     @Override
  *     public void onDataAvailable(Stream stream)
  *     {
  *         Stream.Data data = stream.readData();
@@ -98,15 +96,15 @@
  * }).get(5, TimeUnit.SECONDS);
  *
  * // Use the Stream object to send request content, if any, using a DATA frame.
- * ByteBuffer requestChunk1 = ...;
+ * ByteBuffer requestChunk1 = UTF_8.encode("hello");
  * stream.data(new DataFrame(requestChunk1, false))
  *     // Subsequent sends must wait for previous sends to complete.
- *     .thenCompose(s ->
+ *     .thenCompose(s ->
  *     {
- *         ByteBuffer requestChunk2 = ...;
- *         s.data(new DataFrame(requestChunk2, true)));
- *     }
- * 
+ * ByteBuffer requestChunk2 = UTF_8.encode("world"); + * s.data(new DataFrame(requestChunk2, true)); + * }); + * }
* *

IMPLEMENTATION NOTES.

*

Each call to {@link #connect(SocketAddress, Session.Client.Listener)} creates a new @@ -115,14 +113,14 @@ * corresponding {@link ClientHTTP3Session}.

*

Each {@link ClientHTTP3Session} manages the mandatory encoder, decoder and control * streams, plus zero or more request/response streams.

- *
+ * 
{@code
  * GENERIC, TCP-LIKE, SETUP FOR HTTP/1.1 AND HTTP/2
  * HTTP3Client - dgramEP - ClientQuiConnection - ClientQuicSession - ClientProtocolSession - TCPLikeStream
  *
  * SPECIFIC SETUP FOR HTTP/3
  *                                                                                      /- [Control|Decoder|Encoder]Stream
  * HTTP3Client - dgramEP - ClientQuiConnection - ClientQuicSession - ClientHTTP3Session -* HTTP3Streams
- * 
+ * }
* *

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

@@ -134,20 +132,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 +159,7 @@ public ClientConnector getClientConnector() return connector; } - public QuicConfiguration getQuicConfiguration() + public ClientQuicConfiguration getQuicConfiguration() { return quicConfiguration; } @@ -173,45 +173,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 address, Session.Client.Listener listener, Map context) + public CompletableFuture connect(SocketAddress socketAddress, Session.Client.Listener listener, Map context) { + if (context == null) + context = new ConcurrentHashMap<>(); + return connect(new QuicTransport(getQuicConfiguration()), socketAddress, listener, context); + } + + public CompletableFuture connect(Transport transport, 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(Transport.class.getName(), transport); if (LOG.isDebugEnabled()) - LOG.debug("connecting to {}", address); + LOG.debug("connecting to {}", socketAddress); - connector.connect(address, context); + transport.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-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java b/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java index a3ed361683e7..03ded66c7ec9 100644 --- a/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java +++ b/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java @@ -332,16 +332,21 @@ public String toString() private void notifyInstructionHandler() { - if (!_instructions.isEmpty()) - _handler.onInstructions(_instructions); + if (_instructions.isEmpty()) + return; + // Copy the list to avoid re-entrance. + List instructions = List.copyOf(_instructions); _instructions.clear(); + _handler.onInstructions(instructions); } private void notifyMetaDataHandler(boolean wasBlocked) { + if (_metaDataNotifications.isEmpty()) + return; // Copy the list to avoid re-entrance, where the call to // notifyHandler() may end up calling again this method. - List notifications = new ArrayList<>(_metaDataNotifications); + List notifications = List.copyOf(_metaDataNotifications); _metaDataNotifications.clear(); for (MetaDataNotification notification : notifications) { diff --git a/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java b/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java index c6f4556eee76..4e69605b0c50 100644 --- a/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java +++ b/jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java @@ -499,9 +499,12 @@ private static int encodeInsertCount(int reqInsertCount, int maxTableCapacity) private void notifyInstructionHandler() { - if (!_instructions.isEmpty()) - _handler.onInstructions(_instructions); + if (_instructions.isEmpty()) + return; + // Copy the list to avoid re-entrance. + List instructions = List.copyOf(_instructions); _instructions.clear(); + _handler.onInstructions(instructions); } InstructionHandler getInstructionHandler() diff --git a/jetty-core/jetty-http3/jetty-http3-server/src/main/config/etc/jetty-http3.xml b/jetty-core/jetty-http3/jetty-http3-server/src/main/config/etc/jetty-http3.xml index 7dd5b5f3ccb6..044b86ad909f 100644 --- a/jetty-core/jetty-http3/jetty-http3-server/src/main/config/etc/jetty-http3.xml +++ b/jetty-core/jetty-http3/jetty-http3-server/src/main/config/etc/jetty-http3.xml @@ -4,13 +4,19 @@ - + - + + + + + + + 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..82a29c2cbffe 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 Transport} used by the protocol + */ + public abstract Transport newTransport(); + + /** + *

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 865d3c122159..0d4a4bdf90da 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.StandardProtocolFamily; import java.net.StandardSocketOptions; import java.net.UnixDomainSocketAddress; +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; @@ -84,7 +84,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 Transport.TCPUnix} */ + @Deprecated(since = "12.0.7", forRemoval = true) public static ClientConnector forUnixDomain(Path path) { return new ClientConnector(Configurator.forUnixDomain(path)); @@ -112,6 +114,11 @@ public ClientConnector() this(new Configurator()); } + /** + * @param configurator the {@link Configurator} + * @deprecated replaced by {@link Transport} + */ + @Deprecated(since = "12.0.7", forRemoval = true) public ClientConnector(Configurator configurator) { this.configurator = Objects.requireNonNull(configurator); @@ -123,17 +130,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 Transport#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 Transport} 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 Transport} for this connector + * @deprecated use {@link Transport} instead + */ + @Deprecated(since = "12.0.7", forRemoval = true) + public Transport newTransport() + { + return configurator.newTransport(); + } + public void setExecutor(Executor executor) { if (isStarted()) @@ -400,25 +430,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(); + Transport transport = (Transport)context.get(Transport.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); @@ -461,9 +495,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); @@ -473,9 +509,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); } } @@ -488,9 +522,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()); @@ -520,14 +553,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); + Transport transport = (Transport)context.get(Transport.class.getName()); + return transport.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); + Transport transport = (Transport)context.get(Transport.class.getName()); + return transport.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) @@ -565,15 +607,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 @@ -587,9 +635,20 @@ protected void connectionFailed(SelectableChannel channel, Throwable failure, Ob /** *

Configures a {@link ClientConnector}.

+ * + * @deprecated replaced by {@link Transport} */ + @Deprecated(since = "12.0.7", forRemoval = true) public static class Configurator extends ContainerLifeCycle { + /** + * @return the default {@link Transport} for this configurator + */ + public Transport newTransport() + { + 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 @@ -648,7 +707,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 Transport} */ + @Deprecated(since = "12.0.7", forRemoval = true) public static class ChannelWithAddress { private final SelectableChannel channel; @@ -675,6 +737,12 @@ private static Configurator forUnixDomain(Path path) { return new Configurator() { + @Override + public Transport newTransport() + { + return new Transport.TCPUnix(path); + } + @Override public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map context) throws IOException { 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..06b056a9ad63 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 @@ -24,74 +24,54 @@ import javax.net.ssl.SSLSession; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.FutureCallback; -import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Invocable; /** - *

EndPoint is the abstraction for an I/O channel that transports bytes.

+ *

EndPoint is the abstraction for I/O communication using bytes.

+ *

All the I/O methods are non-blocking; reads may return {@code 0} + * bytes read, and flushes/writes may write {@code 0} bytes.

+ *

Applications are notified of read readiness by registering a + * {@link Callback} via {@link #fillInterested(Callback)}, and then + * using {@link #fill(ByteBuffer)} to read the available bytes.

+ *

Application may use {@link #flush(ByteBuffer...)} to transmit bytes; + * if the flush does not transmit all the bytes, applications must + * arrange to resume flushing when it will be possible to transmit more + * bytes. + * Alternatively, applications may use {@link #write(Callback, ByteBuffer...)} + * and be notified via the {@link Callback} when the write completes + * (i.e. all the buffers have been flushed), either successfully or + * with a failure.

+ *

Connection-less reads are performed using {@link #receive(ByteBuffer)}. + * Similarly, connection-less flushes are performed using + * {@link #send(SocketAddress, ByteBuffer...)} and connection-less writes + * using {@link #write(Callback, SocketAddress, ByteBuffer...)}.

+ *

While all the I/O methods are non-blocking, they can be easily + * converted to blocking using either {@link org.eclipse.jetty.util.Blocker} + * or {@link Callback.Completable}:

+ *
{@code
+ * EndPoint endPoint = ...;
  *
- * 

Asynchronous Methods

- *

The asynchronous scheduling methods of {@link EndPoint} - * has been influenced by NIO.2 Futures and Completion - * handlers, but does not use those actual interfaces because they have - * some inefficiencies.

- *

This class will frequently be used in conjunction with some of the utility - * implementations of {@link Callback}, such as {@link FutureCallback} and - * {@link IteratingCallback}.

- * - *

Reads

- *

A {@link FutureCallback} can be used to block until an endpoint is ready - * to fill bytes - the notification will be emitted by the NIO subsystem:

- *
- * FutureCallback callback = new FutureCallback();
- * endPoint.fillInterested(callback);
- *
- * // Blocks until read to fill bytes.
- * callback.get();
- *
- * // Now bytes can be filled in a ByteBuffer.
- * int filled = endPoint.fill(byteBuffer);
- * 
- * - *

Asynchronous Reads

- *

A {@link Callback} can be used to read asynchronously in its own dispatched - * thread:

- *
- * endPoint.fillInterested(new Callback()
+ * // Block until read ready with Blocker.
+ * try (Blocker.Callback blocker = Blocker.callback())
  * {
- *   public void onSucceeded()
- *   {
- *     executor.execute(() ->
- *     {
- *       // Fill bytes in a different thread.
- *       int filled = endPoint.fill(byteBuffer);
- *     });
- *   }
- *   public void onFailed(Throwable failure)
- *   {
- *     endPoint.close();
- *   }
- * });
- * 
- * - *

Blocking Writes

- *

The write contract is that the callback is completed when all the bytes - * have been written or there is a failure. - * Blocking writes look like this:

- *
- * FutureCallback callback = new FutureCallback();
- * endpoint.write(callback, headerBuffer, contentBuffer);
+ *     endPoint.fillInterested(blocker);
+ *     blocker.block();
+ * }
  *
- * // Blocks until the write succeeds or fails.
- * future.get();
- * 
- *

Note also that multiple buffers may be passed in {@link #write(Callback, ByteBuffer...)} - * so that gather writes can be performed for efficiency.

+ * // Block until write complete with Callback.Completable. + * Callback.Completable completable = new Callback.Completable(); + * endPoint.write(completable, byteBuffer); + * completable.get(); + * }
*/ 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}. */ @@ -148,39 +128,39 @@ default SocketAddress getRemoteSocketAddress() long getCreatedTimeStamp(); /** - * Shutdown the output. - *

This call indicates that no more data will be sent on this endpoint that - * that the remote end should read an EOF once all previously sent data has been - * consumed. Shutdown may be done either at the TCP/IP level, as a protocol exchange (Eg - * TLS close handshake) or both. - *

- * If the endpoint has {@link #isInputShutdown()} true, then this call has the same effect - * as {@link #close()}. + *

Shuts down the output.

+ *

This call indicates that no more data will be sent from this endpoint and + * that the remote endpoint should read an EOF once all previously sent data has been + * read. Shutdown may be done either at the TCP/IP level, as a protocol exchange + * (for example, TLS close handshake) or both.

+ *

If the endpoint has {@link #isInputShutdown()} true, then this call has the + * same effect as {@link #close()}.

*/ void shutdownOutput(); /** - * Test if output is shutdown. - * The output is shutdown by a call to {@link #shutdownOutput()} - * or {@link #close()}. + *

Tests if output is shutdown.

+ *

The output is shutdown by a call to {@link #shutdownOutput()} + * or {@link #close()}.

* * @return true if the output is shutdown or the endpoint is closed. */ boolean isOutputShutdown(); /** - * Test if the input is shutdown. - * The input is shutdown if an EOF has been read while doing - * a {@link #fill(ByteBuffer)}. Once the input is shutdown, all calls to + *

Tests if the input is shutdown.

+ *

The input is shutdown if an EOF has been read while doing + * a {@link #fill(ByteBuffer)}. + * Once the input is shutdown, all calls to * {@link #fill(ByteBuffer)} will return -1, until such time as the - * end point is close, when they will return {@link EofException}. + * end point is close, when they will return {@link EofException}.

* - * @return True if the input is shutdown or the endpoint is closed. + * @return true if the input is shutdown or the endpoint is closed. */ boolean isInputShutdown(); /** - * Close any backing stream associated with the endpoint + *

Closes any backing stream associated with the endpoint.

*/ @Override default void close() @@ -189,16 +169,18 @@ default void close() } /** - * Close any backing stream associated with the endpoint, passing a cause + *

Closes any backing stream associated with the endpoint, passing a + * possibly {@code null} failure cause.

* * @param cause the reason for the close or null */ void close(Throwable cause); /** - * Fill the passed buffer with data from this endpoint. The bytes are appended to any - * data already in the buffer by writing from the buffers limit up to it's capacity. - * The limit is updated to include the filled bytes. + *

Fills the passed buffer with data from this endpoint.

+ *

The bytes are appended to any data already in the buffer + * by writing from the buffers limit up to its capacity. + * The limit is updated to include the filled bytes.

* * @param buffer The buffer to fill. The position and limit are modified during the fill. After the * operation, the position is unchanged and the limit is increased to reflect the new data filled. @@ -212,9 +194,29 @@ default int fill(ByteBuffer buffer) throws IOException } /** - * 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 - * is updated to indicate how many bytes have been consumed. + *

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(); + } + + /** + *

Flushes 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 is updated to indicate how many bytes + * have been consumed.

* * @param buffer the buffers to flush * @return True IFF all the buffers have been consumed and the endpoint has flushed the data to its @@ -226,22 +228,38 @@ 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.) */ Object getTransport(); /** - * Get the max idle time in ms. - *

The max idle time is the time the endpoint can be idle before - * extraordinary handling takes place. + *

Returns the idle timeout in ms.

+ *

The idle timeout is the time the endpoint can be idle before + * its close is initiated.

+ *

A timeout less than or equal to {@code 0} implies an infinite timeout.

* - * @return the max idle time in ms or if ms <= 0 implies an infinite timeout + * @return the idle timeout in ms */ long getIdleTimeout(); /** - * Set the idle timeout. + *

Sets the idle timeout.

* * @param idleTimeout the idle timeout in MS. Timeout <= 0 implies an infinite timeout */ @@ -285,6 +303,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) @@ -330,7 +363,8 @@ default void write(Callback callback, ByteBuffer... buffers) throws WritePending void upgrade(Connection newConnection); /** - * Get the SslSessionData of a secure end point. + *

Returns the SslSessionData of a secure end point.

+ * * @return A {@link SslSessionData} instance (with possibly null field values) if secure, else {@code null}. */ default SslSessionData getSslSessionData() @@ -439,4 +473,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/Transport.java b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Transport.java new file mode 100644 index 000000000000..a46ebf1a84a1 --- /dev/null +++ b/jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Transport.java @@ -0,0 +1,408 @@ +// +// ======================================================================== +// 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 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 + * {@code Transport}, 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 Transport +{ + /** + *

The TCP/IP {@code Transport}.

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

The UDP/IP {@code Transport}.

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

Returns whether this {@code Transport} 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 {@code Transport} requires domain names resolution + */ + default boolean requiresDomainNameResolution() + { + return false; + } + + /** + *

Establishes a connection to the given socket address.

+ *

For {@code Transport}s that {@link #requiresDomainNameResolution() + * require domain name resolution}, this is the IP address resolved from + * the domain name. + * For {@code Transport}s that do not require domain name resolution + * (for example Unix-Domain sockets, or memory) this is the socket address + * to connect to.

+ * + * @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 connect to in case domain name resolution is not required + */ + default SocketAddress getSocketAddress() + { + return null; + } + + /** + *

For {@code Transport}s that are based on sockets, or for {@code Transport}s + * that are layered on top of another {@code Transport} 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 {@code Transport}s that are based on sockets, or for {@code Transport}s + * that are layered on top of another {@code Transport} 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 {@code Transport}s such as TCP/IP, the {@link Connection} is typically + * that of the high-level protocol. + * For layered {@code Transport}s such as QUIC, the {@link Connection} is typically that of the + * layered {@code Transport}.

+ * + * @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 {@code Transport} based on sockets.

+ */ + abstract class Socket implements Transport + { + @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 {@code Transport} based on IP.

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

The TCP/IP {@code Transport}.

+ */ + 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 {@code Transport}.

+ */ + class UDPIP extends Transport.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 {@code Transport} 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 {@code Transport}.

+ */ + 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 {@code Transport}.

+ */ + 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 Transport} instances to allow layering of {@code Transport}s.

+ */ + class Wrapper implements Transport + { + private final Transport wrapped; + + public Wrapper(Transport wrapped) + { + this.wrapped = Objects.requireNonNull(wrapped); + } + + public Transport getWrapped() + { + return wrapped; + } + + public Transport unwrap() + { + Transport 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 requiresDomainNameResolution() + { + return wrapped.requiresDomainNameResolution(); + } + + @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-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java b/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java index 759f7bc3826c..7aa2fa7b7c47 100644 --- a/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java +++ b/jetty-core/jetty-osgi/src/main/java/org/eclipse/jetty/osgi/util/Util.java @@ -16,7 +16,9 @@ import java.io.File; import java.net.URI; import java.net.URL; +import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -28,18 +30,23 @@ import org.eclipse.jetty.osgi.OSGiServerConstants; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Filter; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.service.packageadmin.PackageAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Various useful functions utility methods for OSGi wide use. */ public class Util { + private static final Logger LOG = LoggerFactory.getLogger(Util.class); + /** * Resolve a path either absolutely or against the bundle install location, or * against jetty home. @@ -47,7 +54,7 @@ public class Util * @param path the path to resolve * @param bundle the bundle * @param jettyHome the path to jetty home - * @return the URI within the bundle as a usable URI + * @return the URI resolved either absolutely or against the bundle install location or jetty home. */ public static URI resolvePathAsLocalizedURI(String path, Bundle bundle, Path jettyHome) throws Exception @@ -56,7 +63,21 @@ public static URI resolvePathAsLocalizedURI(String path, Bundle bundle, Path jet return null; if (path.startsWith("/") || path.startsWith("file:/")) //absolute location - return Paths.get(path).toUri(); + return URIUtil.toURI(path); + else + { + try + { + Path p = FileSystems.getDefault().getPath(path); + if (p.isAbsolute()) + return p.toUri(); + } + catch (InvalidPathException x) + { + //ignore and try via the jetty bundle instead + LOG.trace("IGNORED", x); + } + } //relative location //try inside the bundle first diff --git a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java index dfecfc051b78..99baae9f5910 100644 --- a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java +++ b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java @@ -28,6 +28,7 @@ import java.util.stream.Stream; import org.eclipse.jetty.client.AsyncRequestContent; +import org.eclipse.jetty.client.ContentSourceRequestContent; import org.eclipse.jetty.client.ContinueProtocolHandler; import org.eclipse.jetty.client.EarlyHintsProtocolHandler; import org.eclipse.jetty.client.HttpClient; @@ -347,7 +348,7 @@ protected void addViaHeader(Request clientToProxyRequest, org.eclipse.jetty.clie .flatMap(field -> Stream.of(field.getValues())) .filter(value -> !StringUtil.isBlank(value)) .collect(Collectors.joining(separator)); - if (newValue.length() > 0) + if (!newValue.isEmpty()) newValue += separator; newValue += viaHeaderValue; return new HttpField(HttpHeader.VIA, newValue); @@ -578,53 +579,27 @@ protected HttpURI rewriteHttpURI(Request clientToProxyRequest) } } - protected static class ProxyRequestContent implements org.eclipse.jetty.client.Request.Content + protected static class ProxyRequestContent extends ContentSourceRequestContent { - private final Request clientToProxyRequest; - public ProxyRequestContent(Request clientToProxyRequest) { - this.clientToProxyRequest = clientToProxyRequest; + super(clientToProxyRequest, clientToProxyRequest.getHeaders().get(HttpHeader.CONTENT_TYPE)); } @Override - public long getLength() + public Request getContentSource() { - return clientToProxyRequest.getLength(); + return (Request)super.getContentSource(); } @Override public Content.Chunk read() { - Content.Chunk chunk = clientToProxyRequest.read(); + Content.Chunk chunk = super.read(); if (LOG.isDebugEnabled()) - LOG.debug("{} C2P read content {}", requestId(clientToProxyRequest), chunk); + LOG.debug("{} C2P read content {}", requestId(getContentSource()), chunk); return chunk; } - - @Override - public void demand(Runnable demandCallback) - { - clientToProxyRequest.demand(demandCallback); - } - - @Override - public void fail(Throwable failure) - { - clientToProxyRequest.fail(failure); - } - - @Override - public String getContentType() - { - return clientToProxyRequest.getHeaders().get(HttpHeader.CONTENT_TYPE); - } - - @Override - public boolean rewind() - { - return clientToProxyRequest.rewind(); - } } protected class ProxyResponseListener extends Callback.Completable implements org.eclipse.jetty.client.Response.Listener 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..bd9aa1986894 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.Transport; 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 Transport} */ +@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 Transport newTransport() { - 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 QuicTransport(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/QuicTransport.java b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicTransport.java new file mode 100644 index 000000000000..a8d0999adae1 --- /dev/null +++ b/jetty-core/jetty-quic/jetty-quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicTransport.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.Transport; +import org.eclipse.jetty.quic.common.QuicConfiguration; + +/** + *

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

+ *

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

+ */ +public class QuicTransport extends Transport.Wrapper +{ + private final ClientQuicConfiguration quicConfiguration; + + public QuicTransport(ClientQuicConfiguration quicConfiguration) + { + this(UDP_IP, quicConfiguration); + } + + public QuicTransport(Transport 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 QuicTransport 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-quic/jetty-quic-server/src/test/java/org/eclipse/jetty/quic/server/ServerQuicConnectorTest.java b/jetty-core/jetty-quic/jetty-quic-server/src/test/java/org/eclipse/jetty/quic/server/QuicServerConnectorTest.java similarity index 92% rename from jetty-core/jetty-quic/jetty-quic-server/src/test/java/org/eclipse/jetty/quic/server/ServerQuicConnectorTest.java rename to jetty-core/jetty-quic/jetty-quic-server/src/test/java/org/eclipse/jetty/quic/server/QuicServerConnectorTest.java index d625c18c1c61..7e9f776642a1 100644 --- a/jetty-core/jetty-quic/jetty-quic-server/src/test/java/org/eclipse/jetty/quic/server/ServerQuicConnectorTest.java +++ b/jetty-core/jetty-quic/jetty-quic-server/src/test/java/org/eclipse/jetty/quic/server/QuicServerConnectorTest.java @@ -27,7 +27,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -public class ServerQuicConnectorTest +public class QuicServerConnectorTest { @Disabled @Test @@ -43,7 +43,8 @@ public void testSmall() throws Exception config.setHttpCompliance(HttpCompliance.LEGACY); // enable HTTP/0.9 HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config); - QuicServerConnector connector = new QuicServerConnector(server, sslContextFactory, connectionFactory); + ServerQuicConfiguration quicConfig = new ServerQuicConfiguration(sslContextFactory, null); + QuicServerConnector connector = new QuicServerConnector(server, quicConfig, connectionFactory); connector.setPort(8443); server.addConnector(connector); @@ -85,7 +86,8 @@ public void testBig() throws Exception config.setHttpCompliance(HttpCompliance.LEGACY); // enable HTTP/0.9 HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config); - QuicServerConnector connector = new QuicServerConnector(server, sslContextFactory, connectionFactory); + ServerQuicConfiguration quicConfig = new ServerQuicConfiguration(sslContextFactory, null); + QuicServerConnector connector = new QuicServerConnector(server, quicConfig, connectionFactory); connector.setPort(8443); server.addConnector(connector); 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/MemoryTransport.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryTransport.java new file mode 100644 index 000000000000..c2c3e29f84f4 --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/MemoryTransport.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.Transport; +import org.eclipse.jetty.util.Promise; + +/** + *

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

+ */ +public class MemoryTransport implements Transport +{ + private final MemoryConnector connector; + + public MemoryTransport(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 Transport instances. + // Retrieve the outermost instance to call newConnection(). + Transport transport = (Transport)context.get(Transport.class.getName()); + Connection connection = transport.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 MemoryTransport that) + return Objects.equals(connector, that.connector); + return false; + } +} diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java index 68728f4af0c6..087d09c87fd2 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/SizeLimitHandler.java @@ -143,8 +143,8 @@ public void write(boolean last, ByteBuffer content, Callback callback) { if (_responseLimit >= 0 && (_written + content.remaining()) > _responseLimit) { - callback.failed(new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response body is too large: " + - _written + content.remaining() + ">" + _responseLimit)); + String message = "Response body is too large: %d>%d".formatted(_written + content.remaining(), _responseLimit); + callback.failed(new HttpException.RuntimeException(HttpStatus.INTERNAL_SERVER_ERROR_500, message)); return; } _written += content.remaining(); 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-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java index d54af550ed2b..7c141f8696ce 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SizeLimitHandlerTest.java @@ -80,7 +80,7 @@ public void testSmallGET() throws Exception _contextHandler.setHandler(new Handler.Abstract() { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { response.write(true, BufferUtil.toBuffer("Hello World"), callback); return true; @@ -99,7 +99,7 @@ public void testLargeGETContentLengthKnown() throws Exception _contextHandler.setHandler(new Handler.Abstract() { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { response.getHeaders().put(HttpHeader.CONTENT_LENGTH, 8 * 1024 + 1); fail(); @@ -120,9 +120,8 @@ public void testLargeGETSingleByteBuffer() throws Exception _contextHandler.setHandler(new Handler.Abstract() { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { - response.write(true, ByteBuffer.wrap(new byte[8 * 1024 + 1]), callback); return true; } @@ -141,7 +140,7 @@ public void testLargeGETManyWrites() throws Exception _contextHandler.setHandler(new Handler.Abstract() { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { byte[] data = new byte[1024]; Arrays.fill(data, (byte)'X'); @@ -199,10 +198,12 @@ public boolean handle(Request request, Response response, Callback callback) thr }); _server.start(); HttpTester.Response response = HttpTester.parseResponse( - _local.getResponse("POST /ctx/hello HTTP/1.0\r\n" + - "Content-Length: 8\r\n" + - "\r\n" + - "123456\r\n")); + _local.getResponse(""" + POST /ctx/hello HTTP/1.0\r + Content-Length: 8\r + \r + 123456\r + """)); assertThat(response.getStatus(), equalTo(200)); assertThat(response.getContent(), containsString("OK 8")); } @@ -223,10 +224,11 @@ public boolean handle(Request request, Response response, Callback callback) thr }); _server.start(); HttpTester.Response response = HttpTester.parseResponse( - _local.getResponse("POST /ctx/hello HTTP/1.0\r\n" + - "Content-Length: 32768\r\n" + - "\r\n" + - "123456...")); + _local.getResponse(""" + POST /ctx/hello HTTP/1.0\r + Content-Length: 32768\r + \r + 123456...""")); assertThat(response.getStatus(), equalTo(413)); assertThat(response.getContent(), containsString("32768>8192")); } @@ -247,11 +249,13 @@ public boolean handle(Request request, Response response, Callback callback) thr }); _server.start(); - try (LocalConnector.LocalEndPoint endp = _local.executeRequest( - "POST /ctx/hello HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Transfer-Encoding: chunked\r\n" + - "\r\n")) + try (LocalConnector.LocalEndPoint endPoint = _local.executeRequest( + """ + POST /ctx/hello HTTP/1.1\r + Host: localhost\r + Transfer-Encoding: chunked\r + \r + """)) { byte[] data = new byte[1024]; Arrays.fill(data, (byte)'X'); @@ -259,9 +263,9 @@ public boolean handle(Request request, Response response, Callback callback) thr String text = new String(data, 0, 1024, Charset.defaultCharset()); for (int i = 0; i < 9; i++) - endp.addInput("400\r\n" + text + "\r\n"); + endPoint.addInput("400\r\n" + text + "\r\n"); - HttpTester.Response response = HttpTester.parseResponse(endp.getResponse()); + HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse()); assertThat(response.getStatus(), equalTo(413)); assertThat(response.getContent(), containsString(">8192")); @@ -275,7 +279,7 @@ public void testMultipleRequests() throws Exception _contextHandler.setHandler(new Handler.Abstract() { @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public boolean handle(Request request, Response response, Callback callback) { response.write(true, BufferUtil.toBuffer(message), callback); return true; 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/AbstractTransportTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTransportTest.java new file mode 100644 index 000000000000..c4b1713bb999 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/AbstractTransportTest.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// 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.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(WorkDirExtension.class) +public abstract class AbstractTransportTest +{ + protected Server server; + + @BeforeEach + public void setup() + { + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + protected static Path newUnixDomainPath() throws IOException + { + String unixDomainDir = System.getProperty("jetty.unixdomain.dir", System.getProperty("java.io.tmpdir")); + Path unixDomainFile = Files.createTempFile(Path.of(unixDomainDir), "jetty-", ".sock"); + Files.delete(unixDomainFile); + return unixDomainFile; + } +} diff --git a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/CustomTransportTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/CustomTransportTest.java new file mode 100644 index 000000000000..ed8e2b9c4837 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/CustomTransportTest.java @@ -0,0 +1,395 @@ +// +// ======================================================================== +// 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.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.CompletableResponseListener; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.ContentSourceRequestContent; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.StringRequestContent; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.MemoryEndPointPipe; +import org.eclipse.jetty.io.Transport; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.Scheduler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + *

Tests a proxy scenario where the proxy HttpClient wants to send the request bytes to an in-memory gateway, + * and receive response bytes from it. The in-memory gateway sends the request bytes over the network + * for example via SSH, and then receives the response bytes from SSH, which should be relayed back to HttpClient.

+ *

Simulates the following flows:

+ * {@code + * Client -> Proxy -> HttpClient.newRequest() -> Local MemoryEndPoint -> Request Bytes -> In-Memory Gateway (SSH) - -> Remote Server + * | + * Remote Server (SSH) - - > In-Memory Gateway -> Response Bytes -> Remote MemoryEndPoint -> HttpClient -> Proxy -> Client + * } + */ +public class CustomTransportTest +{ + private static final String CONTENT = "CONTENT"; + + 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 testCustomTransport() throws Exception + { + Gateway gateway = new Gateway(); + + ServerConnector connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory()); + server.addConnector(connector); + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + var gatewayRequest = httpClient.newRequest("http://localhost/") + .transport(new GatewayTransport(httpClient.getScheduler(), gateway)) + .method(request.getMethod()) + .path(request.getHttpURI().getPathQuery()) + .timeout(5, TimeUnit.SECONDS); + + // Copy some of the headers. + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + if (contentType != null) + gatewayRequest.headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, contentType)); + + // Copy the request content. + if (request.getLength() != 0) + gatewayRequest.body(new ContentSourceRequestContent(request)); + + // Send the request. + // It will be serialized into bytes and sent to the Gateway. + CompletableFuture completable = new CompletableResponseListener(gatewayRequest).send(); + completable.whenComplete((r, x) -> + { + if (x == null) + { + // Copy the response headers. + response.getHeaders().add(r.getHeaders()); + // Remove Content-Encoding, as the content has already been decoded. + response.getHeaders().remove(HttpHeader.CONTENT_ENCODING); + // Copy the response content. + response.write(true, ByteBuffer.wrap(r.getContent()), callback); + } + else + { + Response.writeError(request, response, callback, x); + } + }); + + return true; + } + }); + server.start(); + + // Make a request to the server, it will be forwarded to the external system in bytes. + CompletableFuture completable = new CompletableResponseListener(httpClient.newRequest("localhost", connector.getLocalPort()) + .method(HttpMethod.POST) + .body(new StringRequestContent("REQUEST")) + .timeout(5, TimeUnit.SECONDS) + ).send(); + + // After a while, simulate that the Gateway sends back data on Channel 1. + Thread.sleep(500); + gateway.onData(1); + + ContentResponse response = completable.get(5, TimeUnit.SECONDS); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContentAsString(), is(CONTENT)); + } + + private static class GatewayTransport implements Transport + { + private final Scheduler scheduler; + private final Gateway gateway; + + private GatewayTransport(Scheduler scheduler, Gateway gateway) + { + this.scheduler = scheduler; + this.gateway = gateway; + } + + @Override + public void connect(SocketAddress socketAddress, Map context) + { + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY); + try + { + // Create the Pipe to connect client and server. + MemoryEndPointPipe pipe = new MemoryEndPointPipe(scheduler, Runnable::run, socketAddress); + + // Set up the server-side. + EndPoint remoteEndPoint = pipe.getRemoteEndPoint(); + gateway.onConnect(remoteEndPoint); + + // Set up the client-side. + EndPoint localEndPoint = pipe.getLocalEndPoint(); + + ClientConnector clientConnector = (ClientConnector)context.get(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY); + localEndPoint.setIdleTimeout(clientConnector.getIdleTimeout().toMillis()); + + Transport transport = (Transport)context.get(Transport.class.getName()); + Connection connection = transport.newConnection(localEndPoint, context); + localEndPoint.setConnection(connection); + + localEndPoint.onOpen(); + connection.onOpen(); + } + catch (Throwable x) + { + promise.failed(x); + } + } + + @Override + public int hashCode() + { + return Objects.hash(gateway); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj instanceof GatewayTransport that) + return Objects.equals(gateway, that.gateway); + return false; + } + } + + private static class Gateway + { + private final Map channels = new ConcurrentHashMap<>(); + + public void onConnect(EndPoint endPoint) + { + // For every new connection, generate a new Channel, + // and associate the Channel with the EndPoint. + Channel channel = new Channel(endPoint); + channels.put(channel.id, channel); + + // Register for read interest with the EndPoint. + endPoint.fillInterested(new EndPointToChannelCallback(channel)); + } + + // Called when there data to read from the Gateway on the given Channel. + public void onData(int id) + { + Channel channel = channels.get(id); + // Simulate the data to read. + channel.data = StandardCharsets.UTF_8.encode(""" + HTTP/1.1 200 OK + Content-Length: %d + + """.formatted(CONTENT.length()) + CONTENT); + new ChannelToEndPointCallback(channel).iterate(); + } + + private class Channel + { + // Channels should have different ids, + // hard-coding the id just for the test. + private final int id = 1; + private final EndPoint endPoint; + private ByteBuffer data; + + public Channel(EndPoint endPoint) + { + this.endPoint = endPoint; + } + + public void close(Throwable failure) + { + // Close the Gateway Channel, possibly due to a failure. + channels.remove(id); + endPoint.close(failure); + } + + public void demand() + { + // Demands to be notified by calling Gateway.onData() + // when there is data to read from the Gateway. + } + + public int read(ByteBuffer buffer) + { + // This simulates response data arriving from the Gateway. + if (data == null) + return 0; + ByteBuffer received = data; + data = null; + int length = received.remaining(); + buffer.put(received).flip(); + return length; + } + + public void write(Callback callback, ByteBuffer byteBuffer) + { + // Write the buffer and simulate that the write succeeded. + byteBuffer.position(byteBuffer.limit()); + callback.succeeded(); + } + } + + // Reads from the EndPoint, and writes to the Gateway Channel. + private static class EndPointToChannelCallback extends IteratingCallback + { + private final Channel channel; + + private EndPointToChannelCallback(Channel channel) + { + this.channel = channel; + } + + @Override + protected Action process() throws Throwable + { + EndPoint endPoint = channel.endPoint; + ByteBuffer buffer = BufferUtil.allocate(1024); + int filled = endPoint.fill(buffer); + if (filled < 0) + return Action.SUCCEEDED; + if (filled == 0) + { + endPoint.fillInterested(this); + return Action.IDLE; + } + channel.write(this, buffer); + return Action.SCHEDULED; + } + + @Override + public void succeeded() + { + // There is data to read from the EndPoint. + // Iterate to read it and send it to the Gateway. + iterate(); + } + + @Override + protected void onCompleteSuccess() + { + // Nothing more to read, close the Gateway Channel. + channel.close(null); + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + // There was a write error, close the Gateway Channel. + channel.close(cause); + } + } + + // Reads from the Gateway Channel, and writes to the EndPoint. + private static class ChannelToEndPointCallback extends IteratingCallback + { + private final Channel channel; + + private ChannelToEndPointCallback(Channel channel) + { + this.channel = channel; + } + + @Override + protected Action process() + { + ByteBuffer buffer = ByteBuffer.allocate(1024); + // Read from the Gateway Channel. + int read = channel.read(buffer); + if (read < 0) + return Action.SUCCEEDED; + if (read == 0) + { + channel.demand(); + return Action.IDLE; + } + // Write to the EndPoint. + channel.endPoint.write(this, buffer); + return Action.SCHEDULED; + } + + @Override + protected void onCompleteSuccess() + { + // Nothing more to read, close the Gateway Channel. + channel.close(null); + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + // There was a write error, close the Gateway Channel. + channel.close(cause); + } + } + } +} 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/HTTP1TransportTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP1TransportTest.java new file mode 100644 index 000000000000..a403cf1eb145 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP1TransportTest.java @@ -0,0 +1,172 @@ +// +// ======================================================================== +// 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.Transport; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransport; +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.MemoryTransport; +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.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +public class HTTP1TransportTest extends AbstractTransportTest +{ + private HttpClient httpClient; + + @BeforeEach + public void prepare() + { + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + httpClient = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); + server.addBean(httpClient); + } + + @Test + public void testDefaultTransport() 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().getTransport(), sameInstance(Transport.TCP_IP)); + + HttpClientTransportOverHTTP httpClientTransport = (HttpClientTransportOverHTTP)httpClient.getTransport(); + int networkConnections = httpClientTransport.getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(1)); + } + + @Test + public void testExplicitTransport() 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()) + .transport(Transport.TCP_IP) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testMemoryTransport() throws Exception + { + MemoryConnector connector = new MemoryConnector(server, new HttpConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transport(new MemoryTransport(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 testUnixDomainTransport() throws Exception + { + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, new HttpConnectionFactory()); + connector.setUnixDomainPath(newUnixDomainPath()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transport(new Transport.TCPUnix(connector.getUnixDomainPath())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testQUICTransport(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()) + .transport(new QuicTransport(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/HTTP2TransportTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP2TransportTest.java new file mode 100644 index 000000000000..b44963701dd7 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP2TransportTest.java @@ -0,0 +1,342 @@ +// +// ======================================================================== +// 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.Transport; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransport; +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.MemoryTransport; +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.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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; + +public class HTTP2TransportTest extends AbstractTransportTest +{ + private HttpClient httpClient; + private HTTP2Client http2Client; + + @BeforeEach + public void prepare() + { + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + http2Client = new HTTP2Client(clientConnector); + httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)); + server.addBean(httpClient); + } + + @Test + public void testDefaultTransport() 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().getTransport(), sameInstance(Transport.TCP_IP)); + + HttpClientTransportOverHTTP2 httpClientTransport = (HttpClientTransportOverHTTP2)httpClient.getTransport(); + int networkConnections = httpClientTransport.getHTTP2Client().getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(1)); + } + + @Test + public void testExplicitTransport() 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()) + .transport(Transport.TCP_IP) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testMemoryTransport() throws Exception + { + MemoryConnector connector = new MemoryConnector(server, new HTTP2CServerConnectionFactory()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transport(new MemoryTransport(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 testUnixDomainTransport() throws Exception + { + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, new HTTP2CServerConnectionFactory()); + connector.setUnixDomainPath(newUnixDomainPath()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + .transport(new Transport.TCPUnix(connector.getUnixDomainPath())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testQUICTransportWithH2C(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()) + .transport(new QuicTransport(clientQuicConfig)) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testQUICTransportWithH2(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()) + .transport(new QuicTransport(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 MemoryTransport(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(newUnixDomainPath()); + server.addConnector(connector); + server.setHandler(new EmptyServerHandler()); + server.start(); + + Session session = http2Client.connect(new Transport.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 QuicTransport(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/HTTP3TransportTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP3TransportTest.java new file mode 100644 index 000000000000..8d020b0aeae0 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTP3TransportTest.java @@ -0,0 +1,230 @@ +// +// ======================================================================== +// 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.Transport; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransport; +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.MemoryTransport; +import org.eclipse.jetty.toolchain.test.MavenPaths; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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; + +public class HTTP3TransportTest extends AbstractTransportTest +{ + private SslContextFactory.Server sslServer; + private Path pemServerDir; + 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); + + SslContextFactory.Client sslClient = new SslContextFactory.Client(true); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslClient, null); + ClientConnector clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + clientConnector.setExecutor(clientThreads); + clientConnector.setSelectors(1); + http3Client = new HTTP3Client(quicConfiguration, clientConnector); + httpClient = new HttpClient(new HttpClientTransportOverHTTP3(http3Client)); + server.addBean(httpClient); + } + + @Test + public void testDefaultTransport() 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); + Transport transport = destination.getOrigin().getTransport(); + if (transport instanceof Transport.Wrapper wrapper) + transport = wrapper.unwrap(); + assertThat(transport, sameInstance(Transport.UDP_IP)); + + HttpClientTransportOverHTTP3 httpClientTransport = (HttpClientTransportOverHTTP3)httpClient.getTransport(); + int networkConnections = httpClientTransport.getHTTP3Client().getClientConnector().getSelectorManager().getTotalKeys(); + assertThat(networkConnections, is(1)); + } + + @Test + public void testExplicitTransport() 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()) + .transport(new QuicTransport(http3Client.getQuicConfiguration())) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + @Test + public void testMemoryTransport() 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/") + .transport(new QuicTransport(new MemoryTransport(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 testUnixDomainTransport() + { + 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(); + + Transport transport = new QuicTransport(new MemoryTransport(connector), http3Client.getQuicConfiguration()); + Session.Client session = http3Client.connect(transport, 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/HTTPDynamicTransportTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTPDynamicTransportTest.java new file mode 100644 index 000000000000..4fa820bcc601 --- /dev/null +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HTTPDynamicTransportTest.java @@ -0,0 +1,530 @@ +// +// ======================================================================== +// 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.Transport; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.quic.client.QuicTransport; +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.MemoryTransport; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +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.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.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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; + +public class HTTPDynamicTransportTest extends AbstractTransportTest +{ + private SslContextFactory.Server sslServer; + private Path pemServerDir; + 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); + + clientConnector = new ClientConnector(); + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.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); + } + + @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()) + .transport(new Transport.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(); + + Transport.TCPUnix transport = new Transport.TCPUnix(unixDomainPath); + Promise.Completable promise = new Promise.Completable<>(); + http2Client.connect(transport, 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/") + .transport(new MemoryTransport(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); + QuicTransport transport = new QuicTransport(clientQuicConfiguration); + + ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) + .transport(transport) + .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(); + + Transport transport = new QuicTransport(new MemoryTransport(connector), http3Client.getQuicConfiguration()); + + ContentResponse response = httpClient.newRequest("https://localhost/") + .transport(transport) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + } + + private static int freePort() throws IOException + { + try (ServerSocket server = new ServerSocket()) + { + server.setReuseAddress(true); + server.bind(new InetSocketAddress("localhost", 0)); + return server.getLocalPort(); + } + } +} 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 620c1696e795..b4debb7f8654 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 @@ -42,7 +42,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; @@ -170,9 +172,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 @@ -224,34 +226,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 8fa783e94882..0629b66e4fba 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 8ce4025e7894..65fa6b8b9e39 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 @@ -18,16 +18,19 @@ 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.Transport; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -113,6 +116,7 @@ public boolean handle(Request request, Response response, Callback callback) } }); + // Use the deprecated APIs for backwards compatibility testing. ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath); HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); httpClient.start(); @@ -149,14 +153,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 Transport.TCPUnix(unixDomainPath) + ); + httpClient.getProxyConfiguration().addProxy(new HttpProxy(proxyOrigin, null)); httpClient.start(); try { ContentResponse response = httpClient.newRequest("localhost", fakeServerPort) + .transport(new Transport.TCPUnix(unixDomainPath)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -184,16 +194,17 @@ public boolean handle(Request request, Response response, Callback callback) 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 { @@ -204,16 +215,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) + .transport(new Transport.TCPUnix(unixDomainPath)) .path("/v1") .tag(new V1.Tag()) .timeout(5, TimeUnit.SECONDS) @@ -224,6 +233,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) + .transport(new Transport.TCPUnix(unixDomainPath)) .path("/v2") .tag(tag) .timeout(5, TimeUnit.SECONDS) @@ -246,11 +256,4 @@ public void testInvalidUnixDomainPath() server.addConnector(connector); assertThrows(IOException.class, () -> server.start()); } - - private static Path toUnixDomainPath(SocketAddress address) - { - if (address instanceof UnixDomainSocketAddress unix) - return unix.getPath(); - throw new AssertionError(); - } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java index f663a2285692..8825d08dab0a 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java @@ -16,10 +16,12 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -1640,7 +1642,7 @@ public static URI addPath(URI uri, String path) // Correct any bad `file:/path` usages, and // force encoding of characters that must be encoded (such as unicode) // for the base - String base = correctFileURI(uri).toASCIIString(); + String base = correctURI(uri).toASCIIString(); // ensure that the base has a safe encoding suitable for both // URI and Paths.get(URI) later usage @@ -1698,8 +1700,36 @@ public static String addQueries(String query1, String query2) * * @param uri the URI to (possibly) correct * @return the new URI with the {@code file:/} substring corrected, or the original URI. + * @deprecated use {@link #correctURI(URI)} instead, will be removed in Jetty 12.1.0 */ + @Deprecated(since = "12.0.7", forRemoval = true) public static URI correctFileURI(URI uri) + { + return correctURI(uri); + } + + /** + *

+ * Corrects any bad {@code file} based URIs (even within a {@code jar:file:} based URIs) from the bad out-of-spec + * format that various older Java APIs creates (most notably: {@link java.io.File} creates with it's {@link File#toURL()} + * and {@link File#toURI()}, along with the side effects of using {@link URL#toURI()}) + *

+ * + *

+ * This correction is currently limited to only the {@code file:/} substring in the URI. + * If there is a {@code file:/} detected, that substring is corrected to + * {@code file:///}, all other uses of {@code file:}, and URIs without a {@code file:} + * substring are left alone. + *

+ * + *

+ * Note that Windows UNC based URIs are left alone, along with non-absolute URIs. + *

+ * + * @param uri the URI to (possibly) correct + * @return the new URI with the {@code file:} scheme specific part corrected, or the original URI. + */ + public static URI correctURI(URI uri) { if ((uri == null) || (uri.getScheme() == null)) return uri; @@ -1845,10 +1875,60 @@ public static URI toURI(String resource) { Objects.requireNonNull(resource); - // Only try URI for string for known schemes, otherwise assume it is a Path - return (ResourceFactory.isSupported(resource)) - ? correctFileURI(URI.create(resource)) - : Paths.get(resource).toUri(); + if (URIUtil.hasScheme(resource)) + { + try + { + URI uri = new URI(resource); + + if (ResourceFactory.isSupported(uri)) + return correctURI(uri); + + // We don't have a supported URI scheme + if (uri.getScheme().length() == 1) + { + // Input is a possible Windows path disguised as a URI "D:/path/to/resource.txt". + try + { + return toURI(Paths.get(resource).toUri().toASCIIString()); + } + catch (InvalidPathException x) + { + LOG.trace("ignored", x); + } + } + + // If we reached this point, that means the input String has a scheme, + // and is not recognized as supported by the registered schemes in ResourceFactory. + if (LOG.isDebugEnabled()) + LOG.debug("URI scheme is not registered: {}", uri.toASCIIString()); + throw new IllegalArgumentException("URI scheme not registered: " + uri.getScheme()); + } + catch (URISyntaxException x) + { + // We have an input string that has what looks like a scheme, but isn't a URI. + // Eg: "C:\path\to\resource.txt" + LOG.trace("ignored", x); + } + } + + // If we reached this point, we have a String with no valid scheme. + // Treat it as a Path, as that's all we have left to investigate. + try + { + return toURI(Paths.get(resource).toUri().toASCIIString()); + } + catch (InvalidPathException x) + { + LOG.trace("ignored", x); + } + + // If we reached this here, that means the input string cannot be used as + // a URI or a File Path. The cause is usually due to bad input (eg: + // characters that are not supported by file system) + if (LOG.isDebugEnabled()) + LOG.debug("Input string cannot be converted to URI \"{}\"", resource); + throw new IllegalArgumentException("Cannot be converted to URI"); } /** @@ -1929,7 +2009,7 @@ public static Stream streamOf(URLClassLoader urlClassLoader) .map(URL::toString) .map(URI::create) .map(URIUtil::unwrapContainer) - .map(URIUtil::correctFileURI); + .map(URIUtil::correctURI); } private static final Index DEFAULT_PORT_FOR_SCHEME = new Index.Builder() 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-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java index 55ac974611a1..14013f9d557f 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/PathResource.java @@ -171,7 +171,7 @@ public static boolean isSameName(Path pathA, Path pathB) { String uriString = uri.toASCIIString(); if (!uriString.endsWith("/")) - uri = URIUtil.correctFileURI(URI.create(uriString + "/")); + uri = URIUtil.correctURI(URI.create(uriString + "/")); } this.path = path; @@ -310,7 +310,7 @@ protected Resource newResource(Path path, URI uri) @Override public boolean isDirectory() { - return Files.isDirectory(getPath(), LinkOption.NOFOLLOW_LINKS); + return Files.isDirectory(getPath()); } @Override diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java index c1c6fd7c1cae..37c417a001ef 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java @@ -109,8 +109,8 @@ public boolean contains(Resource other) return false; // Ensure that if `file` scheme is used, it's using a consistent convention to allow for startsWith check - String thisURIString = URIUtil.correctFileURI(thisURI).toASCIIString(); - String otherURIString = URIUtil.correctFileURI(otherURI).toASCIIString(); + String thisURIString = URIUtil.correctURI(thisURI).toASCIIString(); + String otherURIString = URIUtil.correctURI(otherURI).toASCIIString(); return otherURIString.startsWith(thisURIString) && (thisURIString.length() == otherURIString.length() || otherURIString.charAt(thisURIString.length()) == '/'); @@ -315,7 +315,7 @@ public void copyTo(Path destination) } return; } - throw new UnsupportedOperationException("Directory Resources without a Path must implement copyTo"); + throw new UnsupportedOperationException("Directory Resources without a Path must implement copyTo: " + this); } // Do we have to copy a single file? @@ -326,12 +326,18 @@ public void copyTo(Path destination) { // to a directory, preserve the filename Path destPath = destination.resolve(src.getFileName().toString()); - Files.copy(src, destPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + Files.copy(src, destPath, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.COPY_ATTRIBUTES, + StandardCopyOption.REPLACE_EXISTING); } else { // to a file, use destination as-is - Files.copy(src, destination, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + Files.copy(src, destination, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.COPY_ATTRIBUTES, + StandardCopyOption.REPLACE_EXISTING); } return; } @@ -405,7 +411,16 @@ public Collection getAllResources() boolean noDepth = true; for (Iterator i = children.iterator(); noDepth && i.hasNext(); ) - noDepth = !i.next().isDirectory(); + { + Resource resource = i.next(); + if (resource.isDirectory()) + { + // If the directory is a symlink we do not want to go any deeper. + Path resourcePath = resource.getPath(); + if (resourcePath == null || !Files.isSymbolicLink(resourcePath)) + noDepth = false; + } + } if (noDepth) return children; diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java index a870ae70596c..847659a08900 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceFactoryInternals.java @@ -213,13 +213,13 @@ public Resource newResource(URI uri) // otherwise resolve against the current directory uri = CURRENT_WORKING_DIR.toUri().resolve(uri); - // Correct any `file:/path` to `file:///path` mistakes - uri = URIUtil.correctFileURI(uri); + // Correct any mistakes like `file:/path` (to `file:///path`) + uri = URIUtil.correctURI(uri); } ResourceFactory resourceFactory = RESOURCE_FACTORIES.get(uri.getScheme()); if (resourceFactory == null) - throw new IllegalArgumentException("URI scheme not supported: " + uri); + throw new IllegalArgumentException("URI scheme not registered: " + uri.getScheme()); if (resourceFactory instanceof MountedPathResourceFactory) { FileSystemPool.Mount mount = mountIfNeeded(uri); @@ -233,7 +233,9 @@ public Resource newResource(URI uri) } catch (URISyntaxException | ProviderNotFoundException ex) { - throw new IllegalArgumentException("Unable to create resource from: " + uri, ex); + if (LOG.isDebugEnabled()) + LOG.debug("Unable to create resource from: {}", uri, ex); + throw new IllegalArgumentException("Unable to create resource", ex); } } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java index cbff9509b499..f186bb23c72e 100644 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java +++ b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResourceFactory.java @@ -176,7 +176,7 @@ public boolean isReadable() @Override public URI getURI() { - return URIUtil.correctFileURI(uri); + return URIUtil.correctURI(uri); } @Override diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java index 16a6e55e448e..e482337f0b08 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilTest.java @@ -57,7 +57,6 @@ @ExtendWith(WorkDirExtension.class) public class URIUtilTest { - public WorkDir workDir; public static Stream encodePathSource() @@ -668,10 +667,10 @@ public static Stream correctBadFileURICases() @ParameterizedTest @MethodSource("correctBadFileURICases") - public void testCorrectFileURI(String input, String expected) + public void testCorrectURI(String input, String expected) { URI inputUri = URI.create(input); - URI actualUri = URIUtil.correctFileURI(inputUri); + URI actualUri = URIUtil.correctURI(inputUri); URI expectedUri = URI.create(expected); assertThat(actualUri.toASCIIString(), is(expectedUri.toASCIIString())); } @@ -695,8 +694,8 @@ public void testCorrectBadFileURIActualFile() throws Exception assertThat(fileUri.toASCIIString(), not(containsString("://"))); assertThat(fileUrlUri.toASCIIString(), not(containsString("://"))); - assertThat(URIUtil.correctFileURI(fileUri).toASCIIString(), is(expectedUri.toASCIIString())); - assertThat(URIUtil.correctFileURI(fileUrlUri).toASCIIString(), is(expectedUri.toASCIIString())); + assertThat(URIUtil.correctURI(fileUri).toASCIIString(), is(expectedUri.toASCIIString())); + assertThat(URIUtil.correctURI(fileUrlUri).toASCIIString(), is(expectedUri.toASCIIString())); } public static Stream encodeSpecific() @@ -1063,6 +1062,49 @@ public void testUnwrapContainer(String inputRawUri, String expected) assertThat(actual.toASCIIString(), is(expected)); } + public static Stream toURICases() + { + List args = new ArrayList<>(); + + if (OS.WINDOWS.isCurrentOs()) + { + // Windows format (absolute and relative) + args.add(Arguments.of("C:\\path\\to\\foo.jar", "file:///C:/path/to/foo.jar")); + args.add(Arguments.of("D:\\path\\to\\bogus.txt", "file:///D:/path/to/bogus.txt")); + args.add(Arguments.of("\\path\\to\\foo.jar", "file:///C:/path/to/foo.jar")); + args.add(Arguments.of("\\path\\to\\bogus.txt", "file:///C:/path/to/bogus.txt")); + // unix format (relative) + args.add(Arguments.of("C:/path/to/foo.jar", "file:///C:/path/to/foo.jar")); + args.add(Arguments.of("D:/path/to/bogus.txt", "file:///D:/path/to/bogus.txt")); + args.add(Arguments.of("/path/to/foo.jar", "file:///C:/path/to/foo.jar")); + args.add(Arguments.of("/path/to/bogus.txt", "file:///C:/path/to/bogus.txt")); + // URI format (absolute) + args.add(Arguments.of("file:///D:/path/to/zed.jar", "file:///D:/path/to/zed.jar")); + args.add(Arguments.of("file:/e:/zed/yotta.txt", "file:///e:/zed/yotta.txt")); + args.add(Arguments.of("jar:file:///E:/path/to/bar.jar", "jar:file:///E:/path/to/bar.jar")); + } + else + { + // URI (and unix) format (relative) + args.add(Arguments.of("/path/to/foo.jar", "file:///path/to/foo.jar")); + args.add(Arguments.of("/path/to/bogus.txt", "file:///path/to/bogus.txt")); + } + // URI format (absolute) + args.add(Arguments.of("file:///path/to/zed.jar", "file:///path/to/zed.jar")); + args.add(Arguments.of("jar:file:///path/to/bar.jar", "jar:file:///path/to/bar.jar")); + + return args.stream(); + } + + @ParameterizedTest + @MethodSource("toURICases") + public void testToURI(String inputRaw, String expectedUri) + { + URI actual = URIUtil.toURI(inputRaw); + URI expected = URI.create(expectedUri); + assertEquals(expected, actual); + } + @Test public void testSplitSingleJar() { diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/AttributeNormalizerTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/AttributeNormalizerTest.java index 89434af6509e..e07cd7062d6f 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/AttributeNormalizerTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/AttributeNormalizerTest.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.util.resource; +import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; @@ -335,13 +336,13 @@ public void testCombinedResource() throws Exception assertThat(normalizer.expand("${WAR.uri}/file1"), containsString("/dir1/file1")); assertThat(normalizer.expand("${WAR.uri}/file2"), containsString("/dir2/file2")); assertThat(normalizer.expand("${WAR.uri}/file3"), containsString("/dir3/file3")); - assertThat(normalizer.expand("${WAR.path}/file1"), containsString("/dir1/file1")); - assertThat(normalizer.expand("${WAR.path}/file2"), containsString("/dir2/file2")); - assertThat(normalizer.expand("${WAR.path}/file3"), containsString("/dir3/file3")); + assertThat(normalizer.expand("${WAR.path}/file1"), containsString(FS.separators("/dir1/file1"))); + assertThat(normalizer.expand("${WAR.path}/file2"), containsString(FS.separators("/dir2/file2"))); + assertThat(normalizer.expand("${WAR.path}/file3"), containsString(FS.separators("/dir3/file3"))); // If file cannot be found it just uses the first resource. assertThat(normalizer.expand("${WAR.uri}/file4"), containsString("/dir1/file4")); - assertThat(normalizer.expand("${WAR.path}/file4"), containsString("/dir1/file4")); + assertThat(normalizer.expand("${WAR.path}/file4"), containsString(File.separator + "dir1/file4")); } public static class Scenario diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java index fb586088fed8..f45c7a0ee9e6 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/CombinedResourceTest.java @@ -110,9 +110,9 @@ public void testList() throws Exception .toList(); expected = new String[] { - "dir/1.txt", - "dir/2.txt", - "dir/3.txt" + FS.separators("dir/1.txt"), + FS.separators("dir/2.txt"), + FS.separators("dir/3.txt") }; assertThat(relative, containsInAnyOrder(expected)); @@ -281,9 +281,9 @@ public void testMixedResourceCollectionGetPathTo() throws IOException "2.txt", "3.txt", "4.txt", - "dir/1.txt", - "dir/2.txt", - "dir/3.txt" + FS.separators("dir/1.txt"), + FS.separators("dir/2.txt"), + FS.separators("dir/3.txt") }; assertThat(actual, contains(expected)); @@ -804,9 +804,9 @@ public void testGetAllResources() "2.txt", "3.txt", "dir", - "dir/1.txt", - "dir/2.txt", - "dir/3.txt" + FS.separators("dir/1.txt"), + FS.separators("dir/2.txt"), + FS.separators("dir/3.txt") )); } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java index c3aa50f53458..ab89e1457380 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/FileSystemResourceTest.java @@ -172,14 +172,16 @@ public void testNonAbsoluteURI(WorkDir workDir) throws Exception Files.createDirectories(testdir); Path pwd = Paths.get(System.getProperty("user.dir")); - Path relativePath = pwd.relativize(testdir); + + // Establish a relative path URI that uses uri "/" path separators (now windows "\") + URI relativePath = pwd.toUri().relativize(testdir.toUri()); // Get a path relative name using unix/uri "/" (not windows "\") - String relativeName = FS.separators(relativePath.toString()); - assertThat("Should not have path navigation entries", relativeName, not(containsString(".."))); + assertThat("Should not have path navigation entries", relativePath.toASCIIString(), not(containsString(".."))); + assertFalse(relativePath.isAbsolute()); - resource = ResourceFactory.root().newResource(new URI(relativeName)); - assertThat("Relative newResource: " + relativeName, resource, notNullValue()); + resource = ResourceFactory.root().newResource(relativePath); + assertThat("Relative newResource: " + relativePath, resource, notNullValue()); assertThat(resource.getURI().toString(), startsWith("file:")); assertThat(resource.getURI().toString(), endsWith("/path/to/resource/")); } @@ -1095,7 +1097,8 @@ public void testResolveWindowsSlash(WorkDir workDir) throws Exception if (WINDOWS.isCurrentOs()) { - assertThat("getURI()", r.getPath().toString(), containsString("aa\\/foo.txt")); + // On windows, the extra "\" is stripped when working with java.nio.Path objects + assertThat("getURI()", r.getPath().toString(), containsString("aa\\foo.txt")); assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa%5C/foo.txt")); assertThat("isAlias()", r.isAlias(), is(true)); assertThat("getRealURI()", r.getRealURI(), notNullValue()); @@ -1279,7 +1282,16 @@ public void testUtf8Dir(WorkDir workDir) throws Exception Resource r = base.resolve("file.txt"); assertThat("Exists: " + r, r.exists(), is(true)); - assertThat("Is Not Alias: " + r, r, isNotAlias()); + if (WINDOWS.isCurrentOs()) + { + // On windows, the base.resolve results in a representation of ".../testUtf8Dir/b%C3%A3m/file.txt" + // But that differs from the input URI of ".../testUtf8Dir/bãm/file.txt", so it is viewed as an alias. + assertThat("Is Alias: " + r, r, isAlias()); + URI realURI = r.getRealURI(); + assertThat(realURI, is(file.toUri())); + } + else + assertThat("Is Not Alias: " + r, r, isNotAlias()); } @Test @@ -1287,6 +1299,7 @@ public void testUtf8Dir(WorkDir workDir) throws Exception public void testUncPath() { Resource base = ResourceFactory.root().newResource(URI.create("file:////127.0.0.1/path")); + assumeTrue(base != null); Resource resource = base.resolve("WEB-INF/"); assertThat("getURI()", resource.getURI().toASCIIString(), containsString("path/WEB-INF/")); assertThat("isAlias()", resource.isAlias(), is(false)); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java index b3ea77bb4ee5..a183776aec6a 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/PathResourceTest.java @@ -27,7 +27,6 @@ import java.nio.file.Path; import java.util.Collection; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.eclipse.jetty.toolchain.test.FS; @@ -39,6 +38,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -728,11 +728,6 @@ public void testGetAllResourcesSymlinkLoop(WorkDir workDir) throws Exception resource.resolve("foo.txt") }; - List actual = allResources.stream() - .map(Resource::getURI) - .map(URI::toASCIIString) - .toList(); - assertThat(allResources, containsInAnyOrder(expected)); } } @@ -742,8 +737,21 @@ public void testBrokenSymlink(WorkDir workDir) throws Exception { Path testDir = workDir.getEmptyPathDir(); Path resourcePath = testDir.resolve("resource.txt"); + Path symlinkPath = null; IO.copy(MavenTestingUtils.getTestResourcePathFile("resource.txt").toFile(), resourcePath.toFile()); - Path symlinkPath = Files.createSymbolicLink(testDir.resolve("symlink.txt"), resourcePath); + boolean symlinkSupported; + try + { + symlinkPath = Files.createSymbolicLink(testDir.resolve("symlink.txt"), resourcePath); + symlinkSupported = true; + } + catch (UnsupportedOperationException | FileSystemException e) + { + symlinkSupported = false; + } + + assumeTrue(symlinkSupported, "Symlink not supported"); + assertNotNull(symlinkPath); PathResource fileResource = new PathResource(resourcePath); assertTrue(fileResource.exists()); @@ -792,7 +800,10 @@ public void testResolveNavigation(WorkDir workDir) throws Exception Resource rootRes = resourceFactory.newResource(docroot); // Test navigation through a directory that doesn't exist Resource fileResViaBar = rootRes.resolve("bar/../dir/test.txt"); - assertTrue(Resources.missing(fileResViaBar)); + if (OS.WINDOWS.isCurrentOs()) // windows allows navigation through a non-existent directory + assertTrue(Resources.exists(fileResViaBar)); + else + assertTrue(Resources.missing(fileResViaBar)); // Test navigation through a directory that does exist Resource fileResViaFoo = rootRes.resolve("foo/../dir/test.txt"); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java index c49e3e4e6324..c6a97baaf587 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceAliasTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,7 +74,10 @@ public void testAliasNavigation(WorkDir workDir) throws IOException Resource rootRes = resourceFactory.newResource(docroot); // Test navigation through a directory that doesn't exist Resource fileResViaBar = rootRes.resolve("bar/../dir/test.txt"); - assertTrue(Resources.missing(fileResViaBar), "File doesn't exist"); + if (OS.WINDOWS.isCurrentOs()) // windows allows navigation through a non-existent directory + assertTrue(Resources.exists(fileResViaBar)); + else + assertTrue(Resources.missing(fileResViaBar), "File doesn't exist"); // Test navigation through a directory that does exist Resource fileResViaFoo = rootRes.resolve("foo/../dir/test.txt"); diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceFactoryTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceFactoryTest.java index ab833cd01cab..b0781a62c3c2 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceFactoryTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceFactoryTest.java @@ -135,16 +135,15 @@ public void testCustomUriSchemeNotRegistered() // Try this as a normal String input first. // We are subject to the URIUtil.toURI(String) behaviors here. // Since the `ftp` scheme is not registered, it's not recognized as a supported URI. - // This will be treated as a relative path instead. (and the '//' will be compacted) - Resource resource = ResourceFactory.root().newResource("ftp://webtide.com/favicon.ico"); - // Should not find this, as it doesn't exist on the filesystem. - assertNull(resource); + IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, + () -> ResourceFactory.root().newResource("ftp://webtide.com/favicon.ico")); + assertThat(iae.getMessage(), containsString("URI scheme not registered: ftp")); // Now try it as a formal URI object as input. URI uri = URI.create("ftp://webtide.com/favicon.ico"); // This is an unsupported URI scheme - IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> ResourceFactory.root().newResource(uri)); - assertThat(iae.getMessage(), containsString("URI scheme not supported")); + iae = assertThrows(IllegalArgumentException.class, () -> ResourceFactory.root().newResource(uri)); + assertThat(iae.getMessage(), containsString("URI scheme not registered: ftp")); } @Test diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java index bdddb610385f..e690b8ee9dff 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java @@ -434,7 +434,17 @@ public void testDotAliasFileExists(WorkDir workDir) throws IOException Resource resource = resourceFactory.newResource(file); // Requesting a resource that would point to a location called ".../testDotAliasFileExists/foo/bar.txt/." Resource dot = resource.resolve("."); - assertTrue(Resources.missing(dot), "Cannot reference file as a directory"); + if (OS.WINDOWS.isCurrentOs()) + { + // windows allows this reference, but it's an alias. + assertTrue(Resources.exists(dot), "Reference to directory via dot allowed"); + assertTrue(dot.isAlias(), "Reference to dot is an alias to actual bar.txt"); + assertEquals(dot.getRealURI(), file.toUri()); + } + else + { + assertTrue(Resources.missing(dot), "Cannot reference file as a directory"); + } } @Test diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java index 36037417d46e..7f028f4b5a96 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/resource/UrlResourceFactoryTest.java @@ -274,7 +274,7 @@ public void testResolveNestedUriNoPath() throws MalformedURLException assertThat("resource /path/to/example.jar/WEB-INF/web.xml doesn't exist", webResource.exists(), is(false)); assertThat(webResource.isDirectory(), is(false)); - URI expectedURI = URIUtil.correctFileURI(URI.create("file:" + path.toUri().toASCIIString() + "/WEB-INF/web.xml")); + URI expectedURI = URIUtil.correctURI(URI.create("file:" + path.toUri().toASCIIString() + "/WEB-INF/web.xml")); assertThat(webResource.getURI(), is(expectedURI)); } @@ -296,7 +296,7 @@ public void testResolveFromFile() throws MalformedURLException assertThat("resource /path/to/example.jar/WEB-INF/web.xml doesn't exist", webResource.exists(), is(false)); assertThat(webResource.isDirectory(), is(false)); - URI expectedURI = URIUtil.correctFileURI(URI.create(path.toUri().toASCIIString() + "/WEB-INF/web.xml")); + URI expectedURI = URIUtil.correctURI(URI.create(path.toUri().toASCIIString() + "/WEB-INF/web.xml")); assertThat(webResource.getURI(), is(expectedURI)); } diff --git a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java index 23c8d914676c..c4a77082d1f9 100644 --- a/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java +++ b/jetty-core/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java @@ -43,6 +43,8 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -381,6 +383,7 @@ public void testServerSslContextFactory() throws Exception } @Test + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Will result in java.net.SocketException: An established connection was aborted by the software in your host machine during IO.readBytes(input)") public void testSNIWithPKIX() throws Exception { SslContextFactory.Server serverTLS = new SslContextFactory.Server() diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java index fa6d2524a6b2..ddf6dc9d49bf 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-common/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConnection.java @@ -69,7 +69,7 @@ private enum DemandState private final Flusher flusher; private final Random random; private DemandState demand = DemandState.NOT_DEMANDING; - private boolean fillingAndParsing; + private boolean fillingAndParsing = true; private final LongAdder messagesIn = new LongAdder(); private final LongAdder bytesIn = new LongAdder(); // Read / Parse variables @@ -199,11 +199,6 @@ public void setUseOutputDirectByteBuffers(boolean useOutputDirectByteBuffers) this.useOutputDirectByteBuffers = useOutputDirectByteBuffers; } - /** - * Physical connection disconnect. - *

    - * Not related to WebSocket close handshake. - */ @Override public void onClose(Throwable cause) { @@ -236,11 +231,6 @@ public boolean onIdleExpired(TimeoutException timeoutException) return true; } - /** - * Event for no activity on connection (read or write) - * - * @return true to signal that the endpoint must be closed, false to keep the endpoint open - */ @Override protected boolean onReadTimeout(TimeoutException timeout) { @@ -394,7 +384,7 @@ public boolean moreDemand() case NOT_DEMANDING -> { fillingAndParsing = false; - if (!networkBuffer.hasRemaining()) + if (networkBuffer != null && !networkBuffer.hasRemaining()) releaseNetworkBuffer(); return false; } @@ -530,9 +520,6 @@ protected void setInitialBuffer(ByteBuffer initialBuffer) BufferUtil.flipToFlush(buffer, 0); } - /** - * Physical connection Open. - */ @Override public void onOpen() { @@ -542,6 +529,8 @@ public void onOpen() // Open Session super.onOpen(); coreSession.onOpen(); + if (moreDemand()) + fillAndParse(); } @Override diff --git a/jetty-core/jetty-websocket/jetty-websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ExplicitDemandTest.java b/jetty-core/jetty-websocket/jetty-websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ExplicitDemandTest.java index 49478cb482e3..2994945b7c91 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ExplicitDemandTest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ExplicitDemandTest.java @@ -18,17 +18,23 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Callback; import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.core.CloseStatus; +import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -36,6 +42,7 @@ import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -74,6 +81,37 @@ public void onWebSocketFrame(Frame frame, Callback callback) } } + @WebSocket(autoDemand = false) + public static class OnOpenSocket implements Session.Listener + { + CountDownLatch onOpen = new CountDownLatch(1); + BlockingQueue textMessages = new BlockingArrayQueue<>(); + Session session; + + @Override + public void onWebSocketOpen(Session session) + { + try + { + this.session = session; + session.demand(); + onOpen.await(); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + + @Override + public void onWebSocketFrame(Frame frame, Callback callback) + { + if (frame.getOpCode() == OpCode.TEXT) + textMessages.add(BufferUtil.toString(frame.getPayload())); + callback.succeed(); + } + } + @WebSocket(autoDemand = false) public static class PingSocket extends ListenerSocket { @@ -99,6 +137,7 @@ public void onWebSocketFrame(Frame frame, Callback callback) private final WebSocketClient client = new WebSocketClient(); private final SuspendSocket serverSocket = new SuspendSocket(); private final ListenerSocket listenerSocket = new ListenerSocket(); + private final OnOpenSocket onOpenSocket = new OnOpenSocket(); private final PingSocket pingSocket = new PingSocket(); private ServerConnector connector; @@ -113,6 +152,7 @@ public void start() throws Exception container.addMapping("/suspend", (rq, rs, cb) -> serverSocket); container.addMapping("/listenerSocket", (rq, rs, cb) -> listenerSocket); container.addMapping("/ping", (rq, rs, cb) -> pingSocket); + container.addMapping("/onOpen", (rq, rs, cb) -> onOpenSocket); }); server.setHandler(wsHandler); @@ -213,4 +253,27 @@ public void testServerPing() throws Exception frame = pingSocket.frames.get(2); assertThat(frame.getType(), is(Frame.Type.CLOSE)); } + + @Test + public void testDemandInOnOpen() throws Exception + { + URI uri = new URI("ws://localhost:" + connector.getLocalPort() + "/onOpen"); + EventSocket clientSocket = new EventSocket(); + + Future connect = client.connect(clientSocket, uri); + Session session = connect.get(5, TimeUnit.SECONDS); + session.sendText("test-text", Callback.NOOP); + + // We cannot receive messages while in onOpen, even if we have demanded. + assertNull(onOpenSocket.textMessages.poll(1, TimeUnit.SECONDS)); + + // Once we leave onOpen we receive the message. + onOpenSocket.onOpen.countDown(); + String received = onOpenSocket.textMessages.poll(5, TimeUnit.SECONDS); + assertThat(received, equalTo("test-text")); + + session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientSocket.closeCode, equalTo(CloseStatus.NORMAL)); + } } diff --git a/jetty-ee10/jetty-ee10-fcgi-proxy/src/main/java/org/eclipse/jetty/ee10/fcgi/proxy/FastCGIProxyServlet.java b/jetty-ee10/jetty-ee10-fcgi-proxy/src/main/java/org/eclipse/jetty/ee10/fcgi/proxy/FastCGIProxyServlet.java index 3a22a0974b2e..0fee2ff3bfa9 100644 --- a/jetty-ee10/jetty-ee10-fcgi-proxy/src/main/java/org/eclipse/jetty/ee10/fcgi/proxy/FastCGIProxyServlet.java +++ b/jetty-ee10/jetty-ee10-fcgi-proxy/src/main/java/org/eclipse/jetty/ee10/fcgi/proxy/FastCGIProxyServlet.java @@ -39,6 +39,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Transport; import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.URIUtil; @@ -94,6 +95,7 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent private String originalQueryAttribute; private boolean fcgiHTTPS; private Set fcgiEnvNames; + private Path unixDomainPath; @Override public void init() throws ServletException @@ -118,6 +120,10 @@ public void init() throws ServletException .map(String::trim) .collect(Collectors.toSet()); } + + String path = getInitParameter("unixDomainPath"); + if (path != null) + unixDomainPath = Path.of(path); } @Override @@ -128,21 +134,12 @@ protected HttpClient newHttpClient() if (scriptRoot == null) throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured"); - ClientConnector connector; - String unixDomainPath = config.getInitParameter("unixDomainPath"); - if (unixDomainPath != null) - { - connector = ClientConnector.forUnixDomain(Path.of(unixDomainPath)); - } - else - { - int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2); - String value = config.getInitParameter("selectors"); - if (value != null) - selectors = Integer.parseInt(value); - connector = new ClientConnector(); - connector.setSelectors(selectors); - } + int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2); + String value = config.getInitParameter("selectors"); + if (value != null) + selectors = Integer.parseInt(value); + ClientConnector connector = new ClientConnector(); + connector.setSelectors(selectors); return new HttpClient(new ProxyHttpClientTransportOverFCGI(connector, scriptRoot)); } @@ -219,6 +216,10 @@ protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyRequest.headers(headers -> headers.put(HttpHeader.COOKIE, builder.toString())); } + Path unixDomain = unixDomainPath; + if (unixDomain != null) + proxyRequest.transport(new Transport.TCPUnix(unixDomain)); + super.sendProxyRequest(request, proxyResponse, proxyRequest); } diff --git a/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/EE10Activator.java b/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/EE10Activator.java index 540d7bc63f8b..4c0a487441bf 100644 --- a/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/EE10Activator.java +++ b/jetty-ee10/jetty-ee10-osgi/jetty-ee10-osgi-boot/src/main/java/org/eclipse/jetty/ee10/osgi/boot/EE10Activator.java @@ -48,6 +48,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.FileID; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.URLResourceFactory; @@ -357,7 +358,7 @@ public ContextHandler createContextHandler(AbstractContextProvider provider, App { OSGiApp osgiApp = OSGiApp.class.cast(app); String jettyHome = (String)app.getDeploymentManager().getServer().getAttribute(OSGiServerConstants.JETTY_HOME); - Path jettyHomePath = (StringUtil.isBlank(jettyHome) ? null : Paths.get(jettyHome)); + Path jettyHomePath = StringUtil.isBlank(jettyHome) ? null : Paths.get(URIUtil.toURI(jettyHome)); WebAppContext webApp = new WebAppContext(); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java index 79e735177401..d61b11d5b768 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/HttpOutput.java @@ -432,7 +432,7 @@ public void complete(Callback callback) } if (content != null) - channelWrite(content, true, new CompleteWriteCompleteCB()); + channelWrite(content, true, new WriteCompleteCB()); } /** @@ -1766,15 +1766,4 @@ public InvocationType getInvocationType() return InvocationType.NON_BLOCKING; } } - - private class CompleteWriteCompleteCB extends WriteCompleteCB - { - @Override - public void failed(Throwable x) - { - // TODO why is this needed for h2/h3? - HttpOutput.this._servletChannel.abort(x); - super.failed(x); - } - } } diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java index 8deffb1280f3..9d2bc9b45295 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletChannelState.java @@ -1051,6 +1051,9 @@ protected void completed(Throwable failure) if (_requestState != RequestState.COMPLETING) throw new IllegalStateException(this.getStatusStringLocked()); + if (failure != null) + abortResponse(failure); + if (_event == null) { _requestState = RequestState.COMPLETED; diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java index 19f981f5ec2a..0668fa49a52d 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SizeLimitHandlerServletTest.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -40,7 +41,6 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -49,6 +49,7 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.lessThan; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class SizeLimitHandlerServletTest { @@ -57,8 +58,7 @@ public class SizeLimitHandlerServletTest private ServerConnector _connector; private HttpClient _client; - @BeforeEach - public void before() throws Exception + private void start(HttpServlet servlet) throws Exception { _server = new Server(); _connector = new ServerConnector(_server); @@ -71,15 +71,7 @@ public void before() throws Exception sizeLimitHandler.setHandler(gzipHandler); contextHandler.insertHandler(sizeLimitHandler); - contextHandler.addServlet(new HttpServlet() - { - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException - { - String requestContent = IO.toString(req.getInputStream()); - resp.getWriter().print(requestContent); - } - }, "/"); + contextHandler.addServlet(servlet, "/"); _server.setHandler(contextHandler); _server.start(); @@ -99,6 +91,16 @@ public void after() throws Exception @Test public void testGzippedEcho() throws Exception { + start(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + String requestContent = IO.toString(req.getInputStream()); + resp.getWriter().print(requestContent); + } + }); + String content = "x".repeat(SIZE_LIMIT * 2); URI uri = URI.create("http://localhost:" + _connector.getLocalPort()); @@ -123,6 +125,16 @@ public void testGzippedEcho() throws Exception @Test public void testNormalEcho() throws Exception { + start(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + String requestContent = IO.toString(req.getInputStream()); + resp.getWriter().print(requestContent); + } + }); + String content = "x".repeat(SIZE_LIMIT * 2); URI uri = URI.create("http://localhost:" + _connector.getLocalPort()); @@ -135,6 +147,65 @@ public void testNormalEcho() throws Exception @Test public void testGzipEchoNoAcceptEncoding() throws Exception { + start(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + String requestContent = IO.toString(req.getInputStream()); + // The content will be buffered, and the implementation + // will try to flush it, failing because of SizeLimitHandler. + resp.getWriter().print(requestContent); + } + }); + + String content = "x".repeat(SIZE_LIMIT * 2); + URI uri = URI.create("http://localhost:" + _connector.getLocalPort()); + + StringBuilder contentReceived = new StringBuilder(); + CompletableFuture resultFuture = new CompletableFuture<>(); + _client.POST(uri) + .headers(httpFields -> httpFields.add(HttpHeader.CONTENT_ENCODING, "gzip")) + .body(gzipContent(content)) + .onResponseContentAsync((response, chunk, demander) -> + { + contentReceived.append(BufferUtil.toString(chunk.getByteBuffer())); + demander.run(); + }) + .send(resultFuture::complete); + + Result result = resultFuture.get(5, TimeUnit.SECONDS); + assertNotNull(result); + assertThat(result.getResponse().getStatus(), equalTo(HttpStatus.INTERNAL_SERVER_ERROR_500)); + assertThat(contentReceived.toString(), containsString("Response body is too large")); + } + + @Test + public void testGzipEchoNoAcceptEncodingFlush() throws Exception + { + CountDownLatch flushFailureLatch = new CountDownLatch(1); + start(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + String requestContent = IO.toString(req.getInputStream()); + // The content will be buffered. + resp.getWriter().print(requestContent); + try + { + // The flush will fail because exceeds + // the SizeLimitHandler configuration. + resp.flushBuffer(); + } + catch (Throwable x) + { + flushFailureLatch.countDown(); + throw x; + } + } + }); + String content = "x".repeat(SIZE_LIMIT * 2); URI uri = URI.create("http://localhost:" + _connector.getLocalPort()); @@ -150,6 +221,7 @@ public void testGzipEchoNoAcceptEncoding() throws Exception }) .send(resultFuture::complete); + assertTrue(flushFailureLatch.await(5, TimeUnit.SECONDS)); Result result = resultFuture.get(5, TimeUnit.SECONDS); assertNotNull(result); 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-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/AliasCheckerSymlinkTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/AliasCheckerSymlinkTest.java index 01c40a8b45b1..c87274703e64 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/AliasCheckerSymlinkTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-integration/src/test/java/org/eclipse/jetty/ee10/test/AliasCheckerSymlinkTest.java @@ -253,8 +253,12 @@ public static Stream testCases(ContextHandler context) Arguments.of(null, "/symlinkParentDir/webroot/file", HttpStatus.NOT_FOUND_404, null), Arguments.of(null, "/symlinkParentDir/webroot/WEB-INF/web.xml", HttpStatus.NOT_FOUND_404, null), Arguments.of(null, "/symlinkSiblingDir/file", HttpStatus.NOT_FOUND_404, null), - Arguments.of(null, "/webInfSymlink/web.xml", HttpStatus.NOT_FOUND_404, null) - ); + Arguments.of(null, "/webInfSymlink/web.xml", HttpStatus.NOT_FOUND_404, null), + + // We should only be able to list contents of a symlinked directory if the alias checker is installed. + Arguments.of(null, "/symlinkDir", HttpStatus.NOT_FOUND_404, null), + Arguments.of(allowedResource, "/symlinkDir", HttpStatus.OK_200, null) + ); } public static Stream combinedResourceTestCases() diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java index 6b793044a41d..253f7e296bc1 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebInfConfiguration.java @@ -239,7 +239,8 @@ public void unpack(WebAppContext context) throws IOException if (war != null) { - Path warPath = Path.of(war); + Path warPath = Path.of(URIUtil.toURI(war)); + // look for a sibling like "foo/" to a "foo.war" if (FileID.isWebArchive(warPath) && Files.exists(warPath)) { diff --git a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java index 6cf334c03ef8..3d54cf02593d 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java +++ b/jetty-ee10/jetty-ee10-webapp/src/test/java/org/eclipse/jetty/ee10/webapp/MetaInfConfigurationTest.java @@ -562,8 +562,8 @@ public void testGetContainerPathsWithModuleSystem() throws Exception // we "correct" the bad file URLs that come from the ClassLoader // to be the same as what comes from every non-classloader URL/URI. String[] expectedContainerResources = { - URIUtil.correctFileURI(janbUri).toASCIIString(), - URIUtil.correctFileURI(servletUri).toASCIIString() + URIUtil.correctURI(janbUri).toASCIIString(), + URIUtil.correctURI(servletUri).toASCIIString() }; assertThat("Discovered Container resources", discoveredContainerResources, hasItems(expectedContainerResources)); } diff --git a/jetty-ee9/jetty-ee9-fcgi-proxy/src/main/java/org/eclipse/jetty/ee9/fcgi/proxy/FastCGIProxyServlet.java b/jetty-ee9/jetty-ee9-fcgi-proxy/src/main/java/org/eclipse/jetty/ee9/fcgi/proxy/FastCGIProxyServlet.java index 2f15cbf10054..c10acc892e5b 100644 --- a/jetty-ee9/jetty-ee9-fcgi-proxy/src/main/java/org/eclipse/jetty/ee9/fcgi/proxy/FastCGIProxyServlet.java +++ b/jetty-ee9/jetty-ee9-fcgi-proxy/src/main/java/org/eclipse/jetty/ee9/fcgi/proxy/FastCGIProxyServlet.java @@ -39,6 +39,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Transport; import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.URIUtil; @@ -94,6 +95,7 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent private String originalQueryAttribute; private boolean fcgiHTTPS; private Set fcgiEnvNames; + private Path unixDomainPath; @Override public void init() throws ServletException @@ -118,6 +120,10 @@ public void init() throws ServletException .map(String::trim) .collect(Collectors.toSet()); } + + String path = getInitParameter("unixDomainPath"); + if (path != null) + unixDomainPath = Path.of(path); } @Override @@ -128,21 +134,12 @@ protected HttpClient newHttpClient() if (scriptRoot == null) throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured"); - ClientConnector connector; - String unixDomainPath = config.getInitParameter("unixDomainPath"); - if (unixDomainPath != null) - { - connector = ClientConnector.forUnixDomain(Path.of(unixDomainPath)); - } - else - { - int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2); - String value = config.getInitParameter("selectors"); - if (value != null) - selectors = Integer.parseInt(value); - connector = new ClientConnector(); - connector.setSelectors(selectors); - } + int selectors = Math.max(1, ProcessorUtils.availableProcessors() / 2); + String value = config.getInitParameter("selectors"); + if (value != null) + selectors = Integer.parseInt(value); + ClientConnector connector = new ClientConnector(); + connector.setSelectors(selectors); return new HttpClient(new ProxyHttpClientTransportOverFCGI(connector, scriptRoot)); } @@ -220,6 +217,10 @@ protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyRequest.headers(headers -> headers.put(HttpHeader.COOKIE, builder.toString())); } + Path unixDomain = unixDomainPath; + if (unixDomain != null) + proxyRequest.transport(new Transport.TCPUnix(unixDomain)); + super.sendProxyRequest(request, proxyResponse, proxyRequest); } diff --git a/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java b/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java index 5eeb61b381d6..9f4eb0705f28 100644 --- a/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java +++ b/jetty-ee9/jetty-ee9-osgi/jetty-ee9-osgi-boot/src/main/java/org/eclipse/jetty/ee9/osgi/boot/EE9Activator.java @@ -49,6 +49,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.FileID; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.URLResourceFactory; @@ -356,7 +357,7 @@ public ContextHandler createContextHandler(AbstractContextProvider provider, App { OSGiApp osgiApp = OSGiApp.class.cast(app); String jettyHome = (String)app.getDeploymentManager().getServer().getAttribute(OSGiServerConstants.JETTY_HOME); - Path jettyHomePath = (StringUtil.isBlank(jettyHome) ? null : Paths.get(jettyHome)); + Path jettyHomePath = StringUtil.isBlank(jettyHome) ? null : Paths.get(URIUtil.toURI(jettyHome)); WebAppContext webApp = new WebAppContext(); 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 2569f6d236c5..d477b1c6f8a6 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.Transport; 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; @@ -991,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") + .transport(new Transport.TCPUnix(path)) + .send(); assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); } } @@ -1187,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) diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/GzipModuleTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/GzipModuleTests.java index 7a100b1280b5..26f41a3ea2f7 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/GzipModuleTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/GzipModuleTests.java @@ -21,7 +21,6 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.tests.testers.JettyHomeTester; import org.eclipse.jetty.tests.testers.Tester; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -31,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -@Disabled //TODO gzip module broken public class GzipModuleTests extends AbstractJettyHomeTest { @ParameterizedTest @@ -76,7 +74,7 @@ public void testGzipDefault(String env) throws Exception } } } - + @ParameterizedTest @ValueSource(strings = {"ee9", "ee10"}) public void testGzipDefaultExcludedMimeType(String env) throws Exception @@ -147,7 +145,7 @@ public void testGzipAddWebappSpecificExcludeMimeType(String env) throws Exceptio "jetty.gzip.excludedMimeTypeList=image/vnd.microsoft.icon" }; - Path war = distribution.resolveArtifact("org.eclipse.jetty." + env + " .demos:jetty-" + env + "-demo-simple-webapp:war:" + jettyVersion); + Path war = distribution.resolveArtifact("org.eclipse.jetty." + env + ".demos:jetty-" + env + "-demo-simple-webapp:war:" + jettyVersion); distribution.installWar(war, "demo"); try (JettyHomeTester.Run runStart = distribution.start(argsStart))