Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 98cd0d2

Browse files
committed
support SSL fixes jooby-project#186
1 parent cc4005f commit 98cd0d2

File tree

25 files changed

+2095
-771
lines changed

25 files changed

+2095
-771
lines changed

coverage-report/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@
132132
<configuration>
133133
<excludes>
134134
<exclude>**/ExpandedStmtRewriter*</exclude>
135+
<exclude>**/JdkSslContext*</exclude>
136+
<exclude>**/JdkSslServerContext*</exclude>
137+
<exclude>**/PemReader*</exclude>
138+
<exclude>**/SslContext*</exclude>
139+
<exclude>**/SslContextProvider*</exclude>
135140
</excludes>
136141
</configuration>
137142
<executions>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.jooby;
2+
3+
import org.jooby.test.ServerFeature;
4+
import org.junit.Test;
5+
6+
import com.typesafe.config.ConfigFactory;
7+
import com.typesafe.config.ConfigValueFactory;
8+
9+
public class HelloHttpsFeature extends ServerFeature {
10+
11+
{
12+
use(ConfigFactory.empty().withValue("application.securePort",
13+
ConfigValueFactory.fromAnyRef(8443)));
14+
15+
get("/", req -> "Hello");
16+
17+
get("/bye", req -> "bye!");
18+
}
19+
20+
@Test
21+
public void hello() throws Exception {
22+
request("https")
23+
.get("/")
24+
.expect("Hello");
25+
26+
request("https")
27+
.get("/bye")
28+
.expect("bye!");
29+
}
30+
31+
}

jooby-jetty/src/main/java/org/jooby/internal/jetty/JettyServer.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,16 @@
2727
import java.util.stream.Collectors;
2828

2929
import javax.inject.Inject;
30+
import javax.inject.Provider;
31+
import javax.net.ssl.SSLContext;
3032

3133
import org.eclipse.jetty.server.HttpConfiguration;
3234
import org.eclipse.jetty.server.HttpConnectionFactory;
35+
import org.eclipse.jetty.server.SecureRequestCustomizer;
3336
import org.eclipse.jetty.server.Server;
3437
import org.eclipse.jetty.server.ServerConnector;
38+
import org.eclipse.jetty.server.SslConnectionFactory;
39+
import org.eclipse.jetty.util.ssl.SslContextFactory;
3540
import org.eclipse.jetty.util.thread.QueuedThreadPool;
3641
import org.eclipse.jetty.websocket.api.WebSocketBehavior;
3742
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
@@ -53,11 +58,13 @@ public class JettyServer implements org.jooby.spi.Server {
5358
private Server server;
5459

5560
@Inject
56-
public JettyServer(final HttpHandler handler, final Config config) {
57-
this.server = server(handler, config);
61+
public JettyServer(final HttpHandler handler, final Config config,
62+
final Provider<SSLContext> sslCtx) {
63+
this.server = server(handler, config, sslCtx);
5864
}
5965

60-
private Server server(final HttpHandler handler, final Config config) {
66+
private Server server(final HttpHandler handler, final Config config,
67+
final Provider<SSLContext> sslCtx) {
6168
System.setProperty("org.eclipse.jetty.util.UrlEncoded.charset",
6269
config.getString("jetty.url.charset"));
6370

@@ -71,10 +78,19 @@ private Server server(final HttpHandler handler, final Config config) {
7178
server.setStopAtShutdown(false);
7279

7380
// HTTP connector
74-
ServerConnector http = connector(server, config.getConfig("jetty.http"), "jetty.http");
81+
ServerConnector http = http(server, config.getConfig("jetty.http"), "jetty.http");
7582
http.setPort(config.getInt("application.port"));
7683
http.setHost(config.getString("application.host"));
7784

85+
if (config.hasPath("application.securePort")) {
86+
87+
ServerConnector https = https(server, config.getConfig("jetty.http"), "jetty.http",
88+
sslCtx.get());
89+
https.setPort(config.getInt("application.securePort"));
90+
91+
server.addConnector(https);
92+
}
93+
7894
server.addConnector(http);
7995

8096
WebSocketPolicy wsConfig = conf(new WebSocketPolicy(WebSocketBehavior.SERVER),
@@ -92,7 +108,7 @@ private Server server(final HttpHandler handler, final Config config) {
92108
return server;
93109
}
94110

95-
private ServerConnector connector(final Server server, final Config conf, final String path) {
111+
private ServerConnector http(final Server server, final Config conf, final String path) {
96112
HttpConfiguration httpConfig = conf(new HttpConfiguration(), conf.withoutPath("connector"),
97113
path);
98114

@@ -103,6 +119,25 @@ private ServerConnector connector(final Server server, final Config conf, final
103119
return conf(connector, conf.getConfig("connector"), path + ".connector");
104120
}
105121

122+
private ServerConnector https(final Server server, final Config conf, final String path,
123+
final SSLContext sslContext) {
124+
HttpConfiguration httpConf = conf(new HttpConfiguration(), conf.withoutPath("connector"),
125+
path);
126+
127+
SslContextFactory sslContextFactory = new SslContextFactory();
128+
sslContextFactory.setSslContext(sslContext);
129+
130+
HttpConfiguration httpsConf = new HttpConfiguration(httpConf);
131+
httpsConf.addCustomizer(new SecureRequestCustomizer());
132+
133+
HttpConnectionFactory httpsFactory = new HttpConnectionFactory(httpsConf);
134+
135+
ServerConnector connector = new ServerConnector(server,
136+
new SslConnectionFactory(sslContextFactory, "HTTP/1.1"), httpsFactory);
137+
138+
return conf(connector, conf.getConfig("connector"), path + ".connector");
139+
}
140+
106141
@Override
107142
public void start() throws Exception {
108143
server.start();

jooby-jetty/src/test/java/org/jooby/internal/jetty/JettyServerTest.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import java.util.Map;
77

8+
import javax.inject.Provider;
9+
810
import org.eclipse.jetty.server.ConnectionFactory;
911
import org.eclipse.jetty.server.HttpConfiguration;
1012
import org.eclipse.jetty.server.HttpConnectionFactory;
@@ -167,10 +169,11 @@ public class JettyServerTest {
167169
unit.registerMock(WebSocketServerFactory.class, factory);
168170
};
169171

172+
@SuppressWarnings("unchecked")
170173
@Test
171174
public void startStopServer() throws Exception {
172175

173-
new MockUnit(HttpHandler.class)
176+
new MockUnit(HttpHandler.class, Provider.class)
174177
.expect(pool)
175178
.expect(server)
176179
.expect(httpConf)
@@ -179,18 +182,19 @@ public void startStopServer() throws Exception {
179182
.expect(wsPolicy)
180183
.expect(wsFactory)
181184
.run(unit -> {
182-
JettyServer server = new JettyServer(unit.get(HttpHandler.class), config);
185+
JettyServer server = new JettyServer(unit.get(HttpHandler.class), config, unit.get(Provider.class));
183186

184187
server.start();
185188
server.join();
186189
server.stop();
187190
});
188191
}
189192

193+
@SuppressWarnings("unchecked")
190194
@Test(expected = IllegalArgumentException.class)
191195
public void badOption() throws Exception {
192196

193-
new MockUnit(HttpHandler.class)
197+
new MockUnit(HttpHandler.class, Provider.class)
194198
.expect(unit -> {
195199
QueuedThreadPool pool = unit.mockConstructor(QueuedThreadPool.class);
196200
unit.registerMock(QueuedThreadPool.class, pool);
@@ -199,17 +203,19 @@ public void badOption() throws Exception {
199203
expectLastCall().andThrow(new IllegalArgumentException("10"));
200204
})
201205
.run(unit -> {
202-
new JettyServer(unit.get(HttpHandler.class), config);
206+
new JettyServer(unit.get(HttpHandler.class), config, unit.get(Provider.class));
203207
});
204208
}
205209

210+
@SuppressWarnings("unchecked")
206211
@Test(expected = ConfigException.BadValue.class)
207212
public void badConfOption() throws Exception {
208213

209-
new MockUnit(HttpHandler.class)
214+
new MockUnit(HttpHandler.class, Provider.class)
210215
.run(unit -> {
211216
new JettyServer(unit.get(HttpHandler.class),
212-
config.withValue("jetty.threads.MinThreads", ConfigValueFactory.fromAnyRef("x")));
217+
config.withValue("jetty.threads.MinThreads", ConfigValueFactory.fromAnyRef("x")),
218+
unit.get(Provider.class));
213219
});
214220
}
215221

jooby-netty/src/main/java/org/jooby/internal/netty/NettyInitializer.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,22 @@
1818
*/
1919
package org.jooby.internal.netty;
2020

21+
import java.util.concurrent.TimeUnit;
22+
23+
import org.jooby.spi.HttpHandler;
24+
25+
import com.typesafe.config.Config;
26+
2127
import io.netty.channel.ChannelInitializer;
28+
import io.netty.channel.ChannelPipeline;
2229
import io.netty.channel.socket.SocketChannel;
2330
import io.netty.handler.codec.http.HttpObjectAggregator;
2431
import io.netty.handler.codec.http.HttpServerCodec;
32+
import io.netty.handler.ssl.SslContext;
2533
import io.netty.handler.stream.ChunkedWriteHandler;
2634
import io.netty.handler.timeout.IdleStateHandler;
2735
import io.netty.util.concurrent.EventExecutorGroup;
2836

29-
import java.util.concurrent.TimeUnit;
30-
31-
import org.jooby.spi.HttpHandler;
32-
33-
import com.typesafe.config.Config;
34-
3537
public class NettyInitializer extends ChannelInitializer<SocketChannel> {
3638

3739
private EventExecutorGroup executor;
@@ -50,8 +52,10 @@ public class NettyInitializer extends ChannelInitializer<SocketChannel> {
5052

5153
private long idleTimeOut;
5254

55+
private SslContext sslCtx;
56+
5357
public NettyInitializer(final EventExecutorGroup executor, final HttpHandler handler,
54-
final Config config) {
58+
final Config config, final SslContext sslCtx) {
5559
this.executor = executor;
5660
this.handler = handler;
5761
this.config = config;
@@ -61,12 +65,18 @@ public NettyInitializer(final EventExecutorGroup executor, final HttpHandler han
6165
maxChunkSize = config.getBytes("netty.http.MaxChunkSize").intValue();
6266
maxContentLength = config.getBytes("netty.http.MaxContentLength").intValue();
6367
idleTimeOut = config.getDuration("netty.http.IdleTimeout", TimeUnit.MILLISECONDS);
68+
this.sslCtx = sslCtx;
6469
}
6570

6671
@Override
6772
protected void initChannel(final SocketChannel ch) throws Exception {
68-
ch.pipeline()
69-
.addLast(new HttpServerCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize))
73+
ChannelPipeline pipeline = ch.pipeline();
74+
75+
if (sslCtx != null) {
76+
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
77+
}
78+
79+
pipeline.addLast(new HttpServerCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize))
7080
.addLast(new HttpObjectAggregator(maxContentLength))
7181
.addLast(new ChunkedWriteHandler())
7282
.addLast(new IdleStateHandler(0, 0, idleTimeOut, TimeUnit.MILLISECONDS))

jooby-netty/src/main/java/org/jooby/internal/netty/NettyServer.java

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,19 @@
1818
*/
1919
package org.jooby.internal.netty;
2020

21-
import io.netty.bootstrap.ServerBootstrap;
22-
import io.netty.channel.Channel;
23-
import io.netty.channel.ChannelOption;
24-
import io.netty.channel.nio.NioEventLoopGroup;
25-
import io.netty.channel.socket.nio.NioServerSocketChannel;
26-
import io.netty.handler.logging.LogLevel;
27-
import io.netty.handler.logging.LoggingHandler;
28-
import io.netty.util.concurrent.DefaultEventExecutorGroup;
29-
import io.netty.util.concurrent.DefaultThreadFactory;
30-
21+
import java.io.File;
22+
import java.io.FileNotFoundException;
23+
import java.io.IOException;
24+
import java.io.InputStream;
3125
import java.lang.reflect.Field;
3226
import java.lang.reflect.ParameterizedType;
27+
import java.nio.file.Files;
28+
import java.nio.file.Paths;
29+
import java.nio.file.StandardCopyOption;
30+
import java.security.cert.CertificateException;
3331
import java.util.Map;
3432
import java.util.Map.Entry;
33+
import java.util.concurrent.ThreadFactory;
3534
import java.util.function.BiConsumer;
3635

3736
import javax.inject.Inject;
@@ -44,6 +43,19 @@
4443
import com.google.common.collect.Maps;
4544
import com.typesafe.config.Config;
4645

46+
import io.netty.bootstrap.ServerBootstrap;
47+
import io.netty.channel.Channel;
48+
import io.netty.channel.ChannelOption;
49+
import io.netty.channel.nio.NioEventLoopGroup;
50+
import io.netty.channel.socket.nio.NioServerSocketChannel;
51+
import io.netty.handler.logging.LogLevel;
52+
import io.netty.handler.logging.LoggingHandler;
53+
import io.netty.handler.ssl.SslContext;
54+
import io.netty.handler.ssl.SslContextBuilder;
55+
import io.netty.util.concurrent.DefaultEventExecutorGroup;
56+
import io.netty.util.concurrent.DefaultThreadFactory;
57+
import io.netty.util.concurrent.EventExecutorGroup;
58+
4759
public class NettyServer implements Server {
4860

4961
/** The logging system. */
@@ -76,25 +88,35 @@ public void start() throws Exception {
7688
childGroup = parentGroup;
7789
}
7890

79-
ServerBootstrap bootstrap = new ServerBootstrap();
91+
ThreadFactory threadFactory = new DefaultThreadFactory(config.getString("netty.threads.Name"));
92+
DefaultEventExecutorGroup executor = new DefaultEventExecutorGroup(
93+
config.getInt("netty.threads.Max"), threadFactory);
8094

81-
DefaultEventExecutorGroup executor =
82-
new DefaultEventExecutorGroup(config.getInt("netty.threads.Max"),
83-
new DefaultThreadFactory(config.getString("netty.threads.Name")));
95+
this.ch = bootstrap(executor, null, config.getInt("application.port"));
8496

85-
bootstrap.group(parentGroup)
97+
if (config.hasPath("application.securePort")) {
98+
bootstrap(executor, sslCtx(config), config.getInt("application.securePort"));
99+
}
100+
}
101+
102+
private Channel bootstrap(final EventExecutorGroup executor, final SslContext sslCtx,
103+
final int port) throws InterruptedException {
104+
ServerBootstrap bootstrap = new ServerBootstrap();
105+
106+
bootstrap.group(parentGroup, childGroup)
86107
.channel(NioServerSocketChannel.class)
87108
.handler(new LoggingHandler(Server.class, LogLevel.DEBUG))
88-
.childHandler(new NettyInitializer(executor, dispatcher, config));
109+
.childHandler(new NettyInitializer(executor, dispatcher, config, sslCtx));
89110

90-
configure(config.getConfig("netty.options"), "netty.options", (option, value) ->
91-
bootstrap.option(option, value));
111+
configure(config.getConfig("netty.options"), "netty.options",
112+
(option, value) -> bootstrap.option(option, value));
92113

93-
configure(config.getConfig("netty.child.options"), "netty.child.options", (option, value) ->
94-
bootstrap.childOption(option, value));
114+
configure(config.getConfig("netty.child.options"), "netty.child.options",
115+
(option, value) -> bootstrap.childOption(option, value));
95116

96-
this.ch = bootstrap
97-
.bind(host(config.getString("application.host")), config.getInt("application.port")).sync()
117+
return bootstrap
118+
.bind(host(config.getString("application.host")), port)
119+
.sync()
98120
.channel();
99121
}
100122

@@ -158,4 +180,35 @@ private NioEventLoopGroup eventLoop(final int threads, final String name) {
158180
new DefaultThreadFactory(name, Thread.MAX_PRIORITY));
159181
return group;
160182
}
183+
184+
private SslContext sslCtx(final Config config) throws IOException, CertificateException {
185+
String tmpdir = config.getString("application.tmpdir");
186+
File keyStoreCert = toFile(config.getString("ssl.keystore.cert"), tmpdir);
187+
File keyStoreKey = toFile(config.getString("ssl.keystore.key"), tmpdir);
188+
String keyStorePass = config.hasPath("ssl.keystore.password")
189+
? config.getString("ssl.keystore.password") : null;
190+
SslContextBuilder scb = SslContextBuilder.forServer(keyStoreCert, keyStoreKey, keyStorePass);
191+
if (config.hasPath("ssl.trust.cert")) {
192+
scb.trustManager(toFile(config.getString("ssl.trust.cert"), tmpdir));
193+
}
194+
return scb.build();
195+
}
196+
197+
static File toFile(final String path, final String tmpdir) throws IOException {
198+
File file = new File(path);
199+
if (file.exists()) {
200+
return file;
201+
}
202+
file = new File(tmpdir, Paths.get(path).getFileName().toString());
203+
// classpath resource?
204+
try (InputStream in = NettyServer.class.getClassLoader().getResourceAsStream(path)) {
205+
if (in == null) {
206+
throw new FileNotFoundException(path);
207+
}
208+
Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
209+
}
210+
file.deleteOnExit();
211+
return file;
212+
}
213+
161214
}

0 commit comments

Comments
 (0)