Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resolves #787 add support for SSL #955

Merged
merged 11 commits into from
Oct 28, 2021
20 changes: 20 additions & 0 deletions docs/modules/setup/pages/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,23 @@ You can update this default value by setting `KROKI_MAX_URI_LENGTH` environment
TIP: Keep in mind that browsers also have a URI limit on `<img>` tags.
Most modern browsers https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers/417184#417184[support a URI length greater than 64000] on `<img>` tags but this value is probably a bit excessive.
We recommend to use a maximum length that's not greater than 8192 and not greater than 5120 if you are supporting IE 11.

== Enabling SSL on the server

By default, SSL/TLS is not enabled on the server but you can enable it by setting `KROKI_SSL` environment variable to `true`.

When SSL is enabled, you must provide the certificate and the private key values as PEM format using `KROKI_SSL_KEY` and `KROKI_SSL_CERT` environment variables.

[NOTE]
====
You can generate a self-signed SSL certificate and private key as PEM format using `openssl`:

$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365

The above command will generate two files, `cert.pem` containing the certificate and `key.pem` containing the private key.

You can then set the `KROKI_SSL_CERT` environment variable with the contents of the `cert.pem` file and set the `KROKI_SSL_KEY` environment variable with the contents of the `key.pem` file.
====

If SSL is enabled, both `KROKI_SSL_KEY` and `KROKI_SSL_CERT` must be configured.

32 changes: 25 additions & 7 deletions server/src/main/java/io/kroki/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,24 @@
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.core.net.SocketAddress;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CorsHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Server extends AbstractVerticle {

Expand All @@ -72,11 +74,12 @@ public void start(Promise<Void> startPromise) {
}

static void start(Vertx vertx, JsonObject config, Handler<AsyncResult<HttpServer>> listenHandler) {
Integer maxUriLength = config.getInteger("KROKI_MAX_URI_LENGTH");
HttpServerOptions serverOptions = new HttpServerOptions();
if (maxUriLength != null) {
serverOptions.setMaxInitialLineLength(maxUriLength);
}
Optional<Integer> maxUriLength = Optional.ofNullable(config.getInteger("KROKI_MAX_URI_LENGTH"));
maxUriLength.ifPresent(serverOptions::setMaxInitialLineLength);
boolean enableSSL = config.getBoolean("KROKI_SSL", false);
serverOptions.setSsl(enableSSL);
setPemKeyCertOptions(config, serverOptions, enableSSL);
HttpServer server = vertx.createHttpServer(serverOptions);
Router router = Router.router(vertx);
BodyHandler bodyHandler = BodyHandler.create(false).setBodyLimit(config.getLong("KROKI_BODY_LIMIT", BodyHandler.DEFAULT_BODY_LIMIT));
Expand Down Expand Up @@ -152,6 +155,21 @@ static void start(Vertx vertx, JsonObject config, Handler<AsyncResult<HttpServer
.listen(getListenAddress(config), listenHandler);
}

private static void setPemKeyCertOptions(JsonObject config, HttpServerOptions serverOptions, boolean enableSSL) {
if (enableSSL) {
Optional<String> sslKeyValue = Optional.ofNullable(config.getString("KROKI_SSL_KEY"));
Optional<String> sslCertValue = Optional.ofNullable(config.getString("KROKI_SSL_CERT"));
if (!sslKeyValue.isPresent() || !sslCertValue.isPresent()) {
throw new IllegalArgumentException("KROKI_SSL_KEY and KROKI_SSL_CERT must be configured when SSL is enabled.");
}
serverOptions.setPemKeyCertOptions(
new PemKeyCertOptions()
.addKeyValue(Buffer.buffer(sslKeyValue.get()))
.addCertValue(Buffer.buffer(sslCertValue.get()))
);
}
}

/**
* Get the address the service will listen on.
*
Expand Down
135 changes: 135 additions & 0 deletions server/src/test/java/io/kroki/server/ServerSSLTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package io.kroki.server;

import static org.assertj.core.api.Assertions.assertThat;

import io.vertx.core.AsyncResult;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.core.net.SelfSignedCertificate;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.codec.BodyCodec;
import io.vertx.junit5.VertxExtension;
import io.vertx.junit5.VertxTestContext;

import java.io.IOException;
import java.net.ServerSocket;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(VertxExtension.class)
class ServerSSLTest {
amirabramovich marked this conversation as resolved.
Show resolved Hide resolved

private int port;
private PemKeyCertOptions pemKeyCertOptions;

@BeforeEach
void init() throws IOException {
ServerSocket socket = new ServerSocket(0);
port = socket.getLocalPort();
socket.close();
pemKeyCertOptions = SelfSignedCertificate.create().keyCertOptions();
}

@Test
void with_ssl_disabled(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
WebClient client = WebClient.create(vertx);
// successful request
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.succeeding(response -> testContext.verify(() -> {
assertThat(response.body()).contains("https://kroki.io");
testContext.completeNow();
})));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslDisabled()), handle);
}

@Test
void with_ssl_enabled_and_secure_http_web_client(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// successful request
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.succeeding(response -> testContext.verify(() -> {
assertThat(response.body()).contains("https://kroki.io");
testContext.completeNow();
})));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabled(vertx)), handle);
}

@Test
void with_ssl_enabled_and_insecure_http_web_client(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
WebClient client = WebClient.create(vertx);
// failed request, client must send HTTPS request
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabled(vertx)), handle);
}

@Test
void with_ssl_enabled_and_missing_ssl_key_config(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// failed deployment
testContext.verify(() -> {
assertThat(deployVerticleResult.failed()).isTrue();
assertThat(deployVerticleResult.cause()).isInstanceOf(IllegalArgumentException.class);
assertThat(deployVerticleResult.cause()).hasMessage("KROKI_SSL_KEY and KROKI_SSL_CERT must be configured when SSL is enabled.");
});
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// failed request, server has not started
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledMissingKey(vertx)), handle);
}

private JsonObject configWithSslDisabled() {
return new JsonObject()
.put("KROKI_PORT", port)
.put("KROKI_SSL", false);
}

private JsonObject configWithSslEnabledMissingKey(Vertx vertx) {
JsonObject config = configWithSslEnabled(vertx);
config.remove("KROKI_SSL_KEY");
return config;
}

private JsonObject configWithSslEnabled(Vertx vertx) {
return new JsonObject()
.put("KROKI_PORT", port)
.put("KROKI_SSL", true)
.put("KROKI_SSL_KEY", vertx.fileSystem().readFileBlocking(pemKeyCertOptions.getKeyPath()).toString())
.put("KROKI_SSL_CERT", vertx.fileSystem().readFileBlocking(pemKeyCertOptions.getCertPath()).toString());
}
}
amirabramovich marked this conversation as resolved.
Show resolved Hide resolved