Skip to content

KROKI_LISTEN option to start listening on an address #595

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

Merged
merged 5 commits into from
Feb 14, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions docs/modules/setup/pages/configuration.adoc
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
= Configuration
:url-k8s-environment-variables: https://kubernetes.io/docs/concepts/services-networking/service/#environment-variables

Kroki can be configured using environment variables or Java system properties.

== Port
== Server Listening

By default, Kroki will start a web server on port `8000`.
You can change the port using `KROKI_PORT`:
By default, Kroki will bind to all network interfaces (`0.0.0.0`) on port `8000`.
You can change on which host and port the server will listen for incoming requests using `KROKI_LISTEN`:

[source,java-cli]
KROKI_PORT=1234 java -jar kroki-server.jar
KROKI_LISTEN=127.0.0.1:1234 java -jar kroki-server.jar

[source,java-cli]
java -DKROKI_PORT=1234 -jar kroki-server.jar
java -DKROKI_LISTEN=127.0.0.1:1234 -jar kroki-server.jar

With the above configuration, the server will bind to `127.0.0.1` (i.e., loopback address) on port 1234.

[NOTE]
====
If the port is unspecified, the server will listen on port `8000`.
If the host is unspecified, the server will use `[::]` (i.e., bind to all network interfaces and accept connections from both IPv6 or IPv4 hosts)

`KROKI_LISTEN` also accepts IPv6 enclosed within square brackets (`[` and `]`),
for instance: `KROKI_LISTEN=2001:db8:1f70::999:de8:7648:6e8]:1234`.
====


[IMPORTANT]
====
*`KROKI_PORT` is deprecated and will be removed in the future.*

We are deprecating this option because it conflicts with Kubernetes and Docker built-in environnement variables.
For reference, {url-k8s-environment-variables}[Kubernetes will automatically set the environment variable] `{SERVICE_NAME}_PORT` to `tcp://1.2.3.4:8000`.
As you might have guessed, if you use `KROKI` as a service name, there's going to be a problem!
In fact, Kroki expects the value of `KROKI_PORT` to be an integer value. +
To workaround this issue, until `KROKI_PORT` is removed, you can explicitly define the environment variable `KROKI_PORT=8000`.

If you were using a custom port (for instance, `KROKI_PORT=1234`), you can replace it by `KROKI_LISTEN=0.0.0.0:1234` (which is strictly equivalent). +
If you want to bind to IPv4 and IPv6, you can use `KROKI_LISTEN=:1234` or the longer form `KROKI_LISTEN=[::]:1234`.

If you want to learn about this deprecation, you can read: https://github.com/yuzutech/kroki/issues/576
====

== Safe Mode

Expand Down
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<artifactId>kroki</artifactId>
<packaging>pom</packaging>
<version>0.10.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<modules>
<module>server</module>
<module>umlet</module>
Expand Down
1 change: 1 addition & 0 deletions server/ops/docker/jdk11-alpine/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ RUN apk add --update --no-cache \

COPY ops/docker/logback.xml /etc/logback.xml

ENV KROKI_CONTAINER_SUPPORT=""
ENV KROKI_SAFE_MODE=secure
ENV KROKI_SVGBOB_BIN_PATH=/rust/bin/svgbob
ENV KROKI_ERD_BIN_PATH=/haskell/bin/erd
Expand Down
84 changes: 83 additions & 1 deletion server/src/main/java/io/kroki/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,24 @@
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.json.JsonObject;
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.Set;

public class Server extends AbstractVerticle {

private static final Logger logger = LoggerFactory.getLogger(Server.class);
private static final int DEFAULT_PORT = 8000;

@Override
public void start(Promise<Void> startPromise) {
ConfigRetriever retriever = ConfigRetriever.create(vertx);
Expand Down Expand Up @@ -124,6 +130,82 @@ static void start(Vertx vertx, JsonObject config, Handler<AsyncResult<HttpServer
route.handler(routingContext -> routingContext.fail(404));
route.failureHandler(new ErrorHandler(false));

server.requestHandler(router).listen(config.getInteger("KROKI_PORT", 8000), listenHandler);
server.requestHandler(router).listen(getListenAddress(config), listenHandler);
}

/**
* Get the address the service will listen on.
* @param config configuration
* @return the address
*/
static SocketAddress getListenAddress(JsonObject config) {
String krokiListen = config.getString("KROKI_LISTEN");
// higher precedence over KROKI_PORT
if (krokiListen != null) {
if (krokiListen.charAt(0) == '[') {
return getIPv6ListenAddress(krokiListen);
}
int lastColonIndex = krokiListen.lastIndexOf(":");
if (lastColonIndex < 0) {
// contains a single value without any colon, assume that the value is a host or IP (use the default port)
return SocketAddress.inetSocketAddress(DEFAULT_PORT, krokiListen);
}
// listen address using IPv4 or host must contain a single colon
long colonCount = krokiListen.chars().filter(ch -> ch == ':').count();
if (colonCount > 1) {
throw new IllegalArgumentException(String.format("KROKI_LISTEN is not a valid listen address '%s', format must be: 'host:5678', ':5678', '1.2.3.4:5678', '[2041:0:140f::875b:131b]:5678' or '[2041:0:140f::875b:131b]'", krokiListen));
}
int port = Integer.parseInt(krokiListen.substring(lastColonIndex + 1));
String hostValue = krokiListen.substring(0, lastColonIndex);
String host;
if (hostValue.isEmpty()) {
// default host to listen on both IPv4 and IPv6
host = "[::]";
} else {
host = hostValue;
}
return SocketAddress.inetSocketAddress(port, host);
}
// default host to listen on IPv4
return SocketAddress.inetSocketAddress(getListenPort(config), "0.0.0.0");
}

/**
* Get the address the service will listen on from IPv6.
* @param listen listen value
* @return the address
*/
private static SocketAddress getIPv6ListenAddress(String listen) {
int lastColonIndex = listen.lastIndexOf(":");
int lastSquareBracketIndex = listen.lastIndexOf(']');
if (lastSquareBracketIndex + 1 == listen.length()) {
// value does not contain a port
return SocketAddress.inetSocketAddress(DEFAULT_PORT, listen);
}
return SocketAddress.inetSocketAddress(Integer.parseInt(listen.substring(lastColonIndex + 1)), listen.substring(0, lastSquareBracketIndex + 1));
}

/**
* Get the port the service will listen on.
* @param config configuration
* @return the port
*/
static int getListenPort(JsonObject config) {
int port;
if (config.getString("KROKI_CONTAINER_SUPPORT") != null) {
// Enable container support
// Kubernetes and Docker link automatically set KROKI_PORT to "tcp://ip:port"
String portValue = config.getString("KROKI_PORT", String.valueOf(DEFAULT_PORT));
if (portValue.startsWith("tcp://")) {
logger.warn("KROKI_PORT is not an integer value '{}', it's very likely that the value was automatically set by the container runtime, ignoring and using default port 8000", portValue);
port = DEFAULT_PORT;
} else {
// fail fast if the value is not an integer
port = Integer.parseInt(portValue);
}
} else {
port = config.getInteger("KROKI_PORT", DEFAULT_PORT);
}
return port;
}
}
116 changes: 116 additions & 0 deletions server/src/test/java/io/kroki/server/ServerConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.kroki.server;

import io.vertx.core.json.JsonObject;
import io.vertx.core.net.SocketAddress;
import org.junit.jupiter.api.Test;

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

class ServerConfigTest {

@Test
void throw_exception_when_non_ipv6_listen_address_contains_more_than_one_colon() {
assertThatThrownBy(() -> Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "localhost:1234:5678")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("KROKI_LISTEN is not a valid listen address 'localhost:1234:5678', format must be: 'host:5678', ':5678', '1.2.3.4:5678', '[2041:0:140f::875b:131b]:5678' or '[2041:0:140f::875b:131b]'");
}

@Test
void throw_exception_when_listen_address_contains_host_as_port() {
assertThatThrownBy(() -> Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", ":localhost")))
.isInstanceOf(NumberFormatException.class)
.hasMessageStartingWith("For input string: \"localhost\"");
}

@Test
void throw_exception_when_listen_address_contains_ipv4_as_port() {
assertThatThrownBy(() -> Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", ":192.168.0.1")))
.isInstanceOf(NumberFormatException.class)
.hasMessageStartingWith("For input string: \"192.168.0.1\"");
}

@Test
void throw_exception_when_listen_address_contains_an_empty_port() {
assertThatThrownBy(() -> Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "localhost:")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("For input string: \"\"");
}

@Test
void return_socket_address_when_listen_address_ipv6() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "[2001:db8:1f70::999:de8:7648:6e8]"));
assertThat(listenAddress.host()).isEqualTo("[2001:db8:1f70::999:de8:7648:6e8]");
assertThat(listenAddress.port()).isEqualTo(8000);
}

@Test
void return_socket_address_when_listen_address_ipv4() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "192.168.0.1"));
assertThat(listenAddress.host()).isEqualTo("192.168.0.1");
assertThat(listenAddress.port()).isEqualTo(8000);
}

@Test
void return_socket_address_when_listen_address_host() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "kroki.io"));
assertThat(listenAddress.host()).isEqualTo("kroki.io");
assertThat(listenAddress.port()).isEqualTo(8000);
}

@Test
void return_socket_address_when_listen_address_default_ip() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "0.0.0.0"));
assertThat(listenAddress.host()).isEqualTo("0.0.0.0");
assertThat(listenAddress.port()).isEqualTo(8000);
}

@Test
void return_socket_address_when_listen_address_any_ip() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "[::]"));
assertThat(listenAddress.host()).isEqualTo("[::]");
assertThat(listenAddress.port()).isEqualTo(8000);
}

@Test
void return_socket_address_when_listen_address_contains_ipv6_port() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "[2001:db8::8a2e:370:7334]:1234"));
assertThat(listenAddress.host()).isEqualTo("[2001:db8::8a2e:370:7334]");
assertThat(listenAddress.port()).isEqualTo(1234);
}

@Test
void return_socket_address_when_listen_address_localhost_port() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "localhost:1234"));
assertThat(listenAddress.host()).isEqualTo("localhost");
assertThat(listenAddress.port()).isEqualTo(1234);
}

@Test
void return_socket_address_when_listen_address_domain_port() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "kroki.io:1234"));
assertThat(listenAddress.host()).isEqualTo("kroki.io");
assertThat(listenAddress.port()).isEqualTo(1234);
}

@Test
void return_socket_address_when_listen_address_default_ip_port() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "0.0.0.0:2345"));
assertThat(listenAddress.host()).isEqualTo("0.0.0.0");
assertThat(listenAddress.port()).isEqualTo(2345);
}

@Test
void return_socket_address_when_listen_address_local_ip_port() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", "127.0.0.1:2345"));
assertThat(listenAddress.host()).isEqualTo("127.0.0.1");
assertThat(listenAddress.port()).isEqualTo(2345);
}

@Test
void return_socket_address_when_listen_address_address_port() {
SocketAddress listenAddress = Server.getListenAddress(new JsonObject().put("KROKI_LISTEN", ":3456"));
assertThat(listenAddress.host()).isEqualTo("[::]"); // default host to listen on both IPv4 and IPv6
assertThat(listenAddress.port()).isEqualTo(3456);
}
}
Loading