diff --git a/org.restlet.java/org.restlet.ext.jetty/pom.xml b/org.restlet.java/org.restlet.ext.jetty/pom.xml index cd996044f4..fdfa6280d3 100644 --- a/org.restlet.java/org.restlet.ext.jetty/pom.xml +++ b/org.restlet.java/org.restlet.ext.jetty/pom.xml @@ -17,13 +17,38 @@ org.eclipse.jetty - jetty-client + jetty-server + ${lib-jetty-version} + + + org.eclipse.jetty.http2 + jetty-http2-server ${lib-jetty-version} org.eclipse.jetty - jetty-server - ${lib-jetty-version} + jetty-alpn-server + ${lib-jetty-version} + + + org.eclipse.jetty + jetty-alpn-java-server + ${lib-jetty-version} + + + org.eclipse.jetty.http3 + jetty-http3-server + ${lib-jetty-version} + + + org.eclipse.jetty.quic + jetty-quic-server + ${lib-jetty-version} + + + org.eclipse.jetty + jetty-client + ${lib-jetty-version} org.eclipse.jetty.http2 diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java index 9f15661ba8..a808f9cf11 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpClientHelper.java @@ -46,13 +46,15 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.logging.Level; /** - * HTTP client connector using the Jetty project. Here is the list of parameters - * that are supported. They should be set in the Client's context before it is - * started: + * HTTP client connector using the Jetty project. Here is the list of parameters that are supported. They should be set + * in the Client's context before it is started: * * * @@ -71,36 +73,31 @@ * * * - * + * * * * * * - * + * * * * * * - * + * * * * * * - * + * * * * * * - * + * * * * @@ -118,22 +115,22 @@ * * * - * + * * * * * - * - * + * + * * * * * * - * + * * * * @@ -193,26 +190,29 @@ * * * - * + * * * * * * - * + * + * + * + * + * + * + * * *
list of supported parameters
bindAddressStringnullThe address to bind socket channels to. You must set both this and - * bindPortThe address to bind socket channels to. You must set both this and bindPort
bindPortintnullThe address to bind socket channels to. You must set both this and - * bindAddressThe address to bind socket channels to. You must set both this and bindAddress
cookieSupportedbooleanfalseWhether to support HTTP cookie, storing and automatically sending them - * backWhether to support HTTP cookie, storing and automatically sending them back
connectBlockingbooleanfalseIndicates whether the connect operation is blocking. See - * {@link HttpClient#isConnectBlocking()}.Indicates whether the connect operation is blocking. See {@link HttpClient#isConnectBlocking()}.
connectTimeoutlong15000The max time in milliseconds a connection can take to connect to - * destinationsThe max time in milliseconds a connection can take to connect to destinations
destinationIdleTimeouthttpComplianceModeStringRFC7230Indicate the HTTP compliance mode among the following options: "RFC7230", - * "RFC2616", "LEGACY", "RFC7230_LEGACY". See {@link HttpCompliance}.Indicate the HTTP compliance mode among the following options: "RFC7230", "RFC2616", "LEGACY", "RFC7230_LEGACY". + * See {@link HttpCompliance}.
httpClientTransportModeStringHTTP11Indicate the HTTP client transport mode among the following options: - * "HTTP11", "HTTP2", "HTTP3", "DYNAMIC. See {@link HttpClientTransport}.HTTP1_1Indicate the HTTP client transport mode among the following options: "HTTP1_1", "HTTP2", "HTTP3", "DYNAMIC". See + * {@link HttpClientTransport}.
idleTimeoutlong30000The max time in milliseconds a connection can be idle (that is, without - * traffic of bytes in either direction)The max time in milliseconds a connection can be idle (that is, without traffic of bytes in either + * direction)
maxConnectionsPerDestinationuserAgentFieldStringnullThe "User-Agent" HTTP header string; when null, uses the Jetty - * defaultThe "User-Agent" HTTP header string; when null, uses the Jetty default
sslContextFactoryStringorg.restlet.ext.ssl.DefaultSslContextFactoryLet you specify a {@link SslContextFactory} qualified class name as a - * parameter, or an instance as an attribute for a more complete and flexible - * SSL context settingLet you specify a {@link SslContextFactory} qualified class name as a parameter, or an instance as an attribute + * for a more complete and flexible SSL context setting
http3PemWorkDirstringNo default valueDirectory where are exported trusted certificates, required for HTTP3 support. There is no default value to let + * you configure a secured enough directory.
- * For the default SSL parameters see the Javadocs of the - * {@link DefaultSslContextFactory} class. + * For the default SSL parameters see the Javadocs of the {@link DefaultSslContextFactory} class. * * @author Jerome Louvel * @author Tal Liron */ -public class HttpClientHelper - extends org.restlet.engine.adapter.HttpClientHelper { +public class HttpClientHelper extends org.restlet.engine.adapter.HttpClientHelper { /** * The wrapped Jetty HTTP client. @@ -233,9 +233,8 @@ public class HttpClientHelper private volatile Executor executor; /** - * Constructor. Properties can still be set before the wrapped Jetty HTTP - * client is effectively created and configured via the - * {@link #createHttpClient()} method. + * Constructor. Properties can still be set before the wrapped Jetty HTTP client is effectively created and + * configured via the {@link #createHttpClient()} method. * * @param client The client connector to help. */ @@ -244,9 +243,7 @@ public HttpClientHelper(Client client) { getProtocols().add(Protocol.HTTP); getProtocols().add(Protocol.HTTPS); this.authenticationStore = null; - this.cookieStore = isCookieSupported() - ? new HttpCookieStore.Default() - : new HttpCookieStore.Empty(); + this.cookieStore = isCookieSupported() ? new HttpCookieStore.Default() : new HttpCookieStore.Empty(); this.executor = null; } @@ -261,11 +258,9 @@ public ClientCall create(Request request) { try { result = new JettyClientCall(this, request.getMethod().toString(), - ReferenceUtils.update(request.getResourceRef(), request) - .toString()); + ReferenceUtils.update(request.getResourceRef(), request).toString()); } catch (IOException e) { - getLogger().log(Level.WARNING, - "Unable to create the Jetty HTTP/HTTPS client call", e); + getLogger().log(Level.WARNING, "Unable to create the Jetty HTTP/HTTPS client call", e); } return result; @@ -282,21 +277,15 @@ protected HttpClient createHttpClient() { try { sslContextFactory = new RestletSslContextFactoryClient(SslUtils.getSslContextFactory(this)); } catch (Exception e) { - getLogger().log(Level.WARNING, - "Unable to create the Jetty SSL context factory", e); + getLogger().log(Level.WARNING, "Unable to create the Jetty SSL context factory", e); } - final String httpClientTransportMode = getHttpClientTransportMode(); - final HttpClientTransport httpTransport = switch (httpClientTransportMode) { - case "HTTP2" -> getHttpClientTransportForHttp2(); - case "HTTP3" -> getHttpClientTransportForHttp3(sslContextFactory); - case "DYNAMIC" -> getHttpClientTransportForDynamicMode(sslContextFactory); - case "HTTP11" -> getHttpTransportForHttp1_1(); - default -> { - getLogger().log(Level.WARNING, - "Unknown HTTP client transport mode: {0}, default to HTTP11", httpClientTransportMode); - yield getHttpTransportForHttp1_1(); - } + HttpTransportProtocol httpTransportProtocol = HttpTransportProtocol.fromName(getHttpClientTransportMode()); + final HttpClientTransport httpTransport = switch (httpTransportProtocol) { + case HTTP1_1 -> getHttpTransportForHttp1_1(); + case HTTP2 -> getHttpClientTransportForHttp2(); + case HTTP3 -> getHttpClientTransportForHttp3(sslContextFactory); + case DYNAMIC -> getHttpClientTransportForDynamicMode(sslContextFactory); }; final HttpClient httpClient = new HttpClient(httpTransport); @@ -313,15 +302,14 @@ protected HttpClient createHttpClient() { final String httpComplianceMode = getHttpComplianceMode(); final HttpCompliance httpCompliance = switch (httpComplianceMode) { - case "RFC7230" -> HttpCompliance.RFC7230; - case "RFC7230_LEGACY" -> HttpCompliance.RFC7230_LEGACY; - case "RFC2616" -> HttpCompliance.RFC2616; - case "RFC2616_LEGACY" -> HttpCompliance.RFC2616_LEGACY; - default -> { - getLogger().log(Level.WARNING, - "Unknown HTTP compliance mode: {0}, default to RFC7230", httpComplianceMode); - yield HttpCompliance.RFC7230; - } + case "RFC7230" -> HttpCompliance.RFC7230; + case "RFC7230_LEGACY" -> HttpCompliance.RFC7230_LEGACY; + case "RFC2616" -> HttpCompliance.RFC2616; + case "RFC2616_LEGACY" -> HttpCompliance.RFC2616_LEGACY; + default -> { + getLogger().log(Level.WARNING, "Unknown HTTP compliance mode: {0}, default to RFC7230", httpComplianceMode); + yield HttpCompliance.RFC7230; + } }; httpClient.setHttpCompliance(httpCompliance); @@ -346,8 +334,7 @@ protected HttpClient createHttpClient() { final String userAgentField = getUserAgentField(); if (userAgentField != null) { - httpClient.setUserAgentField( - new HttpField(HttpHeader.USER_AGENT, userAgentField)); + httpClient.setUserAgentField(new HttpField(HttpHeader.USER_AGENT, userAgentField)); } return httpClient; @@ -357,7 +344,7 @@ private static HttpClientTransportOverHTTP getHttpTransportForHttp1_1() { return new HttpClientTransportOverHTTP(); } - private static HttpClientTransport getHttpClientTransportForHttp2() { + private HttpClientTransport getHttpClientTransportForHttp2() { HTTP2Client http2Client = new HTTP2Client(); HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2(http2Client); http2Transport.setUseALPN(true); @@ -365,22 +352,24 @@ private static HttpClientTransport getHttpClientTransportForHttp2() { return http2Transport; } - private static HttpClientTransport getHttpClientTransportForHttp3(SslContextFactory.Client sslContextFactory) { - ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + private HttpClientTransport getHttpClientTransportForHttp3(SslContextFactory.Client sslContextFactory) { + Path pemWorkDirectory = getHttp3PemWorkDirectoryPath(); + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, pemWorkDirectory); HTTP3Client http3Client = new HTTP3Client(quicConfiguration); http3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); return new HttpClientTransportOverHTTP3(http3Client); } - private static HttpClientTransport getHttpClientTransportForDynamicMode(SslContextFactory.Client sslContextFactory) { + private HttpClientTransport getHttpClientTransportForDynamicMode(SslContextFactory.Client sslContextFactory) { ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; HTTP2Client http2Client = new HTTP2Client(); ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); - ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, + getHttp3PemWorkDirectoryPath()); HTTP3Client http3Client = new HTTP3Client(quicConfiguration); ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); @@ -388,14 +377,12 @@ private static HttpClientTransport getHttpClientTransportForDynamicMode(SslConte } /** - * The timeout in milliseconds for the DNS resolution of host addresses. - * Defaults to 15000. + * The timeout in milliseconds for the DNS resolution of host addresses. Defaults to 15000. * * @return The address resolution timeout. */ public long getAddressResolutionTimeout() { - return Long.parseLong(getHelpedParameters() - .getFirstValue("addressResolutionTimeout", "15000")); + return Long.parseLong(getHelpedParameters().getFirstValue("addressResolutionTimeout", "15000")); } /** @@ -412,8 +399,7 @@ public AuthenticationStore getAuthenticationStore() { * * @param authenticationStore The wrapped Jetty authentication store. */ - public void setAuthenticationStore( - AuthenticationStore authenticationStore) { + public void setAuthenticationStore(AuthenticationStore authenticationStore) { this.authenticationStore = authenticationStore; } @@ -423,25 +409,23 @@ public void setAuthenticationStore( * @return The bind address or null. */ public SocketAddress getBindAddress() { - final String bindAddress = getHelpedParameters() - .getFirstValue("bindAddress", null); - final String bindPort = getHelpedParameters().getFirstValue("bindPort", - null); - if ((bindAddress != null) && (bindPort != null)) - return new InetSocketAddress(bindAddress, - Integer.parseInt(bindPort)); - return null; + final String bindAddress = getHelpedParameters().getFirstValue("bindAddress", null); + final String bindPort = getHelpedParameters().getFirstValue("bindPort", null); + + if ((bindAddress != null) && (bindPort != null)) { + return new InetSocketAddress(bindAddress, Integer.parseInt(bindPort)); + } else { + return null; + } } /** - * The max time in milliseconds a connection can take to connect to - * destinations. Defaults to 15000. + * The max time in milliseconds a connection can take to connect to destinations. Defaults to 15000. * * @return The connect timeout. */ public long getConnectTimeout() { - return Long.parseLong( - getHelpedParameters().getFirstValue("connectTimeout", "15000")); + return Long.parseLong(getHelpedParameters().getFirstValue("connectTimeout", "15000")); } /** @@ -463,19 +447,16 @@ public void setCookieStore(HttpCookieStore cookieStore) { } /** - * The timeout in milliseconds for idle destinations to be removed. Defaults - * to 0. + * The timeout in milliseconds for idle destinations to be removed. Defaults to 0. * * @return The address resolution timeout. */ public long getDestinationIdleTimeout() { - return Long.parseLong(getHelpedParameters() - .getFirstValue("destinationIdleTimeout", "0")); + return Long.parseLong(getHelpedParameters().getFirstValue("destinationIdleTimeout", "0")); } /** - * Returns the executor. By default, returns an instance of - * {@link QueuedThreadPool}. + * Returns the executor. By default, returns an instance of {@link QueuedThreadPool}. * * @return Returns the executor. */ @@ -502,56 +483,60 @@ public HttpClient getHttpClient() { } /** - * Returns the HTTP compliance mode among the following options: "RFC7230", - * "RFC2616", "LEGACY", "RFC7230_LEGACY". See {@link HttpCompliance}. - * Default to "RFC7230". + * Returns the HTTP compliance mode among the following options: "RFC7230", "RFC2616", "LEGACY", "RFC7230_LEGACY". + * See {@link HttpCompliance}. Default to "RFC7230". * * @return The HTTP compliance mode. */ public String getHttpComplianceMode() { - return getHelpedParameters().getFirstValue("httpComplianceMode", - "RFC7230"); + return getHelpedParameters().getFirstValue("httpComplianceMode", "RFC7230"); } /** - * Returns the HTTP client transport mode among the following options: - * "HTTP11", "HTTP2", "HTTP3", "DYNAMIC. See {@link HttpClientTransport}. - * Defaults to "HTTP11". + * Returns the HTTP client transport mode among the following options: "HTTP1_1", "HTTP2", "HTTP3", "DYNAMIC. See + * {@link HttpClientTransport}. Default to "HTTP1_1". * * @return The HTTP client transport mode. */ public String getHttpClientTransportMode() { - return getHelpedParameters().getFirstValue("httpClientTransportMode", - "HTTP11"); + return getHelpedParameters().getFirstValue("httpClientTransportMode", HttpTransportProtocol.HTTP1_1.name()); + } + + /** + * Directory where are extracted the supported certificates. + * + * @return Directory where are extracted the supported certificates. + */ + public String getHttp3PemWorkDir() { + return getHelpedParameters().getFirstValue("http3PemWorkDir"); + } + + private Path getHttp3PemWorkDirectoryPath() { + return Optional.ofNullable(getHttp3PemWorkDir()).map(Path::of).orElse(null); } /** - * The max time in milliseconds a connection can be idle (that is, without - * traffic of bytes in either direction). Defaults to 30000. + * The max time in milliseconds a connection can be idle (that is, without traffic of bytes in either direction). + * Defaults to 30000. * * @return The idle timeout. */ public long getIdleTimeout() { - return Long.parseLong( - getHelpedParameters().getFirstValue("idleTimeout", "30000")); + return Long.parseLong(getHelpedParameters().getFirstValue("idleTimeout", "30000")); } /** - * Sets the max number of connections to open to each destination. Defaults - * to 64. + * Sets the max number of connections to open to each destination. Defaults to 64. *

- * RFC 2616 suggests that 2 connections should be opened per each - * destination, but browsers commonly open 6. If this client is used for - * load testing, it is common to have only one destination (the server to - * load test), and it is recommended to set this value to a high value (at - * least as much as the threads present in the {@link #getExecutor() - * executor}). + * RFC 2616 suggests that 2 connections should be opened per each destination, but browsers commonly open 6. If this + * client is used for load testing, it is common to have only one destination (the server to load test), and it is + * recommended to set this value to a high value (at least as much as the threads present in the + * {@link #getExecutor() executor}). * * @return The maximum connections per destination. */ public int getMaxConnectionsPerDestination() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("maxConnectionsPerDestination", "64")); + return Integer.parseInt(getHelpedParameters().getFirstValue("maxConnectionsPerDestination", "64")); } /** @@ -560,38 +545,31 @@ public int getMaxConnectionsPerDestination() { * @return The maximum redirects. */ public int getMaxRedirects() { - return Integer.parseInt( - getHelpedParameters().getFirstValue("maxRedirects", "8")); + return Integer.parseInt(getHelpedParameters().getFirstValue("maxRedirects", "8")); } /** - * Sets the max number of requests that may be queued to a destination. - * Defaults to 1024. + * Sets the max number of requests that may be queued to a destination. Defaults to 1024. *

- * If this client performs a high rate of requests to a destination, and all - * the connections managed by that destination are busy with other requests, - * then new requests will be queued up in the destination. This parameter - * controls how many requests can be queued before starting to reject them. - * If this client is used for load testing, it is common to have this - * parameter set to a high value, although this may impact latency (requests - * sit in the queue for a long time before being sent). + * If this client performs a high rate of requests to a destination, and all the connections managed by that + * destination are busy with other requests, then new requests will be queued up in the destination. This parameter + * controls how many requests can be queued before starting to reject them. If this client is used for load testing, + * it is common to have this parameter set to a high value, although this may impact latency (requests sit in the + * queue for a long time before being sent). * * @return The maximum requests queues per destination. */ public int getMaxRequestsQueuedPerDestination() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("maxRequestsQueuedPerDestination", "1024")); + return Integer.parseInt(getHelpedParameters().getFirstValue("maxRequestsQueuedPerDestination", "1024")); } /** - * Returns the max size in bytes of the response headers. Default is -1 - * that is unlimited. + * Returns the max size in bytes of the response headers. Default is -1 that is unlimited. * * @return the max size in bytes of the response headers. */ public int getMaxResponseHeadersSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("maxResponseHeadersSize", "-1")); + return Integer.parseInt(getHelpedParameters().getFirstValue("maxResponseHeadersSize", "-1")); } /** @@ -600,8 +578,7 @@ public int getMaxResponseHeadersSize() { * @return the host name of the HTTP proxy, if specified. */ public String getProxyHost() { - return getHelpedParameters().getFirstValue("proxyHost", - System.getProperty("http.proxyHost")); + return getHelpedParameters().getFirstValue("proxyHost", System.getProperty("http.proxyHost")); } /** @@ -610,8 +587,8 @@ public String getProxyHost() { * @return the port of the HTTP proxy. */ public int getProxyPort() { - return Integer.parseInt(getHelpedParameters().getFirstValue("proxyPort", - System.getProperty("http.proxyPort", "3128"))); + return Integer.parseInt( + getHelpedParameters().getFirstValue("proxyPort", System.getProperty("http.proxyPort", "3128"))); } /** @@ -620,24 +597,20 @@ public int getProxyPort() { * @return The request buffer size. */ public int getRequestBufferSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("requestBufferSize", "4096")); + return Integer.parseInt(getHelpedParameters().getFirstValue("requestBufferSize", "4096")); } /** - * The size in bytes of the buffer used to read responses. Defaults to - * 16384. + * The size in bytes of the buffer used to read responses. Defaults to 16384. * * @return The response buffer size. */ public int getResponseBufferSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("responseBufferSize", "16384")); + return Integer.parseInt(getHelpedParameters().getFirstValue("responseBufferSize", "16384")); } /** - * The scheduler. Defaults to null. When null, creates a new instance of - * {@link ScheduledExecutorScheduler}. + * The scheduler. Defaults to null. When null, creates a new instance of {@link ScheduledExecutorScheduler}. * * @return The scheduler. */ @@ -646,8 +619,7 @@ public Scheduler getScheduler() { } /** - * The "User-Agent" HTTP header string. When null, uses the Jetty default. - * Default to null. + * The "User-Agent" HTTP header string. When null, uses the Jetty default. Default to null. * * @return The user agent field or null. */ @@ -656,25 +628,21 @@ public String getUserAgentField() { } /** - * Indicates whether the connect operation is blocking. See - * {@link HttpClient#isConnectBlocking()}. + * Indicates whether the connect operation is blocking. See {@link HttpClient#isConnectBlocking()}. * * @return True if the connect operation is blocking. */ public boolean isConnectBlocking() { - return Boolean.parseBoolean(getHelpedParameters() - .getFirstValue("connectBlocking", "false")); + return Boolean.parseBoolean(getHelpedParameters().getFirstValue("connectBlocking", "false")); } /** - * Whether to support cookies, storing and automatically sending them back. - * Defaults to false. + * Whether to support cookies, storing and automatically sending them back. Defaults to false. * * @return Whether to support cookies. */ public boolean isCookieSupported() { - return Boolean.parseBoolean(getHelpedParameters() - .getFirstValue("cookieSupported", "false")); + return Boolean.parseBoolean(getHelpedParameters().getFirstValue("cookieSupported", "false")); } /** @@ -683,45 +651,39 @@ public boolean isCookieSupported() { * @return Whether to follow redirects. */ public boolean isFollowRedirects() { - return Boolean.parseBoolean( - getHelpedParameters().getFirstValue("followRedirects", "true")); + return Boolean.parseBoolean(getHelpedParameters().getFirstValue("followRedirects", "true")); } /** * Whether request events must be strictly ordered. Defaults to false. *

- * Client listeners may send a second request. If the second request is for - * the same destination, there is an inherent race condition for the use of - * the connection: the first request may still be associated with the - * connection, so the second request cannot use that connection and is - * forced to open another one. + * Client listeners may send a second request. If the second request is for the same destination, there is an + * inherent race condition for the use of the connection: the first request may still be associated with the + * connection, so the second request cannot use that connection and is forced to open another one. *

- * From the point of view of connection usage, the connection is reusable - * just before the "complete" event, so it would be possible to reuse that - * connection from complete listeners; but in this case the second request's - * events will fire before the "complete" events of the first request. + * From the point of view of connection usage, the connection is reusable just before the "complete" event, so it + * would be possible to reuse that connection from complete listeners; but in this case the second request's events + * will fire before the "complete" events of the first request. *

- * This setting enforces strict event ordering so that a "begin" event of a - * second request can never fire before the "complete" event of a first - * request, but at the expense of an increased usage of connections. + * This setting enforces strict event ordering so that a "begin" event of a second request can never fire before the + * "complete" event of a first request, but at the expense of an increased usage of connections. *

- * When not enforced, a "begin" event of a second request may happen before - * the "complete" event of a first request and allow for better usage of - * connections. + * When not enforced, a "begin" event of a second request may happen before the "complete" event of a first request + * and allow for better usage of connections. * * @return Whether request events must be strictly ordered. */ public boolean isStrictEventOrdering() { - return Boolean.parseBoolean(getHelpedParameters() - .getFirstValue("strictEventOrdering", "false")); + return Boolean.parseBoolean(getHelpedParameters().getFirstValue("strictEventOrdering", "false")); } @Override public void start() throws Exception { super.start(); - if (this.httpClient == null) + if (this.httpClient == null) { this.httpClient = createHttpClient(); + } final HttpClient httpClient = getHttpClient(); if (httpClient != null) { @@ -740,4 +702,24 @@ public void stop() throws Exception { super.stop(); } + + /** + * Supported HTTP transport protocols. + */ + private enum HttpTransportProtocol { + HTTP1_1, HTTP2, HTTP3, DYNAMIC; + + static HttpTransportProtocol fromName(final String name) { + try { + return HttpTransportProtocol.valueOf(name); + } catch (final IllegalArgumentException iae) { + String supportedHttpTransportProtocols = Arrays.toString(HttpTransportProtocol.values()); + + final String errorMessage = String.format("'%s' is not one of the supported values: %s", name, + supportedHttpTransportProtocols); + + throw new IllegalArgumentException(errorMessage); + } + } + } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpServerHelper.java index 8ea3ccd76f..5df39254c7 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpServerHelper.java @@ -1,23 +1,39 @@ /** * Copyright 2005-2024 Qlik - * + *

* The contents of this file is subject to the terms of the Apache 2.0 open * source license available at http://www.opensource.org/licenses/apache-2.0 - * + *

* Restlet is a registered trademark of QlikTech International AB. */ package org.restlet.ext.jetty; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.restlet.Server; import org.restlet.data.Protocol; +import java.util.Arrays; +import java.util.List; + /** * Jetty HTTP server connector. - * + * + * + * + * + * + * + * + * + * + *
list of supported parameters
http.transport.modestringHTTP1_1Supported protocol. Values: HTTP1_1 or HTTP2. The protocol HTTP 1.1 is always supported, the support of HTTP2 (in + * clear text mode) is done thanks to upgrade from HTTP 1.1 protocol.
+ * * @author Jerome Louvel * @author Tal Liron */ @@ -25,7 +41,7 @@ public class HttpServerHelper extends JettyServerHelper { /** * Constructor. - * + * * @param server The server to help. */ public HttpServerHelper(Server server) { @@ -35,14 +51,54 @@ public HttpServerHelper(Server server) { /** * Create and configure the Jetty HTTP connector - * + * * @param configuration The HTTP configuration. */ @Override - protected ConnectionFactory[] createConnectionFactories( - final HttpConfiguration configuration) { - return new ConnectionFactory[] { + protected ConnectionFactory[] createConnectionFactories(final HttpConfiguration configuration) { + HttpTransportProtocol httpTransportProtocol = HttpTransportProtocol.fromName(getHttpTransportProtocol()); + + return switch (httpTransportProtocol) { + case HTTP1_1 -> new ConnectionFactory[] { new HttpConnectionFactory(configuration) }; + case HTTP2 -> new ConnectionFactory[] { + new HttpConnectionFactory(configuration), // still necessary to support protocol upgrade + new HTTP2CServerConnectionFactory(configuration) }; + }; + } + + @Override + protected List createConnectors(org.eclipse.jetty.server.Server server) { + return List.of(createServerConnector(server, createHttpConfiguration())); + } + + /** + * Supported HTTP transport protocol. Defaults to HTTP1_1. + * + * @return Supported HTTP transport protocol. + */ + public String getHttpTransportProtocol() { + return getHelpedParameters().getFirstValue("http.transport.protocol", HttpTransportProtocol.HTTP1_1.name()); + } + + /** + * Supported HTTP transport protocols. + */ + private enum HttpTransportProtocol { + HTTP1_1, HTTP2; + + static HttpTransportProtocol fromName(final String name) { + try { + return HttpTransportProtocol.valueOf(name); + } catch (final IllegalArgumentException iae) { + String supportedHttpTransportProtocols = Arrays.toString(HttpTransportProtocol.values()); + + final String errorMessage = String.format("'%s' is not one of the supported values: %s", name, + supportedHttpTransportProtocols); + + throw new IllegalArgumentException(errorMessage); + } + } } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java index 4e4fb78749..ba883f8483 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/HttpsServerHelper.java @@ -1,30 +1,36 @@ /** * Copyright 2005-2024 Qlik - * + *

* The contents of this file is subject to the terms of the Apache 2.0 open * source license available at http://www.opensource.org/licenses/apache-2.0 - * + *

* Restlet is a registered trademark of QlikTech International AB. */ package org.restlet.ext.jetty; -import java.util.logging.Level; - -import org.eclipse.jetty.server.AbstractConnectionFactory; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.*; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.restlet.Server; import org.restlet.data.Protocol; import org.restlet.engine.ssl.DefaultSslContextFactory; import org.restlet.ext.jetty.internal.RestletSslContextFactoryServer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; + /** - * Jetty HTTPS server connector. Here is the list of additional parameters that - * are supported. They should be set in the Server's context before it is - * started: + * Jetty HTTPS server connector. Here is the list of additional parameters that are supported. They should be set in the + * Server's context before it is started: * * * @@ -37,17 +43,26 @@ * * * - * + * + * + * + * + * + * + * + * + * + * + * + * + * * *
list of supported parameters
sslContextFactoryStringorg.restlet.engine.ssl.DefaultSslContextFactoryLet you specify a {@link SslContextFactory} qualified class name as a - * parameter, or an instance as an attribute for a more complete and flexible - * SSL context settingLet you specify a {@link SslContextFactory} qualified class name as a parameter, or an instance as an attribute + * for a more complete and flexible SSL context setting
http.transport.protocolsstringHTTP1_1Comma separated and sorted list of supported protocols. Available values: HTTP1_1, HTTP2, HTTP3.
http3.pem.workdirstringNo default valueDirectory where are exported trusted certificates, required for HTTP3 support. There is no default value to let + * you configure a secured enough directory.
- * For the default SSL parameters see the Javadocs of the - * {@link DefaultSslContextFactory} class. - * - * @see How - * to configure SSL for Jetty + * For the default SSL parameters see the Javadocs of the {@link DefaultSslContextFactory} class. + * + * @see Configure SSL for Jetty * @author Jerome Louvel * @author Tal Liron */ @@ -55,7 +70,7 @@ public class HttpsServerHelper extends JettyServerHelper { /** * Constructor. - * + * * @param server The server to help. */ public HttpsServerHelper(Server server) { @@ -64,21 +79,137 @@ public HttpsServerHelper(Server server) { } @Override - protected ConnectionFactory[] createConnectionFactories( - HttpConfiguration configuration) { - ConnectionFactory[] result; + protected List createConnectors(org.eclipse.jetty.server.Server server) { + final List result = new ArrayList<>(); + final List httpTransportProtocols = getHttpTransportProtocols().stream() + .map(HttpTransportProtocol::fromName).toList(); + + if (httpTransportProtocols.stream().anyMatch(HttpTransportProtocol::isTcpProtocol)) { + HttpConfiguration configuration = createHttpConfiguration(); + ServerConnector connector = createServerConnector(server, configuration); + result.add(connector); + } else if (httpTransportProtocols.contains(HttpTransportProtocol.HTTP3)) { + ServerQuicConfiguration configuration = createQuicConfiguration(getServerSslContextFactory()); + QuicServerConnector connector = createQuicServerConnector(server, configuration); + result.add(connector); + } + + return result; + } + + @Override + protected ConnectionFactory[] createConnectionFactories(final HttpConfiguration configuration) { + final List connectionFactories = new ArrayList<>(); + + final List tcpBasedTransportProtocols = getHttpTransportProtocols().stream() + .map(HttpTransportProtocol::fromName).filter(HttpTransportProtocol::isTcpProtocol).toList(); + + for (HttpTransportProtocol tcpBasedTransportProtocol : tcpBasedTransportProtocols) { + final List protocolConnectionFactories = switch (tcpBasedTransportProtocol) { + case HTTP1_1 -> List.of(new HttpConnectionFactory(configuration)); + case HTTP2 -> List.of(new ALPNServerConnectionFactory(), new HTTP2ServerConnectionFactory(configuration)); + default -> { + String supportedHttpTransportProtocols = tcpBasedTransportProtocols.toString(); + final String errorMessage = String.format("'%s' is not one of the supported values: %s", + tcpBasedTransportProtocol, supportedHttpTransportProtocols); + throw new IllegalArgumentException(errorMessage); + } + }; + connectionFactories.addAll(protocolConnectionFactories); + } + + SslContextFactory.Server sslContextFactory = getServerSslContextFactory(); + + return AbstractConnectionFactory.getFactories(sslContextFactory, + connectionFactories.toArray(new ConnectionFactory[0])); + } + + private QuicServerConnector createQuicServerConnector(org.eclipse.jetty.server.Server server, + ServerQuicConfiguration configuration) { + QuicServerConnector connector = new QuicServerConnector(server, configuration, + new HTTP3ServerConnectionFactory(configuration)); + final String address = getHelped().getAddress(); + if (address != null) { + connector.setHost(address); + } + connector.setPort(getHelped().getPort()); + connector.setIdleTimeout(getConnectorIdleTimeout()); + connector.setShutdownIdleTimeout(getShutdownTimeout()); + return connector; + } + + /** + * Supported HTTP transport protocols. Defaults to HTTP1_1. + * + * @return Supported HTTP transport protocols. + */ + public List getHttpTransportProtocols() { + String httpTransportProtocolsAsString = getHelpedParameters().getFirstValue("http.transport.protocols", + HttpTransportProtocol.HTTP1_1.name()); + return Arrays.stream(httpTransportProtocolsAsString.split(",")).map(String::trim).distinct().toList(); + } + + /** + * Directory where are extracted the supported certificates. + * + * @return Directory where are extracted the supported certificates. + */ + public String getHttp3PemWorkDir() { + return getHelpedParameters().getFirstValue("http3.pem.workdir"); + } + + private Path getHttp3PemWorkDirectoryPath() { + return Optional.ofNullable(getHttp3PemWorkDir()).map(Path::of).orElse(null); + } + + private SslContextFactory.Server getServerSslContextFactory() { try { - SslContextFactory.Server sslContextFactory = new RestletSslContextFactoryServer( - org.restlet.engine.ssl.SslUtils.getSslContextFactory(this)); - result = AbstractConnectionFactory.getFactories(sslContextFactory, - new HttpConnectionFactory(configuration)); + return new RestletSslContextFactoryServer(org.restlet.engine.ssl.SslUtils.getSslContextFactory(this)); + } catch (RuntimeException e) { + getLogger().log(Level.WARNING, "Unable to create the Jetty SSL context factory", e); + throw e; } catch (Exception e) { - result = null; - getLogger().log(Level.WARNING, - "Unable to create the Jetty SSL context factory", e); + getLogger().log(Level.WARNING, "Unable to create the Jetty SSL context factory", e); + throw new RuntimeException(e); } + } - return result; + private ServerQuicConfiguration createQuicConfiguration(SslContextFactory.Server sslContextFactory) { + Path pemWorkDirectory = getHttp3PemWorkDirectoryPath(); + ServerQuicConfiguration configuration = new ServerQuicConfiguration(sslContextFactory, pemWorkDirectory); + configuration.setOutputBufferSize(getHttpOutputBufferSize()); + return configuration; + } + + /** + * Supported HTTP transport protocols. + */ + private enum HttpTransportProtocol { + HTTP1_1(true), HTTP2(true), HTTP3(false); + + private final boolean tcpProtocol; + + static HttpTransportProtocol fromName(final String name) { + try { + return HttpTransportProtocol.valueOf(name); + } catch (final IllegalArgumentException iae) { + String supportedHttpTransportProtocols = Arrays.toString(HttpTransportProtocol.values()); + + final String errorMessage = String.format("'%s' is not one of the supported values: %s", name, + supportedHttpTransportProtocols); + + throw new IllegalArgumentException(errorMessage); + } + } + + HttpTransportProtocol(boolean tcpProtocol) { + this.tcpProtocol = tcpProtocol; + } + + public boolean isTcpProtocol() { + return tcpProtocol; + } } + } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java index d1feffb732..238bb957cf 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/JettyServerHelper.java @@ -23,11 +23,13 @@ import java.net.Socket; import java.util.Arrays; +import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** - * Abstract Jetty web server connector. Here is the list of parameters that are - * supported. They should be set in the Server's context before it is started: + * Abstract Jetty web server connector. Here is the list of parameters that are supported. They should be set in the + * Server's context before it is started: * * * @@ -58,15 +60,13 @@ * * * - * + * * * * * * - * + * * * * @@ -78,8 +78,8 @@ * * * - * + * * * * @@ -91,24 +91,15 @@ * * * - * - * - * - * - * - * - * + * * * * * * - * + * * * * @@ -120,34 +111,30 @@ * * * - * + * * * * * * - * + * * * * * * - * + * * * * * * - * + * * * * @@ -159,8 +146,8 @@ * * * - * + * * * * @@ -172,15 +159,14 @@ * * * - * + * * * * * * - * + * * * * @@ -192,14 +178,15 @@ * * * - * + * * * * * * - * + * * * * @@ -209,14 +196,11 @@ * *
list of supported parameters
threadPool.idleTimeoutint60000Thread pool idle timeout in milliseconds; threads that are idle for - * longer than this period may be stoppedThread pool idle timeout in milliseconds; threads that are idle for longer than this period may be stopped
threadPool.stopTimeoutlong5000Thread pool stop timeout in milliseconds; the maximum time allowed for - * the service to shutdownThread pool stop timeout in milliseconds; the maximum time allowed for the service to shutdown
connector.acceptorsconnector.selectorsint-1Connector selector thread count; When less or equal than 0, - * Jetty computes a default value derived from a heuristic over available CPUs and thread pool size.}Connector selector thread count; When less or equal than 0, Jetty computes a default value derived from a + * heuristic over available CPUs and thread pool size.
connector.acceptQueueSizeconnector.idleTimeoutint30000Connector idle timeout in milliseconds; see - * {@link Socket#setSoTimeout(int)}; this value is interpreted as the maximum - * time between some progress being made on the connection; so if a single byte - * is read or written, then the timeout is reset
connector.soLingerTimeint-1Connector TCP/IP SO linger time in milliseconds; when -1 is disabled; see - * {@link Socket#setSoLinger(boolean, int)}Connector idle timeout in milliseconds; see {@link Socket#setSoTimeout(int)}; this value is interpreted as the + * maximum time between some progress being made on the connection; so if a single byte is read or written, then the + * timeout is reset
connector.stopTimeoutlong30000Connector stop timeout in milliseconds; the maximum time allowed for the - * service to shutdownConnector stop timeout in milliseconds; the maximum time allowed for the service to shutdown
http.headerCacheSizehttp.requestHeaderSizeint8*1024HTTP request header size in bytes; larger headers will allow for more - * and/or larger cookies plus larger form content encoded in a URL; however, - * larger headers consume more memory and can make a server more vulnerable to - * denial of service attacksHTTP request header size in bytes; larger headers will allow for more and/or larger cookies plus larger form + * content encoded in a URL; however, larger headers consume more memory and can make a server more vulnerable to denial + * of service attacks
http.responseHeaderSizeint8*1024HTTP response header size in bytes; larger headers will allow for more - * and/or larger cookies and longer HTTP headers (e.g. for redirection); - * however, larger headers will also consume more memoryHTTP response header size in bytes; larger headers will allow for more and/or larger cookies and longer HTTP + * headers (e.g. for redirection); however, larger headers will also consume more memory
http.outputBufferSizeint32*1024HTTP output buffer size in bytes; a larger buffer can improve performance - * by allowing a content producer to run without blocking, however larger - * buffers consume more memory and may induce some latency before a client - * starts processing the contentHTTP output buffer size in bytes; a larger buffer can improve performance by allowing a content producer to run + * without blocking, however larger buffers consume more memory and may induce some latency before a client starts + * processing the content
lowResource.periodint1000Low-resource monitor period in milliseconds; when 0, low-resource - * monitoring is disabledLow-resource monitor period in milliseconds; when 0, low-resource monitoring is disabled
lowResource.threadslowResource.maxMemoryint0Low-resource monitor max memory in bytes; when 0, the check disabled; - * memory used is calculated as (totalMemory-freeMemory)Low-resource monitor max memory in bytes; when 0, the check disabled; memory used is calculated as + * (totalMemory-freeMemory)
lowResource.maxConnectionslowResource.idleTimeoutint1000Low-resource monitor idle timeout in milliseconds; applied to EndPoints - * when in the low-resources stateLow-resource monitor idle timeout in milliseconds; applied to EndPoints when in the low-resources state
lowResource.stopTimeoutlong30000Low-resource monitor stop timeout in milliseconds; the maximum time - * allowed for the service to shutdown. Deprecated, use shutdown.timeout insteadLow-resource monitor stop timeout in milliseconds; the maximum time allowed for the service to shutdown. + * Deprecated, use shutdown.timeout instead
server.maxConnectionsserver.maxConnections.idleTimeoutlong0The endpoint idle timeout in milliseconds to apply when the connection limit is reached; when 0, there is no idle timeout.The endpoint idle timeout in milliseconds to apply when the connection limit is reached; when 0, there is no idle + * timeout.
shutdown.gracefullybooleantrueWhen true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending - * requests to end before shutting down. Otherwise, incoming requests are not blocked and all requests are aborted.When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending requests to end + * before shutting down. Otherwise, incoming requests are not blocked and all requests are aborted.
shutdown.timeout
* - * @see Jetty SPDY - * and NPN page + * @see Jetty 12 documentation * @author Jerome Louvel * @author Tal Liron */ -public abstract class JettyServerHelper - extends org.restlet.engine.adapter.HttpServerHelper { +public abstract class JettyServerHelper extends org.restlet.engine.adapter.HttpServerHelper { /** The wrapped Jetty server. */ private volatile org.eclipse.jetty.server.Server wrappedServer; @@ -235,7 +219,7 @@ public JettyServerHelper(Server server) { * * @return A Jetty HTTP configuration. */ - private HttpConfiguration createConfiguration() { + protected HttpConfiguration createHttpConfiguration() { final HttpConfiguration configuration = new HttpConfiguration(); configuration.setHeaderCacheSize(getHttpHeaderCacheSize()); configuration.setRequestHeaderSize(getHttpRequestHeaderSize()); @@ -245,7 +229,7 @@ private HttpConfiguration createConfiguration() { // ask Jetty connector to let us handling the Date header, // otherwise two Date headers are generated in the request configuration.setSendDateHeader(false); - + return configuration; } @@ -255,42 +239,43 @@ private HttpConfiguration createConfiguration() { * @param configuration The HTTP configuration. * @return New internal Jetty connection factories. */ - protected abstract ConnectionFactory[] createConnectionFactories( - HttpConfiguration configuration); + protected abstract ConnectionFactory[] createConnectionFactories(final HttpConfiguration configuration); /** - * Creates a Jetty connector. + * Creates the Jetty connectors. + * + * @param server The Jetty server. + * @return The Jetty connectors. + */ + protected abstract List createConnectors(org.eclipse.jetty.server.Server server); + + /** + * Creates a Jetty connector based on a classical TCP type of transport. * * @param server The Jetty server. * @return A Jetty connector. */ - private Connector createConnector(org.eclipse.jetty.server.Server server) { - final HttpConfiguration configuration = createConfiguration(); - - final ConnectionFactory[] connectionFactories = createConnectionFactories( - configuration); - + protected ServerConnector createServerConnector(final org.eclipse.jetty.server.Server server, final HttpConfiguration configuration) { final int acceptors = getConnectorAcceptors(); final int selectors = getConnectorSelectors(); final Executor executor = getConnectorExecutor(); final Scheduler scheduler = getConnectorScheduler(); final ByteBufferPool byteBufferPool = getConnectorByteBufferPool(); - final ServerConnector connector = new ServerConnector(server, executor, - scheduler, byteBufferPool, acceptors, selectors, - connectionFactories); + final ConnectionFactory[] connectionFactories = createConnectionFactories(configuration); + + final ServerConnector connector = new ServerConnector(server, executor, scheduler, byteBufferPool, acceptors, + selectors, connectionFactories); final String address = getHelped().getAddress(); if (address != null) { connector.setHost(address); } connector.setPort(getHelped().getPort()); - - connector.setAcceptQueueSize(getConnectorAcceptQueueSize()); connector.setIdleTimeout(getConnectorIdleTimeout()); connector.setShutdownIdleTimeout(getShutdownTimeout()); - // connector.setSoLingerTime(getConnectorSoLingerTime()); + connector.setAcceptQueueSize(getConnectorAcceptQueueSize()); return connector; } @@ -300,8 +285,7 @@ private Connector createConnector(org.eclipse.jetty.server.Server server) { * @param server A Jetty server. * @return A Jetty low-resource monitor or null. */ - private LowResourceMonitor createLowResourceMonitor( - org.eclipse.jetty.server.Server server) { + private LowResourceMonitor createLowResourceMonitor(org.eclipse.jetty.server.Server server) { final LowResourceMonitor result; final int period = getLowResourceMonitorPeriod(); @@ -347,9 +331,9 @@ private org.eclipse.jetty.server.Server createServer() { jettyServer.setHandler(createJettyHandler()); - // Connector - final Connector connector = createConnector(jettyServer); - jettyServer.addConnector(connector); + // Connectors + createConnectors(jettyServer) + .forEach(jettyServer::addConnector); // Low-resource monitor (must be created after connectors have been added) LowResourceMonitor lowResourceMonitor = createLowResourceMonitor(jettyServer); @@ -370,8 +354,8 @@ private Handler.Abstract createJettyHandler() { Handler.Abstract jettyServerHelperWrapperHandler = new Handler.Abstract() { @Override public boolean handle(Request request, Response response, Callback callback) { - JettyServerCall httpCall = new JettyServerCall(jettyServerHelper.getHelped(), - request, response, callback); + JettyServerCall httpCall = new JettyServerCall(jettyServerHelper.getHelped(), request, response, + callback); jettyServerHelper.handle(httpCall); return true; // Indicates that the request is accepted }; @@ -410,32 +394,27 @@ private ThreadPool createThreadPool() { } /** - * Connector acceptor thread count. Defaults to -1. When -1, Jetty will - * default to 1. + * Connector acceptor thread count. Defaults to -1. When -1, Jetty will default to 1. * * @return Connector acceptor thread count. */ public int getConnectorAcceptors() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("connector.acceptors", "-1")); + return Integer.parseInt(getHelpedParameters().getFirstValue("connector.acceptors", "-1")); } /** - * Connector "accept" queue size. - * Defaults to 0. + * Connector "accept" queue size. Defaults to 0. *

* Also known as "accept" backlog. * * @return Connector accept queue size. */ public int getConnectorAcceptQueueSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("connector.acceptQueueSize", "0")); + return Integer.parseInt(getHelpedParameters().getFirstValue("connector.acceptQueueSize", "0")); } /** - * Connector byte buffer pool. Defaults to null. When null, will use a new - * {@link ArrayByteBufferPool}. + * Connector byte buffer pool. Defaults to null. When null, will use a new {@link ArrayByteBufferPool}. * * @return Connector byte buffer pool or null. */ @@ -444,8 +423,7 @@ public ByteBufferPool getConnectorByteBufferPool() { } /** - * Connector executor. Defaults to null. When null, will use the server's - * thread pool. + * Connector executor. Defaults to null. When null, will use the server's thread pool. * * @return Connector executor or null. */ @@ -458,20 +436,17 @@ public Executor getConnectorExecutor() { *

* See {@link Socket#setSoTimeout(int)}. *

- * This value is interpreted as the maximum time between some progress being - * made on the connection. So if a single byte is read or written, then the - * timeout is reset. + * This value is interpreted as the maximum time between some progress being made on the connection. So if a single + * byte is read or written, then the timeout is reset. * * @return Connector idle timeout. */ public int getConnectorIdleTimeout() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("connector.idleTimeout", "30000")); + return Integer.parseInt(getHelpedParameters().getFirstValue("connector.idleTimeout", "30000")); } /** - * Connector scheduler. Defaults to null. When null, will use a new - * {@link ScheduledExecutorScheduler}. + * Connector scheduler. Defaults to null. When null, will use a new {@link ScheduledExecutorScheduler}. * * @return Connector scheduler or null. */ @@ -480,29 +455,13 @@ public Scheduler getConnectorScheduler() { } /** - * Connector selector thread count. - * Defaults to -1. - * When less or equal than 0, - * Jetty computes a default value derived from a heuristic over available CPUs and thread pool size. + * Connector selector thread count. Defaults to -1. When less or equal than 0, Jetty computes a default value + * derived from a heuristic over available CPUs and thread pool size. * * @return Connector acceptor thread count. */ public int getConnectorSelectors() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("connector.selectors", "-1")); - } - - /** - * Connector TCP/IP SO linger time in milliseconds. Defaults to -1 - * (disabled). - *

- * See {@link Socket#setSoLinger(boolean, int)}. - * - * @return Connector TCP/IP SO linger time. - */ - public int getConnectorSoLingerTime() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("connector.soLingerTime", "-1")); + return Integer.parseInt(getHelpedParameters().getFirstValue("connector.selectors", "-1")); } /** @@ -515,8 +474,7 @@ public int getConnectorSoLingerTime() { */ @Deprecated public int getConnectorStopTimeout() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("connector.stopTimeout", "30000")); + return Integer.parseInt(getHelpedParameters().getFirstValue("connector.stopTimeout", "30000")); } /** @@ -525,108 +483,89 @@ public int getConnectorStopTimeout() { * @return HTTP header cache size. */ public int getHttpHeaderCacheSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("http.headerCacheSize", "1024")); + return Integer.parseInt(getHelpedParameters().getFirstValue("http.headerCacheSize", "1024")); } /** * HTTP output buffer size in bytes. Defaults to 32*1024. *

- * A larger buffer can improve performance by allowing a content producer to - * run without blocking, however, larger buffers consume more memory and may - * induce some latency before a client starts processing the content. + * A larger buffer can improve performance by allowing a content producer to run without blocking, however, larger + * buffers consume more memory and may induce some latency before a client starts processing the content. * * @return HTTP output buffer size. */ public int getHttpOutputBufferSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("http.outputBufferSize", "32768")); + return Integer.parseInt(getHelpedParameters().getFirstValue("http.outputBufferSize", "32768")); } /** * HTTP request header size in bytes. Defaults to 8*1024. *

- * Larger headers will allow for more and/or larger cookies plus larger form - * content encoded in a URL. However, larger headers consume more memory and - * can make a server more vulnerable to denial of service attacks. + * Larger headers will allow for more and/or larger cookies plus larger form content encoded in a URL. However, + * larger headers consume more memory and can make a server more vulnerable to denial of service attacks. * * @return HTTP request header size. */ public int getHttpRequestHeaderSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("http.requestHeaderSize", "8192")); + return Integer.parseInt(getHelpedParameters().getFirstValue("http.requestHeaderSize", "8192")); } /** * HTTP response header size in bytes. Defaults to 8*1024. *

- * Larger headers will allow for more and/or larger cookies and longer HTTP - * headers (e.g., for redirection). However, larger headers will also consume - * more memory. + * Larger headers will allow for more and/or larger cookies and longer HTTP headers (e.g., for redirection). + * However, larger headers will also consume more memory. * * @return HTTP response header size. */ public int getHttpResponseHeaderSize() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("http.responseHeaderSize", "8192")); + return Integer.parseInt(getHelpedParameters().getFirstValue("http.responseHeaderSize", "8192")); } /** - * Low-resource monitor idle timeout in milliseconds. - * Defaults to 1000. + * Low-resource monitor idle timeout in milliseconds. Defaults to 1000. *

* Applied to EndPoints when in the low-resources state. * * @return Low-resource monitor idle timeout. */ public int getLowResourceMonitorIdleTimeout() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("lowResource.idleTimeout", "1000")); + return Integer.parseInt(getHelpedParameters().getFirstValue("lowResource.idleTimeout", "1000")); } /** - * Low-resource monitor max connections. - * Defaults to 0. - * When 0, the check is disabled. + * Low-resource monitor max connections. Defaults to 0. When 0, the check is disabled. * * @return Low-resource monitor max connections. * @deprecated cf {@link #getServerMaxConnections()} */ @Deprecated public int getLowResourceMonitorMaxConnections() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("lowResource.maxConnections", "0")); + return Integer.parseInt(getHelpedParameters().getFirstValue("lowResource.maxConnections", "0")); } /** - * Low-resource monitor max memory in bytes. - * Defaults to 0. - * When 0, the check is disabled. + * Low-resource monitor max memory in bytes. Defaults to 0. When 0, the check is disabled. *

* Memory used is calculated as (totalMemory-freeMemory). * * @return Low-resource monitor max memory. */ public long getLowResourceMonitorMaxMemory() { - return Long.parseLong(getHelpedParameters() - .getFirstValue("lowResource.maxMemory", "0")); + return Long.parseLong(getHelpedParameters().getFirstValue("lowResource.maxMemory", "0")); } /** - * Low-resource monitor period in milliseconds. - * Defaults to 1000. - * When 0, low-resource monitoring is disabled. + * Low-resource monitor period in milliseconds. Defaults to 1000. When 0, low-resource monitoring is disabled. * * @return Low-resource monitor period. */ public int getLowResourceMonitorPeriod() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("lowResource.period", "1000")); + return Integer.parseInt(getHelpedParameters().getFirstValue("lowResource.period", "1000")); } /** - * Low-resource monitor stop timeout in milliseconds. - * Defaults to 30000. + * Low-resource monitor stop timeout in milliseconds. Defaults to 30000. *

* The maximum time allowed for the service to shut down. * @@ -635,25 +574,20 @@ public int getLowResourceMonitorPeriod() { */ @Deprecated public long getLowResourceMonitorStopTimeout() { - return Long.parseLong(getHelpedParameters() - .getFirstValue("lowResource.stopTimeout", "30000")); + return Long.parseLong(getHelpedParameters().getFirstValue("lowResource.stopTimeout", "30000")); } /** - * Low-resource monitor, whether to check if we're low on threads. Defaults - * to true. + * Low-resource monitor, whether to check if we're low on threads. Defaults to true. * * @return Low-resource monitor threads. */ public boolean getLowResourceMonitorThreads() { - return Boolean.parseBoolean(getHelpedParameters() - .getFirstValue("lowResource.threads", "true")); + return Boolean.parseBoolean(getHelpedParameters().getFirstValue("lowResource.threads", "true")); } /** - * Server max connections. - * Defaults to 0. - * When 0, the check is disabled. + * Server max connections. Defaults to 0. When 0, the check is disabled. * * @return Low-resource monitor max connections. */ @@ -665,38 +599,34 @@ public int getServerMaxConnections() { if (serverMaxConnectionsValue == null) { result = getLowResourceMonitorMaxConnections(); } else { - result = Integer.parseInt(serverMaxConnectionsValue); + result = Integer.parseInt(serverMaxConnectionsValue); } return result; } /** - * The endpoint idle timeout in milliseconds to apply when the connection limit is reached. - * Defaults to 0. - * When 0, there is no idle timeout. + * The endpoint idle timeout in milliseconds to apply when the connection limit is reached. Defaults to 0. When 0, + * there is no idle timeout. *

* The maximum time allowed for the endpoint to close when the connection limit is reached. * * @return The endpoint idle timeout in milliseconds to apply when the connection limit is reached. */ public long getServerMaxConnectionsIdleTimeout() { - return Long.parseLong(getHelpedParameters() - .getFirstValue("server.maxConnections.idleTimeout", "0")); + return Long.parseLong(getHelpedParameters().getFirstValue("server.maxConnections.idleTimeout", "0")); } /** - * When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending - * requests to end before shutting down. - * Otherwise, incoming requests are not blocked and all requests are aborted. - * Defaults to true. + * When true, upon JVM shutdown, the Jetty server will block incoming requests and wait for pending requests to end + * before shutting down. Otherwise, incoming requests are not blocked and all requests are aborted. Defaults to + * true. + * * @return True if upon JVM shutdown, the Jetty server will block incoming requests and wait for pending requests to - * end before shutting down. - * Otherwise, incoming requests are not blocked and all requests are aborted. + * end before shutting down. Otherwise, incoming requests are not blocked and all requests are aborted. */ public boolean getShutdownGracefully() { - return Boolean.parseBoolean(getHelpedParameters().getFirstValue( - "shutdown.gracefully", "true")); + return Boolean.parseBoolean(getHelpedParameters().getFirstValue("shutdown.gracefully", "true")); } /** @@ -726,8 +656,7 @@ public long getShutdownTimeout() { * @return Thread pool idle timeout. */ public int getThreadPoolIdleTimeout() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("threadPool.idleTimeout", "60000")); + return Integer.parseInt(getHelpedParameters().getFirstValue("threadPool.idleTimeout", "60000")); } /** @@ -736,8 +665,7 @@ public int getThreadPoolIdleTimeout() { * @return Thread pool maximum threads. */ public int getThreadPoolMaxThreads() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("threadPool.maxThreads", "200")); + return Integer.parseInt(getHelpedParameters().getFirstValue("threadPool.maxThreads", "200")); } /** @@ -746,8 +674,7 @@ public int getThreadPoolMaxThreads() { * @return Thread pool minimum threads. */ public int getThreadPoolMinThreads() { - return Integer.parseInt(getHelpedParameters() - .getFirstValue("threadPool.minThreads", "8")); + return Integer.parseInt(getHelpedParameters().getFirstValue("threadPool.minThreads", "8")); } /** @@ -758,8 +685,7 @@ public int getThreadPoolMinThreads() { * @return Thread pool stop timeout. */ public long getThreadPoolStopTimeout() { - return Long.parseLong(getHelpedParameters() - .getFirstValue("threadPool.stopTimeout", "5000")); + return Long.parseLong(getHelpedParameters().getFirstValue("threadPool.stopTimeout", "5000")); } /** @@ -768,8 +694,7 @@ public long getThreadPoolStopTimeout() { * @return Thread pool maximum threads. */ public int getThreadPoolThreadsPriority() { - return Integer.parseInt(getHelpedParameters().getFirstValue( - "threadPool.threadsPriority", + return Integer.parseInt(getHelpedParameters().getFirstValue("threadPool.threadsPriority", String.valueOf(Thread.NORM_PRIORITY))); } @@ -791,6 +716,7 @@ protected org.eclipse.jetty.server.Server getWrappedServer() { * @param wrappedServer The wrapped Jetty server. */ protected void setWrappedServer(org.eclipse.jetty.server.Server wrappedServer) { + Objects.requireNonNull(wrappedServer); this.wrappedServer = wrappedServer; } @@ -799,8 +725,8 @@ public void start() throws Exception { super.start(); org.eclipse.jetty.server.Server server = getWrappedServer(); ServerConnector connector = (ServerConnector) server.getConnectors()[0]; - getLogger().info("Starting the Jetty " + getProtocols() - + " server on port " + getHelped().getPort()); + + getLogger().info("Starting the Jetty " + getProtocols() + " server on port " + getHelped().getPort()); try { server.start(); // We won't know the local port until after the server starts @@ -816,9 +742,10 @@ public void start() throws Exception { @Override public void stop() throws Exception { - getLogger().info("Stopping the Jetty " + getProtocols() - + " server on port " + getHelped().getPort()); - getWrappedServer().stop(); + getLogger().info("Stopping the Jetty " + getProtocols() + " server on port " + getHelped().getPort()); + if (this.wrappedServer != null) { + getWrappedServer().stop(); + } super.stop(); } } diff --git a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java index 589a41846b..40f9dcde68 100644 --- a/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java +++ b/org.restlet.java/org.restlet.ext.jetty/src/main/java/org/restlet/ext/jetty/internal/RestletSslContextFactoryServer.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.net.InetAddress; +import java.net.ServerSocket; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLServerSocket; @@ -52,10 +53,11 @@ public SSLEngine newSSLEngine(String host, int port) { @Override public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException { - SSLServerSocketFactory factory = getSslContext().getServerSocketFactory(); - return (SSLServerSocket) ((host == null) + final SSLServerSocketFactory factory = getSslContext().getServerSocketFactory(); + final ServerSocket serverSocket = (host == null) ? factory.createServerSocket(port, backlog) - : factory.createServerSocket(port, backlog, InetAddress.getByName(host))); + : factory.createServerSocket(port, backlog, InetAddress.getByName(host)); + return (SSLServerSocket) serverSocket; } @Override diff --git a/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/HttpTransportProtocolsTestCase.java b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/HttpTransportProtocolsTestCase.java new file mode 100644 index 0000000000..4472c318c8 --- /dev/null +++ b/org.restlet.java/org.restlet.ext.jetty/src/test/java/org/restlet/ext/jetty/connectors/HttpTransportProtocolsTestCase.java @@ -0,0 +1,357 @@ +/** + * Copyright 2005-2024 Qlik + *

+ * The contents of this file is subject to the terms of the Apache 2.0 open + * source license available at http://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ + +package org.restlet.ext.jetty.connectors; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.restlet.*; +import org.restlet.data.MediaType; +import org.restlet.data.Method; +import org.restlet.data.Parameter; +import org.restlet.data.Protocol; +import org.restlet.engine.Engine; +import org.restlet.engine.ssl.DefaultSslContextFactory; +import org.restlet.ext.jetty.HttpClientHelper; +import org.restlet.util.Series; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test the support of HTTP2 and HTTP3 transport protocols. + */ +public class HttpTransportProtocolsTestCase { + + protected static final String KEYSTORE_FILE_NAME = "dummy.p12"; + protected static final String KEYSTORE_PASSWORD = "testtest"; + protected static final String KEYSTORE_TYPE = "PKCS12"; + protected static File testKeystoreFile; + + @BeforeAll + public static void setup() throws IOException { + Path keystorePath = Files.createTempFile("HttpTransportProtocolsTestCase", KEYSTORE_FILE_NAME); + final InputStream resourceAsStream = SslBaseConnectorsTestCase.class.getResourceAsStream(KEYSTORE_FILE_NAME); + assert resourceAsStream != null; + Files.copy(resourceAsStream, keystorePath, REPLACE_EXISTING); + testKeystoreFile = keystorePath.toFile(); + } + + @AfterAll + protected static void tearDown() { + testKeystoreFile.delete(); + } + + @Nested + class HttpServerTestCase extends HttpTransportProtocolTest { + @BeforeAll + public static void setup() { + Engine.clearThreadLocalVariables(); + Engine nre = Engine.register(false); + nre.getRegisteredServers().add(0, new org.restlet.ext.jetty.HttpServerHelper(null)); + nre.getRegisteredClients().add(0, new HttpClientHelper(null)); + } + + @Override + Server newServer(String httpTransportProtocolOption) { + final Context context = new Context(); + Series parameters = context.getParameters(); + + parameters.add("threadPool.minThreads", "1"); + parameters.add("threadPool.maxThreads", "10"); + parameters.add("shutdown.gracefully", "false"); + parameters.add("http.transport.protocol", httpTransportProtocolOption); + + return new Server(context, Protocol.HTTP, null, 0, HELLO_WORLD_RESTLET); + } + + @Override + List expectedProtocols() { + return List.of("HTTP1_1", "HTTP2"); + } + + static Stream validTestCases() { + return Stream.of(Arguments.of(null, newJdkHttpClient(HttpClient.Version.HTTP_1_1)), // server default to + // HTTP1_1 + Arguments.of(null, newJdkHttpClient(HttpClient.Version.HTTP_2)), // server default to HTTP1_1 + Arguments.of("HTTP1_1", newJdkHttpClient(HttpClient.Version.HTTP_1_1)), + Arguments.of("HTTP1_1", newJdkHttpClient(HttpClient.Version.HTTP_2)), + Arguments.of("HTTP2", newJdkHttpClient(HttpClient.Version.HTTP_1_1)), + Arguments.of("HTTP1_1", newJdkHttpClient(HttpClient.Version.HTTP_2)), + Arguments.of(null, newJettyHttpClient("HTTP1_1")), // server default to HTTP1_1 + Arguments.of(null, newJettyHttpClient("DYNAMIC")), // server default to HTTP1_1 + Arguments.of("HTTP1_1", newJettyHttpClient("HTTP1_1")), + Arguments.of("HTTP1_1", newJettyHttpClient("DYNAMIC")), + Arguments.of("HTTP2", newJettyHttpClient("HTTP1_1")), + Arguments.of("HTTP2", newJettyHttpClient("HTTP2")), + Arguments.of("HTTP2", newJettyHttpClient("DYNAMIC"))); + } + + static Stream invalidTestCases() { + return Stream.of(Arguments.of(null, newJettyHttpClient("HTTP2")), // server default to HTTP1_1 + Arguments.of("HTTP1_1", newJettyHttpClient("HTTP2"))); + } + } + + @Nested + class HttpsServerTestCase extends HttpTransportProtocolTest { + + @BeforeAll + public static void setup() { + Engine.clearThreadLocalVariables(); + Engine nre = Engine.register(false); + nre.getRegisteredServers().add(0, new org.restlet.ext.jetty.HttpsServerHelper(null)); + nre.getRegisteredClients().add(0, new HttpClientHelper(null)); + + } + + @AfterAll + protected static void tearDown() { + // Restore a clean engine + Engine.register(); + Engine.clearThreadLocalVariables(); + } + + @Override + List expectedProtocols() { + return List.of("HTTP1_1", "HTTP2", "HTTP3"); + } + + static Stream validTestCases() { + return Stream.of(Arguments.of(null, newJdkHttpsClient(HttpClient.Version.HTTP_1_1)), // server default to + // HTTP1_1 + Arguments.of(null, newJdkHttpsClient(HttpClient.Version.HTTP_2)), // server default to HTTP1_1 + Arguments.of("HTTP1_1", newJdkHttpsClient(HttpClient.Version.HTTP_1_1)), + Arguments.of("HTTP1_1", newJdkHttpsClient(HttpClient.Version.HTTP_2)), + Arguments.of("HTTP2", newJdkHttpsClient(HttpClient.Version.HTTP_2)), + Arguments.of("HTTP1_1,HTTP2", newJdkHttpsClient(HttpClient.Version.HTTP_1_1)), + Arguments.of("HTTP1_1,HTTP2", newJdkHttpsClient(HttpClient.Version.HTTP_2)), + Arguments.of("HTTP2,HTTP1_1", newJdkHttpsClient(HttpClient.Version.HTTP_1_1)), + Arguments.of("HTTP2,HTTP1_1", newJdkHttpsClient(HttpClient.Version.HTTP_2)), + Arguments.of(null, newJettyHttpsClient("HTTP1_1")), // server default to HTTP1_1 + Arguments.of(null, newJettyHttpsClient("DYNAMIC")), // server default to HTTP1_1 + Arguments.of("HTTP1_1", newJettyHttpsClient("HTTP1_1")), + Arguments.of("HTTP1_1", newJettyHttpsClient("DYNAMIC")), + Arguments.of("HTTP2", newJettyHttpsClient("HTTP2")), + Arguments.of("HTTP2", newJettyHttpsClient("DYNAMIC")), + Arguments.of("HTTP1_1,HTTP2", newJettyHttpsClient("HTTP1_1")), + Arguments.of("HTTP1_1,HTTP2", newJettyHttpsClient("DYNAMIC")), + Arguments.of("HTTP2,HTTP1_1", newJettyHttpsClient("HTTP1_1")), + Arguments.of("HTTP2,HTTP1_1", newJettyHttpsClient("HTTP2")), + Arguments.of("HTTP2,HTTP1_1", newJettyHttpsClient("DYNAMIC"))); + } + + static Stream invalidTestCases() { + return Stream.of(Arguments.of("HTTP2", newJdkHttpsClient(HttpClient.Version.HTTP_1_1)), + Arguments.of(null, newJettyHttpsClient("HTTP2")), // server default to http1 + Arguments.of("HTTP1_1", newJettyHttpsClient("HTTP2")), + Arguments.of("HTTP2", newJettyHttpsClient("HTTP1_1")), + Arguments.of("HTTP1_1,HTTP2", newJettyHttpsClient("HTTP2"))); + } + + @Override + Server newServer(String httpTransportProtocolOption) { + final Context context = new Context(); + Series parameters = context.getParameters(); + + parameters.add("threadPool.minThreads", "1"); + parameters.add("threadPool.maxThreads", "10"); + parameters.add("shutdown.gracefully", "false"); + parameters.add("http.transport.protocols", httpTransportProtocolOption); + + parameters.add("keystorePath", testKeystoreFile.getPath()); + parameters.add("keystorePassword", KEYSTORE_PASSWORD); + parameters.add("keyStoreType", KEYSTORE_TYPE); + parameters.add("keyPassword", KEYSTORE_PASSWORD); + + parameters.add("truststorePath", testKeystoreFile.getPath()); + parameters.add("truststorePassword", KEYSTORE_PASSWORD); + parameters.add("trustStoreType", KEYSTORE_TYPE); + + return new Server(context, Protocol.HTTPS, null, 0, HELLO_WORLD_RESTLET); + } + } + + abstract static class HttpTransportProtocolTest { + + abstract Server newServer(final String httpTransportProtocolOption); + + abstract List expectedProtocols(); + + @ParameterizedTest(name = "server: {0} / client: {1}") + @MethodSource("validTestCases") + public void clientCompliesWithServer(final String httpTransportProtocol, final TestHttpClient testHttpClient) + throws Exception { + final Server server = newServer(httpTransportProtocol); + server.start(); + + assertEquals("hello, world", testHttpClient.sendRequest(server.getActualPort())); + } + + @ParameterizedTest(name = "server: {0} / client: {1}") + @MethodSource("invalidTestCases") + public void clientDoesNotComplyWithServer(final String httpTransportProtocol, + final TestHttpClient testHttpClient) throws Exception { + final Server server = newServer(httpTransportProtocol); + server.start(); + + RuntimeException runtimeException = assertThrows(RuntimeException.class, + () -> testHttpClient.sendRequest(server.getActualPort())); + assertEquals("Error while sending request", runtimeException.getMessage()); + } + + @ParameterizedTest(name = "server: {0}") + @ValueSource(strings = { + "", "invalid", "http3" }) + public void invalidServerConfiguration(final String httpTransportProtocol) { + final Server server = newServer(httpTransportProtocol); + + final Exception exception = assertThrows(IllegalArgumentException.class, server::start); + assertEquals( + format("'%s' is not one of the supported values: %s", httpTransportProtocol, expectedProtocols()), + exception.getMessage()); + } + + } + + private static final Restlet HELLO_WORLD_RESTLET = new Restlet() { + @Override + public void handle(Request request, Response response) { + response.setEntity("hello, world", MediaType.TEXT_PLAIN); + } + }; + + interface TestHttpClient { + String sendRequest(final int port) throws Exception; + } + + private static TestHttpClient newJdkHttpClient(final HttpClient.Version httpVersion) { + return new JdkTestHttpClient(Protocol.HTTP, httpVersion); + } + + private static TestHttpClient newJdkHttpsClient(final HttpClient.Version httpVersion) { + return new JdkTestHttpClient(Protocol.HTTPS, httpVersion); + } + + private static TestHttpClient newJettyHttpClient(final String httpClientTransportMode) { + return new JettyTestHttpClient(Protocol.HTTP, httpClientTransportMode); + } + + private static TestHttpClient newJettyHttpsClient(final String httpClientTransportMode) { + return new JettyTestHttpClient(Protocol.HTTPS, httpClientTransportMode); + } + + static class JdkTestHttpClient implements TestHttpClient { + + final Protocol protocol; + final HttpClient httpClient; + + private JdkTestHttpClient(final Protocol protocol, final HttpClient.Version httpVersion) { + this.protocol = protocol; + if (Protocol.HTTP.equals(protocol)) { + httpClient = HttpClient.newBuilder().version(httpVersion).build(); + } else { + try { + DefaultSslContextFactory sslContextFactory = new DefaultSslContextFactory(); + sslContextFactory.setKeyStorePath(testKeystoreFile.getPath()); + sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD); + sslContextFactory.setKeyStoreKeyPassword(KEYSTORE_PASSWORD); + sslContextFactory.setKeyStoreType(KEYSTORE_TYPE); + + sslContextFactory.setTrustStorePath(testKeystoreFile.getPath()); + sslContextFactory.setTrustStorePassword(KEYSTORE_PASSWORD); + sslContextFactory.setTrustStoreType(KEYSTORE_TYPE); + httpClient = HttpClient.newBuilder().sslContext(sslContextFactory.createSslContext()) + .version(httpVersion).build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public String sendRequest(int port) { + final HttpRequest requestGet = HttpRequest.newBuilder() + .uri(URI.create(protocol.getSchemeName() + "://localhost:" + port)).build(); + try { + return httpClient.send(requestGet, HttpResponse.BodyHandlers.ofString()).body(); + } catch (Exception e) { + throw new RuntimeException("Error while sending request"); + } + } + + @Override + public String toString() { + return "Jdk " + httpClient.version(); + } + } + + static class JettyTestHttpClient implements TestHttpClient { + + final Protocol protocol; + final String httpClientTransportMode; + + private JettyTestHttpClient(final Protocol protocol, final String httpClientTransportMode) { + this.protocol = protocol; + this.httpClientTransportMode = httpClientTransportMode; + } + + @Override + public String sendRequest(int port) { + final Client client = new Client(newClientContext(), List.of(protocol), + HttpClientHelper.class.getCanonicalName()); + + final Request request = new Request(Method.GET, protocol.getSchemeName() + "://localhost:" + port); + final Response response = client.handle(request); + + if (response.getStatus().isError()) { + throw new RuntimeException("Error while sending request"); + } else { + return response.getEntityAsText(); + } + } + + protected Context newClientContext() { + Context context = new Context(); + context.getParameters().add("httpClientTransportMode", httpClientTransportMode); + // context.getParameters().add("http3PemWorkDir", ); + + if (Protocol.HTTPS.equals(protocol)) { + context.getParameters().add("truststorePath", testKeystoreFile.getPath()); + context.getParameters().add("truststorePassword", KEYSTORE_PASSWORD); + context.getParameters().add("trustStoreType", KEYSTORE_TYPE); + } + return context; + } + + @Override + public String toString() { + return "Jetty " + protocol.getSchemeName() + " " + httpClientTransportMode; + } + } +} diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Protocol.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Protocol.java index fb582cd6a4..a87ed066a5 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Protocol.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/data/Protocol.java @@ -22,7 +22,7 @@ public final class Protocol { /** Indicates that the port number is undefined. */ public static final int UNKNOWN_PORT = -1; - /** All protocols wildcard. */ + /** All-protocols wildcard. */ public static final Protocol ALL = new Protocol("all", "ALL", "Wildcard for all protocols", UNKNOWN_PORT); /** @@ -78,7 +78,7 @@ public final class Protocol { * "riap://component/myAppPath/myResource" and * "riap://application/myResource".
*
- * In order to work, RIAP doesn't requires any client connector and is + * In order to work, RIAP doesn't require any client connector and is * automatically supported by the Restlet engine. * * @see org.restlet.data.LocalReference @@ -168,13 +168,13 @@ public static Protocol valueOf(String name, String version) { private final String name; /** The scheme name. */ - private volatile String schemeName; + private final String schemeName; /** The technical name that appears on the wire. */ private final String technicalName; /** The version. */ - private volatile String version; + private final String version; /** * Constructor. diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/RestletHelper.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/RestletHelper.java index ac62bddcdf..33b6a162c4 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/RestletHelper.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/RestletHelper.java @@ -84,12 +84,12 @@ public T getHelped() { * @return The helped Restlet parameters. */ public Series getHelpedParameters() { - Series result = null; + final Series result; if ((getHelped() != null) && (getHelped().getContext() != null)) { result = getHelped().getContext().getParameters(); } else { - result = new Series(Parameter.class); + result = new Series<>(Parameter.class); } return result; diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java index 0b80dce2cd..ba760acbc3 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/engine/ssl/DefaultSslContextFactory.java @@ -16,8 +16,7 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSocketFactory; import java.io.FileInputStream; -import java.security.KeyStore; -import java.security.SecureRandom; +import java.security.*; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -263,71 +262,39 @@ protected final DefaultSslContextFactory clone() throws CloneNotSupportedExcepti */ @Override public javax.net.ssl.SSLContext createSslContext() throws Exception { - javax.net.ssl.SSLContext result = null; - javax.net.ssl.KeyManagerFactory kmf = null; + final javax.net.ssl.SSLContext result; + final javax.net.ssl.KeyManagerFactory kmf; if ((this.keyStorePath != null) || (this.keyStoreProvider != null) || (this.keyStoreType != null)) { - // Loads the key store. - final String nonNullKeyStoreType = (this.keyStoreType != null) - ? this.keyStoreType - : KeyStore.getDefaultType(); - final KeyStore keyStore = (this.keyStoreProvider != null) - ? KeyStore.getInstance(nonNullKeyStoreType, this.keyStoreProvider) - : KeyStore.getInstance(nonNullKeyStoreType); - - FileInputStream keyStoreInputStream = null; - - try { - keyStoreInputStream = ((this.keyStorePath != null) && (!"NONE".equals(this.keyStorePath))) - ? new FileInputStream(this.keyStorePath) - : null; - keyStore.load(keyStoreInputStream, this.keyStorePassword); - } finally { - if (keyStoreInputStream != null) { - keyStoreInputStream.close(); - } - } + final KeyStore keyStore = loadKeyStore(this.keyStorePath, this.keyStoreProvider, this.keyStoreType, this.keyStorePassword); // Creates the key-manager factory. kmf = javax.net.ssl.KeyManagerFactory.getInstance(this.keyManagerAlgorithm); kmf.init(keyStore, this.keyStoreKeyPassword); + } else { + kmf = null; } - javax.net.ssl.TrustManagerFactory tmf = null; - + final javax.net.ssl.TrustManagerFactory tmf; if ((this.trustStorePath != null) || (this.trustStoreProvider != null) || (this.trustStoreType != null)) { - // Loads the trust store. - String nonNullTrustStoreType = (this.trustStoreType != null) ? this.trustStoreType : KeyStore.getDefaultType(); - KeyStore trustStore = (this.trustStoreProvider != null) - ? KeyStore.getInstance(nonNullTrustStoreType, this.trustStoreProvider) - : KeyStore.getInstance(nonNullTrustStoreType); - - FileInputStream trustStoreInputStream = null; - - try { - trustStoreInputStream = ((this.trustStorePath != null) && (!"NONE".equals(this.trustStorePath))) - ? new FileInputStream(this.trustStorePath) - : null; - trustStore.load(trustStoreInputStream, this.trustStorePassword); - } finally { - if (trustStoreInputStream != null) { - trustStoreInputStream.close(); - } - } + final KeyStore trustStore = loadKeyStore(this.trustStorePath, this.trustStoreProvider, this.trustStoreType, this.trustStorePassword); // Creates the trust-manager factory. tmf = javax.net.ssl.TrustManagerFactory.getInstance(this.trustManagerAlgorithm); tmf.init(trustStore); + } else { + tmf = null; } - // Creates the SSL context - javax.net.ssl.SSLContext sslContext = javax.net.ssl.SSLContext.getInstance(this.protocol); - SecureRandom sr = null; - + final SecureRandom sr; if (this.secureRandomAlgorithm != null) { sr = SecureRandom.getInstance(this.secureRandomAlgorithm); + } else { + sr = null; } + // Creates the SSL context + final javax.net.ssl.SSLContext sslContext = javax.net.ssl.SSLContext.getInstance(this.protocol); sslContext.init(kmf != null ? kmf.getKeyManagers() : null, tmf != null ? tmf.getTrustManagers() : null, sr); // Wraps the SSL context to be able to set cipher suites and other @@ -458,7 +425,7 @@ public String getSecureRandomAlgorithm() { /** * Returns the selected cipher suites. The selection is the subset of supported - * suites that are both in the enable suites and out of the disabled suites. + * suites that are both in the enabled suites and out of the disabled suites. * * @param supportedCipherSuites The initial cipher suites to restrict. * @return The selected cipher suites. @@ -483,8 +450,7 @@ public String[] getSelectedCipherSuites(String[] supportedCipherSuites) { /** * Returns the selected SSL protocols. The selection is the subset of supported - * protocols whose name starts with the name of of - * {@link #getEnabledProtocols()} name. + * protocols whose name starts with the name of {@link #getEnabledProtocols()} name. * * @param supportedProtocols The selected SSL protocols. * @return The selected SSL protocols. @@ -675,6 +641,30 @@ public boolean isWantClientAuthentication() { return wantClientAuthentication; } + /** + * Loads a keystore according to its file path, type and password. + * @param path The file path of the keystore. + * @param provider The name of the keystore provider. + * @param type The keystore type of the keystore. + * @param password the optional password of the keystore. + * @return a keystore. + * @throws Exception + */ + protected KeyStore loadKeyStore(String path, String provider, String type, char[] password) throws Exception { + final String nonNullKeyStoreType = (type != null) ? type : KeyStore.getDefaultType(); + final KeyStore keyStore = (provider != null) + ? KeyStore.getInstance(nonNullKeyStoreType, provider) + : KeyStore.getInstance(nonNullKeyStoreType); + + try (FileInputStream keyStoreInputStream = ((path != null) && (!"NONE".equals(path))) + ? new FileInputStream(path) + : null) { + keyStore.load(keyStoreInputStream, password); + } + + return keyStore; + } + /** * Sets the whitespace-separated list of disabled cipher suites. * diff --git a/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java b/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java index 62b7635b59..e143afd028 100644 --- a/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java +++ b/org.restlet.java/org.restlet/src/main/java/org/restlet/util/Series.java @@ -247,7 +247,7 @@ public String getFirstValue(String name, boolean ignoreCase, String defaultValue /** * Returns the value of the first parameter found with the given name. * - * @param name The parameter name (case sensitive). + * @param name The parameter name (case-sensitive). * @param defaultValue The default value to return if no matching parameter * found or if the parameter has a null value. * @return The value of the first parameter found with the given name or the @@ -258,7 +258,7 @@ public String getFirstValue(String name, String defaultValue) { } /** - * Returns the set of parameter names (case sensitive). + * Returns the set of parameter names (case-sensitive). * * @return The set of parameter names. */ @@ -345,7 +345,7 @@ public String[] getValuesArray(String name, boolean ignoreCase) { * Returns an array of all the values associated to the given parameter name. * * @param name The parameter name to match. - * @param ignoreCase Indicates if the name comparison is case sensitive. + * @param ignoreCase Indicates if the name comparison is case-sensitive. * @param defaultValue The default value to return if no matching parameter * found or if the parameter has a null value. * @return The array of values. @@ -402,7 +402,7 @@ public Map getValuesMap() { /** * Removes all the parameters with a given name. * - * @param name The parameter name (case sensitive). + * @param name The parameter name (case-sensitive). * @return True if the list changed. */ public boolean removeAll(String name) { @@ -413,7 +413,7 @@ public boolean removeAll(String name) { * Removes all the parameters with a given name. * * @param name The parameter name. - * @param ignoreCase Indicates if the name comparison is case insensitive. + * @param ignoreCase Indicates if the name comparison is case-insensitive. * @return True if the list changed. */ public boolean removeAll(String name, boolean ignoreCase) { @@ -436,7 +436,7 @@ public boolean removeAll(String name, boolean ignoreCase) { * Removes from this list the first entry whose name equals the specified name * ignoring the case. * - * @param name The name of the entries to be removed (case sensitive). + * @param name The name of the entries to be removed (case-sensitive). * @return false if no entry has been removed, true otherwise. */ public boolean removeFirst(String name) { @@ -448,7 +448,7 @@ public boolean removeFirst(String name) { * ignoring the case or not. * * @param name The name of the entries to be removed. - * @param ignoreCase Indicates if the name comparison is case insensitive. + * @param ignoreCase Indicates if the name comparison is case-insensitive. * @return false if no entry has been removed, true otherwise. */ public boolean removeFirst(String name, boolean ignoreCase) { @@ -468,7 +468,7 @@ public boolean removeFirst(String name, boolean ignoreCase) { /** * Replaces the value of the first parameter with the given name and removes all - * other parameters with the same name. The name matching is case sensitive. + * other parameters with the same name. The name matching is case-sensitive. * * @param name The parameter name. * @param value The value to set. @@ -484,7 +484,7 @@ public T set(String name, String value) { * * @param name The parameter name. * @param value The value to set. - * @param ignoreCase Indicates if the name comparison is case insensitive. + * @param ignoreCase Indicates if the name comparison is case-insensitive. * @return The parameter set or added. */ public T set(String name, String value, boolean ignoreCase) { @@ -531,7 +531,7 @@ public Series subList(int fromIndex, int toIndex) { /** * Returns a list of all the values associated to the parameter name. * - * @param name The parameter name (case sensitive). + * @param name The parameter name (case-sensitive). * @return The list of values. */ public Series subList(String name) { @@ -542,7 +542,7 @@ public Series subList(String name) { * Returns a list of all the values associated to the parameter name. * * @param name The parameter name. - * @param ignoreCase Indicates if the name comparison is case insensitive. + * @param ignoreCase Indicates if the name comparison is case-insensitive. * @return The list of values. */ public Series subList(String name, boolean ignoreCase) {