Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 21 additions & 16 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,27 @@ releases. For the time being use your own instance or a public one._
This application requires [Maven](https://maven.apache.org) and at least Java 21 to run. Use the following environment
variables to adjust the application to your needs:

| EnvVar | Description | Default |
|----------------------------------------|-----------------------------------------------------------------------------------------------|-----------------|
| AUTH_JWKS_URL | URL to retrieve a JWKS for verifying JWTs. | |
| HUB_AUTH_BASE_URL | Base URL to reach the Hub's core component. | |
| HUB_AUTH_ROBOT_ID | Robot ID associated with the node. | |
| HUB_AUTH_ROBOT_SECRET_FILE | Path to the file containing the secret of the node's associated robot account, as plain text. | |
| HUB_BASE_URL | Base URL to reach the Hub's auth component. | |
| HUB_MESSENGER_BASE_URL | Base URL to reach the Hub's messenger component. | |
| LOG_LEVEL | Log level being used. Can be either of `trace`, `debug`, `info`, `warn` or `error`. | `info` |
| MANAGEMENT_SERVER_PORT | Port being used by the management server (providing health check endpoints etc.) | `8090` |
| PERSISTENCE_DATABASE_NAME | Database name to use when connecting to a MongoDB instance. | `messagebroker` |
| PERSISTENCE_HOSTNAME | Hostname to use to connect to a MongoDB instance. | `localhost` |
| PERSISTENCE_PORT | Port to use to connect to a MongoDB instance. | `17017` |
| SECURITY_ADDITIONAL_TRUSTED_CERTS_FILE | Path to a certificate bundle containing additional certificates to be loaded during startup. | |
| SECURITY_NODE_PRIVATE_ECDH_KEY_FILE | Path to the file containing the node's private EC key in PEM format, as plain text. | |
| SERVER_PORT | Port being used by the Web server. | `8080` |
| EnvVar | Description | Default |
|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
| AUTH_JWKS_URL | URL to retrieve a JWKS for verifying JWTs. | |
| HUB_AUTH_BASE_URL | Base URL to reach the Hub's core component. | |
| HUB_AUTH_ROBOT_ID | Robot ID associated with the node. | |
| HUB_AUTH_ROBOT_SECRET_FILE | Path to the file containing the secret of the node's associated robot account, as plain text. | |
| HUB_BASE_URL | Base URL to reach the Hub's auth component. | |
| HUB_MESSENGER_BASE_URL | Base URL to reach the Hub's messenger component. | |
| LOG_LEVEL | Log level being used. Can be either of `trace`, `debug`, `info`, `warn` or `error`. | `info` |
| MANAGEMENT_SERVER_PORT | Port being used by the management server (providing health check endpoints etc.) | `8090` |
| PERSISTENCE_DATABASE_NAME | Database name to use when connecting to a MongoDB instance. | `messagebroker` |
| PERSISTENCE_HOSTNAME | Hostname to use to connect to a MongoDB instance. | `localhost` |
| PERSISTENCE_PORT | Port to use to connect to a MongoDB instance. | `17017` |
| PROXY_HOST | FQDN of the proxy to use. | |
| PROXY_PORT | Port of the proxy to use. | |
| PROXY_WHITELIST | A regex pattern (Java) to describe hosts that bypass the proxy to be reached directly. See [JavaDocs](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html) for more information about the pattern usage. | |
| PROXY_USERNAME | Username being used when authenticating against the proxy. | |
| PROXY_PASSWORD_FILE | Path to the file containing the password used when authenticating against the proxy. | |
| SECURITY_ADDITIONAL_TRUSTED_CERTS_FILE | Path to a certificate bundle containing additional certificates to be loaded during startup. | |
| SECURITY_NODE_PRIVATE_ECDH_KEY_FILE | Path to the file containing the node's private EC key in PEM format, as plain text. | |
| SERVER_PORT | Port being used by the Web server. | `8080` |

## Endpoint Documentation

Expand Down
15 changes: 15 additions & 0 deletions dev/config/proxy/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
events {
worker_connections 4096;
}

http {
server {
listen 8888;

location / {
resolver 8.8.8.8;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://$http_host$uri$is_args$args;
}
}
}
12 changes: 10 additions & 2 deletions dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ services:
keycloak:
image: keycloak/keycloak:24.0.4@sha256:ff02c932f0249c58f32b8ff1b188a48cc90809779a3a05931ab67f5672400ad0
ports:
- 18080:8080
- 10433:8433
- "18080:8080"
- "10433:8433"
command:
- start-dev
- --health-enabled
Expand All @@ -23,3 +23,11 @@ services:
KEYCLOAK_ADMIN_PASSWORD: admin
volumes:
- ./config/keycloak/private-aim-realm.json:/opt/keycloak/data/import/default-path-realm.json:ro
proxy:
image: nginx:1.29.0-alpine@sha256:d67ea0d64d518b1bb04acde3b00f722ac3e9764b3209a9b0a98924ba35e4b779
ports:
- "8888:8888"
volumes:
- ./config/proxy/nginx.conf:/etc/nginx/nginx.conf:ro
command: [nginx-debug, '-g', 'daemon off;']

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import de.privateaim.node_message_broker.common.hub.HttpHubClient;
import de.privateaim.node_message_broker.common.hub.HubClient;
import de.privateaim.node_message_broker.common.hub.auth.HubOIDCAuthenticator;
import io.netty.handler.ssl.SslContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
Expand All @@ -18,10 +20,15 @@
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

@Slf4j
@Configuration
public class CommonSpringConfig {

Expand All @@ -40,6 +47,21 @@ public class CommonSpringConfig {
@Value("${app.hub.auth.robotSecretFile}")
private String hubAuthRobotSecretFile;

@Value("${app.proxy.host}")
private String proxyHost;

@Value("${app.proxy.port}")
private Integer proxyPort;

@Value("${app.proxy.whitelist}")
private String proxyWhitelist;

@Value("${app.proxy.username}")
private String proxyUsername;

@Value("${app.proxy.passwordFile}")
private String proxyPasswordFile;

@Qualifier("HUB_AUTH_ROBOT_SECRET")
@Bean
public String hubAuthRobotSecret() throws IOException {
Expand All @@ -58,11 +80,70 @@ HttpRetryConfig exchangeRetryConfig() {
return new HttpRetryConfig(EXCHANGE__MAX_RETRIES, EXCHANGE__MAX_RETRY_DELAY_MS);
}

@Qualifier("CORE_HTTP_CLIENT")
@Bean
HttpClient decoratedHttpClient(@Qualifier("COMMON_NETTY_SSL_CONTEXT") SslContext sslContext) {
var client = HttpClient.create();
client = decorateClientWithSSLContext(client, sslContext);
client = decorateClientWithProxySettings(client);

return client;
}

private HttpClient decorateClientWithSSLContext(HttpClient client, SslContext sslContext) {
return client.secure(t -> t.sslContext(sslContext));
}

private HttpClient decorateClientWithProxySettings(HttpClient client) {
if (!proxyHost.isBlank() && proxyPort != null) {
log.info("configuring usage of proxy at `{}:{}` with the following hosts whitelisted (via regex): `{}`",
proxyHost, proxyPort, proxyWhitelist);
return client.proxy(proxy -> {
var proxyBuilder = proxy.type(ProxyProvider.Proxy.HTTP)
.host(proxyHost)
.port(proxyPort);

if (!proxyWhitelist.isBlank()) {
log.info("configuring whitelist for proxy at `{}:{}`", proxyHost, proxyPort);
proxyBuilder.nonProxyHosts(proxyWhitelist);
} else {
log.info("skipping whitelist configuration for proxy at `{}:{}` since no whitelist " +
"is configured", proxyHost, proxyPort);
}

if (!proxyUsername.isBlank() && !proxyPasswordFile.isBlank()) {
try {
log.info("configuring authentication for proxy");
var proxyPassword = Files.readString(Paths.get(proxyPasswordFile));
proxyBuilder.username(proxyUsername)
.password((_username) -> proxyPassword);
} catch (IOException e) {
log.error("cannot read password file for proxy at `{}`", proxyPasswordFile, e);
throw new RuntimeException(e);
}
} else {
log.info("skipping authentication configuration for proxy at `{}:{}` since no " +
"credentials are configured", proxyHost, proxyPort);
}
}
);
} else {
log.info("skipping proxy configuration due to no specified settings");
return client;
}
}

@Qualifier("CORE_HTTP_CONNECTOR")
@Bean
ReactorClientHttpConnector httpConnector(@Qualifier("CORE_HTTP_CLIENT") HttpClient httpClient) {
return new ReactorClientHttpConnector(httpClient);
}

@Qualifier("HUB_CORE_WEB_CLIENT")
@Bean
public WebClient alwaysReAuthenticatedWebClient(
@Qualifier("HUB_AUTHENTICATION_MIDDLEWARE") ExchangeFilterFunction authenticationMiddleware,
@Qualifier("BASE_SSL_HTTP_CLIENT_CONNECTOR") ReactorClientHttpConnector baseSslHttpClientConnector) {
@Qualifier("CORE_HTTP_CONNECTOR") ReactorClientHttpConnector httpConnector) {
// We can't use Spring's default security mechanisms out-of-the-box here since HUB uses a non-standard grant
// type which is not supported. There's a way by using a custom grant type accompanied by a client manager.
// However, this endeavour is not pursued for the sake of simplicity.
Expand All @@ -76,7 +157,7 @@ public WebClient alwaysReAuthenticatedWebClient(
.uriBuilderFactory(factory)
.defaultHeaders(httpHeaders -> httpHeaders.setAccept(List.of(MediaType.APPLICATION_JSON)))
.filter(authenticationMiddleware)
.clientConnector(baseSslHttpClientConnector)
.clientConnector(httpConnector)
.build();
}

Expand All @@ -91,11 +172,11 @@ public HubClient hubClient(
@Qualifier("HUB_AUTH_WEB_CLIENT")
@Bean
WebClient hubAuthWebClient(
@Qualifier("BASE_SSL_HTTP_CLIENT_CONNECTOR") ReactorClientHttpConnector baseSslHttpClientConnector) {
@Qualifier("CORE_HTTP_CONNECTOR") ReactorClientHttpConnector httpConnector) {
return WebClient.builder()
.baseUrl(hubAuthBaseUrl)
.defaultHeaders(httpHeaders -> httpHeaders.setAccept(List.of(MediaType.APPLICATION_JSON)))
.clientConnector(baseSslHttpClientConnector)
.clientConnector(httpConnector)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
Expand Down Expand Up @@ -45,15 +43,6 @@ public class CommonSslSpringConfig {
@Value("${app.security.additionalTrustedCertsFile}")
private String additionalTrustedCertsFile;

@Qualifier("BASE_SSL_HTTP_CLIENT_CONNECTOR")
@Bean
ReactorClientHttpConnector baseHttpClientConnector(@Qualifier("COMMON_NETTY_SSL_CONTEXT") SslContext sslContext) {
return new ReactorClientHttpConnector(
HttpClient
.create()
.secure(t -> t.sslContext(sslContext)));
}

@Qualifier("COMMON_NETTY_SSL_CONTEXT")
@Bean
SslContext sslContextNetty(@Qualifier("COMMON_TRUST_MANAGER_FACTORY") TrustManagerFactory tmf) throws IOException {
Expand Down
Loading
Loading