Skip to content

Commit 52435b7

Browse files
mvilligercguglielmo
authored andcommitted
Add HTTP/2 support to Jetty
In addition to HTTP/1.1 also HTTP/2 is supported with and without TLS. When using TLS also ALPN is supported which can upgrade an HTTP/1.1 request to an HTTP/2 request if the client supports it. Furthermore, a new property 'scout.jetty.useTls' is introduced to enable TLS without providing a path to a keystore. In this situation a new self-signed in memory certificate is created on each server startup. The creation of a self-signed certificate is only active in development. 359560
1 parent 1416598 commit 52435b7

File tree

13 files changed

+362
-145
lines changed

13 files changed

+362
-145
lines changed

org.eclipse.scout.dev.jetty/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
<groupId>org.eclipse.jetty</groupId>
4040
<artifactId>jetty-plus</artifactId>
4141
</dependency>
42+
<dependency>
43+
<groupId>org.eclipse.jetty.http2</groupId>
44+
<artifactId>http2-server</artifactId>
45+
</dependency>
46+
<dependency>
47+
<!-- alpn implementation, needed to enable HTTP/2 over TLS, see https://xy2401.com/local-docs/java/jetty.9.4.24.v20191120/alpn-chapter.html -->
48+
<groupId>org.eclipse.jetty</groupId>
49+
<artifactId>jetty-alpn-java-server</artifactId>
50+
</dependency>
4251
<dependency>
4352
<groupId>org.eclipse.scout.rt</groupId>
4453
<artifactId>org.eclipse.scout.rt.platform</artifactId>

org.eclipse.scout.dev.jetty/src/main/java/org/eclipse/scout/dev/jetty/JettyConfiguration.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
*/
1010
package org.eclipse.scout.dev.jetty;
1111

12+
import org.eclipse.scout.rt.platform.BEANS;
13+
import org.eclipse.scout.rt.platform.config.AbstractBooleanConfigProperty;
1214
import org.eclipse.scout.rt.platform.config.AbstractPortConfigProperty;
1315
import org.eclipse.scout.rt.platform.config.AbstractStringConfigProperty;
16+
import org.eclipse.scout.rt.platform.config.CONFIG;
17+
import org.eclipse.scout.rt.platform.util.StringUtility;
1418

1519
public final class JettyConfiguration {
1620

@@ -46,7 +50,31 @@ public String getKey() {
4650

4751
@Override
4852
public String description() {
49-
return "Setting this property enables the jetty https connector. For example 'file:/dev/my-https.jks'";
53+
return "Setting this property enables the jetty https connector using this keystore. "
54+
+ "For example 'classpath:/dev/my-https.jks' or 'file:///C:/Users/usr/Desktop/my-store.jks' or 'C:/Users/usr/Desktop/my-store.jks'.";
55+
}
56+
}
57+
58+
/**
59+
* @since 24.1
60+
*/
61+
public static class ScoutJettyUseTlsProperty extends AbstractBooleanConfigProperty {
62+
63+
@Override
64+
public String getKey() {
65+
return "scout.jetty.useTls";
66+
}
67+
68+
@Override
69+
public String description() {
70+
return "Specifies if the Jetty server should use TLS. If true, the server must either be started in development mode (then a new self-singed certificate is created automatically),"
71+
+ "or a Java KeyStore must be configured using property '" + BEANS.get(ScoutJettyKeyStorePathProperty.class).getKey() + "'."
72+
+ "By default this property is true, if a Java KeyStore has been specified.";
73+
}
74+
75+
@Override
76+
public Boolean getDefaultValue() {
77+
return StringUtility.hasText(CONFIG.getPropertyValue(ScoutJettyKeyStorePathProperty.class));
5078
}
5179
}
5280

@@ -61,10 +89,10 @@ public String getKey() {
6189

6290
@Override
6391
public String description() {
64-
return "Setting this property to a valid X-500 name will automatically generate a self-signed SSL certificate and store it in the keystore file path specified.\n"
65-
+ "This property is the X500 name (DN) for which the certificate is issued.\n"
92+
return "Specifies the X-500 name to use in the self-signed certificate when starting Jetty in development mode with TLS enabled."
6693
+ "For example 'CN=my-host.my-domain.com,C=US,ST=CA,L=Sunnyvale,O=My Company Inc.'.\n"
67-
+ "Use in development only!";
94+
+ "This property is only used in development mode and only if the property '" + BEANS.get(ScoutJettyUseTlsProperty.class).getKey() + "' is true "
95+
+ "and no existing Java keystore is specified (property '" + BEANS.get(ScoutJettyKeyStorePathProperty.class).getKey() + "').";
6896
}
6997
}
7098

@@ -94,7 +122,7 @@ public String getKey() {
94122

95123
@Override
96124
public String description() {
97-
return "Https private key password. Supports obfuscated values prefixed with 'OBF:'.";
125+
return "The password (if any) for the specific key within the key store. Supports obfuscated values prefixed with 'OBF:'.";
98126
}
99127
}
100128

@@ -109,7 +137,7 @@ public String getKey() {
109137

110138
@Override
111139
public String description() {
112-
return "Https certificate alias in keystore.";
140+
return "Https certificate alias of the key in the keystore to use.";
113141
}
114142
}
115143
}

org.eclipse.scout.dev.jetty/src/main/java/org/eclipse/scout/dev/jetty/JettyServer.java

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414
import java.io.IOException;
1515
import java.io.InputStreamReader;
1616
import java.net.MalformedURLException;
17+
import java.net.URI;
1718
import java.net.URISyntaxException;
1819
import java.net.URL;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
1922
import java.nio.file.Paths;
23+
import java.security.KeyStore;
2024
import java.util.Collections;
2125
import java.util.Enumeration;
2226
import java.util.HashSet;
@@ -31,7 +35,9 @@
3135
import javax.servlet.http.HttpServletRequest;
3236
import javax.servlet.http.HttpServletResponse;
3337

34-
import org.eclipse.jetty.http.HttpVersion;
38+
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
39+
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
40+
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
3541
import org.eclipse.jetty.server.Handler;
3642
import org.eclipse.jetty.server.HttpConfiguration;
3743
import org.eclipse.jetty.server.HttpConnectionFactory;
@@ -50,9 +56,12 @@
5056
import org.eclipse.scout.dev.jetty.JettyConfiguration.ScoutJettyKeyStorePasswordProperty;
5157
import org.eclipse.scout.dev.jetty.JettyConfiguration.ScoutJettyKeyStorePathProperty;
5258
import org.eclipse.scout.dev.jetty.JettyConfiguration.ScoutJettyPrivateKeyPasswordProperty;
59+
import org.eclipse.scout.dev.jetty.JettyConfiguration.ScoutJettyUseTlsProperty;
5360
import org.eclipse.scout.rt.platform.BEANS;
61+
import org.eclipse.scout.rt.platform.Platform;
5462
import org.eclipse.scout.rt.platform.config.CONFIG;
5563
import org.eclipse.scout.rt.platform.config.PlatformConfigProperties.PlatformDevModeProperty;
64+
import org.eclipse.scout.rt.platform.config.PropertiesHelper;
5665
import org.eclipse.scout.rt.platform.exception.PlatformException;
5766
import org.eclipse.scout.rt.platform.exception.ProcessingException;
5867
import org.eclipse.scout.rt.platform.security.ICertificateProvider;
@@ -111,7 +120,7 @@ protected String getContextPath() {
111120
}
112121

113122
protected boolean isUseTls() {
114-
return CONFIG.getPropertyValue(ScoutJettyKeyStorePathProperty.class) != null;
123+
return CONFIG.getPropertyValue(ScoutJettyUseTlsProperty.class);
115124
}
116125

117126
protected void startInternal() throws Exception {
@@ -233,7 +242,8 @@ protected ServerConnector createHttpServerConnector(int port) {
233242
HttpConfiguration httpConfig = new HttpConfiguration();
234243
httpConfig.setSendServerVersion(false);
235244
httpConfig.setSendDateHeader(false);
236-
ServerConnector http = new ServerConnector(m_server, new HttpConnectionFactory(httpConfig));
245+
httpConfig.setSendXPoweredBy(false);
246+
ServerConnector http = new ServerConnector(m_server, new HttpConnectionFactory(httpConfig), new HTTP2CServerConnectionFactory(httpConfig));
237247
http.setPort(port);
238248
return http;
239249
}
@@ -244,25 +254,59 @@ protected ServerConnector createHttpsServerConnector(int port) {
244254
httpsConfig.addCustomizer(new SecureRequestCustomizer());
245255
httpsConfig.setSendServerVersion(false);
246256
httpsConfig.setSendDateHeader(false);
247-
ServerConnector https = new ServerConnector(m_server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig));
257+
httpsConfig.setSendXPoweredBy(false);
258+
259+
HttpConnectionFactory http11 = new HttpConnectionFactory(httpsConfig);
260+
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpsConfig);
261+
262+
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
263+
alpn.setDefaultProtocol(http11.getProtocol());
264+
265+
SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, alpn.getProtocol());
266+
ServerConnector https = new ServerConnector(m_server, tls, alpn, http2, http11);
267+
248268
https.setPort(port);
249269
return https;
250270
}
251271

252272
protected SslContextFactory.Server createSslContextFactory() {
253273
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
254-
String keyStorePath = resolveKeyStorePath(CONFIG.getPropertyValue(ScoutJettyKeyStorePathProperty.class));
255-
String autoCertName = CONFIG.getPropertyValue(ScoutJettyAutoCreateSelfSignedCertificateProperty.class);
256-
String storepass = CONFIG.getPropertyValue(ScoutJettyKeyStorePasswordProperty.class);
257-
String keypass = CONFIG.getPropertyValue(ScoutJettyPrivateKeyPasswordProperty.class);
274+
Path keyStorePath = resolveKeyStorePath(CONFIG.getPropertyValue(ScoutJettyKeyStorePathProperty.class));
275+
String keyStoreUri = keyStorePath == null ? null : keyStorePath.toUri().toString();
276+
String storePass = ObjectUtility.nvl(CONFIG.getPropertyValue(ScoutJettyKeyStorePasswordProperty.class), "");
277+
String keyPass = ObjectUtility.nvl(CONFIG.getPropertyValue(ScoutJettyPrivateKeyPasswordProperty.class), "");
258278
String certAlias = CONFIG.getPropertyValue(ScoutJettyCertificateAliasProperty.class);
259-
if (autoCertName != null) {
260-
BEANS.optional(ICertificateProvider.class).ifPresent(p -> p.autoCreateSelfSignedCertificate(keyStorePath, storepass.toCharArray(), keypass.toCharArray(), certAlias, autoCertName));
279+
280+
boolean keyStoreExists = keyStorePath != null && Files.isRegularFile(keyStorePath);
281+
if (Platform.get().inDevelopmentMode() && !keyStoreExists) {
282+
String autoCertNamePropValue = CONFIG.getPropertyValue(ScoutJettyAutoCreateSelfSignedCertificateProperty.class);
283+
String autoCertName = StringUtility.hasText(autoCertNamePropValue) ? autoCertNamePropValue : "CN=localhost";
284+
if (!StringUtility.hasText(certAlias)) {
285+
certAlias = "localhost";
286+
}
287+
288+
LOG.info("No existing keystore was provided to setup TLS. Creating a self-signed certificate '{}'.", autoCertName);
289+
ICertificateProvider certificateProvider = BEANS.optional(ICertificateProvider.class)
290+
.orElseThrow(() -> new PlatformException("No certificate-provider available to create a self-signed certificate to use for TLS."
291+
+ " Add a certificate-provider or specify an existing keystore using property '{}'.", BEANS.get(ScoutJettyKeyStorePathProperty.class).getKey()));
292+
if (keyStorePath == null) {
293+
// no path available: create in memory only
294+
KeyStore ks = certificateProvider.createSelfSignedCertificate(certAlias, autoCertName, storePass.toCharArray(), keyPass.toCharArray());
295+
sslContextFactory.setKeyStore(ks);
296+
}
297+
else {
298+
// a non-existing key-store path was provided: create a new keystore file at that location
299+
LOG.info("Storing created keystore in '{}'.", keyStoreUri);
300+
certificateProvider.autoCreateSelfSignedCertificate(keyStoreUri, storePass.toCharArray(), keyPass.toCharArray(), certAlias, autoCertName);
301+
sslContextFactory.setKeyStorePath(keyStoreUri);
302+
}
303+
}
304+
else {
305+
LOG.info("Setup TLS certificate using alias '{}' from keystore '{}'.", certAlias, keyStoreUri);
306+
sslContextFactory.setKeyStorePath(keyStoreUri);
261307
}
262-
LOG.info("Setup SSL certificate using alias '{}' from keystore '{}'.", certAlias, keyStorePath);
263-
sslContextFactory.setKeyStorePath(keyStorePath);
264-
sslContextFactory.setKeyStorePassword(storepass);
265-
sslContextFactory.setKeyManagerPassword(keypass);
308+
sslContextFactory.setKeyStorePassword(storePass);
309+
sslContextFactory.setKeyManagerPassword(keyPass);
266310
sslContextFactory.setCertAlias(certAlias);
267311
sslContextFactory.setEndpointIdentificationAlgorithm("https");
268312
sslContextFactory.setIncludeCipherSuites(
@@ -291,25 +335,31 @@ protected SslContextFactory.Server createSslContextFactory() {
291335
return sslContextFactory;
292336
}
293337

294-
protected String resolveKeyStorePath(String path) {
295-
if (path != null && path.startsWith("classpath:")) {
296-
String subPath = path.substring(10);
297-
URL res;
298-
res = getClass().getResource(subPath);
338+
protected Path resolveKeyStorePath(String path) {
339+
if (!StringUtility.hasText(path)) {
340+
return null;
341+
}
342+
if (path.startsWith(PropertiesHelper.CLASSPATH_PREFIX)) {
343+
String subPath = path.substring(PropertiesHelper.CLASSPATH_PREFIX.length());
344+
URL res = getClass().getResource(subPath);
299345
if (res == null) {
300346
res = getClass().getClassLoader().getResource(subPath);
301347
}
302348
if (res == null) {
303349
res = ClassLoader.getSystemClassLoader().getResource(subPath);
304350
}
305351
if (res == null) {
306-
throw new ProcessingException("Missing resource defined by config property: {}={}",
307-
BEANS.get(ScoutJettyKeyStorePathProperty.class).getKey(),
308-
path);
352+
throw new ProcessingException("Missing resource defined by config property: {}={}", BEANS.get(ScoutJettyKeyStorePathProperty.class).getKey(), path);
309353
}
310-
return res.toExternalForm();
354+
path = res.toExternalForm();
355+
}
356+
try {
357+
return Paths.get(URI.create(path));
358+
}
359+
catch (Exception e) {
360+
LOG.debug("Path '{}' is no valid URI. Trying to read as file path.", path, e);
361+
return Paths.get(path);
311362
}
312-
return path;
313363
}
314364

315365
protected static class P_WebAppContext extends WebAppContext {
@@ -382,7 +432,7 @@ public Set<String> getResourcePaths(String path) {
382432
* redirected.
383433
* <p>
384434
* Example for contextPath = <code>/myapp</code>:
385-
* <table border=1 cellspacing=0 cellpadding=3>
435+
* <table border=1>
386436
* <tr>
387437
* <th>Request URI</th>
388438
* <th>Redirected to</th>

org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/util/ConnectionErrorDetectorTest.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
*/
1010
package org.eclipse.scout.rt.platform.util;
1111

12+
import static org.junit.Assert.assertEquals;
13+
1214
import java.io.IOException;
1315
import java.io.InterruptedIOException;
1416
import java.net.SocketException;
@@ -33,12 +35,14 @@ public class ConnectionErrorDetectorTest {
3335
@Parameters
3436
public static List<IScoutTestParameter> getParameters() {
3537
List<IScoutTestParameter> parametersList = new LinkedList<>();
38+
parametersList.add(new ExceptionTestParameter(null, false));
3639
parametersList.add(new ExceptionTestParameter(new SocketException(CONNECTION_RESET_MESSAGE), true));
3740
parametersList.add(new ExceptionTestParameter(new SocketException(BROKEN_PIPE_MESSAGE), true));
3841
parametersList.add(new ExceptionTestParameter(new SocketException("unknown error"), false));
3942

4043
parametersList.add(new ExceptionTestParameter(new EofException(CONNECTION_RESET_MESSAGE), true));
4144
parametersList.add(new ExceptionTestParameter(new EofException(BROKEN_PIPE_MESSAGE), true));
45+
parametersList.add(new ExceptionTestParameter(new EofException("cancel_stream_error"), true));
4246
parametersList.add(new ExceptionTestParameter(new EofException("unknown error"), false));
4347

4448
parametersList.add(new ExceptionTestParameter(new ClientAbortException(CONNECTION_RESET_MESSAGE), true));
@@ -50,6 +54,8 @@ public static List<IScoutTestParameter> getParameters() {
5054
parametersList.add(new ExceptionTestParameter(new InterruptedIOException("unknown error"), false));
5155

5256
parametersList.add(new ExceptionTestParameter(new IOException(CONNECTION_RESET_MESSAGE), true));
57+
parametersList.add(new ExceptionTestParameter(new IOException("Connection reset"), true));
58+
parametersList.add(new ExceptionTestParameter(new IOException("An established connection was aborted by the software in your host machine"), true));
5359
parametersList.add(new ExceptionTestParameter(new IOException(BROKEN_PIPE_MESSAGE), true));
5460
parametersList.add(new ExceptionTestParameter(new IOException("unknown error"), false));
5561

@@ -86,7 +92,7 @@ private static CycledCauseException createUndetectedCycledCauseException() {
8692
return cycledCauseException;
8793
}
8894

89-
private final ExceptionTestParameter m_testParameter;
95+
public final ExceptionTestParameter m_testParameter;
9096

9197
public ConnectionErrorDetectorTest(ExceptionTestParameter testParameter) {
9298
m_testParameter = testParameter;
@@ -95,12 +101,7 @@ public ConnectionErrorDetectorTest(ExceptionTestParameter testParameter) {
95101
@Test
96102
public void testIsConnectionError() {
97103
ConnectionErrorDetector connectionErrorDetector = BEANS.get(ConnectionErrorDetector.class);
98-
if (m_testParameter.isDetectable()) {
99-
Assert.assertTrue(connectionErrorDetector.isConnectionError(m_testParameter.getException()));
100-
}
101-
else {
102-
Assert.assertFalse(connectionErrorDetector.isConnectionError(m_testParameter.getException()));
103-
}
104+
assertEquals(m_testParameter.isDetectable(), connectionErrorDetector.isConnectionError(m_testParameter.getException()));
104105
}
105106

106107
private static class ClientAbortException extends IOException {
@@ -148,13 +149,13 @@ public synchronized Throwable getCause() {
148149
}
149150
}
150151

151-
private static class ExceptionTestParameter extends AbstractScoutTestParameter {
152+
public static class ExceptionTestParameter extends AbstractScoutTestParameter {
152153

153154
private Exception m_exception;
154155
private boolean m_isDetectable;
155156

156157
public ExceptionTestParameter(Exception exception, boolean isDetectable) {
157-
super(exception.getClass().getSimpleName() + ":" + exception.getMessage());
158+
super(exception == null ? "null" : exception.getClass().getSimpleName() + ":" + exception.getMessage());
158159
m_exception = exception;
159160
m_isDetectable = isDetectable;
160161
}

0 commit comments

Comments
 (0)