Skip to content

Commit cb3764b

Browse files
committed
Merge branch 'ssl' into 2.x
2 parents 3176cbf + 369e333 commit cb3764b

File tree

28 files changed

+735
-44
lines changed

28 files changed

+735
-44
lines changed

examples/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
<artifactId>jooby</artifactId>
3737
<version>${jooby.version}</version>
3838
</dependency>
39+
<dependency>
40+
<groupId>io.jooby</groupId>
41+
<artifactId>jooby-ssl-x509</artifactId>
42+
<version>${jooby.version}</version>
43+
</dependency>
3944
<dependency>
4045
<groupId>io.jooby</groupId>
4146
<artifactId>jooby-jackson</artifactId>
@@ -53,7 +58,7 @@
5358
</dependency>
5459
<dependency>
5560
<groupId>io.jooby</groupId>
56-
<artifactId>jooby-netty</artifactId>
61+
<artifactId>jooby-jetty</artifactId>
5762
<version>${jooby.version}</version>
5863
</dependency>
5964
<dependency>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package examples;
7+
8+
import io.jooby.Jooby;
9+
import io.jooby.SSLOptions;
10+
import io.jooby.ServerOptions;
11+
12+
public class HelloHttpsApp extends Jooby {
13+
14+
{
15+
ServerOptions options = new ServerOptions();
16+
options.setSecurePort(8443);
17+
options.setSsl(SSLOptions.x509());
18+
setServerOptions(options);
19+
20+
get("/", ctx -> {
21+
return "Hello " + ctx.getScheme();
22+
});
23+
}
24+
25+
public static void main(String[] args) {
26+
runApp(args, HelloHttpsApp::new);
27+
}
28+
}

jooby/src/main/java/io/jooby/Context.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,15 @@ public interface Context extends Registry {
414414
*/
415415
@Nonnull String getProtocol();
416416

417+
/**
418+
*
419+
* Returns a boolean indicating whether this request was made using a secure channel, such as
420+
* HTTPS.
421+
*
422+
* @return a boolean indicating if the request was made using a secure channel
423+
*/
424+
boolean isSecure();
425+
417426
/**
418427
* HTTP scheme in lower case.
419428
*

jooby/src/main/java/io/jooby/DefaultContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ public interface DefaultContext extends Context {
220220
.orElse(getRemoteAddress());
221221
}
222222

223+
@Override default boolean isSecure() {
224+
return getScheme().equals("https");
225+
}
226+
223227
@Override @Nonnull default Map<String, List<String>> formMultimap() {
224228
return form().toMultimap();
225229
}

jooby/src/main/java/io/jooby/ForwardingContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public ForwardingContext(@Nonnull Context context) {
4040
this.ctx = context;
4141
}
4242

43+
@Override public boolean isSecure() {
44+
return ctx.isSecure();
45+
}
46+
4347
@Override @Nonnull public Map<String, Object> getAttributes() {
4448
return ctx.getAttributes();
4549
}

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ public Jooby errorCode(@Nonnull Class<? extends Throwable> type,
595595
Server server = servers.get(0);
596596
try {
597597
if (serverOptions == null) {
598-
serverOptions = ServerOptions.from(getEnvironment().getConfig()).orElse(null);
598+
serverOptions = ServerOptions.parse(getEnvironment().getConfig()).orElse(null);
599599
}
600600
if (serverOptions != null) {
601601
serverOptions.setServer(server.getClass().getSimpleName().toLowerCase());
@@ -669,9 +669,26 @@ public Jooby errorCode(@Nonnull Class<? extends Throwable> type,
669669
log.info(" app dir: {}", System.getProperty("user.dir"));
670670
log.info(" tmp dir: {}", tmpdir);
671671

672-
log.info("routes: \n\n{}\n\nlistening on:\n http://localhost:{}{}\n", router,
673-
server.getOptions().getPort(),
674-
router.getContextPath());
672+
StringBuilder buff = new StringBuilder();
673+
buff.append("routes: \n\n{}\n\nlistening on:\n");
674+
675+
ServerOptions options = server.getOptions();
676+
String host = options.getHost().replace("0.0.0.0", "localhost");
677+
List<Object> args = new ArrayList<>();
678+
args.add(router);
679+
args.add(host);
680+
args.add(options.getPort());
681+
args.add(router.getContextPath());
682+
buff.append(" http://{}:{}{}\n");
683+
684+
if (options.isSSLEnabled()) {
685+
args.add(host);
686+
args.add(options.getSecurePort());
687+
args.add(router.getContextPath());
688+
buff.append(" https://{}:{}{}\n");
689+
}
690+
691+
log.info(buff.toString(), args.toArray(new Object[args.size()]));
675692
return this;
676693
}
677694

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.jooby;
2+
3+
import javax.net.ssl.SSLContext;
4+
import java.io.FileNotFoundException;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.Paths;
10+
import java.util.stream.Stream;
11+
12+
public interface SSLContextProvider {
13+
14+
boolean supports(String type);
15+
16+
SSLContext create(ClassLoader loader, SSLOptions options);
17+
18+
static InputStream loadFile(ClassLoader loader, String path) throws IOException {
19+
InputStream in = loadFileFromFileSystem(path);
20+
if (in == null) {
21+
in = loadFileFromClasspath(loader, path);
22+
if (in == null) {
23+
throw new FileNotFoundException(path);
24+
}
25+
}
26+
return in;
27+
}
28+
29+
static InputStream loadFileFromClasspath(ClassLoader loader, String path) {
30+
return path.startsWith("/")
31+
? loader.getResourceAsStream(path.substring(1))
32+
: loader.getResourceAsStream(path);
33+
}
34+
35+
static InputStream loadFileFromFileSystem(String path) throws IOException {
36+
Path file = Stream.of(Paths.get(path), Paths.get(System.getProperty("user.dir"), path))
37+
.filter(Files::exists)
38+
.findFirst()
39+
.orElse(null);
40+
if (file != null) {
41+
return Files.newInputStream(file);
42+
}
43+
return null;
44+
}
45+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package io.jooby;
2+
3+
import com.typesafe.config.Config;
4+
5+
import javax.annotation.Nonnull;
6+
import java.util.Optional;
7+
8+
public final class SSLOptions {
9+
public static final String X509 = "X509";
10+
11+
public static final String PKCS12 = "PKCS12";
12+
13+
private String password;
14+
15+
private String type;
16+
17+
private String cert;
18+
19+
private String privateKey;
20+
21+
private SSLOptions() {
22+
}
23+
24+
public String getType() {
25+
return type;
26+
}
27+
28+
public SSLOptions setType(String type) {
29+
this.type = type;
30+
return this;
31+
}
32+
33+
public String getCert() {
34+
return cert;
35+
}
36+
37+
public SSLOptions setCert(String cert) {
38+
this.cert = cert;
39+
return this;
40+
}
41+
42+
public String getPrivateKey() {
43+
return privateKey;
44+
}
45+
46+
public SSLOptions setPrivateKey(String privateKey) {
47+
this.privateKey = privateKey;
48+
return this;
49+
}
50+
51+
public static SSLOptions x509(String crt, String key) {
52+
SSLOptions options = new SSLOptions();
53+
options.setType(X509);
54+
options.setPrivateKey(key);
55+
options.setCert(crt);
56+
return options;
57+
}
58+
59+
public static SSLOptions x509() {
60+
return x509("io/jooby/ssl/localhost.crt", "io/jooby/ssl/localhost.key");
61+
}
62+
63+
public static SSLOptions pkcs12(String cert, String password) {
64+
SSLOptions options = new SSLOptions();
65+
options.setType(PKCS12);
66+
options.setCert(cert);
67+
options.setPassword(password);
68+
return options;
69+
}
70+
71+
public static SSLOptions pkcs12() {
72+
return pkcs12("io/jooby/ssl/localhost.p12", "changeit");
73+
}
74+
75+
public void setPassword(String password) {
76+
this.password = password;
77+
}
78+
79+
public String getPassword() {
80+
return password;
81+
}
82+
83+
public static @Nonnull Optional<SSLOptions> parse(@Nonnull Config conf) {
84+
if (conf.hasPath("server.ssl")) {
85+
SSLOptions options = new SSLOptions();
86+
if (conf.hasPath("sever.ssl.type")) {
87+
String type = conf.getString("server.ssl.type").toUpperCase();
88+
if (type.equals(PKCS12)) {
89+
options.setType(type);
90+
} else if (type.equals(X509)) {
91+
options.setType(type);
92+
} else {
93+
throw new IllegalArgumentException("Unsupported SSL type: " + type.toUpperCase());
94+
}
95+
} else {
96+
options.setType(PKCS12);
97+
}
98+
String type = options.getType();
99+
switch (type) {
100+
101+
}
102+
103+
return Optional.of(options);
104+
}
105+
return Optional.empty();
106+
}
107+
108+
@Override public String toString() {
109+
return type;
110+
}
111+
}

jooby/src/main/java/io/jooby/ServerOptions.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66
package io.jooby;
77

88
import com.typesafe.config.Config;
9+
import io.jooby.internal.SSLDefaultProvider;
910

1011
import javax.annotation.Nonnull;
12+
import javax.annotation.Nullable;
13+
import javax.net.ssl.SSLContext;
1114
import java.io.IOException;
1215
import java.net.ServerSocket;
16+
import java.util.Objects;
1317
import java.util.Optional;
18+
import java.util.ServiceLoader;
19+
import java.util.stream.Stream;
20+
import java.util.stream.StreamSupport;
21+
22+
import static java.util.stream.Stream.concat;
23+
import static java.util.stream.StreamSupport.stream;
1424

1525
/**
1626
* Available server options.
@@ -75,14 +85,18 @@ public class ServerOptions {
7585

7686
private String host = "0.0.0.0";
7787

88+
private SSLOptions ssl;
89+
90+
private Integer securePort;
91+
7892
/**
7993
* Creates server options from config object. The configuration options must provided entries
8094
* like: <code>server.port</code>, <code>server.ioThreads</code>, etc...
8195
*
8296
* @param conf Configuration object.
8397
* @return Server options.
8498
*/
85-
public static @Nonnull Optional<ServerOptions> from(@Nonnull Config conf) {
99+
public static @Nonnull Optional<ServerOptions> parse(@Nonnull Config conf) {
86100
if (conf.hasPath("server")) {
87101
ServerOptions options = new ServerOptions();
88102
if (conf.hasPath("server.port")) {
@@ -175,6 +189,23 @@ public int getPort() {
175189
return this;
176190
}
177191

192+
public Integer getSecurePort() {
193+
return securePort;
194+
}
195+
196+
public boolean isSSLEnabled() {
197+
return securePort != null || ssl != null;
198+
}
199+
200+
public ServerOptions setSecurePort(Integer securePort) {
201+
if (securePort != null && securePort.intValue() == 0) {
202+
this.securePort = randomPort();
203+
} else {
204+
this.securePort = securePort;
205+
}
206+
return this;
207+
}
208+
178209
/**
179210
* Number of IO threads used by the server. Required by Netty and Undertow.
180211
*
@@ -347,6 +378,33 @@ public void setHost(String host) {
347378
}
348379
}
349380

381+
public @Nullable SSLOptions getSsl() {
382+
return ssl;
383+
}
384+
385+
public ServerOptions setSsl(@Nullable SSLOptions ssl) {
386+
this.ssl = ssl;
387+
return this;
388+
}
389+
390+
public @Nullable SSLContext getSSLContext() {
391+
if (isSSLEnabled()) {
392+
setSecurePort(Optional.ofNullable(securePort).orElse(8443));
393+
setSsl(Optional.ofNullable(ssl).orElseGet(SSLOptions::pkcs12));
394+
SSLOptions options = getSsl();
395+
SSLContextProvider provider = concat(
396+
stream(ServiceLoader.load(SSLContextProvider.class).spliterator(), false),
397+
Stream.of(new SSLDefaultProvider())
398+
)
399+
.filter(it -> it.supports(options.getType()))
400+
.findFirst()
401+
.orElseThrow(
402+
() -> new UnsupportedOperationException("KeyStore: " + options.getType()));
403+
return provider.create(ServerOptions.class.getClassLoader(), options);
404+
}
405+
return null;
406+
}
407+
350408
private int randomPort() {
351409
try (ServerSocket socket = new ServerSocket(0)) {
352410
return socket.getLocalPort();

0 commit comments

Comments
 (0)