diff --git a/documentation/jetty-documentation/pom.xml b/documentation/jetty-documentation/pom.xml index 01d3db94affd..48dd43ee6834 100644 --- a/documentation/jetty-documentation/pom.xml +++ b/documentation/jetty-documentation/pom.xml @@ -201,6 +201,11 @@ http2-http-client-transport ${project.version} + + org.eclipse.jetty + jetty-unixdomain-server + ${project.version} + org.eclipse.jetty jetty-slf4j-impl diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/deploy/deploy-virtual-hosts.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/deploy/deploy-virtual-hosts.adoc index 7e63f26756ad..9412f3da1f93 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/deploy/deploy-virtual-hosts.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/deploy/deploy-virtual-hosts.adoc @@ -41,8 +41,8 @@ A wildcard domain name which will match only one level of arbitrary subdomains. An IP address may be set as a virtual host to indicate that a web application should handle requests received on the network interface with that IP address for protocols that do not indicate a host name such as HTTP/0.9 or HTTP/1.0. `@ConnectorName`:: -A Jetty `ServerConnector` name to indicate that a web application should handle requests received on the `ServerConnector` with that name, and therefore received on a specific IP port. -A `ServerConnector` name can be set via link:{javadoc-url}/org/eclipse/jetty/server/AbstractConnector.html#setName(java.lang.String)[]. +A Jetty server `Connector` name to indicate that a web application should handle requests received on the server `Connector` with that name, and therefore received on a specific socket address (either an IP port for `ServerConnector`, or a Unix-Domain path for `UnixDomainServerConnector`). +A server `Connector` name can be set via link:{javadoc-url}/org/eclipse/jetty/server/AbstractConnector.html#setName(java.lang.String)[]. `www.√integral.com`:: Non-ASCII and https://en.wikipedia.org/wiki/Internationalized_domain_name[IDN] domain names can be set as virtual hosts using https://en.wikipedia.org/wiki/Punycode[Puny Code] equivalents that may be obtained from a https://www.punycoder.com/[Punycode/IDN converters]. @@ -134,7 +134,7 @@ To achieve this, you simply use the same context path of `/` for each of your we [[og-deploy-virtual-hosts-port]] ===== Different Port, Different Web Application -Sometimes it is required to serve different web applications from different IP ports, and therefore from different ``ServerConnector``s. +Sometimes it is required to serve different web applications from different socket addresses (either different IP ports, or different Unix-Domain paths), and therefore from different server ``Connector``s. For example, you want requests to `+http://localhost:8080/+` to be served by one web application, but requests to `+http://localhost:9090/+` to be served by another web application. diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-custom.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-custom.adoc index 826570e42e16..15d23edc5d2d 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-custom.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/modules-custom.adoc @@ -131,7 +131,7 @@ In the cases where you need to enhance Jetty with a custom functionality, you ca For example, let's assume that you need to add a custom auditing component that integrates with the auditing tools used by your company. This custom auditing component should measure the HTTP request processing times and record them (how they are recorded is irrelevant here -- could be in a local log file or sent via network to an external service). -The Jetty libraries already provide a way to measure HTTP request processing times via xref:{prog-guide}#pg-server-http-channel-events[`HttpChannel` events]: you write a custom component that implements the `HttpChannel.Listener` interface and add it as a bean to the `ServerConnector` that receives the HTTP requests. +The Jetty libraries already provide a way to measure HTTP request processing times via xref:{prog-guide}#pg-server-http-channel-events[`HttpChannel` events]: you write a custom component that implements the `HttpChannel.Listener` interface and add it as a bean to the server `Connector` that receives the HTTP requests. The steps to create a Jetty module are similar to those necessary to xref:og-modules-custom-modify[modify an existing module]: diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc index 5b2b8a6a995c..23f4de0d797d 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/arch-io.adoc @@ -26,7 +26,7 @@ Each `ManagedSelector` wraps an instance of `java.nio.channels.Selector` that in NOTE: TODO: add image -`SocketChannel` instances can be created by network clients when connecting to a server and by a network server when accepting connections from network clients. +`SocketChannel` instances can be created by clients when connecting to a server and by a server when accepting connections from clients. In both cases the `SocketChannel` instance is passed to `SelectorManager` (which passes it to `ManagedSelector` and eventually to `java.nio.channels.Selector`) to be registered for use within Jetty. It is possible for an application to create the `SocketChannel` instances outside Jetty, even perform some initial network traffic also outside Jetty (for example for authentication purposes), and then pass the `SocketChannel` instance to `SelectorManager` for use within Jetty. @@ -50,7 +50,7 @@ include::{doc_code}/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[ ``SocketChannel``s that are passed to `SelectorManager` are wrapped into two related components: an link:{javadoc-url}/org/eclipse/jetty/io/EndPoint.html[`EndPoint`] and a link:{javadoc-url}/org/eclipse/jetty/io/Connection.html[`Connection`]. -`EndPoint` is the Jetty abstraction for a `SocketChannel`: you can read bytes from an `EndPoint` via `EndPoint.fill(ByteBuffer)`, you can write bytes to an `EndPoint` via `EndPoint.flush(ByteBuffer...)` and `EndPoint.write(Callback, ByteBuffer...)`, you can close an `EndPoint` via `EndPoint.close()`, etc. +`EndPoint` is the Jetty abstraction for a `SocketChannel`: you can read bytes from an `EndPoint` via `EndPoint.fill(ByteBuffer)`, you can write bytes to an `EndPoint` via `EndPoint.flush(ByteBuffer\...)` and `EndPoint.write(Callback, ByteBuffer\...)`, you can close an `EndPoint` via `EndPoint.close()`, etc. `Connection` is the Jetty abstraction that is responsible to read bytes from the `EndPoint` and to deserialize the read bytes into objects. For example, an HTTP/1.1 server-side `Connection` implementation is responsible to deserialize HTTP/1.1 request bytes into an HTTP request object. @@ -63,9 +63,9 @@ The writing side for a specific protocol _may_ be implemented in the `Connection While there is primarily just one implementation of `EndPoint`,link:{javadoc-url}/org/eclipse/jetty/io/SocketChannelEndPoint.html[`SocketChannelEndPoint`] (used both on the client-side and on the server-side), there are many implementations of `Connection`, typically two for each protocol (one for the client-side and one for the server-side). The `EndPoint` and `Connection` pairs can be chained, for example in case of encrypted communication using the TLS protocol. -There is an `EndPoint` and `Connection` TLS pair where the `EndPoint` reads the encrypted bytes from the network and the `Connection` decrypts them; next in the chain there is an `EndPoint` and `Connection` pair where the `EndPoint` "reads" decrypted bytes (provided by the previous `Connection`) and the `Connection` deserializes them into specific protocol objects (for example HTTP/2 frame objects). +There is an `EndPoint` and `Connection` TLS pair where the `EndPoint` reads the encrypted bytes from the socket and the `Connection` decrypts them; next in the chain there is an `EndPoint` and `Connection` pair where the `EndPoint` "reads" decrypted bytes (provided by the previous `Connection`) and the `Connection` deserializes them into specific protocol objects (for example HTTP/2 frame objects). -Certain protocols, such as WebSocket, start the communication with the server using one protocol (e.g. HTTP/1.1), but then change the communication to use another protocol (e.g. WebSocket). +Certain protocols, such as WebSocket, start the communication with the server using one protocol (for example, HTTP/1.1), but then change the communication to use another protocol (for example, WebSocket). `EndPoint` supports changing the `Connection` object on-the-fly via `EndPoint.upgrade(Connection)`. This allows to use the HTTP/1.1 `Connection` during the initial communication and later to replace it with a WebSocket `Connection`. @@ -75,9 +75,9 @@ NOTE: TODO: add a section on `UpgradeFrom` and `UpgradeTo`? Creating `Connection` instances is performed on the server-side by link:{javadoc-url}/org/eclipse/jetty/server/ConnectionFactory.html[`ConnectionFactory`]s and on the client-side by link:{javadoc-url}/org/eclipse/jetty/io/ClientConnectionFactory.html[`ClientConnectionFactory`]s -On the server-side, the component that aggregates a `SelectorManager` with a set of ``ConnectionFactory``s is link:{javadoc-url}/org/eclipse/jetty/server/ServerConnector.html[`ServerConnector`]s, see xref:pg-server-io-arch[]. +On the server-side, the component that aggregates a `SelectorManager` with a set of ``ConnectionFactory``s is link:{javadoc-url}/org/eclipse/jetty/server/ServerConnector.html[`ServerConnector`]s for TCP/IP sockets, and link:{JDURL}/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.html[`UnixDomainServerConnector`] for Unix-Domain sockets (see the xref:pg-server-io-arch[server-side architecture section] for more information). -On the client-side, the components that aggregates a `SelectorManager` with a set of ``ClientConnectionFactory``s are link:{javadoc-url}/org/eclipse/jetty/client/HttpClientTransport.html[`HttpClientTransport`] subclasses, see xref:pg-client-io-arch[]. +On the client-side, the components that aggregates a `SelectorManager` with a set of ``ClientConnectionFactory``s are link:{javadoc-url}/org/eclipse/jetty/client/HttpClientTransport.html[`HttpClientTransport`] subclasses (see the xref:pg-client-io-arch[client-side architecture section] for more information). [[pg-arch-io-endpoint]] ==== Jetty I/O: `EndPoint` @@ -86,7 +86,7 @@ The Jetty I/O library use Java NIO to handle I/O, so that I/O is non-blocking. At the Java NIO level, in order to be notified when a `SocketChannel` has data to be read, the `SelectionKey.OP_READ` flag must be set. -In the Jetty I/O library, you can call `EndPoint.fillInterested(Callback)` to declare interest in the "read" (or "fill") event, and the `Callback` parameter is the object that is notified when such an event occurs. +In the Jetty I/O library, you can call `EndPoint.fillInterested(Callback)` to declare interest in the "read" (also called "fill") event, and the `Callback` parameter is the object that is notified when such an event occurs. At the Java NIO level, a `SocketChannel` is always writable, unless it becomes TCP congested. In order to be notified when a `SocketChannel` uncongests and it is therefore writable again, the `SelectionKey.OP_WRITE` flag must be set. 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 a736a9ddfd7f..767c94a82d3b 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 @@ -16,20 +16,31 @@ A `Connector` is the component that handles incoming requests from clients, and works in conjunction with `ConnectionFactory` instances. -The primary implementation is `org.eclipse.jetty.server.ServerConnector`. -`ServerConnector` uses a `java.nio.channels.ServerSocketChannel` to listen to a TCP port and to accept TCP connections. +The available implementations are: -Since `ServerConnector` wraps a `ServerSocketChannel`, it can be configured in a similar way, for example the port to listen to, the network address to bind to, etc.: +* `org.eclipse.jetty.server.ServerConnector`, for TCP/IP sockets. +* `org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector` for Unix-Domain sockets. + +Both use a `java.nio.channels.ServerSocketChannel` to listen to a socket address and to accept socket connections. + +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.: [source,java,indent=0] ---- include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnector] ---- -The _acceptors_ are threads (typically only one) that compete to accept TCP connections on the listening port. +Likewise, `UnixDomainServerConnector` also wraps a `ServerSocketChannel` and can be configured with the Unix-Domain path to listen to: + +[source,java,indent=0] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorUnix] +---- + +The _acceptors_ are threads (typically only one) that compete to accept socket connections. When a connection is accepted, `ServerConnector` wraps the accepted `SocketChannel` and passes it to the xref:pg-arch-io-selector-manager[`SelectorManager`]. 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 `ServerConnector.acceptQueueSize` parameter. +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. @@ -42,7 +53,7 @@ In this case a single selector may be able to manage many sockets because chance On the contrary, web messaging applications tend to send many small messages at a very high frequency so that sockets are rarely idle. In this case a single selector may be able to manage less sockets because chances are that many of them will be active at the same time. -It is possible to configure more than one `ServerConnector`, each listening on a different port: +It is possible to configure more than one `ServerConnector` (each listening on a different port), or more than one `UnixDomainServerConnector` (each listening on a different path), or ``ServerConnector``s and ``UnixDomainServerConnector``s, for example: [source,java,indent=0] ---- @@ -52,9 +63,9 @@ include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPSer [[pg-server-http-connector-protocol]] ===== Configuring Protocols -For each accepted TCP connection, `ServerConnector` asks a `ConnectionFactory` to create a `Connection` object that handles the network traffic on that TCP connection, parsing and generating bytes for a specific protocol (see xref:pg-arch-io[this section] for more details about `Connection` objects). +For each accepted socket connection, the server `Connector` asks a `ConnectionFactory` to create a `Connection` object that handles the traffic on that socket connection, parsing and generating bytes for a specific protocol (see xref:pg-arch-io[this section] for more details about `Connection` objects). -A `ServerConnector` can be configured with one or more ``ConnectionFactory``s. +A server `Connector` can be configured with one or more ``ConnectionFactory``s. If no `ConnectionFactory` is specified then `HttpConnectionFactory` is implicitly configured. [[pg-server-http-connector-protocol-http11]] @@ -87,7 +98,7 @@ By using those ports, a client had _prior knowledge_ that the server would speak HTTP/2 was designed to be a smooth transition from HTTP/1.1 for users and as such the HTTP ports were not changed. However the HTTP/2 protocol is, on the wire, a binary protocol, completely different from HTTP/1.1. -Therefore, with HTTP/2, clients that connect to port `80` may speak either HTTP/1.1 or HTTP/2, and the server must figure out which version of the HTTP protocol the client is speaking. +Therefore, with HTTP/2, clients that connect to port `80` (or to a specific Unix-Domain path) may speak either HTTP/1.1 or HTTP/2, and the server must figure out which version of the HTTP protocol the client is speaking. 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: @@ -128,7 +139,7 @@ Note also that the default protocol set in the ALPN ``ConnectionFactory``, which It is often the case that Jetty receives connections from a load balancer configured to distribute the load among many Jetty backend servers. -From the Jetty point of view, all the connections arrive from the load balancer, rather than the real clients, but is possible to configure the load balancer to forward the real client IP address and port to the backend Jetty server using the link:https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt[PROXY protocol]. +From the Jetty point of view, all the connections arrive from the load balancer, rather than the real clients, but is possible to configure the load balancer to forward the real client IP address and IP port to the backend Jetty server using the link:https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt[PROXY protocol]. NOTE: The PROXY protocol is widely supported by load balancers such as link:http://cbonte.github.io/haproxy-dconv/2.2/configuration.html#5.2-send-proxy[HAProxy] (via its `send-proxy` directive), link:https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol[Nginx](via its `proxy_protocol on` directive) and others. @@ -144,3 +155,13 @@ Note also how the PROXY `ConnectionFactory` needs to know its _next_ protocol (i Each `ConnectionFactory` is asked to create a `Connection` object for each accepted TCP connection; the `Connection` objects will be chained together to handle the bytes, each for its own protocol. Therefore the `ProxyConnection` will handle the PROXY protocol bytes and `HttpConnection` will handle the HTTP/1.1 bytes producing a request object and response object that will be processed by ``Handler``s. + +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] +---- +include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=proxyHTTPUnix] +---- + +Note that the only difference when using Unix-Domain sockets is instantiating `UnixDomainServerConnector` instead of `ServerConnector` and configuring the Unix-Domain path instead of the IP port. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server-io-arch.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server-io-arch.adoc index 3e98026afcc3..ab6c9b8bd010 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server-io-arch.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/server-io-arch.adoc @@ -12,23 +12,30 @@ // [[pg-server-io-arch]] -=== Server Libraries I/O Architecture +=== Server I/O Architecture The Jetty server libraries provide the basic components and APIs to implement a network server. They build on the common xref:pg-arch-io[Jetty I/O Architecture] and provide server specific concepts. -The central I/O server-side component is `org.eclipse.jetty.server.ServerConnector`. +The Jetty server libraries provide I/O support for TCP/IP sockets (for both IPv4 and IPv6) and, when using Java 16 or later, for Unix-Domain sockets. + +Support for Unix-Domain sockets is interesting when Jetty is deployed behind a proxy or a load-balancer: it is possible to configure the proxy or load balancer to communicate with Jetty via Unix-Domain sockets, rather than via the loopback network interface. + +The central I/O server-side component are `org.eclipse.jetty.server.ServerConnector`, that handles the TCP/IP socket traffic, and `org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector`, that handles the Unix-Domain socket traffic. + +`ServerConnector` and `UnixDomainServerConnector` are very similar, and while in the following sections `ServerConnector` is used, the same concepts apply to `UnixDomainServerConnector`, unless otherwise noted. + A `ServerConnector` manages a list of ``ConnectionFactory``s, that indicate what protocols the connector is able to speak. [[pg-server-io-arch-connection-factory]] ==== Creating Connections with `ConnectionFactory` -Recall from the xref:pg-arch-io-connection[`Connection` section] of the Jetty I/O architecture that `Connection` instances are responsible for parsing bytes read from a TCP connection and generating bytes to write to that TCP connection. +Recall from the xref:pg-arch-io-connection[`Connection` section] of the Jetty I/O architecture that `Connection` instances are responsible for parsing bytes read from a socket and generating bytes to write to that socket. On the server-side, a `ConnectionFactory` creates `Connection` instances that know how to parse and generate bytes for the specific protocol they support -- it can be either HTTP/1.1, or TLS, or FastCGI, or the link:https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt[PROXY protocol]. -For example, this is how clear-text HTTP/1.1 is configured: +For example, this is how clear-text HTTP/1.1 is configured for TCP/IP sockets: [source,java,indent=0] ---- @@ -36,7 +43,24 @@ include::../{doc_code}/org/eclipse/jetty/docs/programming/server/ServerDocs.java ---- With this configuration, the `ServerConnector` will listen on port `8080`. -When a new TCP connection is established, `ServerConnector` delegates to the `ConnectionFactory` the creation of the `Connection` instance for that TCP connection, that is linked to the corresponding `EndPoint`: + +Similarly, this is how clear-text HTTP/1.1 is configured for Unix-Domain sockets: + +[source,java,indent=0] +---- +include::../{doc_code}/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=httpUnix] +---- + +With this configuration, the `UnixDomainServerConnector` will listen on file `/tmp/jetty.sock`. + +[NOTE] +==== +`ServerConnector` and `UnixDomainServerConnector` only differ by how they are configured -- for `ServerConnector` you specify the IP port it listens to, for `UnixDomainServerConnector` you specify the Unix-Domain path it listens to. + +Both configure ``ConnectionFactory``s in exactly the same way. +==== + +When a new socket connection is established, `ServerConnector` delegates to the `ConnectionFactory` the creation of the `Connection` instance for that socket connection, that is linked to the corresponding `EndPoint`: [plantuml] ---- @@ -53,12 +77,12 @@ scale 1.5 circle network circle application -network - SocketEndPoint -SocketEndPoint - HttpConnection +network - SocketChannelEndPoint +SocketChannelEndPoint - HttpConnection HttpConnection - application ---- -For every TCP connection there will be an `EndPoint` + `Connection` pair. +For every socket connection there will be an `EndPoint` + `Connection` pair. [[pg-server-io-arch-connection-factory-wrapping]] ==== Wrapping a `ConnectionFactory` @@ -72,7 +96,7 @@ include::../{doc_code}/org/eclipse/jetty/docs/programming/server/ServerDocs.java ---- With this configuration, the `ServerConnector` will listen on port `8443`. -When a new TCP connection is established, the first `ConnectionFactory` configured in `ServerConnector` is invoked to create a `Connection`. +When a new socket connection is established, the first `ConnectionFactory` configured in `ServerConnector` is invoked to create a `Connection`. In the example above, `SslConnectionFactory` creates a `SslConnection` and then asks to its wrapped `ConnectionFactory` (in the example, `HttpConnectionFactory`) to create the wrapped `Connection` (an `HttpConnection`) and will then link the two ``Connection``s together, in this way: [plantuml] @@ -90,16 +114,16 @@ scale 1.5 circle network circle application -network - SocketEndPoint -SocketEndPoint - SslConnection +network - SocketChannelEndPoint +SocketChannelEndPoint - SslConnection SslConnection -- DecryptedEndPoint DecryptedEndPoint - HttpConnection HttpConnection - application ---- -Bytes read by the `SocketEndPoint` will be interpreted as TLS bytes by the `SslConnection`, then decrypted and made available to the `DecryptedEndPoint` (a component part of `SslConnection`), which will then provide them to `HttpConnection`. +Bytes read by the `SocketChannelEndPoint` will be interpreted as TLS bytes by the `SslConnection`, then decrypted and made available to the `DecryptedEndPoint` (a component part of `SslConnection`), which will then provide them to `HttpConnection`. -The application writes bytes through the `HttpConnection` to the `DecryptedEndPoint`, which will encrypt them through the `SslConnection` and write the encrypted bytes to the `SocketEndPoint`. +The application writes bytes through the `HttpConnection` to the `DecryptedEndPoint`, which will encrypt them through the `SslConnection` and write the encrypted bytes to the `SocketChannelEndPoint`. [[pg-server-io-arch-connection-factory-detecting]] ==== Choosing `ConnectionFactory` via Bytes Detection @@ -124,18 +148,18 @@ With this configuration, the detector will delegate to `SslConnectionFactory` to <2> Creates the `ServerConnector` with `DetectorConnectionFactory` as the first `ConnectionFactory`, and `HttpConnectionFactory` as the next `ConnectionFactory` to invoke if the detection fails. In the example above `ServerConnector` will listen on port 8181. -When a new TCP connection is established, `DetectorConnectionFactory` is invoked to create a `Connection`, because it is the first `ConnectionFactory` specified in the `ServerConnector` list. +When a new socket connection is established, `DetectorConnectionFactory` is invoked to create a `Connection`, because it is the first `ConnectionFactory` specified in the `ServerConnector` list. `DetectorConnectionFactory` reads the initial bytes and asks to its detecting ``ConnectionFactory``s if they recognize the bytes. In the example above, the detecting ``ConnectionFactory`` is `SslConnectionFactory` which will therefore detect whether the initial bytes are TLS bytes. If one of the detecting ``ConnectionFactory``s recognizes the bytes, it creates a `Connection`; otherwise `DetectorConnectionFactory` will try the next `ConnectionFactory` after itself in the `ServerConnector` list. In the example above, the next `ConnectionFactory` after `DetectorConnectionFactory` is `HttpConnectionFactory`. -The final result is that when new TCP connection is established, the initial bytes are examined: if they are TLS bytes, a `SslConnectionFactory` will create a `SslConnection` that wraps an `HttpConnection` as explained xref:pg-server-io-arch-connection-factory-wrapping[here], therefore supporting `https`; otherwise they are not TLS bytes and an `HttpConnection` is created, therefore supporting `http`. +The final result is that when new socket connection is established, the initial bytes are examined: if they are TLS bytes, a `SslConnectionFactory` will create a `SslConnection` that wraps an `HttpConnection` as explained xref:pg-server-io-arch-connection-factory-wrapping[here], therefore supporting `https`; otherwise they are not TLS bytes and an `HttpConnection` is created, therefore supporting `http`. [[pg-server-io-arch-connection-factory-custom]] ==== Writing a Custom `ConnectionFactory` -This section explains how to use the Jetty server-side libraries to write a generic network server able to parse and generate any protocol based on TCP. +This section explains how to use the Jetty server-side libraries to write a generic network server able to parse and generate any protocol.. Let's suppose that we want to write a custom protocol that is based on JSON but has the same semantic as HTTP; let's call this custom protocol `JSONHTTP`, so that a request would look like this: diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java index 5c063daf3428..6829c6d19719 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.docs.programming.server; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.Map; import java.util.concurrent.Executor; @@ -31,6 +32,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; @@ -57,6 +59,23 @@ public void http() throws Exception // end::http[] } + public void httpUnix() throws Exception + { + // tag::httpUnix[] + // Create the HTTP/1.1 ConnectionFactory. + HttpConnectionFactory http = new HttpConnectionFactory(); + + Server server = new Server(); + + // Create the connector with the ConnectionFactory. + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, http); + connector.setUnixDomainPath(Path.of("/tmp/jetty.sock")); + + server.addConnector(connector); + server.start(); + // end::httpUnix[] + } + public void tlsHttp() throws Exception { // tag::tlsHttp[] 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 103d24402cfd..7a33df93ef4b 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 @@ -14,6 +14,7 @@ package org.eclipse.jetty.docs.programming.server.http; import java.io.IOException; +import java.nio.file.Path; import java.util.EnumSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -62,6 +63,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; @@ -165,12 +167,13 @@ public void configureConnector() throws Exception // Create a ServerConnector instance. ServerConnector connector = new ServerConnector(server, 1, 1, new HttpConnectionFactory()); - // Configure TCP parameters. + // Configure IP parameters. - // The TCP port to listen to. + // The IP port to listen to. connector.setPort(8080); - // The TCP address to bind to. + // The IP address to bind to. connector.setHost("127.0.0.1"); + // The TCP accept queue size. connector.setAcceptQueueSize(128); @@ -179,6 +182,33 @@ public void configureConnector() throws Exception // end::configureConnector[] } + public void configureConnectorUnix() throws Exception + { + // tag::configureConnectorUnix[] + Server server = new Server(); + + // The number of acceptor threads. + int acceptors = 1; + + // The number of selectors. + int selectors = 1; + + // Create a ServerConnector instance. + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, 1, 1, new HttpConnectionFactory()); + + // Configure Unix-Domain parameters. + + // The Unix-Domain path to listen to. + connector.setUnixDomainPath(Path.of("/tmp/jetty.sock")); + + // The TCP accept queue size. + connector.setAcceptQueueSize(128); + + server.addConnector(connector); + server.start(); + // end::configureConnectorUnix[] + } + public void configureConnectors() throws Exception { // tag::configureConnectors[] @@ -248,6 +278,31 @@ public void proxyHTTP() throws Exception // end::proxyHTTP[] } + public void proxyHTTPUnix() throws Exception + { + // tag::proxyHTTPUnix[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + // Configure the HTTP support, for example: + httpConfig.setSendServerVersion(false); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // The ConnectionFactory for the PROXY protocol. + ProxyConnectionFactory proxy = new ProxyConnectionFactory(http11.getProtocol()); + + // Create the ServerConnector. + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, proxy, http11); + connector.setUnixDomainPath(Path.of("/tmp/jetty.sock")); + + server.addConnector(connector); + server.start(); + // end::proxyHTTPUnix[] + } + public void tlsHttp11() throws Exception { // tag::tlsHttp11[] diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpClientTransport.java index 4fd7f4c72e27..2ed04c31a020 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractHttpClientTransport.java @@ -25,7 +25,7 @@ @ManagedObject public abstract class AbstractHttpClientTransport extends ContainerLifeCycle implements HttpClientTransport { - protected static final Logger LOG = LoggerFactory.getLogger(HttpClientTransport.class); + private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransport.class); private HttpClient client; private ConnectionPool.Factory factory; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 84f19e47f9a6..3fb12a61602f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -159,7 +159,7 @@ public HttpClient(HttpClientTransport transport) { this.transport = Objects.requireNonNull(transport); addBean(transport); - this.connector = ((AbstractHttpClientTransport)transport).getBean(ClientConnector.class); + this.connector = ((AbstractHttpClientTransport)transport).getContainedBeans(ClientConnector.class).stream().findFirst().orElseThrow(); addBean(handlers); addBean(decoderFactories); } @@ -549,24 +549,25 @@ public List getDestinations() return new ArrayList<>(destinations.values()); } - protected void send(final HttpRequest request, List listeners) + protected void send(HttpRequest request, List listeners) { HttpDestination destination = (HttpDestination)resolveDestination(request); destination.send(request, listeners); } - protected void newConnection(final HttpDestination destination, final Promise promise) + protected void newConnection(HttpDestination destination, Promise promise) { + // Multiple threads may access the map, especially with DEBUG logging enabled. + Map context = new ConcurrentHashMap<>(); + context.put(ClientConnectionFactory.CLIENT_CONTEXT_KEY, HttpClient.this); + context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination); + Origin.Address address = destination.getConnectAddress(); resolver.resolve(address.getHost(), address.getPort(), new Promise<>() { @Override public void succeeded(List socketAddresses) { - // Multiple threads may access the map, especially with DEBUG logging enabled. - Map context = new ConcurrentHashMap<>(); - context.put(ClientConnectionFactory.CLIENT_CONTEXT_KEY, HttpClient.this); - context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination); connect(socketAddresses, 0, context); } @@ -1225,7 +1226,7 @@ public boolean containsAll(Collection c) @Override public Iterator iterator() { - final Iterator iterator = set.iterator(); + Iterator iterator = set.iterator(); return new Iterator<>() { @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java index c45f3664e65a..f52eff81a7d2 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java @@ -41,6 +41,8 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

A {@link HttpClientTransport} that can dynamically switch among different application protocols.

@@ -79,6 +81,8 @@ */ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport { + private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportDynamic.class); + private final List factoryInfos; private final List protocols; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java index 4064a3a0b1c5..3d5d935cb5a4 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java @@ -26,14 +26,16 @@ import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ManagedObject("The HTTP/1.1 client transport") public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTransport { public static final Origin.Protocol HTTP11 = new Origin.Protocol(List.of("http/1.1"), false); + private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportOverHTTP.class); private final ClientConnectionFactory factory = new HttpClientConnectionFactory(); private int headerCacheSize = 1024; @@ -41,7 +43,7 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran public HttpClientTransportOverHTTP() { - this(Math.max(1, ProcessorUtils.availableProcessors() / 2)); + this(1); } public HttpClientTransportOverHTTP(int selectors) diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java index a33fa967e10d..039f813ed319 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpClientTransportOverFCGI.java @@ -34,10 +34,14 @@ import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ManagedObject("The FastCGI/1.0 client transport") public class HttpClientTransportOverFCGI extends AbstractConnectorHttpClientTransport { + private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportOverFCGI.class); + private final String scriptRoot; public HttpClientTransportOverFCGI(String scriptRoot) diff --git a/jetty-home/pom.xml b/jetty-home/pom.xml index d0daeeb66574..a7ee899e87b0 100644 --- a/jetty-home/pom.xml +++ b/jetty-home/pom.xml @@ -534,6 +534,7 @@ + @@ -648,6 +649,12 @@ jetty-proxy ${project.version} + + org.eclipse.jetty + jetty-unixdomain-server + ${project.version} + true + org.eclipse.jetty jetty-unixsocket-server diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index f2d5ebb4ce8b..09fa46152252 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -14,18 +14,24 @@ package org.eclipse.jetty.io; import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ProtocolFamily; import java.net.SocketAddress; import java.net.SocketException; +import java.net.StandardProtocolFamily; import java.net.StandardSocketOptions; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executor; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.JavaVersion; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -43,6 +49,7 @@ public class ClientConnector extends ContainerLifeCycle public static final String CONNECTION_PROMISE_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".connectionPromise"; private static final Logger LOG = LoggerFactory.getLogger(ClientConnector.class); + private final SocketChannelWithAddress.Factory factory; private Executor executor; private Scheduler scheduler; private ByteBufferPool byteBufferPool; @@ -55,6 +62,16 @@ public class ClientConnector extends ContainerLifeCycle private SocketAddress bindAddress; private boolean reuseAddress = true; + public ClientConnector() + { + this((address, context) -> new SocketChannelWithAddress(SocketChannel.open(), address)); + } + + public ClientConnector(SocketChannelWithAddress.Factory factory) + { + this.factory = Objects.requireNonNull(factory); + } + public Executor getExecutor() { return executor; @@ -221,20 +238,16 @@ public void connect(SocketAddress address, Map context) context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this); context.putIfAbsent(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address); - channel = SocketChannel.open(); + SocketChannelWithAddress channelWithAddress = factory.newSocketChannelWithAddress(address, context); + channel = channelWithAddress.getSocketChannel(); + address = channelWithAddress.getSocketAddress(); SocketAddress bindAddress = getBindAddress(); if (bindAddress != null) - { - boolean reuseAddress = getReuseAddress(); - if (LOG.isDebugEnabled()) - LOG.debug("Binding to {} to connect to {}{}", bindAddress, address, (reuseAddress ? " reusing address" : "")); - channel.setOption(StandardSocketOptions.SO_REUSEADDR, reuseAddress); - channel.bind(bindAddress); - } + bind(channel, bindAddress); configure(channel); boolean connected = true; - boolean blocking = isConnectBlocking(); + boolean blocking = isConnectBlocking() && address instanceof InetSocketAddress; if (LOG.isDebugEnabled()) LOG.debug("Connecting {} to {}", blocking ? "blocking" : "non-blocking", address); if (blocking) @@ -288,9 +301,34 @@ public void accept(SocketChannel channel, Map context) } } + private void bind(SocketChannel channel, SocketAddress bindAddress) + { + try + { + boolean reuseAddress = getReuseAddress(); + if (LOG.isDebugEnabled()) + LOG.debug("Binding to {} reusing address {}", bindAddress, reuseAddress); + channel.setOption(StandardSocketOptions.SO_REUSEADDR, reuseAddress); + channel.bind(bindAddress); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("Could not bind {}", channel); + } + } + protected void configure(SocketChannel channel) throws IOException { - channel.socket().setTcpNoDelay(true); + try + { + channel.setOption(StandardSocketOptions.TCP_NODELAY, true); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("Could not configure {}", channel); + } } protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) @@ -351,4 +389,78 @@ protected void connectionFailed(SelectableChannel channel, Throwable failure, Ob connectFailed(failure, context); } } + + /** + *

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

+ */ + public static class SocketChannelWithAddress + { + private final SocketChannel channel; + private final SocketAddress address; + + public SocketChannelWithAddress(SocketChannel channel, SocketAddress address) + { + this.channel = channel; + this.address = address; + } + + public SocketChannel getSocketChannel() + { + return channel; + } + + public SocketAddress getSocketAddress() + { + return address; + } + + /** + *

A factory for {@link SocketChannelWithAddress} instances.

+ */ + public interface Factory + { + public static Factory forUnixDomain(Path path) + { + return (address, context) -> + { + try + { + ProtocolFamily family = Enum.valueOf(StandardProtocolFamily.class, "UNIX"); + Class channelClass = Class.forName("java.nio.channels.SocketChannel"); + SocketChannel socketChannel = (SocketChannel)channelClass.getMethod("open", ProtocolFamily.class).invoke(null, family); + Class addressClass = Class.forName("java.net.UnixDomainSocketAddress"); + SocketAddress socketAddress = (SocketAddress)addressClass.getMethod("of", Path.class).invoke(null, path); + return new SocketChannelWithAddress(socketChannel, socketAddress); + } + catch (Throwable x) + { + String message = "Unix-Domain SocketChannels are available starting from Java 16, your Java version is: " + JavaVersion.VERSION; + throw new UnsupportedOperationException(message, x); + } + }; + } + + /** + *

Creates a new {@link SocketChannel} to connect to a {@link SocketAddress} + * derived from the input socket address.

+ *

The input socket address represents the destination socket address to + * connect to, as it is typically specified by a URI authority, for example + * {@code localhost:8080} if the URI is {@code http://localhost:8080/path}.

+ *

However, the returned socket address may be different as the implementation + * may use a Unix-Domain socket address to physically connect to the virtual + * destination socket address given as input.

+ *

The return type is a pair/record holding the socket channel and the + * socket address, with the socket channel not yet connected. + * The implementation of this methos must not call + * {@link SocketChannel#connect(SocketAddress)}, as this is done later, + * after configuring the socket, by the {@link ClientConnector} implementation.

+ * + * @param address the destination socket address, typically specified in a URI + * @param context the context to create the new socket channel + * @return a new {@link SocketChannel} with an associated {@link SocketAddress} to connect to + * @throws IOException if the socket channel or the socket address cannot be created + */ + public SocketChannelWithAddress newSocketChannelWithAddress(SocketAddress address, Map context) throws IOException; + } + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java index 1258424ddbb6..2ff61f4ae84c 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java @@ -165,8 +165,10 @@ public SocketAddress getLocalSocketAddress() { return _channel.getLocalAddress(); } - catch (IOException x) + catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("Could not retrieve local socket address", x); return null; } } @@ -178,8 +180,10 @@ public SocketAddress getRemoteSocketAddress() { return _channel.getRemoteAddress(); } - catch (IOException e) + catch (Throwable x) { + if (LOG.isDebugEnabled()) + LOG.debug("Could not retrieve remote socket address", x); return null; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java index b84e2fad1566..1bfe3e65e4a7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java @@ -585,30 +585,43 @@ private void parseBodyAndUpgrade() throws IOException { byte[] addr = new byte[4]; _buffer.get(addr); - InetAddress src = Inet4Address.getByAddress(addr); + InetAddress srcAddr = Inet4Address.getByAddress(addr); _buffer.get(addr); - InetAddress dst = Inet4Address.getByAddress(addr); - int sp = _buffer.getChar(); - int dp = _buffer.getChar(); - local = new InetSocketAddress(dst, dp); - remote = new InetSocketAddress(src, sp); + InetAddress dstAddr = Inet4Address.getByAddress(addr); + int srcPort = _buffer.getChar(); + int dstPort = _buffer.getChar(); + local = new InetSocketAddress(dstAddr, dstPort); + remote = new InetSocketAddress(srcAddr, srcPort); break; } case INET6: { byte[] addr = new byte[16]; _buffer.get(addr); - InetAddress src = Inet6Address.getByAddress(addr); + InetAddress srcAddr = Inet6Address.getByAddress(addr); _buffer.get(addr); - InetAddress dst = Inet6Address.getByAddress(addr); - int sp = _buffer.getChar(); - int dp = _buffer.getChar(); - local = new InetSocketAddress(dst, dp); - remote = new InetSocketAddress(src, sp); + InetAddress dstAddr = Inet6Address.getByAddress(addr); + int srcPort = _buffer.getChar(); + int dstPort = _buffer.getChar(); + local = new InetSocketAddress(dstAddr, dstPort); + remote = new InetSocketAddress(srcAddr, srcPort); + break; + } + case UNIX: + { + byte[] addr = new byte[108]; + _buffer.get(addr); + String src = UnixDomain.toPath(addr); + _buffer.get(addr); + String dst = UnixDomain.toPath(addr); + local = UnixDomain.newSocketAddress(dst); + remote = UnixDomain.newSocketAddress(src); break; } default: - throw new IllegalStateException(); + { + throw new IllegalStateException("Unsupported family " + _family); + } } proxyEndPoint = new ProxyEndPoint(endPoint, local, remote); @@ -706,7 +719,7 @@ private void parseHeader() throws IOException } Transport transport; - switch (0xF & transportAndFamily) + switch (transportAndFamily & 0xF) { case 0: transport = Transport.UNSPEC; @@ -723,7 +736,7 @@ private void parseHeader() throws IOException _length = _buffer.getChar(); - if (!_local && (_family == Family.UNSPEC || _family == Family.UNIX || transport != Transport.STREAM)) + if (!_local && (_family == Family.UNSPEC || transport != Transport.STREAM)) throw new IOException(String.format("Proxy v2 unsupported PROXY mode 0x%x,0x%x", versionAndCommand, transportAndFamily)); if (_length > getMaxProxyHeader()) @@ -957,4 +970,47 @@ public void write(Callback callback, ByteBuffer... buffers) throws WritePendingE _endPoint.write(callback, buffers); } } + + private static class UnixDomain + { + private static final Class unixDomainSocketAddress = probe(); + + private static Class probe() + { + try + { + return ClassLoader.getPlatformClassLoader().loadClass("java.net.UnixDomainSocketAddress"); + } + catch (Throwable ignored) + { + return null; + } + } + + private static SocketAddress newSocketAddress(String path) + { + try + { + if (unixDomainSocketAddress != null) + return (SocketAddress)unixDomainSocketAddress.getMethod("of", String.class).invoke(null, path); + return null; + } + catch (Throwable ignored) + { + return null; + } + } + + private static String toPath(byte[] bytes) + { + // Unix-Domain paths are zero-terminated. + int i = 0; + while (i < bytes.length) + { + if (bytes[i++] == 0) + break; + } + return new String(bytes, 0, i, StandardCharsets.US_ASCII).trim(); + } + } } diff --git a/jetty-unixdomain-server/pom.xml b/jetty-unixdomain-server/pom.xml new file mode 100644 index 000000000000..9a984099acd9 --- /dev/null +++ b/jetty-unixdomain-server/pom.xml @@ -0,0 +1,56 @@ + + + + org.eclipse.jetty + jetty-project + 10.0.7-SNAPSHOT + + + 4.0.0 + jetty-unixdomain-server + Jetty :: Unix-Domain Sockets :: Server + Jetty Unix-Domain Sockets Server + + + ${project.groupId}.unixdomain.server + org.eclipse.jetty.unixdomain.* + + + + + + maven-compiler-plugin + + 16 + 16 + 16 + + + + + + + + org.eclipse.jetty + jetty-server + ${project.version} + + + org.slf4j + slf4j-api + + + + org.eclipse.jetty + jetty-client + ${project.version} + test + + + org.eclipse.jetty + jetty-slf4j-impl + test + + + + diff --git a/jetty-unixdomain-server/src/main/config/etc/jetty-unixdomain.xml b/jetty-unixdomain-server/src/main/config/etc/jetty-unixdomain.xml new file mode 100644 index 000000000000..d59074567bea --- /dev/null +++ b/jetty-unixdomain-server/src/main/config/etc/jetty-unixdomain.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + /jetty.sock + + + + + + + + + + + diff --git a/jetty-unixdomain-server/src/main/config/modules/unixdomain.mod b/jetty-unixdomain-server/src/main/config/modules/unixdomain.mod new file mode 100644 index 000000000000..48c30d12b44d --- /dev/null +++ b/jetty-unixdomain-server/src/main/config/modules/unixdomain.mod @@ -0,0 +1,36 @@ +[description] +Enables support for Java 16 Unix-Domain server sockets. + +[tag] +connector +unixdomain + +[depends] +server + +[lib] +lib/jetty-unixdomain-server-*.jar + +[xml] +etc/jetty-unixdomain.xml + +[ini-template] +# tag::documentation[] +## The number of acceptors (-1 picks a default value based on number of cores). +# jetty.unixdomain.acceptors=1 + +## The number of selectors (-1 picks a default value based on number of cores). +# jetty.unixdomain.selectors=-1 + +## The Unix-Domain path the ServerSocketChannel listens to. +# jetty.unixdomain.path=/tmp/jetty.sock + +## The ServerSocketChannel accept queue backlog (0 picks the platform default). +# jetty.unixdomain.acceptQueueSize=0 + +## The SO_RCVBUF option for accepted SocketChannels (0 picks the platform default). +# jetty.unixdomain.acceptedReceiveBufferSize=0 + +## The SO_SNDBUF option for accepted SocketChannels (0 picks the platform default). +# jetty.unixdomain.acceptedSendBufferSize=0 +# end::documentation[] diff --git a/jetty-unixdomain-server/src/main/java/module-info.java b/jetty-unixdomain-server/src/main/java/module-info.java new file mode 100644 index 000000000000..97065cabeadf --- /dev/null +++ b/jetty-unixdomain-server/src/main/java/module-info.java @@ -0,0 +1,20 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +module org.eclipse.jetty.unixdomain.server +{ + exports org.eclipse.jetty.unixdomain.server; + + requires transitive org.eclipse.jetty.server; + requires org.slf4j; +} diff --git a/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java b/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java new file mode 100644 index 000000000000..9270ca3460e7 --- /dev/null +++ b/jetty-unixdomain-server/src/main/java/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.java @@ -0,0 +1,326 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 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.unixdomain.server; + +import java.io.Closeable; +import java.io.IOException; +import java.net.ProtocolFamily; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.net.StandardSocketOptions; +import java.nio.channels.Channel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.JavaVersion; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.thread.Scheduler; + +/** + *

A {@link Connector} implementation for Unix-Domain server socket channels.

+ *

UnixDomainServerConnector "listens" to a {@link #setUnixDomainPath(Path) Unix-Domain path} + * and behaves {@link ServerConnector} with respect to acceptors, selectors and connection + * factories.

+ *

Important: the unix-domain path must be less than 108 bytes. + * This limit is set by the way Unix-Domain sockets work at the OS level.

+ */ +@ManagedObject +public class UnixDomainServerConnector extends AbstractConnector +{ + private final AtomicReference acceptor = new AtomicReference<>(); + private final SelectorManager selectorManager; + private ServerSocketChannel serverChannel; + private Path unixDomainPath; + private boolean inheritChannel; + private int acceptQueueSize; + private int acceptedReceiveBufferSize; + private int acceptedSendBufferSize; + + public UnixDomainServerConnector(Server server, ConnectionFactory... factories) + { + this(server, null, null, null, -1, -1, factories); + } + + public UnixDomainServerConnector(Server server, int acceptors, int selectors, ConnectionFactory... factories) + { + this(server, null, null, null, acceptors, selectors, factories); + } + + public UnixDomainServerConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool pool, int acceptors, int selectors, ConnectionFactory... factories) + { + super(server, executor, scheduler, pool, acceptors, factories.length > 0 ? factories : new ConnectionFactory[]{new HttpConnectionFactory()}); + selectorManager = newSelectorManager(getExecutor(), getScheduler(), selectors); + addBean(selectorManager, true); + } + + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new UnixDomainSelectorManager(executor, scheduler, selectors); + } + + @ManagedAttribute("The Unix-Domain path this connector listens to") + public Path getUnixDomainPath() + { + return unixDomainPath; + } + + public void setUnixDomainPath(Path unixDomainPath) + { + this.unixDomainPath = unixDomainPath; + } + + @ManagedAttribute("Whether this connector uses a server channel inherited from the JVM") + public boolean isInheritChannel() + { + return inheritChannel; + } + + public void setInheritChannel(boolean inheritChannel) + { + this.inheritChannel = inheritChannel; + } + + @ManagedAttribute("The accept queue size (backlog) for the server socket") + public int getAcceptQueueSize() + { + return acceptQueueSize; + } + + public void setAcceptQueueSize(int acceptQueueSize) + { + this.acceptQueueSize = acceptQueueSize; + } + + @ManagedAttribute("The SO_RCVBUF option for accepted sockets") + public int getAcceptedReceiveBufferSize() + { + return acceptedReceiveBufferSize; + } + + public void setAcceptedReceiveBufferSize(int acceptedReceiveBufferSize) + { + this.acceptedReceiveBufferSize = acceptedReceiveBufferSize; + } + + @ManagedAttribute("The SO_SNDBUF option for accepted sockets") + public int getAcceptedSendBufferSize() + { + return acceptedSendBufferSize; + } + + public void setAcceptedSendBufferSize(int acceptedSendBufferSize) + { + this.acceptedSendBufferSize = acceptedSendBufferSize; + } + + @Override + protected void doStart() throws Exception + { + getBeans(SelectorManager.SelectorManagerListener.class).forEach(selectorManager::addEventListener); + serverChannel = open(); + addBean(serverChannel); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + removeBean(serverChannel); + close(); + getBeans(SelectorManager.SelectorManagerListener.class).forEach(selectorManager::removeEventListener); + } + + @Override + protected void accept(int acceptorID) throws IOException + { + ServerSocketChannel serverChannel = this.serverChannel; + if (serverChannel != null) + { + SocketChannel channel = serverChannel.accept(); + accepted(channel); + } + } + + private void accepted(SocketChannel channel) throws IOException + { + channel.configureBlocking(false); + configure(channel); + selectorManager.accept(channel); + } + + protected void configure(SocketChannel channel) throws IOException + { + // Unix-Domain does not support TCP_NODELAY. + // Unix-Domain does not support SO_REUSEADDR. + int rcvBufSize = getAcceptedReceiveBufferSize(); + if (rcvBufSize > 0) + channel.setOption(StandardSocketOptions.SO_RCVBUF, rcvBufSize); + int sndBufSize = getAcceptedSendBufferSize(); + if (sndBufSize > 0) + channel.setOption(StandardSocketOptions.SO_SNDBUF, sndBufSize); + } + + @Override + public Object getTransport() + { + return serverChannel; + } + + private ServerSocketChannel open() throws IOException + { + ServerSocketChannel serverChannel = openServerSocketChannel(); + if (getAcceptors() == 0) + { + serverChannel.configureBlocking(false); + acceptor.set(selectorManager.acceptor(serverChannel)); + } + return serverChannel; + } + + private void close() throws IOException + { + ServerSocketChannel serverChannel = this.serverChannel; + this.serverChannel = null; + IO.close(serverChannel); + Files.deleteIfExists(getUnixDomainPath()); + } + + private ServerSocketChannel openServerSocketChannel() throws IOException + { + ServerSocketChannel serverChannel = null; + if (isInheritChannel()) + { + Channel channel = System.inheritedChannel(); + if (channel instanceof ServerSocketChannel) + serverChannel = (ServerSocketChannel)channel; + else + LOG.warn("Unable to use System.inheritedChannel() {}. Trying a new Unix-Domain ServerSocketChannel at {}", channel, getUnixDomainPath()); + } + if (serverChannel == null) + serverChannel = bindServerSocketChannel(); + return serverChannel; + } + + private ServerSocketChannel bindServerSocketChannel() + { + try + { + ProtocolFamily family = Enum.valueOf(StandardProtocolFamily.class, "UNIX"); + Class channelClass = Class.forName("java.nio.channels.ServerSocketChannel"); + ServerSocketChannel serverChannel = (ServerSocketChannel)channelClass.getMethod("open", ProtocolFamily.class).invoke(null, family); + // Unix-Domain does not support SO_REUSEADDR. + Class addressClass = Class.forName("java.net.UnixDomainSocketAddress"); + SocketAddress socketAddress = (SocketAddress)addressClass.getMethod("of", Path.class).invoke(null, getUnixDomainPath()); + serverChannel.bind(socketAddress, getAcceptQueueSize()); + return serverChannel; + } + catch (Throwable x) + { + String message = "Unix-Domain SocketChannels are available starting from Java 16, your Java version is: " + JavaVersion.VERSION; + throw new UnsupportedOperationException(message, x); + } + } + + @Override + public void setAccepting(boolean accepting) + { + super.setAccepting(accepting); + if (getAcceptors() == 0) + return; + if (accepting) + { + if (acceptor.get() == null) + { + Closeable acceptor = selectorManager.acceptor(serverChannel); + if (!this.acceptor.compareAndSet(null, acceptor)) + IO.close(acceptor); + } + } + else + { + Closeable acceptor = this.acceptor.get(); + if (acceptor != null && this.acceptor.compareAndSet(acceptor, null)) + IO.close(acceptor); + } + } + + @Override + public String toString() + { + return String.format("%s@%h[%s]", getClass().getSimpleName(), hashCode(), getUnixDomainPath()); + } + + protected class UnixDomainSelectorManager extends SelectorManager + { + public UnixDomainSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + super(executor, scheduler, selectors); + } + + @Override + protected void accepted(SelectableChannel channel) throws IOException + { + UnixDomainServerConnector.this.accepted((SocketChannel)channel); + } + + @Override + protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) + { + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, selectionKey, getScheduler()); + endPoint.setIdleTimeout(getIdleTimeout()); + return endPoint; + } + + @Override + public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) + { + return getDefaultConnectionFactory().newConnection(UnixDomainServerConnector.this, endpoint); + } + + @Override + protected void endPointOpened(EndPoint endpoint) + { + super.endPointOpened(endpoint); + onEndPointOpened(endpoint); + } + + @Override + protected void endPointClosed(EndPoint endpoint) + { + onEndPointClosed(endpoint); + super.endPointClosed(endpoint); + } + } +} diff --git a/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java b/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java new file mode 100644 index 000000000000..8183677cf3bc --- /dev/null +++ b/jetty-unixdomain-server/src/test/java/org/eclipse/jetty/unixdomain/server/UnixDomainTest.java @@ -0,0 +1,249 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 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.unixdomain.server; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.dynamic.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.server.ConnectionFactory; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.ProxyConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.component.LifeCycle; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V1; +import static org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V2; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +public class UnixDomainTest +{ + private ConnectionFactory[] factories = new ConnectionFactory[]{new HttpConnectionFactory()}; + private Server server; + private Path unixDomainPath; + + private void start(Handler handler) throws Exception + { + server = new Server(); + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, factories); + unixDomainPath = Files.createTempFile(Path.of("/tmp"), "unixdomain_", ".sock"); + Files.delete(unixDomainPath); + connector.setUnixDomainPath(unixDomainPath); + server.addConnector(connector); + server.setHandler(handler); + server.start(); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(server); + } + + @Test + public void testHTTPOverUnixDomain() throws Exception + { + String uri = "http://localhost:1234/path"; + start(new AbstractHandler() + { + @Override + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) + { + jettyRequest.setHandled(true); + + // Verify the URI is preserved. + assertEquals(uri, request.getRequestURL().toString()); + + EndPoint endPoint = jettyRequest.getHttpChannel().getEndPoint(); + + // Verify the SocketAddresses. + SocketAddress local = endPoint.getLocalSocketAddress(); + assertThat(local, Matchers.instanceOf(UnixDomainSocketAddress.class)); + SocketAddress remote = endPoint.getRemoteSocketAddress(); + assertThat(remote, Matchers.instanceOf(UnixDomainSocketAddress.class)); + + // Verify that other address methods don't throw. + local = assertDoesNotThrow(endPoint::getLocalAddress); + assertNull(local); + remote = assertDoesNotThrow(endPoint::getRemoteAddress); + assertNull(remote); + + assertDoesNotThrow(endPoint::toString); + } + }); + + // Java 16 way of implementing the SocketChannelWithAddress.Factory. + // See tests below for a Java 11+ version. + ClientConnector clientConnector = new ClientConnector((address, context) -> + { + SocketChannel socketChannel = SocketChannel.open(StandardProtocolFamily.UNIX); + UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(unixDomainPath); + return new ClientConnector.SocketChannelWithAddress(socketChannel, socketAddress); + }); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + httpClient.start(); + try + { + ContentResponse response = httpClient.newRequest(uri) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testHTTPOverUnixDomainWithHTTPProxy() throws Exception + { + int fakeProxyPort = 4567; + int fakeServerPort = 5678; + start(new AbstractHandler() + { + @Override + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) + { + jettyRequest.setHandled(true); + // Proxied requests must have an absolute URI. + HttpURI uri = jettyRequest.getMetaData().getURI(); + assertNotNull(uri.getScheme()); + assertEquals(fakeServerPort, uri.getPort()); + } + }); + + ClientConnector clientConnector = new ClientConnector((address, context) -> + { + if (address instanceof InetSocketAddress inet && inet.getPort() == fakeProxyPort) + { + SocketChannel socketChannel = SocketChannel.open(StandardProtocolFamily.UNIX); + UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(unixDomainPath); + return new ClientConnector.SocketChannelWithAddress(socketChannel, socketAddress); + } + throw new IOException("request was not proxied"); + }); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + httpClient.getProxyConfiguration().getProxies().add(new HttpProxy("localhost", fakeProxyPort)); + httpClient.start(); + try + { + ContentResponse response = httpClient.newRequest("localhost", fakeServerPort) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + finally + { + httpClient.stop(); + } + } + + @Test + public void testHTTPOverUnixDomainWithProxyProtocol() throws Exception + { + String srcAddr = "/proxySrcAddr"; + String dstAddr = "/proxyDstAddr"; + factories = new ConnectionFactory[]{new ProxyConnectionFactory(), new HttpConnectionFactory()}; + start(new AbstractHandler() + { + @Override + public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) + { + jettyRequest.setHandled(true); + EndPoint endPoint = jettyRequest.getHttpChannel().getEndPoint(); + assertThat(endPoint, Matchers.instanceOf(ProxyConnectionFactory.ProxyEndPoint.class)); + assertThat(endPoint.getLocalSocketAddress(), Matchers.instanceOf(UnixDomainSocketAddress.class)); + assertThat(endPoint.getRemoteSocketAddress(), Matchers.instanceOf(UnixDomainSocketAddress.class)); + if ("/v1".equals(target)) + { + // As PROXYv1 does not support UNIX, the wrapped EndPoint data is used. + assertThat(((UnixDomainSocketAddress)endPoint.getLocalSocketAddress()).getPath(), Matchers.equalTo(unixDomainPath)); + } + else if ("/v2".equals(target)) + { + assertThat(((UnixDomainSocketAddress)endPoint.getLocalSocketAddress()).getPath().toString(), Matchers.equalTo(dstAddr)); + assertThat(((UnixDomainSocketAddress)endPoint.getRemoteSocketAddress()).getPath().toString(), Matchers.equalTo(srcAddr)); + } + else + { + fail("Invalid PROXY protocol version " + target); + } + } + }); + + // Java 11+ portable way to implement SocketChannelWithAddress.Factory. + ClientConnector clientConnector = new ClientConnector(ClientConnector.SocketChannelWithAddress.Factory.forUnixDomain(unixDomainPath)); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + 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) + .path("/v1") + .tag(new V1.Tag()) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response1.getStatus()); + + // 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) + .path("/v2") + .tag(tag) + .timeout(5, TimeUnit.SECONDS) + .send(); + + assertEquals(HttpStatus.OK_200, response2.getStatus()); + } + finally + { + httpClient.stop(); + } + } +} diff --git a/jetty-unixdomain-server/src/test/resources/jetty-logging.properties b/jetty-unixdomain-server/src/test/resources/jetty-logging.properties new file mode 100644 index 000000000000..bc2cf0effd0d --- /dev/null +++ b/jetty-unixdomain-server/src/test/resources/jetty-logging.properties @@ -0,0 +1,2 @@ +#org.eclipse.jetty.LEVEL=DEBUG +#org.eclipse.jetty.unixdomain.LEVEL=DEBUG diff --git a/pom.xml b/pom.xml index 27102d67fcfe..f7d41aa237ce 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,7 @@ jetty-bom documentation jetty-keystore + jetty-unixdomain-server diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 7de00f6112c2..b6865310f62b 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http2.client.HTTP2Client; @@ -48,6 +49,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.params.ParameterizedTest; @@ -906,4 +908,37 @@ public void testDefaultLoggingProviderNotActiveWhenExplicitProviderIsPresent() t assertFalse(Files.exists(jettyBase.resolve("resources/jetty-logging.properties"))); } } + + @Test + @EnabledForJreRange(min = JRE.JAVA_16) + public void testUnixDomain() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + try (JettyHomeTester.Run run1 = distribution.start("--add-modules=unixdomain")) + { + assertTrue(run1.awaitFor(10, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); + + int maxUnixDomainPathLength = 108; + Path path = Files.createTempFile("unix", ".sock"); + if (path.normalize().toAbsolutePath().toString().length() > maxUnixDomainPathLength) + path = Files.createTempFile(Path.of("/tmp"), "unix", ".sock"); + assertTrue(Files.deleteIfExists(path)); + try (JettyHomeTester.Run run2 = distribution.start("jetty.unixdomain.path=" + path)) + { + assertTrue(run2.awaitConsoleLogsFor("Started Server@", 10, TimeUnit.SECONDS)); + + ClientConnector connector = new ClientConnector(ClientConnector.SocketChannelWithAddress.Factory.forUnixDomain(path)); + client = new HttpClient(new HttpClientTransportDynamic(connector)); + client.start(); + ContentResponse response = client.GET("http://localhost/path"); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + } }