Skip to content

Commit 0faaef8

Browse files
authored
Https support (#251)
Add HTTPS support
1 parent 40d603f commit 0faaef8

File tree

5 files changed

+225
-5
lines changed

5 files changed

+225
-5
lines changed

config/config.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ pipelinesDirectory="config/pipelines"
66
# ~~~~
77
httpHost="0.0.0.0"
88
httpPort=7090
9+
httpsHost="0.0.0.0"
10+
httpsPort=7091
11+
enableHttps=true
12+
#httpsCertificatePath=/opt/mad/tls/cert.pem
13+
#httpsKeyPath=/opt/mad/tls/key.pem
914
#httpHealthCheckPath="/ping"
1015
#httpStatusPath="/status"
1116
#supplementalHttpRoutesClass="com.example.MyAkkaRoutes"

pom.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
<arpnetworking.commons.version>1.19.0</arpnetworking.commons.version>
7979
<aspectjrt.version>1.9.2</aspectjrt.version>
8080
<asynchttpclient.version>2.6.0</asynchttpclient.version>
81+
<bouncy.castle.version>1.69</bouncy.castle.version>
8182
<cglib.version>3.3.0</cglib.version>
8283
<client.protocol.version>0.12.0</client.protocol.version>
8384
<fastutil.version>8.2.2</fastutil.version>
@@ -87,6 +88,7 @@
8788
<javassist.version>3.24.1-GA</javassist.version>
8889
<javassist.maven.core.version>0.2.2</javassist.maven.core.version>
8990
<jsr305.version>3.0.2</jsr305.version>
91+
<kafka.client.version>2.2.1</kafka.client.version>
9092
<logback.version>1.2.3</logback.version>
9193
<logback.steno.version>1.18.2</logback.steno.version>
9294
<log4j.over.slf4j.version>1.7.25</log4j.over.slf4j.version>
@@ -458,6 +460,7 @@
458460
<ports>
459461
<port>+mad.ip:${debugJavaPort}:${debugJavaPort}</port>
460462
<port>+mad.ip:7090:7090</port>
463+
<port>+mad.ip:7091:7091</port>
461464
<port>+mad.ip:8125:8125/udp</port>
462465
</ports>
463466
<env>
@@ -920,7 +923,17 @@
920923
<dependency>
921924
<groupId>org.apache.kafka</groupId>
922925
<artifactId>kafka-clients</artifactId>
923-
<version>2.2.1</version>
926+
<version>${kafka.client.version}</version>
927+
</dependency>
928+
<dependency>
929+
<groupId>org.bouncycastle</groupId>
930+
<artifactId>bcprov-jdk15on</artifactId>
931+
<version>${bouncy.castle.version}</version>
932+
</dependency>
933+
<dependency>
934+
<groupId>org.bouncycastle</groupId>
935+
<artifactId>bcpkix-jdk15on</artifactId>
936+
<version>${bouncy.castle.version}</version>
924937
</dependency>
925938
</dependencies>
926939

src/main/docker/mad/Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
FROM openjdk:8u212-jre-alpine
15+
FROM openjdk:8u302-jre-slim
1616

1717
MAINTAINER arpnetworking
1818

@@ -30,10 +30,13 @@ ENV MAD_CONFIG="/opt/mad/config/config.conf"
3030
ENV JAVA_OPTS=""
3131

3232
# Build
33-
RUN apk add --no-cache su-exec && \
33+
RUN apt update && \
34+
apt install -y gosu openssl && \
3435
mkdir -p /opt/mad/lib/ext && \
3536
mkdir -p /opt/mad/logs && \
36-
mkdir -p /opt/mad/config/pipelines
37+
mkdir -p /opt/mad/config/pipelines && \
38+
mkdir -p /opt/mad/tls && \
39+
openssl req -x509 -newkey rsa:4096 -keyout /opt/mad/tls/key.pem -out /opt/mad/tls/cert.pem -days 3650 -nodes -addext "subjectAltName = DNS:localhost" -subj '/CN=localhost'
3740
ADD deps /opt/mad/lib/
3841
ADD bin /opt/mad/bin/
3942
ADD config /opt/mad/config/

src/main/java/com/arpnetworking/metrics/mad/Main.java

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import akka.actor.Props;
2222
import akka.actor.Terminated;
2323
import akka.dispatch.Dispatcher;
24+
import akka.http.javadsl.ConnectionContext;
2425
import akka.http.javadsl.Http;
26+
import akka.http.javadsl.HttpsConnectionContext;
2527
import akka.stream.Materializer;
2628
import ch.qos.logback.classic.LoggerContext;
2729
import com.arpnetworking.commons.builder.Builder;
@@ -51,6 +53,7 @@
5153
import com.arpnetworking.utility.Launchable;
5254
import com.fasterxml.jackson.databind.JsonNode;
5355
import com.fasterxml.jackson.databind.ObjectMapper;
56+
import com.google.common.base.Charsets;
5457
import com.google.common.collect.ImmutableList;
5558
import com.google.common.collect.Maps;
5659
import com.google.common.collect.Sets;
@@ -61,13 +64,35 @@
6164
import com.typesafe.config.Config;
6265
import com.typesafe.config.ConfigFactory;
6366
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
67+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
68+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
69+
import org.bouncycastle.openssl.PEMParser;
70+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
71+
import org.bouncycastle.util.io.pem.PemObject;
6472
import scala.concurrent.Await;
6573
import scala.concurrent.ExecutionContextExecutor;
6674
import scala.concurrent.Future;
6775
import scala.concurrent.duration.Duration;
6876

77+
import java.io.ByteArrayInputStream;
6978
import java.io.File;
79+
import java.io.FileInputStream;
80+
import java.io.FileNotFoundException;
81+
import java.io.IOException;
82+
import java.io.InputStreamReader;
7083
import java.net.URI;
84+
import java.security.KeyManagementException;
85+
import java.security.KeyStore;
86+
import java.security.KeyStoreException;
87+
import java.security.NoSuchAlgorithmException;
88+
import java.security.PrivateKey;
89+
import java.security.SecureRandom;
90+
import java.security.Security;
91+
import java.security.UnrecoverableKeyException;
92+
import java.security.cert.Certificate;
93+
import java.security.cert.CertificateException;
94+
import java.security.cert.CertificateFactory;
95+
import java.security.cert.X509Certificate;
7196
import java.util.ArrayList;
7297
import java.util.List;
7398
import java.util.Locale;
@@ -81,6 +106,9 @@
81106
import java.util.concurrent.Semaphore;
82107
import java.util.concurrent.TimeUnit;
83108
import javax.annotation.Nullable;
109+
import javax.net.ssl.KeyManagerFactory;
110+
import javax.net.ssl.SSLContext;
111+
import javax.net.ssl.TrustManagerFactory;
84112

85113
/**
86114
* Class containing entry point for Metrics Data Aggregator (MAD).
@@ -248,7 +276,73 @@ private void launchActors(final Injector injector) {
248276
supplementalHttpRoutes.build());
249277
final Http http = Http.get(actorSystem);
250278

251-
http.newServerAt(_configuration.getHttpHost(), _configuration.getHttpPort()).bind(routes);
279+
http.newServerAt(_configuration.getHttpHost(), _configuration.getHttpPort()).withMaterializer(materializer).bind(routes);
280+
if (_configuration.getEnableHttps()) {
281+
Security.addProvider(new BouncyCastleProvider());
282+
final HttpsConnectionContext httpsContext = createHttpsContext();
283+
http.newServerAt(_configuration.getHttpsHost(), _configuration.getHttpsPort())
284+
.withMaterializer(materializer).enableHttps(httpsContext)
285+
.bind(routes);
286+
}
287+
}
288+
289+
private HttpsConnectionContext createHttpsContext() {
290+
final PrivateKey privateKey;
291+
try (PEMParser keyReader = new PEMParser(
292+
new InputStreamReader(
293+
new FileInputStream(_configuration.getHttpsKeyPath()), Charsets.UTF_8))) {
294+
final Object keyObject = keyReader.readObject();
295+
296+
if (keyObject instanceof PrivateKeyInfo) {
297+
final PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) keyObject;
298+
privateKey = new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
299+
} else {
300+
throw new RuntimeException(
301+
String.format("Invalid private key format, expected PEM private key, got %s", keyObject.getClass().getName()));
302+
}
303+
} catch (final FileNotFoundException e) {
304+
throw new RuntimeException(String.format("Could not find https key file: %s", _configuration.getHttpsKeyPath()), e);
305+
} catch (final IOException e) {
306+
throw new RuntimeException(String.format("Error reading https key file: %s", _configuration.getHttpsKeyPath()), e);
307+
}
308+
309+
final X509Certificate cert;
310+
try (PEMParser certReader = new PEMParser(
311+
new InputStreamReader(
312+
new FileInputStream(_configuration.getHttpsCertificatePath()), Charsets.UTF_8))) {
313+
final PemObject certObject = certReader.readPemObject();
314+
final CertificateFactory cf = CertificateFactory.getInstance("X.509");
315+
cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certObject.getContent()));
316+
} catch (final FileNotFoundException e) {
317+
throw new RuntimeException(
318+
String.format("Could not find https certificate file: %s", _configuration.getHttpsCertificatePath()), e);
319+
} catch (final IOException e) {
320+
throw new RuntimeException(
321+
String.format("Error reading https certificate file: %s", _configuration.getHttpsCertificatePath()), e);
322+
} catch (final CertificateException e) {
323+
throw new RuntimeException("Error building X509 certificate", e);
324+
}
325+
326+
try {
327+
final KeyStore ks = KeyStore.getInstance("JKS");
328+
ks.load(null, null);
329+
ks.setCertificateEntry("cert", cert);
330+
331+
ks.setKeyEntry("key", privateKey, new char[0], new Certificate[]{cert});
332+
final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
333+
keyManagerFactory.init(ks, new char[0]);
334+
335+
final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
336+
tmf.init(ks);
337+
338+
final SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
339+
sslContext.init(keyManagerFactory.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
340+
341+
return ConnectionContext.httpsServer(sslContext);
342+
} catch (final UnrecoverableKeyException | KeyManagementException | NoSuchAlgorithmException | IOException
343+
| KeyStoreException | CertificateException e) {
344+
throw new RuntimeException("Could not create SSLContext for https configuration", e);
345+
}
252346
}
253347

254348
@SuppressWarnings("deprecation")

src/main/java/com/arpnetworking/metrics/mad/configuration/AggregatorConfiguration.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@ public int getHttpPort() {
8686
return _httpPort;
8787
}
8888

89+
public String getHttpsHost() {
90+
return _httpsHost;
91+
}
92+
93+
public int getHttpsPort() {
94+
return _httpsPort;
95+
}
96+
97+
public String getHttpsKeyPath() {
98+
return _httpsKeyPath;
99+
}
100+
101+
public String getHttpsCertificatePath() {
102+
return _httpsCertificatePath;
103+
}
104+
105+
public boolean getEnableHttps() {
106+
return _enableHttps;
107+
}
108+
89109
public String getHttpHealthCheckPath() {
90110
return _httpHealthCheckPath;
91111
}
@@ -120,6 +140,11 @@ public String toString() {
120140
.add("PipelinesDirectory", _pipelinesDirectory)
121141
.add("HttpHost", _httpHost)
122142
.add("HttpPort", _httpPort)
143+
.add("HttpsHost", _httpsHost)
144+
.add("HttpsPort", _httpsPort)
145+
.add("HttpsKeyPath", _httpsKeyPath)
146+
.add("HttpsCertificatePath", _httpsCertificatePath)
147+
.add("EnableHttps", _enableHttps)
123148
.add("HttpHealthCheckPath", _httpHealthCheckPath)
124149
.add("HttpStatusPath", _httpStatusPath)
125150
.add("SupplementalHttpRoutesClass", _supplementalHttpRoutesClass)
@@ -137,6 +162,11 @@ private AggregatorConfiguration(final Builder builder) {
137162
_pipelinesDirectory = builder._pipelinesDirectory;
138163
_httpHost = builder._httpHost;
139164
_httpPort = builder._httpPort;
165+
_httpsHost = builder._httpsHost;
166+
_httpsPort = builder._httpsPort;
167+
_httpsKeyPath = builder._httpsKeyPath;
168+
_httpsCertificatePath = builder._httpsCertificatePath;
169+
_enableHttps = builder._enableHttps;
140170
_httpHealthCheckPath = builder._httpHealthCheckPath;
141171
_httpStatusPath = builder._httpStatusPath;
142172
_supplementalHttpRoutesClass = Optional.ofNullable(builder._supplementalHttpRoutesClass);
@@ -157,11 +187,16 @@ private AggregatorConfiguration(final Builder builder) {
157187
private final File _logDirectory;
158188
private final File _pipelinesDirectory;
159189
private final String _httpHost;
190+
private final String _httpsHost;
160191
private final String _httpHealthCheckPath;
161192
private final String _httpStatusPath;
162193
private final int _httpPort;
194+
private final int _httpsPort;
163195
private final Optional<Class<? extends SupplementalRoutes>> _supplementalHttpRoutesClass;
164196
private final boolean _logDeadLetters;
197+
private final boolean _enableHttps;
198+
private final String _httpsKeyPath;
199+
private final String _httpsCertificatePath;
165200
private final Map<String, ?> _akkaConfiguration;
166201

167202
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getInstance();
@@ -292,6 +327,17 @@ public Builder setHttpHost(final String value) {
292327
return this;
293328
}
294329

330+
/**
331+
* The https host address to bind to. Cannot be null or empty.
332+
*
333+
* @param value The host address to bind to.
334+
* @return This instance of {@link Builder}.
335+
*/
336+
public Builder setHttpsHost(final String value) {
337+
_httpsHost = value;
338+
return this;
339+
}
340+
295341
/**
296342
* The http health check path. Cannot be null or empty. Optional. Default is "/ping".
297343
*
@@ -314,6 +360,28 @@ public Builder setHttpStatusPath(final String value) {
314360
return this;
315361
}
316362

363+
/**
364+
* The location of the https key file. Cannot be null or empty. Optional. Default is "/opt/mad/tls/key.pem".
365+
*
366+
* @param value The path to the https key file
367+
* @return This instance of {@link Builder}.
368+
*/
369+
public Builder setHttpsKeyPath(final String value) {
370+
_httpsKeyPath = value;
371+
return this;
372+
}
373+
374+
/**
375+
* The location of the https certificate file. Cannot be null or empty. Optional. Default is "/opt/mad/tls/cert.pem".
376+
*
377+
* @param value The path to the https cert file
378+
* @return This instance of {@link Builder}.
379+
*/
380+
public Builder setHttpsCertificatePath(final String value) {
381+
_httpsCertificatePath = value;
382+
return this;
383+
}
384+
317385
/**
318386
* The http port to listen on. Cannot be null, must be between 1 and
319387
* 65535 (inclusive).
@@ -326,6 +394,29 @@ public Builder setHttpPort(final Integer value) {
326394
return this;
327395
}
328396

397+
/**
398+
* The https port to listen on. Cannot be null, must be between 1 and
399+
* 65535 (inclusive).
400+
*
401+
* @param value The port to listen on.
402+
* @return This instance of {@link Builder}.
403+
*/
404+
public Builder setHttpsPort(final Integer value) {
405+
_httpsPort = value;
406+
return this;
407+
}
408+
409+
/**
410+
* Whether to start the https server. Value cannot be null. Defaults to {@code false}
411+
*
412+
* @param value {@code True} if the https server should be enabled
413+
* @return This instance of {@link Builder}.
414+
*/
415+
public Builder setEnableHttps(final Boolean value) {
416+
_enableHttps = value;
417+
return this;
418+
}
419+
329420
/**
330421
* The supplemental routes class. Optional.
331422
*
@@ -397,6 +488,20 @@ public Builder setAkkaConfiguration(final Map<String, ?> value) {
397488
private Integer _httpPort;
398489
@NotNull
399490
@NotEmpty
491+
private String _httpsHost;
492+
@NotNull
493+
@Range(min = 1, max = 65535)
494+
private Integer _httpsPort;
495+
@NotNull
496+
@NotEmpty
497+
private String _httpsKeyPath = "/opt/mad/tls/key.pem";
498+
@NotNull
499+
@NotEmpty
500+
private String _httpsCertificatePath = "/opt/mad/tls/cert.pem";
501+
@NotNull
502+
private Boolean _enableHttps = false;
503+
@NotNull
504+
@NotEmpty
400505
private String _httpHealthCheckPath = "/ping";
401506
@NotNull
402507
@NotEmpty

0 commit comments

Comments
 (0)