Skip to content

Commit f605e91

Browse files
committed
Merge branch 'ssl-integration'
2 parents 81246fa + b48963d commit f605e91

File tree

3 files changed

+124
-7
lines changed

3 files changed

+124
-7
lines changed

core/src/main/java/com/xatkit/core/server/XatkitServer.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import org.apache.http.config.SocketConfig;
1313
import org.apache.http.impl.bootstrap.HttpServer;
1414
import org.apache.http.impl.bootstrap.ServerBootstrap;
15+
import org.apache.http.ssl.SSLContexts;
1516

1617
import javax.annotation.Nonnull;
1718
import javax.annotation.Nullable;
19+
import javax.net.ssl.SSLContext;
1820
import java.io.File;
1921
import java.io.IOException;
2022
import java.net.BindException;
@@ -23,6 +25,11 @@
2325
import java.nio.file.NoSuchFileException;
2426
import java.nio.file.Path;
2527
import java.nio.file.Paths;
28+
import java.security.KeyManagementException;
29+
import java.security.KeyStoreException;
30+
import java.security.NoSuchAlgorithmException;
31+
import java.security.UnrecoverableKeyException;
32+
import java.security.cert.CertificateException;
2633
import java.text.MessageFormat;
2734
import java.util.Collection;
2835
import java.util.Collections;
@@ -117,11 +124,21 @@ public XatkitServer(Configuration configuration) {
117124
(), Configuration.class.getSimpleName(), configuration);
118125
Log.info("Creating {0}", this.getClass().getSimpleName());
119126
this.isStarted = false;
127+
if (configuration.containsKey(XatkitServerUtils.SERVER_KEYSTORE_LOCATION_KEY)
128+
&& !configuration.containsKey(XatkitServerUtils.SERVER_PUBLIC_URL_KEY)) {
129+
/*
130+
* We could automatically set the default URL to https://localhost, but it doesn't really make sense. If
131+
* this is an issue we can update XatkitServer's behavior to adapt the default public URL when there is
132+
* an SSL context.
133+
*/
134+
throw new XatkitException(MessageFormat.format("Cannot start the {0}: the configuration contains a " +
135+
"keystore location but does not contain the server''s public_url ({1}))",
136+
this.getClass().getSimpleName(), XatkitServerUtils.SERVER_PUBLIC_URL_KEY));
137+
}
120138
String publicUrl = configuration.getString(XatkitServerUtils.SERVER_PUBLIC_URL_KEY,
121139
XatkitServerUtils.DEFAULT_SERVER_LOCATION);
122140
this.port = configuration.getInt(XatkitServerUtils.SERVER_PORT_KEY, XatkitServerUtils.DEFAULT_SERVER_PORT);
123141
this.baseURL = publicUrl + ":" + Integer.toString(this.port);
124-
Log.info("{0} started on {1}", this.getClass().getSimpleName(), this.getBaseURL());
125142
this.restEndpoints = new HashMap<>();
126143
this.contentDirectory = FileUtils.getFile(XatkitServerUtils.PUBLIC_DIRECTORY_NAME, configuration);
127144
this.contentDirectory.mkdirs();
@@ -131,6 +148,8 @@ public XatkitServer(Configuration configuration) {
131148
} catch (IOException e) {
132149
throw new XatkitException("Cannot initialize the Xatkit server, see the attached exception", e);
133150
}
151+
SSLContext sslContext = createSSLContext(configuration);
152+
134153
SocketConfig socketConfig = SocketConfig.custom()
135154
.setSoTimeout(15000)
136155
.setTcpNoDelay(true)
@@ -139,6 +158,10 @@ public XatkitServer(Configuration configuration) {
139158
server = ServerBootstrap.bootstrap()
140159
.setListenerPort(port)
141160
.setServerInfo("Xatkit/1.1")
161+
/*
162+
* createSSLContext is @Nullable: setting a null SSLContext is similar to not setting it.
163+
*/
164+
.setSslContext(sslContext)
142165
.setSocketConfig(socketConfig)
143166
.setExceptionLogger(e -> {
144167
if (e instanceof SocketTimeoutException) {
@@ -157,6 +180,53 @@ public XatkitServer(Configuration configuration) {
157180
.create();
158181
}
159182

183+
/**
184+
* Creates a {@link SSLContext} from the provided {@code configuration}.
185+
*
186+
* @param configuration the {@link Configuration} containing the SSL configuration
187+
* @return the {@link SSLContext}, or {@code null} if the provided {@code configuration} does not contain an SSL
188+
* configuration
189+
* @throws XatkitException if the provided keystore does not exist of if an error occurred when loading the
190+
* keystore content
191+
* @throws NullPointerException if the {@code configuration} contains a keystore location but does not contain a
192+
* store/key password
193+
*/
194+
private @Nullable
195+
SSLContext createSSLContext(@Nonnull Configuration configuration) {
196+
checkNotNull(configuration, "Cannot get the %s from the provided %s %s", SSLContext.class.getSimpleName(),
197+
Configuration.class.getSimpleName(), configuration);
198+
String keystorePath = configuration.getString(XatkitServerUtils.SERVER_KEYSTORE_LOCATION_KEY);
199+
if (isNull(keystorePath)) {
200+
Log.info("No SSL context to load");
201+
return null;
202+
}
203+
File keystoreFile = FileUtils.getFile(keystorePath, configuration);
204+
if (keystoreFile.exists()) {
205+
String storePassword = configuration.getString(XatkitServerUtils.SERVER_KEYSTORE_STORE_PASSWORD_KEY);
206+
String keyPassword = configuration.getString(XatkitServerUtils.SERVER_KEYSTORE_KEY_PASSWORD_KEY);
207+
checkNotNull(storePassword, "Cannot load the provided keystore, property %s not set",
208+
XatkitServerUtils.SERVER_KEYSTORE_STORE_PASSWORD_KEY);
209+
checkNotNull(keyPassword, "Cannot load the provided keystore, property %s not set",
210+
XatkitServerUtils.SERVER_KEYSTORE_KEY_PASSWORD_KEY);
211+
SSLContext sslContext = null;
212+
213+
try {
214+
sslContext = SSLContexts.custom().loadKeyMaterial(keystoreFile,
215+
storePassword.toCharArray(),
216+
keyPassword.toCharArray())
217+
.build();
218+
return sslContext;
219+
} catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException | CertificateException | IOException | KeyManagementException e) {
220+
throw new XatkitException(MessageFormat.format("Cannot get the {0}: an error occurred when loading " +
221+
"the keystore, see attached exception", SSLContext.class.getSimpleName()));
222+
223+
}
224+
} else {
225+
throw new XatkitException(MessageFormat.format("Cannot get the {0} from the provided keystore location " +
226+
"{1}: the file does not exist", SSLContext.class.getSimpleName(), keystorePath));
227+
}
228+
}
229+
160230
/**
161231
* Returns the port the server is listening to.
162232
*
@@ -650,8 +720,9 @@ private static EndpointEntry of(HttpMethod httpMethod, String uri) {
650720

651721
/**
652722
* Constructs an {@link EndpointEntry} with the provided {@code httpMethod} and {@code uri}.
723+
*
653724
* @param httpMethod the Http method of the entry
654-
* @param uri the URI of the entry
725+
* @param uri the URI of the entry
655726
*/
656727
private EndpointEntry(HttpMethod httpMethod, String uri) {
657728
this.httpMethod = httpMethod;

core/src/main/java/com/xatkit/core/server/XatkitServerUtils.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,24 @@ public interface XatkitServerUtils {
5252
* The directory name used to store Xatkit public content.
5353
*/
5454
String PUBLIC_DIRECTORY_NAME = "public";
55+
56+
/**
57+
* The {@link Configuration} key used to specify the location of the keystore to create the SSL context from.
58+
* <p>
59+
* This property can contain an absolute path to the keystore, or a relative path that will be resolved from the
60+
* location of the configuration file.
61+
*/
62+
String SERVER_KEYSTORE_LOCATION_KEY = "xatkit.server.ssl.keystore";
63+
64+
/**
65+
* The {@link Configuration} key used to specify the {@code store password} of the SSL keystore.
66+
*/
67+
String SERVER_KEYSTORE_STORE_PASSWORD_KEY = "xatkit.server.ssl.keystore.store.password";
68+
69+
/**
70+
* The {@link Configuration} key used to specify the {@code key password} of the SSL keystore.
71+
* <p>
72+
* The value of this property is usually equal to {@link #SERVER_KEYSTORE_STORE_PASSWORD_KEY}.
73+
*/
74+
String SERVER_KEYSTORE_KEY_PASSWORD_KEY = "xatkit.server.ssl.keystore.key.password";
5575
}

core/src/test/java/com/xatkit/core/server/XatkitServerTest.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,28 @@ public void constructConfigurationWithPort() {
108108
this.server = new XatkitServer(configuration);
109109
}
110110

111+
@Test(expected = XatkitException.class)
112+
public void constructConfigurationWithKeystoreNullPublicURL() {
113+
Configuration configuration = new BaseConfiguration();
114+
/*
115+
* test.jks doesn't exist, but the exception should be thrown anyway.
116+
*/
117+
configuration.setProperty(XatkitServerUtils.SERVER_KEYSTORE_LOCATION_KEY, "test.jks");
118+
this.server = new XatkitServer(configuration);
119+
}
120+
121+
@Test(expected = XatkitException.class)
122+
public void constructConfigurationInvalidKeystoreLocation() {
123+
Configuration configuration = new BaseConfiguration();
124+
configuration.setProperty(XatkitServerUtils.SERVER_KEYSTORE_LOCATION_KEY, "test.jks");
125+
configuration.setProperty(XatkitServerUtils.SERVER_PUBLIC_URL_KEY, "https://localhost");
126+
this.server = new XatkitServer(configuration);
127+
}
128+
129+
/*
130+
* TODO test cases with valid keystore
131+
*/
132+
111133
@Test
112134
public void startEmptyConfiguration() {
113135
this.server = new XatkitServer(new BaseConfiguration());
@@ -198,7 +220,8 @@ public void notifyNotAcceptedContentType() {
198220
this.server = getValidXatkitServer();
199221
StubJsonWebhookEventProvider stubJsonWebhookEventProvider = getStubWebhookEventProvider();
200222
this.server.registerWebhookEventProvider(stubJsonWebhookEventProvider);
201-
this.server.notifyRestHandler(HttpMethod.POST, stubJsonWebhookEventProvider.getEndpointURI(), Collections.emptyList(),
223+
this.server.notifyRestHandler(HttpMethod.POST, stubJsonWebhookEventProvider.getEndpointURI(),
224+
Collections.emptyList(),
202225
Collections.emptyList(), "test", "not valid");
203226
assertThat(stubJsonWebhookEventProvider.hasReceivedEvent()).as("WebhookEventProvider hasn't received an " +
204227
"event").isFalse();
@@ -256,7 +279,8 @@ public void isRestEndpointRegisteredUriTrailingSlash() {
256279
// See https://github.com/xatkit-bot-platform/xatkit-runtime/issues/254
257280
this.server = getValidXatkitServer();
258281
this.server.registerRestEndpoint(HttpMethod.POST, VALID_REST_URI, VALID_REST_HANDLER);
259-
assertThat(this.server.isRestEndpoint(HttpMethod.POST, VALID_REST_URI + "/")).as("The URI is valid even with a trailing /").isTrue();
282+
assertThat(this.server.isRestEndpoint(HttpMethod.POST, VALID_REST_URI + "/")).as("The URI is valid even with " +
283+
"a trailing /").isTrue();
260284
}
261285

262286
@Test
@@ -273,7 +297,7 @@ public void isRestEndpointNotRegisteredMethod() {
273297
assertThat(result).as("Provided URI + Method is not a rest endpoint").isFalse();
274298
}
275299

276-
@Test (expected = NullPointerException.class)
300+
@Test(expected = NullPointerException.class)
277301
public void notifyRestHandlerNullMethod() {
278302
this.server = getValidXatkitServer();
279303
this.server.registerRestEndpoint(HttpMethod.POST, VALID_REST_URI, VALID_REST_HANDLER);
@@ -285,7 +309,8 @@ public void notifyRestHandlerNullMethod() {
285309
public void notifyRestHandlerNullUri() {
286310
this.server = getValidXatkitServer();
287311
this.server.registerRestEndpoint(HttpMethod.POST, VALID_REST_URI, VALID_REST_HANDLER);
288-
this.server.notifyRestHandler(HttpMethod.POST, null, Collections.emptyList(), Collections.emptyList(), new JsonObject(),
312+
this.server.notifyRestHandler(HttpMethod.POST, null, Collections.emptyList(), Collections.emptyList(),
313+
new JsonObject(),
289314
ContentType.APPLICATION_JSON.getMimeType());
290315
}
291316

@@ -336,7 +361,8 @@ public void notifyRestHandlerTrailingSlash() {
336361
@Test(expected = XatkitException.class)
337362
public void notifyRestHandlerNotRegisteredUri() {
338363
this.server = getValidXatkitServer();
339-
this.server.notifyRestHandler(HttpMethod.POST, VALID_REST_URI, Collections.emptyList(), Collections.emptyList(), null,
364+
this.server.notifyRestHandler(HttpMethod.POST, VALID_REST_URI, Collections.emptyList(),
365+
Collections.emptyList(), null,
340366
ContentType.APPLICATION_JSON.getMimeType());
341367
}
342368

0 commit comments

Comments
 (0)