Skip to content

Commit

Permalink
FDP-94: Refactored, updated copyright
Browse files Browse the repository at this point in the history
  • Loading branch information
sanderv committed Nov 14, 2023
1 parent f5b45bf commit 560df4a
Show file tree
Hide file tree
Showing 68 changed files with 1,711 additions and 1,831 deletions.
16 changes: 10 additions & 6 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright 2023 Alliander N.V.
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0

plugins {
id("org.springframework.boot")
Expand All @@ -10,16 +12,20 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-logging")
implementation("org.springframework.kafka:spring-kafka")
implementation("com.microsoft.azure:msal4j:1.13.10")
implementation("org.apache.httpcomponents:httpclient:4.5.13")
implementation(kotlin("reflect"))
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")

implementation(project(":components:kafka"))
implementation(project(":components:soap"))

implementation("org.springframework:spring-aspects")

runtimeOnly("io.micrometer:micrometer-registry-prometheus")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.junit.jupiter:junit-jupiter-engine")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("org.mockito:mockito-junit-jupiter")
}

tasks.withType<org.springframework.boot.gradle.tasks.bundling.BootBuildImage> {
Expand Down Expand Up @@ -47,8 +53,6 @@ testing {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.wiremock:wiremock:3.3.1")

implementation(project(":components:soap"))

implementation("org.testcontainers:kafka:1.17.6")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright 2023 Alliander N.V.
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0

package org.gxf.soapbridge

Expand All @@ -7,7 +9,7 @@ import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
import org.assertj.core.api.Assertions.assertThat
import org.gxf.soapbridge.application.factories.SslContextFactory
import org.gxf.soapbridge.application.properties.SoapConfigurationProperties
import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
Expand Down Expand Up @@ -47,7 +49,7 @@ class EndToEndTest(

@Test
fun testRequestResponse(applicationContext: ApplicationContext) {
// Setup an SSL context for organisation "testClient" using its client certificate
// Arrange an SSL context for organisation "testClient" using its client certificate
val sslContextForOrganisation = sslContextFactory.createSslContext("testClient")
val httpClient = HttpClient.newBuilder()
.sslContext(sslContextForOrganisation)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright 2023 Alliander N.V.
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0

package org.gxf.soapbridge

Expand Down
4 changes: 3 additions & 1 deletion application/src/integrationTest/resources/generate_certs.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/bash
# Copyright 2023 Alliander N.V.
# SPDX-FileCopyrightText: Copyright Contributors to the GXF project
#
# SPDX-License-Identifier: Apache-2.0

set -x

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0

package org.gxf.soapbridge.application.configuration;

import jakarta.servlet.http.HttpServletRequest;
import org.gxf.soapbridge.soap.endpoints.SoapEndpoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;

@Component
public class SoapConfiguration extends AbstractHandlerMapping {
private final SoapEndpoint soapEndpoint;

public SoapConfiguration(final SoapEndpoint soapEndpoint) {
this.soapEndpoint = soapEndpoint;
}

@Override
protected Object getHandlerInternal(final HttpServletRequest request) {
return soapEndpoint;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0

package org.gxf.soapbridge.application.factories;

import static org.gxf.soapbridge.configuration.properties.HostnameVerificationStrategy.ALLOW_ALL_HOSTNAMES;
import static org.gxf.soapbridge.configuration.properties.HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES;

import javax.net.ssl.HostnameVerifier;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties;
import org.gxf.soapbridge.soap.exceptions.ProxyServerException;
import org.springframework.stereotype.Component;

@Component
public class HostnameVerifierFactory {
private final SoapConfigurationProperties soapConfiguration;

public HostnameVerifierFactory(final SoapConfigurationProperties soapConfiguration) {
this.soapConfiguration = soapConfiguration;
}

public HostnameVerifier getHostnameVerifier() throws ProxyServerException {
if (soapConfiguration.getHostnameVerificationStrategy() == ALLOW_ALL_HOSTNAMES) {
return new NoopHostnameVerifier();
} else if (soapConfiguration.getHostnameVerificationStrategy()
== BROWSER_COMPATIBLE_HOSTNAMES) {
return new DefaultHostnameVerifier();
} else {
throw new ProxyServerException("No hostname verification strategy set!");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.soapbridge.application.factories;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import org.gxf.soapbridge.application.services.SslContextCacheService;
import org.gxf.soapbridge.soap.exceptions.ProxyServerException;
import org.gxf.soapbridge.soap.exceptions.UnableToCreateHttpsURLConnectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/** This {@link @Component} class can create {@link HttpsURLConnection} instances. */
@Component
public class HttpsUrlConnectionFactory {

private static final Logger LOGGER = LoggerFactory.getLogger(HttpsUrlConnectionFactory.class);

/** Cache of {@link SSLContext} instances used to obtain {@link HttpsURLConnection} instances. */
@Autowired private SslContextCacheService sslContextCacheService;

@Autowired private HostnameVerifierFactory hostnameVerifierFactory;

/**
* Create an {@link HttpsURLConnection} instance for the given arguments.
*
* @param uri The full URI of the end-point for this connection.
* @param host The host consists of domain name, server name or IP address followed by the port.
* Example: localhost:443
* @param contentLength The content length of the SOAP payload which will be sent to the end-point
* using this connection.
* @param commonName The common name for the organization.
* @return Null in case the {@link SSLContext} cannot be created or fetched from the {@link
* SslContextCacheService}, or a configured and initialized {@link HttpsURLConnection}
* instance.
* @throws UnableToCreateHttpsURLConnectionException In case the configuration and/or
* initialization of an {@link HttpsURLConnection} instance fails.
*/
public HttpsURLConnection createConnection(
final String uri, final String host, final String contentLength, final String commonName)
throws UnableToCreateHttpsURLConnectionException {
try {
// Get SSLContext instance.
SSLContext sslContext = null;
if (StringUtils.isEmpty(commonName)) {
sslContext = sslContextCacheService.getSslContext();
} else {
sslContext = sslContextCacheService.getSslContextForCommonName(commonName);
}
// Check SSLContext instance.
if (sslContext == null) {
LOGGER.error(
"SSLContext instance is null. Unable to create HttpsURLConnection instance for uri: {}, host: {}, content length: {}, common name: {}",
uri,
host,
contentLength,
commonName);
return null;
}
// Create connection.
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifierFactory.getHostnameVerifier());
final HttpsURLConnection connection = (HttpsURLConnection) new URL(uri).openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty(
"Accept-Encoding", "text/xml;charset=" + StandardCharsets.UTF_8.name());
connection.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.name());
connection.setRequestProperty(
"Content-Type", "text/xml;charset=" + StandardCharsets.UTF_8.name());
connection.setRequestProperty("SOAP-ACTION", "");
connection.setRequestProperty("Content-Length", contentLength);
connection.setRequestProperty("Host", host);
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36");
LOGGER.debug(
"Created HttpsURLConnection instance for uri: {}, host: {}, content length: {}, common name: {}",
uri,
host,
contentLength,
commonName);

return connection;
} catch (final IOException | ProxyServerException e) {
LOGGER.error("Creating connection failed.", e);
throw new UnableToCreateHttpsURLConnectionException(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.soapbridge.application.factories;

import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import javax.net.ssl.*;
import org.gxf.soapbridge.application.services.SslContextCacheService;
import org.gxf.soapbridge.configuration.properties.SecurityConfigurationProperties;
import org.gxf.soapbridge.configuration.properties.StoreConfigurationProperties;
import org.gxf.soapbridge.soap.exceptions.UnableToCreateKeyManagersException;
import org.gxf.soapbridge.soap.exceptions.UnableToCreateTrustManagersException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/** This {@link @Component} class can create {@link SSLContext} instances. */
@Component
public class SslContextFactory {

private static final Logger LOGGER = LoggerFactory.getLogger(SslContextFactory.class);

/**
* In order to create a proper instance of {@link SSLContext}, a protocol must be specified. Note
* that if {@link SSLContext#getDefault()} is used, the created instance does not support the
* custom {@link TrustManager} and {@link KeyManager} which are needed for this application!!!
*/
private static final String SSL_CONTEXT_PROTOCOL = "TLS";

@Autowired private SecurityConfigurationProperties securityConfiguration;

/**
* Using the trust store, a {@link TrustManager} instance is created. This instance is created
* once, and assigned to this field. Subsequent calls to {@link
* SslContextCacheService#getSslContext()} will use the instance in order to create {@link
* SSLContext} instances.
*/
private TrustManager[] trustManagersForHttps;

/**
* Using the trust store, a {@link TrustManager} instance is created. This instance is created
* once, and assigned to this field. Subsequent calls to {@link
* SslContextCacheService#getSslContextForCommonName(String)} will use the instance in order to
* create {@link SSLContext} instances.
*/
private TrustManager[] trustManagersForHttpsWithClientCertificate;

/**
* Create an {@link SSLContext} instance.
*
* @return An {@link SSLContext} instance.
*/
public SSLContext createSslContext() {
return createSslContext("");
}

/**
* Create an {@link SSLContext} instance for the given common name.
*
* @param commonName The common name used to open the key store for an organization. May be an
* empty string if no client certificate is required.
* @return An {@link SSLContext} instance.
*/
public SSLContext createSslContext(final String commonName) {
if (commonName.isEmpty()) {
try {
// Only create an instance of the trust manager array once.
if (trustManagersForHttps == null) {
trustManagersForHttps = openTrustStoreAndCreateTrustManagers();
}
// Use the trust manager to initialize an SSLContext instance.
final SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
sslContext.init(null, trustManagersForHttps, null);
LOGGER.info("Created SSL context using trust manager for HTTPS");
return sslContext;
} catch (final Exception e) {
LOGGER.error("Unexpected exception while creating SSL context using trust manager", e);
return null;
}
} else {
try {
// Only create an instance of the trust manager array once.
if (trustManagersForHttpsWithClientCertificate == null) {
trustManagersForHttpsWithClientCertificate = openTrustStoreAndCreateTrustManagers();
}
final KeyManager[] keyManagerArray = openKeyStoreAndCreateKeyManagers(commonName);
// Use the key manager and trust manager to initialize an
// SSLContext instance.
final SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
sslContext.init(keyManagerArray, trustManagersForHttpsWithClientCertificate, null);
// It is not possible to set the SSLContext instance as default
// using: "SSLContext.setDefault(sslContext);" The SSl context
// is unique for each organization because each organization has
// their own *.pfx key store, which is the client certificate.
LOGGER.info("Created SSL context using trust manager and key manager for HTTPS");
return sslContext;
} catch (final Exception e) {
LOGGER.error(
"Unexpected exception while creating SSL context using trust manager and key manager",
e);
return null;
}
}
}

private TrustManager[] openTrustStoreAndCreateTrustManagers()
throws UnableToCreateTrustManagersException {
final StoreConfigurationProperties trustStore = securityConfiguration.getTrustStore();
LOGGER.debug("Opening trust store, pathToTrustStore: {}", trustStore.getLocation());
try (final InputStream trustStream = new FileInputStream(trustStore.getLocation())) {
// Create trust manager using *.jks file.
final char[] trustPassword = trustStore.getPassword().toCharArray();
final KeyStore trustStoreInstance = KeyStore.getInstance(trustStore.getType());
trustStoreInstance.load(trustStream, trustPassword);
final TrustManagerFactory trustFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustFactory.init(trustStoreInstance);
return trustFactory.getTrustManagers();
} catch (final Exception e) {
throw new UnableToCreateTrustManagersException(
"Unexpected exception while creating trust managers using trust store", e);
}
}

private KeyManager[] openKeyStoreAndCreateKeyManagers(final String commonName)
throws UnableToCreateKeyManagersException {
// Assume the path does not have a trailing slash ( '/' ) and assume the
// file extension to be *.pfx.
final StoreConfigurationProperties keyStore = securityConfiguration.getKeyStore();
final String pathToKeyStore = String.format("%s/%s.pfx", keyStore.getLocation(), commonName);
LOGGER.debug("Opening key store, pathToKeyStore: {}", pathToKeyStore);
try (final InputStream keyStoreStream = new FileInputStream(pathToKeyStore)) {
// Create key manager using *.pfx file.
final KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final KeyStore keyStoreInstance = KeyStore.getInstance(keyStore.getType());
final char[] keyPassword = keyStore.getPassword().toCharArray();
keyStoreInstance.load(keyStoreStream, keyPassword);
keyManagerFactory.init(keyStoreInstance, keyPassword);
return keyManagerFactory.getKeyManagers();
} catch (final Exception e) {
throw new UnableToCreateKeyManagersException(
"Unexpected exception while creating key managers using key store", e);
}
}
}
Loading

0 comments on commit 560df4a

Please sign in to comment.