From 1e585d2ccc711f22a954101f80241d3c6e0aff12 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Tue, 14 Nov 2023 13:25:47 +0100 Subject: [PATCH 01/46] FDP-94: Initial setup Signed-off-by: Sander Verbruggen --- .github/workflows/gradle.yml | 37 ++ .gitignore | 37 ++ LICENSES/Apache-2.0.txt | 73 +++ application/build.gradle.kts | 56 +++ .../kotlin/org/gxf/soapbridge/EndToEndTest.kt | 100 ++++ .../soapbridge/SoapBridgeApplicationTests.kt | 17 + .../resources/application.yaml | 65 +++ .../resources/generate_certs.sh | 47 ++ .../integrationTest/resources/localhost.ext | 5 + .../resources/organisations/testClient.pfx | Bin 0 -> 4344 bytes .../resources/proxy.keystore.jks | Bin 0 -> 4494 bytes .../resources/proxy.truststore.jks | Bin 0 -> 1830 bytes .../integrationTest/resources/sign-key.der | Bin 0 -> 1216 bytes .../integrationTest/resources/verify-key.der | Bin 0 -> 294 bytes .../gxf/soapbridge/SoapBridgeApplication.kt | 17 + .../configuration/SecurityConfiguration.kt | 41 ++ .../ObservabilityConfiguration.kt | 18 + .../src/main/resources/application-dev.yml | 10 + .../src/main/resources/application.yaml | 45 ++ .../src/main/resources/proxy-server.yml | 140 ++++++ build.gradle.kts | 64 +++ components/core/build.gradle.kts | 9 + .../messaging/ProxyRequestsMessageSender.kt | 9 + .../messaging/ProxyResponsesMessageSender.kt | 9 + .../exceptions/ProxyMessageException.kt | 10 + .../messages/ProxyServerBaseMessage.kt | 25 + .../messages/ProxyServerRequestMessage.kt | 83 ++++ .../messages/ProxyServerResponseMessage.kt | 50 ++ .../services/ProxyRequestHandler.kt | 9 + .../services/ProxyResponseHandler.kt | 9 + components/kafka/build.gradle.kts | 17 + .../listeners/ProxyRequestKafkaListener.kt | 32 ++ .../listeners/ProxyResponseKafkaListener.kt | 34 ++ .../TopicsConfigurationProperties.kt | 16 + .../kafka/senders/ProxyRequestKafkaSender.kt | 25 + .../kafka/senders/ProxyResponseKafkaSender.kt | 25 + components/soap/build.gradle.kts | 34 ++ .../configuration/SoapConfiguration.java | 22 + .../factories/HostnameVerifierFactory.java | 33 ++ .../factories/HttpsUrlConnectionFactory.java | 106 +++++ .../factories/SslContextFactory.java | 152 ++++++ .../services/ClientCommunicationService.java | 67 +++ .../services/ConnectionCacheService.java | 95 ++++ .../PlatformCommunicationService.java | 66 +++ .../application/services/SigningService.java | 86 ++++ .../services/SslContextCacheService.java | 79 ++++ .../utils/RandomStringFactory.java | 52 +++ .../soapbridge/soap/clients/Connection.java | 72 +++ .../soapbridge/soap/clients/SoapClient.java | 173 +++++++ .../soap/endpoints/SoapEndpoint.java | 440 ++++++++++++++++++ .../ConnectionNotFoundInCacheException.java | 14 + .../soap/exceptions/ProxyServerException.java | 25 + ...leToCreateHttpsURLConnectionException.java | 14 + .../UnableToCreateKeyManagersException.java | 14 + .../UnableToCreateTrustManagersException.java | 14 + .../soap/valueobjects/ClientCertificate.java | 24 + .../SecurityConfigurationProperties.kt | 77 +++ .../properties/SoapConfigurationProperties.kt | 34 ++ .../utils/RandomStringServiceTests.java | 32 ++ .../soap/clients/SoapClientTest.java | 106 +++++ docker-compose.yaml | 35 ++ gradle.properties | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 240 ++++++++++ gradlew.bat | 91 ++++ settings.gradle.kts | 8 + 67 files changed, 3346 insertions(+) create mode 100644 .github/workflows/gradle.yml create mode 100644 .gitignore create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 application/build.gradle.kts create mode 100644 application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt create mode 100644 application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt create mode 100644 application/src/integrationTest/resources/application.yaml create mode 100644 application/src/integrationTest/resources/generate_certs.sh create mode 100644 application/src/integrationTest/resources/localhost.ext create mode 100644 application/src/integrationTest/resources/organisations/testClient.pfx create mode 100644 application/src/integrationTest/resources/proxy.keystore.jks create mode 100644 application/src/integrationTest/resources/proxy.truststore.jks create mode 100644 application/src/integrationTest/resources/sign-key.der create mode 100644 application/src/integrationTest/resources/verify-key.der create mode 100644 application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt create mode 100644 application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt create mode 100644 application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt create mode 100644 application/src/main/resources/application-dev.yml create mode 100644 application/src/main/resources/application.yaml create mode 100644 application/src/main/resources/proxy-server.yml create mode 100644 build.gradle.kts create mode 100644 components/core/build.gradle.kts create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt create mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt create mode 100644 components/kafka/build.gradle.kts create mode 100644 components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt create mode 100644 components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt create mode 100644 components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt create mode 100644 components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt create mode 100644 components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt create mode 100644 components/soap/build.gradle.kts create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java create mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java create mode 100644 components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt create mode 100644 components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt create mode 100644 components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java create mode 100644 components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java create mode 100644 docker-compose.yaml create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..403c8df --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,37 @@ +# Copyright 2023 Alliander N.V. + +name: Gradle Pipeline + +on: + push: + branches: [ "main" , "develop" ] + tags: [ "v**" ] + pull_request: + branches: [ "main", "develop" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.8.1 + with: + arguments: build integrationTest bootBuildImage sonar + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Publish Docker image + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.ref_type == 'tag' + uses: gradle/gradle-build-action@v2.8.1 + with: + arguments: bootBuildImage -PpublishImage + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/application/build.gradle.kts b/application/build.gradle.kts new file mode 100644 index 0000000..453f647 --- /dev/null +++ b/application/build.gradle.kts @@ -0,0 +1,56 @@ +// Copyright 2023 Alliander N.V. + +plugins { + id("org.springframework.boot") +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-web") + 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(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") +} + +tasks.withType { + imageName.set("ghcr.io/osgp/gxf-soap-bridge:${version}") + if (project.hasProperty("publishImage")) { + publish.set(true) + docker { + publishRegistry { + username.set(System.getenv("GITHUB_ACTOR")) + password.set(System.getenv("GITHUB_TOKEN")) + } + } + } +} + +testing { + suites { + val integrationTest by registering(JvmTestSuite::class) { + useJUnitJupiter() + dependencies { + implementation(project()) + implementation("org.springframework.boot:spring-boot-starter-test") + implementation("org.springframework.kafka:spring-kafka-test") + implementation("org.assertj:assertj-core") + 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") + } + } + } +} diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt new file mode 100644 index 0000000..ce539e9 --- /dev/null +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt @@ -0,0 +1,100 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge + +import com.github.tomakehurst.wiremock.client.WireMock.* +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.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.ComponentScan +import org.springframework.http.client.reactive.JdkClientHttpConnector +import org.springframework.kafka.test.context.EmbeddedKafka +import org.springframework.web.reactive.function.client.WebClient +import java.net.http.HttpClient + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ComponentScan(basePackages = ["org.gxf.soapbridge"]) +@EmbeddedKafka(topics = ["requests", "responses"]) +class EndToEndTest( + @LocalServerPort private val soapPort: Int, + @Autowired private val sslContextFactory: SslContextFactory, + @Autowired private val soapConfigProperties: SoapConfigurationProperties +) { + private val proxyUrl = "https://localhost:$soapPort/proxy-server" + private val methodPath = "/someSoapMethod" + private val callUrl = "$proxyUrl$methodPath" + + @BeforeEach + fun setUp() { + wireMockExtension.stubFor( + post(methodPath) + .withRequestBody(equalToXml(soapBody)) + .willReturn( + ok().withBody(soapResponse) + ) + ) + } + + @Test + fun testRequestResponse(applicationContext: ApplicationContext) { + // Setup an SSL context for organisation "testClient" using its client certificate + val sslContextForOrganisation = sslContextFactory.createSslContext("testClient") + val httpClient = HttpClient.newBuilder() + .sslContext(sslContextForOrganisation) + .build() + val webClient = WebClient.builder() + .clientConnector(JdkClientHttpConnector(httpClient)) + .build() + + // Act: send SOAP request and get the answer + val responseBody = webClient.post().uri(callUrl) + .bodyValue(soapBody) + .exchangeToMono { it.bodyToMono(String::class.java) } + .block() + + // Assert + assertThat(responseBody).isEqualTo(soapResponse) + } + + val soapBody = """ + + + + + + T + + + + """.trimIndent() + + val soapResponse = "Read This Fine Message" + + companion object { + @JvmField + @RegisterExtension + val wireMockExtension: WireMockExtension = + WireMockExtension.newInstance().options( + wireMockConfig() + .httpDisabled(true).httpsPort(8888) + .keystorePath("src/integrationTest/resources/proxy.keystore.jks") + .keystorePassword("123456") + .keyManagerPassword("123456") + .keystoreType("PKCS12") + .trustStorePath("src/integrationTest/resources/proxy.truststore.jks") + .trustStorePassword("123456") + .trustStoreType("PKCS12") + .needClientAuth(true) + ).build() + } +} diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt new file mode 100644 index 0000000..3566f8c --- /dev/null +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt @@ -0,0 +1,17 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.kafka.test.context.EmbeddedKafka + +@SpringBootTest +@EmbeddedKafka(topics = ["avroTopic"]) +class SoapBridgeApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/application/src/integrationTest/resources/application.yaml b/application/src/integrationTest/resources/application.yaml new file mode 100644 index 0000000..8c4b888 --- /dev/null +++ b/application/src/integrationTest/resources/application.yaml @@ -0,0 +1,65 @@ +spring: + kafka: + bootstrap-servers: ${spring.embedded.kafka.brokers} +server: + ssl: + enabled: true + protocol: TLS + client-auth: need + key-store: "classpath:proxy.keystore.jks" + key-store-password: 123456 + key-store-type: PKCS12 + key-alias: localhost + key-password: 123456 + trust-store: "classpath:proxy.truststore.jks" + trust-store-password: 123456 + trust-store-type: PKCS12 + +wiremock: + call-proxy: + url: "https://localhost:9000" + notification-proxy: + url: "https://localhost:9001" + +proxy-server: + network-zone: BOTH + +security: + key-store: + location: src/integrationTest/resources/organisations/ + password: 123456 + type: pkcs12 + trust-store: + location: src/integrationTest/resources/proxy.truststore.jks + password: 123456 + type: jks + signing: + key-type: RSA + sign-key-file: src/integrationTest/resources/sign-key.der + verify-key-file: src/integrationTest/resources/verify-key.der + provider: SunRsaSign + signature: SHA256withRSA + +topics: +# short circuit configuration: topics are linked back to the same proxy instance + outgoing: + requests: requests + responses: responses + incoming: + requests: requests + responses: responses + +soap: + call-endpoint: + host: localhost + port: 8888 + protocol: https + hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES + time-out: 45 + custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 + +logging: + level: + root: warn + org: + gxf: info diff --git a/application/src/integrationTest/resources/generate_certs.sh b/application/src/integrationTest/resources/generate_certs.sh new file mode 100644 index 0000000..33e99f8 --- /dev/null +++ b/application/src/integrationTest/resources/generate_certs.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Copyright 2023 Alliander N.V. + +set -x + +mkdir -p organisations + +# Clean up previous key/trust store +rm *.jks +rm organisations/* + + +PASSWORD=123456 +echo Generating proxy certificate +openssl req -x509 -sha256 -days 3650 -newkey rsa:4096 -keyout rootCA.key -out rootCA.crt -passout pass:$PASSWORD -subj "/C=NL/ST=Gelderland/L=Arnhem/O=Alliander/OU=IT/CN=soap-bridge-CA" +openssl req -new -newkey rsa:4096 -passout pass:$PASSWORD -keyout proxy.key -out proxy.csr -subj "/C=NL/ST=Gelderland/L=Arnhem/O=Alliander/OU=IT/CN=localhost" +openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in proxy.csr -out proxy.crt -days 3650 -CAcreateserial -passin pass:$PASSWORD -extfile localhost.ext +openssl pkcs12 -export -out proxy.p12 -name "localhost" -inkey proxy.key -in proxy.crt -passout pass:$PASSWORD -passin pass:$PASSWORD + +echo +echo +echo Creating proxy keystore +keytool -importkeystore -srckeystore proxy.p12 -srcstoretype PKCS12 -destkeystore proxy.keystore.jks -deststoretype PKCS12 -srcstorepass $PASSWORD -deststorepass $PASSWORD -noprompt + +echo +echo +echo Creating proxy truststore +keytool -import -trustcacerts -noprompt -alias ca -ext san=dns:localhost,ip:127.0.0.1 -file rootCA.crt -keystore proxy.truststore.jks -srcstorepass $PASSWORD -deststorepass $PASSWORD + +echo +echo +echo Generating client certificate +openssl req -new -newkey rsa:4096 -nodes -keyout testClient.key -out testClient.csr -passout pass:$PASSWORD \ + -subj "/C=NL/ST=Gelderland/L=Arnhem/O=Alliander/OU=IT/CN=testClient" + + +openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in proxy.csr -out proxy.crt -days 3650 -CAcreateserial -passin pass:$PASSWORD -extfile localhost.ext +openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in testClient.csr -out testClient.crt -days 3650 -CAcreateserial -passin pass:$PASSWORD +openssl pkcs12 -export -out organisations/testClient.pfx -name "testClient" -inkey testClient.key -in testClient.crt -passout pass:$PASSWORD -passin pass:$PASSWORD + +echo Creating sign and verify keys +openssl genrsa -out signing.pem 2048 +openssl rsa -in signing.pem -pubout -outform DER -out verify-key.der +openssl pkcs8 -topk8 -inform PEM -outform DER -in signing.pem -out sign-key.der -nocrypt + +# Clean up intermediate files +rm *.crt *.key *.csr *.p12 *.pem diff --git a/application/src/integrationTest/resources/localhost.ext b/application/src/integrationTest/resources/localhost.ext new file mode 100644 index 0000000..45324cc --- /dev/null +++ b/application/src/integrationTest/resources/localhost.ext @@ -0,0 +1,5 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost diff --git a/application/src/integrationTest/resources/organisations/testClient.pfx b/application/src/integrationTest/resources/organisations/testClient.pfx new file mode 100644 index 0000000000000000000000000000000000000000..b880a09abec0aded0a6f855f647123f95f7551a8 GIT binary patch literal 4344 zcmai&WmFT6*T*-?k-})CWOR=1mXa8Z4FORJ>F!q8KtMuL8bL-5lo%zTlmgO?(jg_I zQxG5j=XuWW_q_YRxb?mFocr=~&xN9>PY8g7P!x3~38~-<-52MSz+1pV6m>2UiaPVJ zoCZaa*#6rhDMXQ&{*_<^K)~Oi`)>ybeeh2~ObNXY75KNLfU**Sm`rPMMrDwE0s;^K zN!~iH8390o9U7$T75m1nbTDA&fG2bZv2T@TFX2&FE9DyQUeqaPIS~L)N zZ7JtOKbUNvep$nULZ^NEoKR-QB_nUqTg`aO#ltoTkw@N;Z~ zQYpK4De{smz@EYo&ao5uuPkFbQo19#9fbu>2`xvi6KbBS{6yPk$r zN*MSGl}eAH+}umzYbzDYEATY#Oz~8_j;qbwqqgjVji}-Ui~Vk;YsXoOvMwGbs|xuQmvmNVduW9a5=%W=q6ZJVWJ6=!RZ$5Ylp| zrYr5Od=`{`qau+xB9AndoOe6!gBic<)pQG7Dk8L60ncypLT%-N%Q?R5B*(i0D>aEG zJ36=nNNJDtc-iT|-KAk1y>an{ptFy6$^mXfYCEKW4D+t!aVt=kDy{`xwAE}>*e)Nz z2|>03sft(fig#m0%LfxJ(|WtHr@7B2pwHm3b)?;>gUb1KT$V*RvQRJLu2ft@aAO{i zJ}oWv{+%@)!{wTR_^sFhM>WO~T(z%wew@)6{$q3_@}T^u_wT*40qMlVXnJh18fX)}=@K_tbF z_Y{g49yoB8Uyt=1PPWmyA%5+Rv)l0k#SQJ^FxF3PL*UH4&i3p_EI+GPo1>^b6RCcH#Q9XZ{pq1YDqFei)3gG08pd{*;; z%sxU~(US1*TrVWUNB@W&13%yEY7d8OKD$*k+-9wrZB^-Oc+MnJ2>TJ8RF3(&scSWD zUc3JkJ^diZrP;0c4LMD_b|4o-cN1KNkVSki-fEh7lnbWW^O?HFvqTK(s}atf5|!y- z-G^qS)rH-!kPG>Yfzxa1;Q~R4YzOfE>s&D*VQcVQIsPlqV$Dr3x#gZL?0u;kVM*Jc zsa#@FYgKvf`|ZwZaf*mUUiQ(9Mka=(P*6Yq*`O6q<0Mb4!F={ZRpzOj1_EfY?AnB#50?pVU~s2W6WFmpSbfD)<$~Cmz-ctCVaTnne8( zjOX=j2&!6zfWlt}KhJhLb19jc{r1YQq+bpkrE??R?X|gx&V1rumhEM#M{?`i6h|M! z27l_XyDlZNSzk9w!1ze`YH0+ZtA4KbKH>|3J04$P@gX6Q&M9xjcJ9$$5?xr@gU0f% zAOi=Z#&MDwPv zr;MBhtgHW!-{eh(SKt?}#g*-T9~&Zv4q~RG+mZ$w>q35YB4M4NP~pnG_a#|n$or4Q zRhXyv6$@2oRR=?yzfORe-92uTgJLx`p}(VMHGSy?&6xG3_TEPJl@4Dk)V1`KP*|!n zgj1Knk#ps>Wj--G&3BPCs|k4|lucx#lM4-J4W8>huGgJCkuKg}WwvS`T@2tm+Bj$vC4gT5w za2cmaft5rHkoUFtZEBDRM%&Y_=l7Pp7HJy}LDZYDn@3J+#x6opWdHKwKL8?QAwrQc z{FT9fjf#Z)|JX!91|%p%k=8;{q}BfoIKR6O^|exf3I7#17>SVvUE0Xr4=MMzLjv5w zh)|@)SiSwn@ZcKXLWhrC{&Ii1jr(sTF-B8-hK)X%RkEM@SVTA`5_c{({lFmU5v!FVwK%Vc>qKx_#C9|*HZCdk6suOX z8^B?pj_}e0%kAGsL}aw)wB_V|yvE5`hi9cOwO1wEwhyKhpYkXq{A%WJAn27eIDrIW z2Oi2%QSIHae}TJGY%-0pj>&1nD$t>|jtpPNjvgBDXe$y{>x5XQj?*`-o%9yNyhf~! z z?4x5UxqFYU+M76~l!9_`wW%f94g>n}?s_Lk-wAZEC0>E>n&~Pf8yU{7U0do8x=I}i zu4ER&H))wQESn?+@DOY4GHVVfDeULy2qo=}h&2@wNz}^ zuLBzIEIvU73_;b3bh=l-RuprOS;Zaat8BVbh?I+Z5&_#=Jpm%WZs~oq2z$k8J1ypQ z&rZKLDINIqAy-^I5@T<^A{Wgn0;Bz=XA#oWR^!7{$w>0THQGKnPZ$+e0A>stA%9cO?Fd8ML7(b;&s&UcHquUdd!r zQOVsT`b67ON=PW42BhxQ0g{JoJYR3*+EFXKRTqj;p+ECiz;3BK@CLKV!nGkmei_ms zk=w8o>B#0gqBjsu7~gHyvaZf)5jvgMb*dEt@5JM1_QjX%ZZyiJsq|l4y*ybGFXUMc z&FvGnn-UG=9N_KX?gsOnicE9_&+P-U=FZ|}41V*}AfKt+aD1x*fZ|0lLmLinTL;_h zkm6!I+`UhU#AGnP6DdY*`z9CEN@OR3J)DB$Jz7t>eN|%P1RQ;2qb-}(BOZ=uLrMOGKu9x~V8r^bolfA-|;@ zznMw62$i^my*Hn+lOx|6&98Y{H0%!+3VUU(+rd-533lsL^NJ+CKGG&fg>LVrzuM3q zZhy@rw|M$-yw^EM^J7b=$a5nI@*w#BSozOkX1sf_(nX=Y$rH!(x-=D#CtF5PDK>hv zJdOKtONrQwC-fHK5T9>tVh{qIn{*&!K9Kf>DJ$^GgTw6HPg9a{McJ0EG?XN<_QciE zQhs-6ykTDo>@%gJ2_qJ-=hw;F-=uvQ?;RBIHOLKQcxhz@F7=)S{&)yF1Mf|tV2K{;)9H=eBuaLiC21Nqrs;4H)Mm`v?Rmb(|9Ed-0g@P zOu^OWR#Co98?9(@_38Zeea!227u6R&L}BSXJ?32FJ6Dw2pw{r?TZ&n@W*MKEv;H2a z_8b_+M;5lkZQ)8oGS6S>hM7&-9yA&JZYdGoF-5!CsPVFE*BM1ORQL#aIu$?2&nk!X z$^XcDJFkd3S>Vp-yC;>_#I6&zaKrOtX{n)$Ki}Dv9eFJpou}{}txrPBRZM2|l~}k8 zG4~o9lLkFhmM>E4J@C@6txUKKv=C@)f?&VubxfY68DW$AS1p)~av=!0GHgz{VEfEe z#$!{c(4wOBEJ7}n+m9@NmXB6}hu6>lrq@WQd_#>s6eh0vDVT+E&c{2iQWHCU7BFdW ze%^i{cA(bI@8N(ZP|WC?+$AhohxSUa-tY%;YcHr9e;smiqKr>i4T|GXMPR(HDu`Ai zlV*2EPV$yY6l1CDZh~cX<&~b7(g+W{n+(tV8AKY}*r9Y@$BiF>Fyc4H&OvJxwmzh$ z$|jZlt3%nBS0+vL)1K4C)0ronoZQkYj{n_6%msx%)fsuFS2v6&*iicY)tfO4N!q{$ZaGO9`r{3jLuK zHK4aPE9KN092$=QnRFT4#CH`!&6(n8uo$-aWy<7_5Thh!MIycdQU; zlYYo<>o#v+M*UtgY2SD6KTMGRjFJ@lp9Wt z3BPg)Qco67%2n_(ciGBM#~nP9or?JRHeT!jKw-2bNmV4`ayB6>^lp~|0TZ?bw=X&G z5Ym3jKy{m^Ui!7Q(*mBg3~NdRueXQE)@NwmFSXvDtxfi9C(NuqEAAZ6f2+FXaG&Zo z*((EtQZdg%#?s(cV3+Y zDbbxj+%Ur>gPUYHs1SKD>84t5LmW*Q) zQp}7`D+2zk59LZH*Ps7;b-iy(Yj7;QTlD0Qmj?A$oBJBD$JyH==xdaA5}BZBxY}3%O9w zmw`}qs5q4JpPvB%kQhM331QC$u)g451fg#(-|~?!Ts7>3`Kbmp{Js~ECsB#P5rLL8 OQ?@m6$`=2*2mc4%ivhv_ literal 0 HcmV?d00001 diff --git a/application/src/integrationTest/resources/proxy.keystore.jks b/application/src/integrationTest/resources/proxy.keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..eae9f2cf12f5e173c750bbd551220a5f9e20ce0f GIT binary patch literal 4494 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 literal 0 HcmV?d00001 diff --git a/application/src/integrationTest/resources/proxy.truststore.jks b/application/src/integrationTest/resources/proxy.truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..d7da0d3a91de9afbddc084a01b98ac299524f473 GIT binary patch literal 1830 zcmV+>2if>Af(If30Ru3C2FwNtDuzgg_YDCD0ic2gy##^=xiEqTwJ?GPu?7h$hDe6@ z4FLxRpn?XXFoFi20s#Opf(Dlc2`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1Q7juY#N)WPZ;3V4_{)6B)))x1|ai;hx~25mZb6hVc*Rg)GyR= z@@y%>S1YPlqibXiEHFSDQcf}>4!rnXk@2!xevsyPUQ_}6FJAhy!<7?D1WBXznm&@Owp~1802{HFf^zih z(KP;SfWi(#6-mfC;-OG))0f$??S#dkKZ>SSZt4$~%iZ0=)L(hRMLCDcbzYr-!cvX( za65s2U~BrGex)22ut%<*B3UT^)-UqzoNy8}ag?SQ;C-z?xDbh$!9g(4?R((Eaa%kR zVuI3V5dQHbW^(KzSMrNtxYPX*p(U4n;5Sh$aw_u!cFQV~MreJre2GK^#%-^*0BzsI z>Oa6$&`2+4E@{#So-tJN0UkH?3CI|H2e_�XYxMsFI{ly2TBOgRNb5U+?M-L!VkF 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 eGC0XRSjYUS0K5f)$_b+G3Yyo literal 0 HcmV?d00001 diff --git a/application/src/integrationTest/resources/verify-key.der b/application/src/integrationTest/resources/verify-key.der new file mode 100644 index 0000000000000000000000000000000000000000..7bda5f95e15846014bd7ca970946681f298301c1 GIT binary patch literal 294 zcmV+>0ondAf&n5h4F(A+hDe6@4FLfG1potr0S^E$f&mHwf&l>lssizsUy{>70jb1P~iv6q&|H`YMxW zA}zCglDtsv5EFthg)m+1+sg}1J2>36RwyDNIJi+En`Qw)XlOOc9xWub-YOKzQ$u47D{pw3IA17UzupSoIngVRQbd=p_ s0UVqBHwF?U!VOU~AHE>V&%+CxFu@Nnb^4!r)edd_a3ZUd0s{d60ko@u8UO$Q literal 0 HcmV?d00001 diff --git a/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt b/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt new file mode 100644 index 0000000..2706b03 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt @@ -0,0 +1,17 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.runApplication +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +@SpringBootApplication +@EnableWebMvc +@ConfigurationPropertiesScan +class SoapBridgeApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt new file mode 100644 index 0000000..03a4c53 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt @@ -0,0 +1,41 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.web.SecurityFilterChain + +@Configuration +class SecurityConfiguration { + @Bean + @Throws(Exception::class) + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http.authorizeHttpRequests { + it + .anyRequest().authenticated() + } + http.x509 { + it + .subjectPrincipalRegex("CN=(.*?)(?:,|$)") + .userDetailsService(userDetailsService()) + } + http.csrf { it.disable() } + return http.build() + } + + /** + * Uses the CN of the client certificate as the username for Springs Principal object + */ + @Bean + fun userDetailsService(): UserDetailsService { + return UserDetailsService { username -> + return@UserDetailsService User( + username, "", emptyList() + ) + } + } +} diff --git a/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt new file mode 100644 index 0000000..67806a2 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt @@ -0,0 +1,18 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.observability + +import io.micrometer.observation.ObservationRegistry +import io.micrometer.observation.aop.ObservedAspect +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration(proxyBeanMethods = false) +internal class ObservabilityConfiguration { + + @Bean + fun observedAspect(observationRegistry: ObservationRegistry): ObservedAspect { + return ObservedAspect(observationRegistry) + } +} diff --git a/application/src/main/resources/application-dev.yml b/application/src/main/resources/application-dev.yml new file mode 100644 index 0000000..98d3b87 --- /dev/null +++ b/application/src/main/resources/application-dev.yml @@ -0,0 +1,10 @@ +logging: + level: + org: + gxf: + soapbridge: DEBUG +spring: + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: gxf-proxy diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml new file mode 100644 index 0000000..0ddc75a --- /dev/null +++ b/application/src/main/resources/application.yaml @@ -0,0 +1,45 @@ +management: + endpoints: + web: + exposure: + include: prometheus + +proxy-server: + network-zone: IT + +security: + key-store: + location: /etc/ssl/certs + password: 1234 + type: pkcs12 + trust-store: + location: /etc/ssl/certs/trust.jks + password: 123456 + type: jks + signing: + key-type: RSA + sign-key-file: /etc/ssl/certs/proxy-server/sign-key.der + verify-key-file: /etc/ssl/certs/proxy-server/verify-key.der + provider: SunRsaSign + signature: SHA256withRSA + +topics: + calls: + requests: proxy-server-calls-requests + responses: proxy-server-calls-responses + notifications: + requests: proxy-server-notification-requests + responses: proxy-server-notification-responses + +soap: + notification: + host: localhost + port: 443 + protocol: https + platform: + host: localhost + port: 443 + protocol: https + hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES + time-out: 45 + custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 diff --git a/application/src/main/resources/proxy-server.yml b/application/src/main/resources/proxy-server.yml new file mode 100644 index 0000000..1533737 --- /dev/null +++ b/application/src/main/resources/proxy-server.yml @@ -0,0 +1,140 @@ +jms: + proxy: + server: + back: + 'off': + multiplier: 2 + initial: + redelivery: + delay: 60000 + maximum: + redeliveries: 3 + redelivery: + delay: 300000 + notification: + back: + 'off': + multiplier: 2 + initial: + redelivery: + delay: 60000 + maximum: + redeliveries: 3 + redelivery: + delay: 300000 + redelivery: + delay: 60000 + requests: + concurrent: + consumers: 10 + delivery: + persistent: false + explicit: + qos: + enabled: true + max: + concurrent: + consumers: 10 + receive: + timeout: 10 + time: + to: + live: 3600000 + responses: + concurrent: + consumers: 10 + delivery: + persistent: false + explicit: + qos: + enabled: true + max: + concurrent: + consumers: 10 + receive: + timeout: 10 + time: + to: + live: 3600000 + use: + exponential: + back: + 'off': true + redelivery: + delay: 60000 + requests: + concurrent: + consumers: 10 + delivery: + persistent: false + explicit: + qos: + enabled: true + max: + concurrent: + consumers: 10 + receive: + timeout: 10 + time: + to: + live: 3600000 + responses: + concurrent: + consumers: 10 + delivery: + persistent: false + explicit: + qos: + enabled: true + max: + concurrent: + consumers: 10 + receive: + timeout: 10 + time: + to: + live: 3600000 + use: + exponential: + back: + 'off': true + +topics: + calls: + requests: proxy-server-webapp-requests + responses: proxy-server-webapp-responses + notification: + requests: proxy-server-notification-requests + responses: proxy-server-notification-responses + +proxy-server: + custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 + network-zone: BOTH + notification: + host: localhost + port: 443 + protocol: https + platform: + host: localhost + port: 443 + protocol: https + security: + key-store: + location: /etc/ssl/certs + password: 1234 + type: pkcs12 + trust-store: + location: /etc/ssl/certs/trust.jks + password: 123456 + signing: + key-type: RSA + sign-key: /etc/ssl/certs/proxy-server/sign-key.der + verify-key: /etc/ssl/certs/proxy-server/verify-key.der + provider: SunRsaSign + signature: SHA256withRSA + time-out: 45 +soap: + client: + hostname: + verification: + strategy: BROWSER_COMPATIBLE_HOSTNAMES diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..517ea15 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,64 @@ +// Copyright 2023 Alliander N.V. + +import io.spring.gradle.dependencymanagement.internal.dsl.StandardDependencyManagementExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "3.1.4" apply false + id("io.spring.dependency-management") version "1.1.3" apply false + kotlin("jvm") version "1.9.10" apply false + kotlin("plugin.spring") version "1.9.10" apply false + kotlin("plugin.jpa") version "1.9.10" apply false + id("com.github.davidmc24.gradle.plugin.avro") version "1.8.0" apply false + id("org.sonarqube") version "4.2.1.3168" + id("eclipse") +} + +version = System.getenv("GITHUB_REF_NAME")?.replace("/", "-")?.lowercase() ?: "develop" + +sonarqube { + properties { + property("sonar.host.url", "https://sonarcloud.io") + property("sonar.projectKey", "SSS-gxf-soap-bridge") + property("sonar.organization", "digitalisering") + } +} +tasks.sonar + +subprojects { + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.spring") + apply(plugin = "io.spring.dependency-management") + apply(plugin = "eclipse") + apply(plugin = "org.jetbrains.kotlin.plugin.jpa") + + group = "org.gxf.soap-bridge" + version = rootProject.version + + repositories { + mavenCentral() + } + + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + + extensions.configure { + imports { + mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) + } + } + + tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "17" + } + } + + tasks.withType { + useJUnitPlatform() + } +} diff --git a/components/core/build.gradle.kts b/components/core/build.gradle.kts new file mode 100644 index 0000000..ce4a92e --- /dev/null +++ b/components/core/build.gradle.kts @@ -0,0 +1,9 @@ +// Copyright 2023 Alliander N.V. + +dependencies { + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.jupiter:junit-jupiter-params") +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt new file mode 100644 index 0000000..f1ad634 --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.messaging + +import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage + +interface ProxyRequestsMessageSender { + fun send(requestMessage: ProxyServerRequestMessage) +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt new file mode 100644 index 0000000..cdc04e7 --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.messaging + +import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage + +interface ProxyResponsesMessageSender { + fun send(responseMessage: ProxyServerResponseMessage) +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt new file mode 100644 index 0000000..71f7471 --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt @@ -0,0 +1,10 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.messaging.exceptions + + +class ProxyMessageException : Exception { + constructor() : super() + constructor(message: String?) : super(message) + constructor(message: String?, t: Throwable?) : super(message, t) +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt new file mode 100644 index 0000000..1a9a832 --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt @@ -0,0 +1,25 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.messaging.messages + +import java.util.* + + +/** Base class for proxy-server messages. */ +abstract class ProxyServerBaseMessage(val connectionId: String) { + var signature: String? = null + + protected abstract fun getFieldsForMessage(): List + + /** Constructs a string separated by '~' from the fields of this instance. */ + fun constructString() = getFieldsForMessage().joinToString(SEPARATOR, postfix = SEPARATOR) + + /** Constructs a string separated by '~' from the fields of this instance followed by the signature. */ + fun constructSignedString() = constructString() + signature + + companion object { + const val SEPARATOR = "~" + fun encode(input: String): String = Base64.getEncoder().encodeToString(input.toByteArray()) + fun decode(input: String?): String = String(Base64.getDecoder().decode(input)) + } +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt new file mode 100644 index 0000000..4aa1c9a --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt @@ -0,0 +1,83 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.messaging.messages + +import mu.KotlinLogging +import org.gxf.soapbridge.messaging.exceptions.ProxyMessageException +import java.util.* + + +class ProxyServerRequestMessage( + connectionId: String, + val commonName: String, + val context: String, + val soapPayload: String +) : ProxyServerBaseMessage(connectionId) { + + override fun getFieldsForMessage(): List = listOf( + connectionId, + encode(context), + encode(soapPayload), + encode(commonName) + ) + + companion object { + private val LOGGER = KotlinLogging.logger { } + + /** + * Constructs a ProxyServerRequestMessage instance from a string separated by '~'. + * + * @param string The input string. + * @return A ProxyServerRequestMessage instance. + * @throws ProxyServerException + */ + @Throws(ProxyMessageException::class) + fun createInstanceFromString(string: String): ProxyServerRequestMessage { + val split = string.split(SEPARATOR) + "een of andere string".lines() + val numTokens = split.size + LOGGER.debug("split.length: {}", numTokens) + if (numTokens < 4 || numTokens > 5) { + throw ProxyMessageException( + "Invalid number of tokens, not trying to create ProxyServerRequestMessage." + ) + } + if (LOGGER.isDebugEnabled()) { + printValues(numTokens, split) + } + val connectionId = split[0] + val context = decode(split[1]) + val soapRequest = decode(split[2]) + val commonName: String + val signature: String + if (numTokens == 4) { + // No common name used. + commonName = "" + signature = split[3] + } else { + // Common name used. + commonName = decode(split[3]) + signature = split[4] + } + val proxyServerRequestMessage = + ProxyServerRequestMessage(connectionId, commonName, context, soapRequest) + proxyServerRequestMessage.signature = signature + return proxyServerRequestMessage + } + + private fun printValues(numTokens: Int, split: List) { + if (LOGGER.isDebugEnabled) { + LOGGER.debug("split[0] connection-id: {}", split[0]) + LOGGER.debug("split[2] context : {}", decode(split[1])) + LOGGER.debug("split[3] encoded soap-request length: {}", split[2]!!.length) + LOGGER.debug("split[3] soap-request : {}", decode(split[2])) + if (numTokens == 5) { + LOGGER.debug("split[4] security-key : {}", split[3]) + } else { + LOGGER.debug("split[4] common-name : {}", decode(split[3])) + LOGGER.debug("split[5] security-signature : {}", split[4]) + } + } + } + } +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt new file mode 100644 index 0000000..234444a --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt @@ -0,0 +1,50 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.messaging.messages + +import mu.KotlinLogging +import org.gxf.soapbridge.messaging.exceptions.ProxyMessageException +import java.util.* + +class ProxyServerResponseMessage(connectionId: String, val soapResponse: String) : + ProxyServerBaseMessage(connectionId) { + + + /** Constructs a string separated by '~' from the fields of this instance. */ + override fun getFieldsForMessage(): List = listOf( + connectionId, + encode(soapResponse) + ) + + companion object { + val logger = KotlinLogging.logger { } + + /** + * Constructs a ProxyServerResponseMessage instance from a string separated by '~'. + * + * @param string The input string. + * @return A ProxyServerResponseMessage instance. + * @throws ProxyServerException + */ + @Throws(ProxyMessageException::class) + fun createInstanceFromString(string: String): ProxyServerResponseMessage { + val split = string.split(SEPARATOR) + val numTokens = split.size + logger.debug("split.length: {}", numTokens) + if (numTokens < 3) { + throw ProxyMessageException( + "Invalid number of tokens, don't try to create ProxyServerResponseMessage" + ) + } + if (logger.isDebugEnabled) { + logger.debug("split[0] connection-id: {}", split[0]) + logger.debug("split[1] encoded soap-response length: {}", split[1].length) + logger.debug("split[1] soap-response: {}", decode(split[1])) + logger.debug("split[2] security-key : {}", split[2]) + } + val proxyServerResponseMessage = ProxyServerResponseMessage(split[0], decode(split[1])) + proxyServerResponseMessage.signature = split[2] + return proxyServerResponseMessage + } + } +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt new file mode 100644 index 0000000..d22f5f7 --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.services + +import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage + +interface ProxyRequestHandler { + fun handleIncomingRequest(proxyServerRequestMessage: ProxyServerRequestMessage) +} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt new file mode 100644 index 0000000..f14b0cf --- /dev/null +++ b/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt @@ -0,0 +1,9 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.services + +import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage + +interface ProxyResponseHandler { + fun handleIncomingResponse(proxyServerResponseMessage: ProxyServerResponseMessage) +} diff --git a/components/kafka/build.gradle.kts b/components/kafka/build.gradle.kts new file mode 100644 index 0000000..485cce7 --- /dev/null +++ b/components/kafka/build.gradle.kts @@ -0,0 +1,17 @@ +// Copyright 2023 Alliander N.V. + +dependencies { + implementation("org.springframework.boot:spring-boot-autoconfigure") + implementation("org.springframework.boot:spring-boot-starter-logging") + implementation("org.springframework:spring-aop") + implementation("org.springframework.kafka:spring-kafka") + implementation("com.microsoft.azure:msal4j:1.13.10") + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + implementation(project(":components:core")) + + testImplementation("org.springframework:spring-test") + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.jupiter:junit-jupiter-params") +} diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt new file mode 100644 index 0000000..f32d77e --- /dev/null +++ b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt @@ -0,0 +1,32 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.kafka.listeners + +import io.micrometer.observation.annotation.Observed +import mu.KotlinLogging +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage +import org.gxf.soapbridge.services.ProxyRequestHandler +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.kafka.annotation.RetryableTopic +import org.springframework.retry.annotation.Backoff +import org.springframework.stereotype.Component +import java.net.SocketTimeoutException + +@Component +class ProxyRequestKafkaListener(private val proxyRequestHandler: ProxyRequestHandler) { + private val logger = KotlinLogging.logger { } + + @Observed(name = "requests.consumed") + @KafkaListener(topics = ["\${topics.incoming.requests}"], id = "gxf-request-consumer") + @RetryableTopic( + backoff = Backoff(value = 3000L), + attempts = "2", + include = [SocketTimeoutException::class] + ) + fun consume(record: ConsumerRecord) { + logger.info("Received message") + val requestMessage = ProxyServerRequestMessage.createInstanceFromString(record.value()) + proxyRequestHandler.handleIncomingRequest(requestMessage) + } +} diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt new file mode 100644 index 0000000..115a396 --- /dev/null +++ b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt @@ -0,0 +1,34 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.kafka.listeners + +import io.micrometer.observation.annotation.Observed +import mu.KotlinLogging +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage +import org.gxf.soapbridge.services.ProxyResponseHandler +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.kafka.annotation.RetryableTopic +import org.springframework.retry.annotation.Backoff +import org.springframework.stereotype.Component +import java.net.SocketTimeoutException + +@Component +class ProxyResponseKafkaListener( + private val proxyResponseHandler: ProxyResponseHandler +) { + private val logger = KotlinLogging.logger { } + + @Observed(name = "responses.consumed") + @KafkaListener(topics = ["\${topics.incoming.responses}"], id = "gxf-response-consumer") + @RetryableTopic( + backoff = Backoff(value = 3000L), + attempts = "2", + include = [SocketTimeoutException::class] + ) + fun consume(record: ConsumerRecord) { + logger.info("Received response") + val responseMessage = ProxyServerResponseMessage.createInstanceFromString(record.value()) + proxyResponseHandler.handleIncomingResponse(responseMessage) + } +} diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt new file mode 100644 index 0000000..6d3eb80 --- /dev/null +++ b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt @@ -0,0 +1,16 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.kafka.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("topics") +class TopicsConfigurationProperties( + val outgoing: RequestResponseTopics, + val incoming: RequestResponseTopics +) + +class RequestResponseTopics( + val requests: String, + val responses: String +) diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt new file mode 100644 index 0000000..cbe45f4 --- /dev/null +++ b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt @@ -0,0 +1,25 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.kafka.senders + +import mu.KotlinLogging +import org.gxf.soapbridge.kafka.properties.TopicsConfigurationProperties +import org.gxf.soapbridge.messaging.ProxyRequestsMessageSender +import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Component + +@Component +class ProxyRequestKafkaSender( + private val kafkaTemplate: KafkaTemplate, + topicConfiguration: TopicsConfigurationProperties +) : ProxyRequestsMessageSender { + private val logger = KotlinLogging.logger {} + + private val topic = topicConfiguration.outgoing.requests + + override fun send(requestMessage: ProxyServerRequestMessage) { + logger.debug("SOAP payload: ${requestMessage.soapPayload} to $topic") + kafkaTemplate.send(topic, requestMessage.constructSignedString()) + } +} diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt new file mode 100644 index 0000000..1304501 --- /dev/null +++ b/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt @@ -0,0 +1,25 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.kafka.senders + +import mu.KotlinLogging +import org.gxf.soapbridge.kafka.properties.TopicsConfigurationProperties +import org.gxf.soapbridge.messaging.ProxyResponsesMessageSender +import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Component + +@Component +class ProxyResponseKafkaSender( + private val kafkaTemplate: KafkaTemplate, + topicConfiguration: TopicsConfigurationProperties +) : ProxyResponsesMessageSender { + private val logger = KotlinLogging.logger {} + + private val topic = topicConfiguration.outgoing.responses + + override fun send(responseMessage: ProxyServerResponseMessage) { + logger.debug("SOAP payload: ${responseMessage.soapResponse} to $topic") + kafkaTemplate.send(topic, responseMessage.constructSignedString()) + } +} diff --git a/components/soap/build.gradle.kts b/components/soap/build.gradle.kts new file mode 100644 index 0000000..f52db11 --- /dev/null +++ b/components/soap/build.gradle.kts @@ -0,0 +1,34 @@ +// Copyright 2023 Alliander N.V. + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-logging") + implementation(kotlin("reflect")) + implementation("org.apache.httpcomponents:httpclient:4.5.13") + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + implementation(project(":components:core")) + + runtimeOnly("io.micrometer:micrometer-registry-prometheus") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + + testImplementation("org.springframework:spring-test") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-engine") + testImplementation("org.junit.jupiter:junit-jupiter-params") +} + +testing { + suites { + val integrationTest by registering(JvmTestSuite::class) { + useJUnitJupiter() + dependencies { + implementation(project()) + implementation("org.springframework.boot:spring-boot-starter-test") + implementation("org.springframework.kafka:spring-kafka-test") + implementation("org.testcontainers:kafka:1.17.6") + } + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java b/components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java new file mode 100644 index 0000000..6c3a65f --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java @@ -0,0 +1,22 @@ +// Copyright 2023 Alliander N.V. + +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; + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java new file mode 100644 index 0000000..9182d33 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java @@ -0,0 +1,33 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.application.factories; + +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.soap.exceptions.ProxyServerException; +import org.springframework.stereotype.Component; + +import javax.net.ssl.HostnameVerifier; + +import static org.gxf.soapbridge.application.properties.HostnameVerificationStrategy.ALLOW_ALL_HOSTNAMES; +import static org.gxf.soapbridge.application.properties.HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES; + +@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!"); + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java new file mode 100644 index 0000000..04486ba --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -0,0 +1,106 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.factories; + +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; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * 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()); + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java new file mode 100644 index 0000000..828754f --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java @@ -0,0 +1,152 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.factories; + +import org.gxf.soapbridge.application.properties.SecurityConfigurationProperties; +import org.gxf.soapbridge.application.properties.StoreConfigurationProperties; +import org.gxf.soapbridge.application.services.SslContextCacheService; +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; + +import javax.net.ssl.*; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; + +/** + * 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); + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java new file mode 100644 index 0000000..4816892 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java @@ -0,0 +1,67 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage; +import org.gxf.soapbridge.services.ProxyResponseHandler; +import org.gxf.soapbridge.soap.clients.Connection; +import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Service which handles SOAP responses from OSGP. The SOAP response will be set for the connection + * which correlates with the connection-id. + */ +@Service +public class ClientCommunicationService implements ProxyResponseHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClientCommunicationService.class); + + /** + * Service used to cache incoming connections from client applications. + */ + @Autowired + private ConnectionCacheService connectionCacheService; + + /** + * Service used to sign and/or verify the content of queue messages. + */ + @Autowired + private SigningService signingService; + + /** + * Process an incoming queue message. The content of the message has to be verified by the + * {@link SigningService}. Then a response from OSGP will set for the pending connection from a + * client. + * + * @param proxyServerResponseMessage The incoming queue message to process. + */ + @Override + public void handleIncomingResponse(final ProxyServerResponseMessage proxyServerResponseMessage) { + final boolean isValid = signingService.verifyContent( + proxyServerResponseMessage.constructString(), proxyServerResponseMessage.getSignature()); + if (!isValid) { + LOGGER.error("ProxyServerResponseMessage failed to pass security check."); + return; + } + + try { + final Connection connection = + connectionCacheService.findConnection(proxyServerResponseMessage.getConnectionId()); + if (connection != null) { + if (isValid) { + LOGGER.debug("Connection valid, set SOAP response"); + connection.setResponse(proxyServerResponseMessage.getSoapResponse()); + } else { + connection.setResponse("Security check has failed."); + } + } else { + LOGGER.error("Cached connection is null"); + } + } catch (final ConnectionNotFoundInCacheException e) { + LOGGER.error("ConnectionNotFoundInCacheException", e); + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java new file mode 100644 index 0000000..0c699a5 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java @@ -0,0 +1,95 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.application.utils.RandomStringFactory; +import org.gxf.soapbridge.soap.clients.Connection; +import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * This {@link @Service} class caches connections from client applications. + */ +@Service +public class ConnectionCacheService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionCacheService.class); + + /** + * Map used to cache connections. The key is a random string generated by + * {@link RandomStringFactory}. The value is a {@link Connection} instance. + */ + private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + /** + * Creates a connection and puts it in the cache. + * + * @return the created Connection + */ + public Connection cacheConnection() { + final Connection connection = new Connection(); + final String connectionId = connection.getConnectionId(); + LOGGER.debug("Caching connection with connectionId: {}", connectionId); + cache.put(connectionId, connection); + return connection; + } + + /** + * Get a {@link Connection} instance from the {@link ConnectionCacheService#cache}. + * + * @param connectionId The key for the {@link Connection} instance obtained by calling + * {@link ConnectionCacheService#cacheConnection()}. + * @return A {@link Connection} instance. + * @throws ConnectionNotFoundInCacheException In case the connection is not present in the + * {@link ConnectionCacheService#cache}. + */ + public Connection findConnection(final String connectionId) + throws ConnectionNotFoundInCacheException { + LOGGER.debug("Trying to find connection with connectionId: {}", connectionId); + final Connection connection = cache.get(connectionId); + if (connection == null) { + throw new ConnectionNotFoundInCacheException( + String.format("Unable to find connection for connectionId: %s", connectionId)); + } + return connection; + } + + /** + * Determine if the response is available for a particular {@link Connection} instance. + * + * @param connectionId The key for the {@link Connection} instance obtained by calling + * {@link ConnectionCacheService#cacheConnection()}. + * @return True in case the response from the Platform has resolved and is available, false + * otherwise. + * @throws ConnectionNotFoundInCacheException In case the connection is not present in the + * {@link ConnectionCacheService#cache}. + */ + public boolean hasResponseResolved(final String connectionId) + throws ConnectionNotFoundInCacheException { + final Connection connection = cache.get(connectionId); + if (connection != null) { + LOGGER.debug( + "hasResponseResolved called with connectionId: {}, this.cache.size(): {}", + connectionId, + cache.size()); + return connection.isResponseResolved(); + } else { + throw new ConnectionNotFoundInCacheException( + String.format("Unable to find connection for connectionId: %s", connectionId)); + } + } + + /** + * Removes a {@link Connection} instance from the {@link ConnectionCacheService#cache}. + * + * @param connectionId The key for the {@link Connection} instance obtained by calling + * {@link ConnectionCacheService#cacheConnection()}. + */ + public void removeConnection(final String connectionId) { + LOGGER.debug("Removing connection with connectionId: {}", connectionId); + cache.remove(connectionId); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java new file mode 100644 index 0000000..60ae07d --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java @@ -0,0 +1,66 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage; +import org.gxf.soapbridge.services.ProxyRequestHandler; +import org.gxf.soapbridge.soap.clients.SoapClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Service which can send SOAP requests to OSGP. + */ +@Service +public class PlatformCommunicationService implements ProxyRequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(PlatformCommunicationService.class); + + /** + * SOAP client used to sent request messages to OSGP. + */ + @Autowired + private SoapClient soapClient; + + /** + * Service used to sign and/or verify the content of queue messages. + */ + @Autowired + private SigningService signingService; + + /** + * Process an incoming queue message. The content of the message has to be verified by the + * {@link SigningService}. Then a SOAP message can be sent to OSGP using {@link SoapClient}. + * + * @param proxyServerRequestMessage The incoming queue message to process. + */ + @Override + public void handleIncomingRequest( + final ProxyServerRequestMessage proxyServerRequestMessage) { + + final String proxyServerRequestMessageAsString = proxyServerRequestMessage.constructString(); + final String securityKey = proxyServerRequestMessage.getSignature(); + + final boolean isValid = + signingService.verifyContent(proxyServerRequestMessageAsString, securityKey); + if (!isValid) { + LOGGER.error("ProxyServerRequestMessage failed to pass security check."); + return; + } + + final String connectionId = proxyServerRequestMessage.getConnectionId(); + final String context = proxyServerRequestMessage.getContext(); + final String commonName = proxyServerRequestMessage.getCommonName(); + final String soapPayload = proxyServerRequestMessage.getSoapPayload(); + + final boolean result = + soapClient.sendRequest( + connectionId, context, commonName, soapPayload); + if (!result) { + LOGGER.error("Unsuccessful at sending request to platform."); + } else { + LOGGER.debug("Successfully sent response message to queue"); + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java new file mode 100644 index 0000000..ed2241a --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java @@ -0,0 +1,86 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.application.properties.SecurityConfigurationProperties; +import org.gxf.soapbridge.application.properties.SigningConfigurationProperties; +import org.gxf.soapbridge.soap.exceptions.ProxyServerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.util.HexFormat; + +/** + * This {@link @Service} class can generate a signature for a given content, and verify the content + * using the signature. + */ +@Service +public class SigningService { + private final SigningConfigurationProperties signingConfiguration; + + private static final Logger LOGGER = LoggerFactory.getLogger(SigningService.class); + + private SigningService(final SecurityConfigurationProperties securityConfiguration) { + signingConfiguration = securityConfiguration.getSigning(); + } + + /** + * Using the given content, create a signature. The signature is converted from byte array to + * hexadecimal string. + * + * @param content The content to sign. + * @return The signature which can be used later to verify if the content is intact and unaltered. + * @throws ProxyServerException thrown when an error occurs during signing. + */ + public String signContent(final String content) throws ProxyServerException { + final byte[] bytes = content.getBytes(StandardCharsets.UTF_8); + try { + final byte[] signature = createSignature(bytes); + LOGGER.debug("signature.length: {}", signature.length); + return HexFormat.of().formatHex(signature); + } catch (final GeneralSecurityException e) { + throw new ProxyServerException( + "Unexpected GeneralSecurityException when trying to sign the content", e); + } + } + + /** + * For the given content and security key, verify if the content is intact and unaltered. + * + * @param content The content to verify. + * @param securityKey The signature a.k.a. security key which was created using + * {@link SigningService#signContent(String)}. + * @return True when the verification succeeds. + */ + public boolean verifyContent(final String content, final String securityKey) { + final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + final byte[] securityKeyBytes = HexFormat.of().parseHex(securityKey); + LOGGER.debug("securityKeyBytes.length: {}", securityKeyBytes.length); + try { + return validateSignature(contentBytes, securityKeyBytes); + } catch (final GeneralSecurityException e) { + LOGGER.error( + "Unexpected GeneralSecurityException when trying to verify the content using the security key", + e); + return false; + } + } + + private byte[] createSignature(final byte[] message) throws GeneralSecurityException { + final Signature signatureBuilder = Signature.getInstance(signingConfiguration.getSignature(), signingConfiguration.getProvider()); + signatureBuilder.initSign(signingConfiguration.getSignKey()); + signatureBuilder.update(message); + return signatureBuilder.sign(); + } + + private boolean validateSignature(final byte[] message, final byte[] securityKey) + throws GeneralSecurityException { + final Signature signatureBuilder = Signature.getInstance(signingConfiguration.getSignature(), signingConfiguration.getProvider()); + signatureBuilder.initVerify(signingConfiguration.getVerifyKey()); + signatureBuilder.update(message); + return signatureBuilder.verify(securityKey); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java new file mode 100644 index 0000000..8f8a5a3 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java @@ -0,0 +1,79 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.application.factories.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This {@link @Service} class encapsulates the creation of a suitable {@link SSLContext} instance + * to use with a HTTPS connection, like {@link HttpsURLConnection} for example. Further, the created + * instance is cached using a {@link ConcurrentHashMap} in order to maintain performance even when + * many calls are handled simultaneously. The actual creation of {@link SSLContext} instances is + * delegated to {@link SslContextFactory} class. + */ +@Service +public class SslContextCacheService { + + private static final Logger LOGGER = LoggerFactory.getLogger(SslContextCacheService.class); + + /** + * Map used to cache {@link SSLContext} instances. The key is the common name for an organization, + * which is equal to the organization identification. + */ + private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + /** + * Factory which assists in creating {@link SSLContext} instances. + */ + @Autowired + private SslContextFactory sslContextFactory; + + /** + * Creates a new {@link SSLContext} instance and caches it, or fetches an existing instance from + * the cache. + * + * @return A {@link SSLContext} instance. + */ + public SSLContext getSslContext() { + final String key = "SSLContextWithoutCommonName"; + if (cache.containsKey(key)) { + LOGGER.debug("Returning SSL Context from cache for key: {}", key); + return cache.get(key); + } else { + LOGGER.debug( + "Creating new SSL Context and putting the instance in the cache for key: {}", key); + final SSLContext sslContext = sslContextFactory.createSslContext(); + cache.put(key, sslContext); + return sslContext; + } + } + + /** + * Creates a new {@link SSLContext} instance for the given common name and caches it, or fetches + * an existing instance from the cache. + * + * @param commonName The common name which is used to look up a Personal Information Exchange + * (*.pfx) file that is used as key store. + * @return A {@link SSLContext} instance. + */ + public SSLContext getSslContextForCommonName(final String commonName) { + if (cache.containsKey(commonName)) { + LOGGER.debug("Returning SSL Context from cache for common name: {}", commonName); + return cache.get(commonName); + } else { + LOGGER.debug( + "Creating new SSL Context and putting the instance in the cache for common name: {}", + commonName); + final SSLContext sslContext = sslContextFactory.createSslContext(commonName); + cache.put(commonName, sslContext); + return sslContext; + } + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java new file mode 100644 index 0000000..e5a3e7a --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java @@ -0,0 +1,52 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.utils; + +import java.security.SecureRandom; + +/** + * This class can generate random string values. + */ +public class RandomStringFactory { + + /** + * The random used to generate random strings. + */ + private static final SecureRandom random = new SecureRandom(); + + /** + * Default length of the generated random strings. + */ + private static final int DEFAULT_LENGTH = 16; + + private RandomStringFactory() { + // Only static. + } + + /** + * Generate a random string. + * + * @return A string of length {@link RandomStringFactory#defaultLength} + */ + public static String generateRandomString() { + return randomString(DEFAULT_LENGTH); + } + + /** + * Generate a random string. + * + * @param length The length of the random string to generate. + * @return A string of the given length. + */ + public static String generateRandomString(final int length) { + return randomString(length); + } + + private static String randomString(final int length) { + final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final StringBuilder buf = new StringBuilder(); + for (int i = 0; i < length; i++) { + buf.append(chars.charAt(random.nextInt(chars.length()))); + } + return buf.toString(); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java new file mode 100644 index 0000000..4780037 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java @@ -0,0 +1,72 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.clients; + +import org.gxf.soapbridge.application.services.ConnectionCacheService; +import org.gxf.soapbridge.application.utils.RandomStringFactory; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class Connection { + + /** + * Default length for the connection id, which is the key for the + * {@link ConnectionCacheService#cache}. + */ + private static final int CONNECTION_ID_LENGTH = 32; + + private volatile boolean responseResolved; + + private String soapResponse; + + private String connectionId; + + private final Semaphore responseReceived; + + public Connection() { + responseResolved = false; + responseReceived = new Semaphore(0); + connectionId = RandomStringFactory.generateRandomString(CONNECTION_ID_LENGTH); + } + + public boolean isResponseResolved() { + return responseResolved; + } + + public void setResponse(final String soapResponse) { + responseResolved = true; + this.soapResponse = soapResponse; + responseReceived(); + } + + public String getSoapResponse() { + return soapResponse; + } + + public void setConnectionId(final String connectionId) { + this.connectionId = connectionId; + } + + public String getConnectionId() { + return connectionId; + } + + /* + * Indicates the response for this connection has been received. + */ + public void responseReceived() { + responseReceived.release(); + } + + /* + * Waits for a response on this connection. + * + * @timeout The number of seconds to wait for a response. + * + * @returns true, if the response was received within @timeout seconds, + * false otherwise. + */ + public boolean waitForResponseReceived(final int timeout) throws InterruptedException { + return responseReceived.tryAcquire(timeout, TimeUnit.SECONDS); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java new file mode 100644 index 0000000..7f92f83 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java @@ -0,0 +1,173 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.clients; + +import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; +import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.application.properties.SoapEndpointConfiguration; +import org.gxf.soapbridge.application.services.SigningService; +import org.gxf.soapbridge.messaging.ProxyResponsesMessageSender; +import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage; +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 javax.net.ssl.HttpsURLConnection; +import java.io.*; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; + +/** + * This {@link @Component} class can send SOAP messages to the Platform. + */ +@Component +public class SoapClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(SoapClient.class); + + /** + * Message sender to send messages to a queue. + */ + @Autowired + private ProxyResponsesMessageSender callResponsesMessageSender; + + @Autowired + private SoapConfigurationProperties soapConfiguration; + + /** + * Factory which assist in creating {@link HttpsURLConnection} instances. + */ + @Autowired + private HttpsUrlConnectionFactory httpsUrlConnectionFactory; + + /** + * Service used to sign the content of a message. + */ + @Autowired + private SigningService signingService; + + /** + * Send a request to the Platform. + * + * @param connectionId The connectionId for this connection. + * @param context The part of the URL indicating the SOAP web-service. + * @param commonName The common name (organisation identification). + * @param soapPayload The SOAP message to send to the platform. + * @return True if the request has been sent and the response has been received and written to a + * queue, false otherwise. + */ + public boolean sendRequest( + final String connectionId, + final String context, + final String commonName, + final String soapPayload) { + + HttpsURLConnection connection = null; + + try { + // Try to create a connection. + connection = createConnection(context, soapPayload, commonName); + if (connection == null) { + LOGGER.warn("Could not create connection for sending SOAP request."); + return false; + } + + // Send the SOAP payload to the server. + sendRequest(connection, soapPayload); + // Read the response. + LOGGER.debug("SOAP request sent, trying to read SOAP response..."); + final String soapResponse = readResponse(connection); + LOGGER.debug("SOAP response: {}", soapResponse); + // Always disconnect the connection. + connection.disconnect(); + + // Create proxy-server response message. + final ProxyServerResponseMessage responseMessage = + createProxyServerResponseMessage(connectionId, soapResponse); + + // Send queue message. + callResponsesMessageSender.send(responseMessage); + + return true; + } catch (final Exception e) { + if (connection != null) { + connection.disconnect(); + } + LOGGER.error("Unexpected exception while sending SOAP request", e); + return false; + } + } + + private HttpsURLConnection createConnection( + final String context, + final String soapPayload, + final String commonName) + throws UnableToCreateHttpsURLConnectionException { + final String contentLength = String.format("%d", soapPayload.length()); + + final SoapEndpointConfiguration callEndpoint = soapConfiguration.getCallEndpoint(); + + final String uri = callEndpoint.getUri().concat(context); + LOGGER.info("Preparing to open connection for WEBAPP_REQUEST using URI: {}", uri); + return httpsUrlConnectionFactory.createConnection( + uri, callEndpoint.getHostAndPort(), contentLength, commonName); + } + + private void sendRequest(final HttpsURLConnection connection, final String soapPayLoad) + throws IOException { + try (final OutputStream outputStream = connection.getOutputStream(); + final OutputStreamWriter outputStreamWriter = + new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { + outputStreamWriter.write(soapPayLoad); + outputStreamWriter.flush(); + } catch (final IOException e) { + LOGGER.debug("Rethrow IOException while sending SOAP request."); + throw e; + } + } + + private String readResponse(final HttpsURLConnection connection) throws IOException { + // Use a BufferedReader and an InputStreamReader configured with UTF-8 + // character encoding. This will ensure that the response from the + // Platform is read correctly. + try (final InputStream inputStream = getInputStream(connection); + final InputStreamReader inputStreamReader = + new InputStreamReader(inputStream, StandardCharsets.UTF_8); + final BufferedReader reader = new BufferedReader(inputStreamReader)) { + final StringBuilder response = new StringBuilder(); + String line = reader.readLine(); + while (line != null) { + response.append(line); + line = reader.readLine(); + } + return response.toString(); + } catch (final IOException e) { + LOGGER.debug("Rethrow IOException while reading SOAP response"); + throw e; + } + } + + private InputStream getInputStream(final HttpsURLConnection connection) throws IOException { + final InputStream inputStream; + + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + inputStream = connection.getInputStream(); + } else { + inputStream = connection.getErrorStream(); + } + + return inputStream; + } + + private ProxyServerResponseMessage createProxyServerResponseMessage( + final String connectionId, final String soapResponse) throws ProxyServerException { + final ProxyServerResponseMessage responseMessage = + new ProxyServerResponseMessage(connectionId, soapResponse); + final String signature = signingService.signContent(responseMessage.constructString()); + responseMessage.setSignature(signature); + return responseMessage; + } + +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java new file mode 100644 index 0000000..7638797 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -0,0 +1,440 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.endpoints; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.application.services.ConnectionCacheService; +import org.gxf.soapbridge.application.services.SigningService; +import org.gxf.soapbridge.messaging.ProxyRequestsMessageSender; +import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage; +import org.gxf.soapbridge.soap.clients.Connection; +import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; +import org.gxf.soapbridge.soap.exceptions.ProxyServerException; +import org.gxf.soapbridge.soap.valueobjects.ClientCertificate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; +import org.springframework.stereotype.Component; +import org.springframework.web.HttpRequestHandler; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.Rdn; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.*; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.*; + +/** + * This {@link @Component} class is the endpoint for incoming SOAP requests from client applications. + */ +@Component +public class SoapEndpoint implements HttpRequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(SoapEndpoint.class); + + private static final String SOAP_HEADER_KEY_APPLICATION_NAME = "ApplicationName"; + private static final String SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION = "OrganisationIdentification"; + private static final String SOAP_HEADER_KEY_USER_NAME = "UserName"; + private static final List SOAP_HEADER_KEYS = List.of( + SOAP_HEADER_KEY_APPLICATION_NAME, + SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION, + SOAP_HEADER_KEY_USER_NAME); + + private static final String URL_PROXY_SERVER = "/proxy-server"; + + private static final int INVALID_CUSTOM_TIME_OUT = -1; + public static final String X_509_CERTIFICATE_REQUEST_ATTRIBUTE = "jakarta.servlet.request.X509Certificate"; + + /** + * Service used to cache incoming connections from client applications. + */ + @Autowired + private ConnectionCacheService connectionCacheService; + + @Autowired + private SoapConfigurationProperties soapConfiguration; + + /** + * Message sender which can send a webapp request message to ActiveMQ. + */ + @Autowired + private ProxyRequestsMessageSender proxyRequestsMessageSender; + + /** + * Service used to sign the content of a message. + */ + @Autowired + private SigningService signingService; + + /** + * Map of time outs for specific functions. + */ + private final Map customTimeOutsMap = new HashMap<>(); + + @PostConstruct + public void init() { + final String[] split = soapConfiguration.getCustomTimeouts().split(","); + for (int i = 0; i < split.length; i += 2) { + final String key = split[i]; + final Integer value = Integer.valueOf(split[i + 1]); + LOGGER.debug("Adding custom time out with key: {} and value: {}", key, value); + customTimeOutsMap.put(key, value); + } + LOGGER.debug("Added {} custom time outs to the map", customTimeOutsMap.size()); + } + + /** + * Handles incoming SOAP requests. + */ + @Override + public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + + // For debugging, print all headers and parameters. + LOGGER.debug("Start of SoapEndpoint.handleRequest()"); + printHeaderValues(request); + printParameterValues(request); + + // Get the context, which should be an OSGP SOAP end-point or a + // NOTIFICATION SOAP end-point. + final String context = getContextForRequestType(request); + LOGGER.debug("Context: {}", context); + + // Try to read the SOAP request. + final String soapPayload = readSoapPayload(request); + if (soapPayload == null) { + LOGGER.error("Unable to read SOAP request, returning 500."); + createErrorResponse(response); + return; + } + + String organisationName = null; + if (request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME) instanceof final SecurityContext securityContext) { + if (securityContext.getAuthentication().getPrincipal() instanceof final User organisation) { + organisationName = organisation.getUsername(); + } + } + if (organisationName == null) { + LOGGER.error("Unable to find client certificate, returning 500."); + createErrorResponse(response); + return; + } + + // Cache the incoming connection. + final Connection newConnection = connectionCacheService.cacheConnection(); + final String connectionId = newConnection.getConnectionId(); + + // Create a queue message and sign it. + final ProxyServerRequestMessage requestMessage = + new ProxyServerRequestMessage( + connectionId, + organisationName, + context, + soapPayload); + try { + final String signature = signingService.signContent(requestMessage.constructString()); + requestMessage.setSignature(signature); + } catch (final ProxyServerException e) { + LOGGER.error("Unable to sign message or set security key", e); + createErrorResponse(response); + connectionCacheService.removeConnection(connectionId); + return; + } + + final Integer customTimeOut = shouldUseCustomTimeOut(soapPayload); + final int timeOut; + if (customTimeOut == INVALID_CUSTOM_TIME_OUT) { + timeOut = soapConfiguration.getTimeOut(); + LOGGER.debug("Using default time out: {} seconds", timeOut); + } else { + LOGGER.debug("Using custom time out: {} seconds", customTimeOut); + timeOut = customTimeOut; + } + + try { + proxyRequestsMessageSender.send(requestMessage); + + final boolean responseReceived = newConnection.waitForResponseReceived(timeOut); + if (!responseReceived) { + LOGGER.info("No response received within the specified time out of {} seconds", timeOut); + createErrorResponse(response); + connectionCacheService.removeConnection(connectionId); + return; + } + } catch (final Exception e) { + LOGGER.info("Error while waiting for response", e); + createErrorResponse(response); + connectionCacheService.removeConnection(connectionId); + return; + } + + final String soap = readResponse(connectionId); + if (soap == null) { + LOGGER.error("Unable to read SOAP response: null"); + createErrorResponse(response); + } else { + LOGGER.debug("Request handled, trying to send response..."); + createSuccessFulResponse(response, soap); + } + + LOGGER.debug( + "End of SoapEndpoint.handleRequest() --> incoming request handled and response returned."); + } + + public String[] sander() { + return new String[]{"a", "poipoi", "frats"}; + } + + private void printHeaderValues(final HttpServletRequest request) { + if (LOGGER.isDebugEnabled()) { + for (final Enumeration headerNames = request.getHeaderNames(); + headerNames.hasMoreElements(); ) { + final String headerName = headerNames.nextElement(); + final String headerValue = request.getHeader(headerName); + LOGGER.debug(" header name: {} header value: {}", headerName, headerValue); + } + } + } + + private void printParameterValues(final HttpServletRequest request) { + if (LOGGER.isDebugEnabled()) { + for (final Enumeration parameterNames = request.getParameterNames(); + parameterNames.hasMoreElements(); ) { + final String parameterName = parameterNames.nextElement(); + final String[] parameterValues = request.getParameterValues(parameterName); + String str = ""; + for (final String parameterValue : parameterValues) { + str = str.concat(parameterValue).concat(" "); + } + LOGGER.debug(" parameter name: {} parameter value: {}", parameterName, str); + } + } + } + + private String getContextForRequestType( + final HttpServletRequest request) { + return request.getRequestURI().replace(URL_PROXY_SERVER, ""); + } + + private String readSoapPayload(final HttpServletRequest request) { + final StringBuilder stringBuilder = new StringBuilder(); + String line; + String soapPayload = null; + try { + final BufferedReader reader = request.getReader(); + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + soapPayload = stringBuilder.toString(); + LOGGER.debug(" payload: {}", soapPayload); + } catch (final Exception e) { + LOGGER.error("Unexpected error while reading request body", e); + } + return soapPayload; + } + + private Map getSoapHeaderValues( + final String soapPayload, final List soapHeaderKeys) { + final Map values = new HashMap<>(); + try { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + factory.setNamespaceAware(false); + final InputStream inputStream = + new ByteArrayInputStream(soapPayload.getBytes(StandardCharsets.UTF_8)); + // Try to find the desired XML elements in the document. + final Document document = factory.newDocumentBuilder().parse(inputStream); + for (final String soapHeaderKey : soapHeaderKeys) { + final String value = evaluateXPathExpression(document, soapHeaderKey); + values.put(soapHeaderKey, value); + } + inputStream.close(); + } catch (final Exception e) { + LOGGER.error("Exception", e); + } + return values; + } + + /** + * Search an XML element using an XPath expression. + * + * @param document The XML document. + * @param element The name of the desired XML element. + * @return The content of the XML element, or null if the element is not found. + * @throws XPathExpressionException In case the expression fails to compile or evaluate, an + * exception will be thrown. + */ + private String evaluateXPathExpression(final Document document, final String element) + throws XPathExpressionException { + final String expression = String.format("//*[contains(local-name(), '%s')]", element); + + final XPathFactory xFactory = XPathFactory.newInstance(); + final XPath xPath = xFactory.newXPath(); + + final XPathExpression xPathExpression = xPath.compile(expression); + final NodeList nodeList = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET); + + if (nodeList != null && nodeList.getLength() > 0) { + return nodeList.item(0).getTextContent(); + } else { + return null; + } + } + + private ClientCertificate tryToFindClientCertificate( + final HttpServletRequest request, final String soapPayload) { + final X509Certificate[] x509Certificates = getX509CertificatesFromServlet(request); + ClientCertificate clientCertificate = null; + + if (x509Certificates.length == 0) { + LOGGER.error(" HTTPServletRequest's attribute was an empty array of X509Certificates."); + } else if (x509Certificates.length == 1) { + LOGGER.debug(" HTTPServletRequest's attribute was array of X509Certificates of length 1."); + + // Get the client certificate. + clientCertificate = getClientCertificate(x509Certificates[0]); + } else { + LOGGER.debug( + " HTTPServletRequest's attribute was array of X509Certificates of length {}.", + x509Certificates.length); + + final Map soapHeaderValues = + getSoapHeaderValues(soapPayload, SOAP_HEADER_KEYS); + LOGGER.debug( + " SOAP Header ApplicationName: {}", + soapHeaderValues.get(SOAP_HEADER_KEY_APPLICATION_NAME)); + LOGGER.debug( + " SOAP Header OrganisationIdentification: {}", + soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); + LOGGER.debug(" SOAP Header UserName: {}", soapHeaderValues.get(SOAP_HEADER_KEY_USER_NAME)); + + // Try to extract the client certificate for the organization + // identification. + clientCertificate = + getClientCertificateByOrganisationIdentification( + x509Certificates, soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); + } + + return clientCertificate; + } + + private X509Certificate[] getX509CertificatesFromServlet(final HttpServletRequest request) { + final Object x509CertificateAttribute = request.getAttribute(X_509_CERTIFICATE_REQUEST_ATTRIBUTE); + LOGGER.debug(X_509_CERTIFICATE_REQUEST_ATTRIBUTE + ": {}", x509CertificateAttribute); + if (x509CertificateAttribute instanceof X509Certificate[]) { + LOGGER.debug(" x509CertificateAttribute instanceof X509Certificate[]"); + return (X509Certificate[]) x509CertificateAttribute; + } else { + return new X509Certificate[0]; + } + } + + private ClientCertificate getClientCertificateByOrganisationIdentification( + final X509Certificate[] array, final String organisationCommonName) { + + for (final X509Certificate x509Certificate : array) { + final Principal principal = x509Certificate.getSubjectDN(); + LOGGER.debug(" principal: {}", principal); + final String subjectDn = principal.getName(); + LOGGER.debug(" subjectDn: {}", subjectDn); + try { + final String commonName = getCommonName(subjectDn); + if (commonName.equals(organisationCommonName)) { + LOGGER.debug( + "Found client certificate for right organisation {}", organisationCommonName); + return new ClientCertificate(x509Certificate, commonName); + } + } catch (final NamingException e) { + LOGGER.info("Failed to extract CommonName from this ClientCertificate", e); + } + } + return null; + } + + private ClientCertificate getClientCertificate(final X509Certificate x509Certificate) { + ClientCertificate clientCertificate = null; + + final Principal principal = x509Certificate.getSubjectDN(); + LOGGER.debug(" principal: {}", principal); + final String subjectDn = principal.getName(); + LOGGER.debug(" subjectDn: {}", subjectDn); + try { + final String commonName = getCommonName(subjectDn); + clientCertificate = new ClientCertificate(x509Certificate, commonName); + } catch (final NamingException e) { + LOGGER.info("Failed to extract CommonName from ClientCertificate", e); + } + + return clientCertificate; + } + + private String getCommonName(final String subjectDn) throws NamingException { + final Rdn rdn = new Rdn(subjectDn); + LOGGER.debug(" rdn: {}", rdn); + final Attributes attributes = rdn.toAttributes(); + LOGGER.debug(" attributes: {}", attributes); + final Attribute attribute = attributes.get("cn"); + LOGGER.debug(" attribute: {}", attribute); + final String commonName = (String) attribute.get(); + LOGGER.debug(" common name: {}", commonName); + return commonName; + } + + private Integer shouldUseCustomTimeOut(final String soapPayload) { + final Set keys = customTimeOutsMap.keySet(); + for (final String key : keys) { + if (soapPayload.contains(key)) { + return customTimeOutsMap.get(key); + } + } + return INVALID_CUSTOM_TIME_OUT; + } + + private String readResponse(final String connectionId) throws ServletException { + final String soap; + try { + final Connection connection = connectionCacheService.findConnection(connectionId); + soap = connection.getSoapResponse(); + connectionCacheService.removeConnection(connectionId); + } catch (final ConnectionNotFoundInCacheException e) { + LOGGER.error("Unexpected error while trying to find a cached connection", e); + throw new ServletException("Unable to obtain response"); + } + return soap; + } + + private void createErrorResponse(final HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + private void createSuccessFulResponse(final HttpServletResponse response, final String soap) + throws IOException { + LOGGER.debug("Start - creating successful response"); + response.setStatus(HttpServletResponse.SC_OK); + response.addHeader("SOAP-ACTION", ""); + response.addHeader("Keep-Alive", "timeout=5, max=100"); + response.addHeader("Accept", "text/xml"); + response.addHeader("Connection", "Keep-Alive"); + response.setContentType("text/xml; charset=" + StandardCharsets.UTF_8.name()); + response.getWriter().write(soap); + LOGGER.debug("End - creating successful response"); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java new file mode 100644 index 0000000..fe8701a --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java @@ -0,0 +1,14 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.exceptions; + +public class ConnectionNotFoundInCacheException extends ProxyServerException { + + /** + * Serial Version UID. + */ + private static final long serialVersionUID = -858760086093512799L; + + public ConnectionNotFoundInCacheException(final String message) { + super(message); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java new file mode 100644 index 0000000..33d801c --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java @@ -0,0 +1,25 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.exceptions; + +/** + * Base type for exceptions for proxy server component. + */ +public class ProxyServerException extends Exception { + + /** + * Serial Version UID. + */ + private static final long serialVersionUID = -8696835428244659385L; + + public ProxyServerException() { + super(); + } + + public ProxyServerException(final String message) { + super(message); + } + + public ProxyServerException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java new file mode 100644 index 0000000..5ab543e --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java @@ -0,0 +1,14 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.exceptions; + +public class UnableToCreateHttpsURLConnectionException extends ProxyServerException { + + /** + * Serial Version UID. + */ + private static final long serialVersionUID = -8807766325167125880L; + + public UnableToCreateHttpsURLConnectionException(final String message) { + super(message); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java new file mode 100644 index 0000000..a497607 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java @@ -0,0 +1,14 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.exceptions; + +public class UnableToCreateKeyManagersException extends ProxyServerException { + + /** + * Serial Version UID. + */ + private static final long serialVersionUID = -100586751704652623L; + + public UnableToCreateKeyManagersException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java new file mode 100644 index 0000000..0c54802 --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java @@ -0,0 +1,14 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.exceptions; + +public class UnableToCreateTrustManagersException extends ProxyServerException { + + /** + * Serial Version UID. + */ + private static final long serialVersionUID = -855694158211466200L; + + public UnableToCreateTrustManagersException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java new file mode 100644 index 0000000..8643c2d --- /dev/null +++ b/components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java @@ -0,0 +1,24 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.valueobjects; + +import java.security.cert.X509Certificate; + +public class ClientCertificate { + + private final X509Certificate x509Certificate; + + private final String commonName; + + public ClientCertificate(final X509Certificate x509Certificate, final String commonName) { + this.x509Certificate = x509Certificate; + this.commonName = commonName; + } + + public X509Certificate getX509Certificate() { + return x509Certificate; + } + + public String getCommonName() { + return commonName; + } +} diff --git a/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt b/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt new file mode 100644 index 0000000..fd7ce79 --- /dev/null +++ b/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt @@ -0,0 +1,77 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.application.properties + +import mu.KotlinLogging +import org.springframework.boot.context.properties.ConfigurationProperties +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec + +@ConfigurationProperties("security") +class SecurityConfigurationProperties( + val keyStore: StoreConfigurationProperties, + val trustStore: StoreConfigurationProperties, + val signing: SigningConfigurationProperties +) + +class StoreConfigurationProperties( + val location: String, + val password: String, + val type: String +) + + +class SigningConfigurationProperties( + val keyType: String, + val signKeyFile: String, + val verifyKeyFile: String, + /** Indicates which provider is used for signing and verification. */ + val provider: String, + /** Indicates which signature is used for signing and verification. */ + val signature: String +) { + private val logger = KotlinLogging.logger {} + + /** Private key used for signing. */ + val signKey = createPrivateKey(signKeyFile, keyType, provider) + + /** Public key used for verification. */ + val verifyKey = createPublicKey(verifyKeyFile, keyType, provider) + + private fun createPrivateKey( + keyPath: String, keyType: String, provider: String + ): PrivateKey? { + return try { + val key = readKeyFromDisk(keyPath) + val privateKeySpec = PKCS8EncodedKeySpec(key) + val privateKeyFactory = KeyFactory.getInstance(keyType, provider) + privateKeyFactory.generatePrivate(privateKeySpec) + } catch (e: Exception) { + logger.error("Unexpected exception during private key creation", e) + null + } + } + + private fun createPublicKey( + keyPath: String, keyType: String, provider: String + ): PublicKey? { + return try { + val key = readKeyFromDisk(keyPath) + val publicKeySpec = X509EncodedKeySpec(key) + val publicKeyFactory = KeyFactory.getInstance(keyType, provider) + publicKeyFactory.generatePublic(publicKeySpec) + } catch (e: Exception) { + logger.error("Unexpected exception during public key creation", e) + null + } + } + + @Throws(IOException::class) + private fun readKeyFromDisk(keyPath: String): ByteArray = Files.readAllBytes(Paths.get(keyPath)) +} diff --git a/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt b/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt new file mode 100644 index 0000000..ba81758 --- /dev/null +++ b/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt @@ -0,0 +1,34 @@ +// Copyright 2023 Alliander N.V. + +package org.gxf.soapbridge.application.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("soap") +class SoapConfigurationProperties( + val hostnameVerificationStrategy: HostnameVerificationStrategy, + /** + * Maximum number of seconds this {@link SoapEndpoint} will wait for a response from the other end + * before terminating the connection with the client application. + */ + val timeOut: Int, + /** + * Time outs for specific functions. + */ + val customTimeouts: String, + val callEndpoint: SoapEndpointConfiguration, +) + +enum class HostnameVerificationStrategy { + ALLOW_ALL_HOSTNAMES, BROWSER_COMPATIBLE_HOSTNAMES +} + +class SoapEndpointConfiguration( + host: String, + port: Int, + protocol: String +) { + + val hostAndPort = "$host:$port" + val uri = "$protocol://${hostAndPort}" +} diff --git a/components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java b/components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java new file mode 100644 index 0000000..d834d4e --- /dev/null +++ b/components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java @@ -0,0 +1,32 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.application.utils; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class RandomStringServiceTests { + + @Test + void test1() { + final String randomString = RandomStringFactory.generateRandomString(); + + assertNotNull(randomString, "It is expected that the generated random string not is null"); + assertEquals( + 16, + randomString.length(), + "It is expected that the generated random string is of length 16"); + } + + @Test + void test2() { + final String randomString = RandomStringFactory.generateRandomString(42); + + assertNotNull(randomString, "It is expected that the generated random string not is null"); + assertEquals( + 42, + randomString.length(), + "It is expected that the generated random string is of length 42"); + } +} diff --git a/components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java b/components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java new file mode 100644 index 0000000..ed952d3 --- /dev/null +++ b/components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java @@ -0,0 +1,106 @@ +// Copyright 2023 Alliander N.V. +package org.gxf.soapbridge.soap.clients; + +import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; +import org.gxf.soapbridge.application.properties.HostnameVerificationStrategy; +import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.application.properties.SoapEndpointConfiguration; +import org.gxf.soapbridge.application.services.SigningService; +import org.gxf.soapbridge.messaging.ProxyResponsesMessageSender; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.net.ssl.HttpsURLConnection; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SoapClientTest { + + @Mock + ProxyResponsesMessageSender proxyResponsesMessageSender; + @Mock + HttpsUrlConnectionFactory httpsUrlConnectionFactory; + @Mock + SigningService signingService; + + byte[] testContent = "test content".getBytes(StandardCharsets.UTF_8); + + @Spy + SoapConfigurationProperties soapConfigurationProperties = new SoapConfigurationProperties( + HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES, + 45, + "", + new SoapEndpointConfiguration( + "localhost", 443, "https" + ) + ); + + @InjectMocks + SoapClient soapClient; + + @Test + void shouldSendSoapRequestAndJmsResponse() throws Exception { + // arrange + final HttpsURLConnection connection = setupConnectionMock(); + when(httpsUrlConnectionFactory.createConnection( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(connection); + + // act + soapClient.sendRequest( + "connectionId", + "context", + "commonName", + "payload"); + + // assert + verify(connection).disconnect(); + } + + @Test + void shoudDisconnectWhenSoapRequestFails() throws Exception { + // arrange + final HttpsURLConnection connection = setupFailingConnectionMock(); + when(httpsUrlConnectionFactory.createConnection( + anyString(), anyString(), anyString(), anyString())) + .thenReturn(connection); + + // act + soapClient.sendRequest( + "connectionId", + "context", + "commonName", + "payload"); + + // assert + verify(connection).disconnect(); + verifyNoInteractions(proxyResponsesMessageSender); + } + + private HttpsURLConnection setupConnectionMock() throws Exception { + final HttpsURLConnection connection = mock(HttpsURLConnection.class); + final InputStream inputStream = new ByteArrayInputStream(testContent); + when(connection.getOutputStream()).thenReturn(mock(OutputStream.class)); + when(connection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(connection.getInputStream()).thenReturn(inputStream); + return connection; + } + + private HttpsURLConnection setupFailingConnectionMock() throws Exception { + final HttpsURLConnection connection = mock(HttpsURLConnection.class); + when(connection.getOutputStream()).thenThrow(ConnectException.class); + return connection; + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..e9652be --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,35 @@ +# Copyright 2023 Alliander N.V. +version: '2' +services: + zookeeper: + image: confluentinc/cp-zookeeper:7.3.0 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "22181:2181" + kafka: + image: confluentinc/cp-kafka:7.3.0 + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_CREATE_TOPICS: "avroTopic:1:1" + schema-registry: + image: confluentinc/cp-schema-registry:7.3.0 + depends_on: + - zookeeper + - kafka + ports: + - "8081:8081" + environment: + SCHEMA_REGISTRY_HOST_NAME: schema-registry + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka:29092' + SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aec0a55 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.caching=true +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..91fd055 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +// Copyright 2023 Alliander N.V. + +rootProject.name = "gxf-soap-bridge" + +include("application") +include("components:core") +include("components:kafka") +include("components:soap") From 5c2282669abc9040a9eb027edbd875dc2ab79409 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Tue, 14 Nov 2023 15:20:21 +0100 Subject: [PATCH 02/46] FDP-94: Refactored, updated copyright Signed-off-by: Sander Verbruggen --- application/build.gradle.kts | 16 +- .../kotlin/org/gxf/soapbridge/EndToEndTest.kt | 8 +- .../soapbridge/SoapBridgeApplicationTests.kt | 4 +- .../resources/generate_certs.sh | 4 +- .../configuration/SoapConfiguration.java | 24 + .../factories/HostnameVerifierFactory.java | 35 ++ .../factories/HttpsUrlConnectionFactory.java | 99 ++++ .../factories/SslContextFactory.java | 149 ++++++ .../services/ClientCommunicationService.java | 62 +++ .../services/ConnectionCacheService.java | 94 ++++ .../PlatformCommunicationService.java | 55 +++ .../application/services/SigningService.java | 91 ++++ .../services/SslContextCacheService.java | 77 +++ .../utils/RandomStringFactory.java | 48 ++ .../soapbridge/soap/clients/Connection.java | 73 +++ .../soapbridge/soap/clients/SoapClient.java | 159 +++++++ .../soap/endpoints/SoapEndpoint.java | 428 +++++++++++++++++ .../ConnectionNotFoundInCacheException.java | 14 + .../soap/exceptions/ProxyServerException.java | 23 + ...leToCreateHttpsURLConnectionException.java | 14 + .../UnableToCreateKeyManagersException.java | 14 + .../UnableToCreateTrustManagersException.java | 14 + .../soap/valueobjects/ClientCertificate.java | 26 ++ .../gxf/soapbridge/SoapBridgeApplication.kt | 4 +- .../configuration/SecurityConfiguration.kt | 4 +- .../SecurityConfigurationProperties.kt | 6 +- .../properties/SoapConfigurationProperties.kt | 6 +- .../exceptions/ProxyMessageException.kt | 4 +- .../listeners/ProxyRequestKafkaListener.kt | 12 +- .../listeners/ProxyResponseKafkaListener.kt | 12 +- .../TopicsConfigurationProperties.kt | 4 +- .../kafka/senders/ProxyRequestKafkaSender.kt | 11 +- .../kafka/senders/ProxyResponseKafkaSender.kt | 11 +- .../ObservabilityConfiguration.kt | 4 +- .../valueobjects}/ProxyServerBaseMessage.kt | 6 +- .../ProxyServerRequestMessage.kt | 6 +- .../ProxyServerResponseMessage.kt | 6 +- .../utils/RandomStringServiceTests.java | 34 ++ .../soap/clients/SoapClientTest.java | 96 ++++ components/core/build.gradle.kts | 9 - .../messaging/ProxyRequestsMessageSender.kt | 9 - .../messaging/ProxyResponsesMessageSender.kt | 9 - .../services/ProxyRequestHandler.kt | 9 - .../services/ProxyResponseHandler.kt | 9 - components/kafka/build.gradle.kts | 17 - components/soap/build.gradle.kts | 34 -- .../configuration/SoapConfiguration.java | 22 - .../factories/HostnameVerifierFactory.java | 33 -- .../factories/HttpsUrlConnectionFactory.java | 106 ----- .../factories/SslContextFactory.java | 152 ------ .../services/ClientCommunicationService.java | 67 --- .../services/ConnectionCacheService.java | 95 ---- .../PlatformCommunicationService.java | 66 --- .../application/services/SigningService.java | 86 ---- .../services/SslContextCacheService.java | 79 ---- .../utils/RandomStringFactory.java | 52 --- .../soapbridge/soap/clients/Connection.java | 72 --- .../soapbridge/soap/clients/SoapClient.java | 173 ------- .../soap/endpoints/SoapEndpoint.java | 440 ------------------ .../ConnectionNotFoundInCacheException.java | 14 - .../soap/exceptions/ProxyServerException.java | 25 - ...leToCreateHttpsURLConnectionException.java | 14 - .../UnableToCreateKeyManagersException.java | 14 - .../UnableToCreateTrustManagersException.java | 14 - .../soap/valueobjects/ClientCertificate.java | 24 - .../utils/RandomStringServiceTests.java | 32 -- .../soap/clients/SoapClientTest.java | 106 ----- settings.gradle.kts | 3 - 68 files changed, 1711 insertions(+), 1831 deletions(-) create mode 100644 application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java create mode 100644 application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java create mode 100644 application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java rename {components/soap/src/main/kotlin/org/gxf/soapbridge/application => application/src/main/kotlin/org/gxf/soapbridge/configuration}/properties/SecurityConfigurationProperties.kt (93%) rename {components/soap/src/main/kotlin/org/gxf/soapbridge/application => application/src/main/kotlin/org/gxf/soapbridge/configuration}/properties/SoapConfigurationProperties.kt (83%) rename {components/core/src/main/kotlin/org/gxf/soapbridge/messaging => application/src/main/kotlin/org/gxf/soapbridge}/exceptions/ProxyMessageException.kt (68%) rename {components/kafka => application}/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt (69%) rename {components/kafka => application}/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt (71%) rename {components/kafka => application}/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt (76%) rename {components/kafka => application}/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt (71%) rename {components/kafka => application}/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt (71%) rename {components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages => application/src/main/kotlin/org/gxf/soapbridge/valueobjects}/ProxyServerBaseMessage.kt (84%) rename {components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages => application/src/main/kotlin/org/gxf/soapbridge/valueobjects}/ProxyServerRequestMessage.kt (95%) rename {components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages => application/src/main/kotlin/org/gxf/soapbridge/valueobjects}/ProxyServerResponseMessage.kt (92%) create mode 100644 application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java create mode 100644 application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java delete mode 100644 components/core/build.gradle.kts delete mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt delete mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt delete mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt delete mode 100644 components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt delete mode 100644 components/kafka/build.gradle.kts delete mode 100644 components/soap/build.gradle.kts delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java delete mode 100644 components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java delete mode 100644 components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java delete mode 100644 components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java diff --git a/application/build.gradle.kts b/application/build.gradle.kts index 453f647..9b58bcf 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -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") @@ -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 { @@ -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") } } diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt index ce539e9..55c8182 100644 --- a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt @@ -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 @@ -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 @@ -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) diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt index 3566f8c..b550b51 100644 --- a/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt @@ -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 diff --git a/application/src/integrationTest/resources/generate_certs.sh b/application/src/integrationTest/resources/generate_certs.sh index 33e99f8..5f7bc86 100644 --- a/application/src/integrationTest/resources/generate_certs.sh +++ b/application/src/integrationTest/resources/generate_certs.sh @@ -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 diff --git a/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java b/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java new file mode 100644 index 0000000..0773d7d --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java @@ -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; + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java new file mode 100644 index 0000000..c40078d --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java @@ -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!"); + } + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java new file mode 100644 index 0000000..7d7a5e2 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -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()); + } + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java new file mode 100644 index 0000000..c762554 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java @@ -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); + } + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java new file mode 100644 index 0000000..d9c7e38 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.soap.clients.Connection; +import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; +import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * Service which handles SOAP responses from OSGP. The SOAP response will be set for the connection + * which correlates with the connection-id. + */ +@Service +public class ClientCommunicationService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClientCommunicationService.class); + + /** Service used to cache incoming connections from client applications. */ + @Autowired private ConnectionCacheService connectionCacheService; + + /** Service used to sign and/or verify the content of queue messages. */ + @Autowired private SigningService signingService; + + /** + * Process an incoming queue message. The content of the message has to be verified by the {@link + * SigningService}. Then a response from OSGP will set for the pending connection from a client. + * + * @param proxyServerResponseMessage The incoming queue message to process. + */ + public void handleIncomingResponse(final ProxyServerResponseMessage proxyServerResponseMessage) { + final boolean isValid = + signingService.verifyContent( + proxyServerResponseMessage.constructString(), + proxyServerResponseMessage.getSignature()); + if (!isValid) { + LOGGER.error("ProxyServerResponseMessage failed to pass security check."); + return; + } + + try { + final Connection connection = + connectionCacheService.findConnection(proxyServerResponseMessage.getConnectionId()); + if (connection != null) { + if (isValid) { + LOGGER.debug("Connection valid, set SOAP response"); + connection.setResponse(proxyServerResponseMessage.getSoapResponse()); + } else { + connection.setResponse("Security check has failed."); + } + } else { + LOGGER.error("Cached connection is null"); + } + } catch (final ConnectionNotFoundInCacheException e) { + LOGGER.error("ConnectionNotFoundInCacheException", e); + } + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java new file mode 100644 index 0000000..44de523 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.services; + +import java.util.concurrent.ConcurrentHashMap; +import org.gxf.soapbridge.application.utils.RandomStringFactory; +import org.gxf.soapbridge.soap.clients.Connection; +import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** This {@link @Service} class caches connections from client applications. */ +@Service +public class ConnectionCacheService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionCacheService.class); + + /** + * Map used to cache connections. The key is a random string generated by {@link + * RandomStringFactory}. The value is a {@link Connection} instance. + */ + private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + /** + * Creates a connection and puts it in the cache. + * + * @return the created Connection + */ + public Connection cacheConnection() { + final Connection connection = new Connection(); + final String connectionId = connection.getConnectionId(); + LOGGER.debug("Caching connection with connectionId: {}", connectionId); + cache.put(connectionId, connection); + return connection; + } + + /** + * Get a {@link Connection} instance from the {@link ConnectionCacheService#cache}. + * + * @param connectionId The key for the {@link Connection} instance obtained by calling {@link + * ConnectionCacheService#cacheConnection()}. + * @return A {@link Connection} instance. + * @throws ConnectionNotFoundInCacheException In case the connection is not present in the {@link + * ConnectionCacheService#cache}. + */ + public Connection findConnection(final String connectionId) + throws ConnectionNotFoundInCacheException { + LOGGER.debug("Trying to find connection with connectionId: {}", connectionId); + final Connection connection = cache.get(connectionId); + if (connection == null) { + throw new ConnectionNotFoundInCacheException( + String.format("Unable to find connection for connectionId: %s", connectionId)); + } + return connection; + } + + /** + * Determine if the response is available for a particular {@link Connection} instance. + * + * @param connectionId The key for the {@link Connection} instance obtained by calling {@link + * ConnectionCacheService#cacheConnection()}. + * @return True in case the response from the Platform has resolved and is available, false + * otherwise. + * @throws ConnectionNotFoundInCacheException In case the connection is not present in the {@link + * ConnectionCacheService#cache}. + */ + public boolean hasResponseResolved(final String connectionId) + throws ConnectionNotFoundInCacheException { + final Connection connection = cache.get(connectionId); + if (connection != null) { + LOGGER.debug( + "hasResponseResolved called with connectionId: {}, this.cache.size(): {}", + connectionId, + cache.size()); + return connection.isResponseResolved(); + } else { + throw new ConnectionNotFoundInCacheException( + String.format("Unable to find connection for connectionId: %s", connectionId)); + } + } + + /** + * Removes a {@link Connection} instance from the {@link ConnectionCacheService#cache}. + * + * @param connectionId The key for the {@link Connection} instance obtained by calling {@link + * ConnectionCacheService#cacheConnection()}. + */ + public void removeConnection(final String connectionId) { + LOGGER.debug("Removing connection with connectionId: {}", connectionId); + cache.remove(connectionId); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java new file mode 100644 index 0000000..c3eb513 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.services; + +import org.gxf.soapbridge.soap.clients.SoapClient; +import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** Service which can send SOAP requests to OSGP. */ +@Service +public class PlatformCommunicationService { + + private static final Logger LOGGER = LoggerFactory.getLogger(PlatformCommunicationService.class); + + /** SOAP client used to sent request messages to OSGP. */ + @Autowired private SoapClient soapClient; + + /** Service used to sign and/or verify the content of queue messages. */ + @Autowired private SigningService signingService; + + /** + * Process an incoming queue message. The content of the message has to be verified by the {@link + * SigningService}. Then a SOAP message can be sent to OSGP using {@link SoapClient}. + * + * @param proxyServerRequestMessage The incoming queue message to process. + */ + public void handleIncomingRequest(final ProxyServerRequestMessage proxyServerRequestMessage) { + + final String proxyServerRequestMessageAsString = proxyServerRequestMessage.constructString(); + final String securityKey = proxyServerRequestMessage.getSignature(); + + final boolean isValid = + signingService.verifyContent(proxyServerRequestMessageAsString, securityKey); + if (!isValid) { + LOGGER.error("ProxyServerRequestMessage failed to pass security check."); + return; + } + + final String connectionId = proxyServerRequestMessage.getConnectionId(); + final String context = proxyServerRequestMessage.getContext(); + final String commonName = proxyServerRequestMessage.getCommonName(); + final String soapPayload = proxyServerRequestMessage.getSoapPayload(); + + final boolean result = soapClient.sendRequest(connectionId, context, commonName, soapPayload); + if (!result) { + LOGGER.error("Unsuccessful at sending request to platform."); + } else { + LOGGER.debug("Successfully sent response message to queue"); + } + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java b/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java new file mode 100644 index 0000000..f45d569 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.services; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.util.HexFormat; +import org.gxf.soapbridge.configuration.properties.SecurityConfigurationProperties; +import org.gxf.soapbridge.configuration.properties.SigningConfigurationProperties; +import org.gxf.soapbridge.soap.exceptions.ProxyServerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * This {@link @Service} class can generate a signature for a given content, and verify the content + * using the signature. + */ +@Service +public class SigningService { + private final SigningConfigurationProperties signingConfiguration; + + private static final Logger LOGGER = LoggerFactory.getLogger(SigningService.class); + + private SigningService(final SecurityConfigurationProperties securityConfiguration) { + signingConfiguration = securityConfiguration.getSigning(); + } + + /** + * Using the given content, create a signature. The signature is converted from byte array to + * hexadecimal string. + * + * @param content The content to sign. + * @return The signature which can be used later to verify if the content is intact and unaltered. + * @throws ProxyServerException thrown when an error occurs during signing. + */ + public String signContent(final String content) throws ProxyServerException { + final byte[] bytes = content.getBytes(StandardCharsets.UTF_8); + try { + final byte[] signature = createSignature(bytes); + LOGGER.debug("signature.length: {}", signature.length); + return HexFormat.of().formatHex(signature); + } catch (final GeneralSecurityException e) { + throw new ProxyServerException( + "Unexpected GeneralSecurityException when trying to sign the content", e); + } + } + + /** + * For the given content and security key, verify if the content is intact and unaltered. + * + * @param content The content to verify. + * @param securityKey The signature a.k.a. security key which was created using {@link + * SigningService#signContent(String)}. + * @return True when the verification succeeds. + */ + public boolean verifyContent(final String content, final String securityKey) { + final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); + final byte[] securityKeyBytes = HexFormat.of().parseHex(securityKey); + LOGGER.debug("securityKeyBytes.length: {}", securityKeyBytes.length); + try { + return validateSignature(contentBytes, securityKeyBytes); + } catch (final GeneralSecurityException e) { + LOGGER.error( + "Unexpected GeneralSecurityException when trying to verify the content using the security key", + e); + return false; + } + } + + private byte[] createSignature(final byte[] message) throws GeneralSecurityException { + final Signature signatureBuilder = + Signature.getInstance( + signingConfiguration.getSignature(), signingConfiguration.getProvider()); + signatureBuilder.initSign(signingConfiguration.getSignKey()); + signatureBuilder.update(message); + return signatureBuilder.sign(); + } + + private boolean validateSignature(final byte[] message, final byte[] securityKey) + throws GeneralSecurityException { + final Signature signatureBuilder = + Signature.getInstance( + signingConfiguration.getSignature(), signingConfiguration.getProvider()); + signatureBuilder.initVerify(signingConfiguration.getVerifyKey()); + signatureBuilder.update(message); + return signatureBuilder.verify(securityKey); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java b/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java new file mode 100644 index 0000000..14995c6 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.services; + +import java.util.concurrent.ConcurrentHashMap; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import org.gxf.soapbridge.application.factories.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * This {@link @Service} class encapsulates the creation of a suitable {@link SSLContext} instance + * to use with a HTTPS connection, like {@link HttpsURLConnection} for example. Further, the created + * instance is cached using a {@link ConcurrentHashMap} in order to maintain performance even when + * many calls are handled simultaneously. The actual creation of {@link SSLContext} instances is + * delegated to {@link SslContextFactory} class. + */ +@Service +public class SslContextCacheService { + + private static final Logger LOGGER = LoggerFactory.getLogger(SslContextCacheService.class); + + /** + * Map used to cache {@link SSLContext} instances. The key is the common name for an organization, + * which is equal to the organization identification. + */ + private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + /** Factory which assists in creating {@link SSLContext} instances. */ + @Autowired private SslContextFactory sslContextFactory; + + /** + * Creates a new {@link SSLContext} instance and caches it, or fetches an existing instance from + * the cache. + * + * @return A {@link SSLContext} instance. + */ + public SSLContext getSslContext() { + final String key = "SSLContextWithoutCommonName"; + if (cache.containsKey(key)) { + LOGGER.debug("Returning SSL Context from cache for key: {}", key); + return cache.get(key); + } else { + LOGGER.debug( + "Creating new SSL Context and putting the instance in the cache for key: {}", key); + final SSLContext sslContext = sslContextFactory.createSslContext(); + cache.put(key, sslContext); + return sslContext; + } + } + + /** + * Creates a new {@link SSLContext} instance for the given common name and caches it, or fetches + * an existing instance from the cache. + * + * @param commonName The common name which is used to look up a Personal Information Exchange + * (*.pfx) file that is used as key store. + * @return A {@link SSLContext} instance. + */ + public SSLContext getSslContextForCommonName(final String commonName) { + if (cache.containsKey(commonName)) { + LOGGER.debug("Returning SSL Context from cache for common name: {}", commonName); + return cache.get(commonName); + } else { + LOGGER.debug( + "Creating new SSL Context and putting the instance in the cache for common name: {}", + commonName); + final SSLContext sslContext = sslContextFactory.createSslContext(commonName); + cache.put(commonName, sslContext); + return sslContext; + } + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java b/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java new file mode 100644 index 0000000..581bb78 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.utils; + +import java.security.SecureRandom; + +/** This class can generate random string values. */ +public class RandomStringFactory { + + /** The random used to generate random strings. */ + private static final SecureRandom random = new SecureRandom(); + + /** Default length of the generated random strings. */ + private static final int DEFAULT_LENGTH = 16; + + private RandomStringFactory() { + // Only static. + } + + /** + * Generate a random string. + * + * @return A string of length {@link RandomStringFactory#defaultLength} + */ + public static String generateRandomString() { + return randomString(DEFAULT_LENGTH); + } + + /** + * Generate a random string. + * + * @param length The length of the random string to generate. + * @return A string of the given length. + */ + public static String generateRandomString(final int length) { + return randomString(length); + } + + private static String randomString(final int length) { + final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final StringBuilder buf = new StringBuilder(); + for (int i = 0; i < length; i++) { + buf.append(chars.charAt(random.nextInt(chars.length()))); + } + return buf.toString(); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java new file mode 100644 index 0000000..61e42ad --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.clients; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import org.gxf.soapbridge.application.services.ConnectionCacheService; +import org.gxf.soapbridge.application.utils.RandomStringFactory; + +public class Connection { + + /** + * Default length for the connection id, which is the key for the {@link + * ConnectionCacheService#cache}. + */ + private static final int CONNECTION_ID_LENGTH = 32; + + private volatile boolean responseResolved; + + private String soapResponse; + + private String connectionId; + + private final Semaphore responseReceived; + + public Connection() { + responseResolved = false; + responseReceived = new Semaphore(0); + connectionId = RandomStringFactory.generateRandomString(CONNECTION_ID_LENGTH); + } + + public boolean isResponseResolved() { + return responseResolved; + } + + public void setResponse(final String soapResponse) { + responseResolved = true; + this.soapResponse = soapResponse; + responseReceived(); + } + + public String getSoapResponse() { + return soapResponse; + } + + public void setConnectionId(final String connectionId) { + this.connectionId = connectionId; + } + + public String getConnectionId() { + return connectionId; + } + + /* + * Indicates the response for this connection has been received. + */ + public void responseReceived() { + responseReceived.release(); + } + + /* + * Waits for a response on this connection. + * + * @timeout The number of seconds to wait for a response. + * + * @returns true, if the response was received within @timeout seconds, + * false otherwise. + */ + public boolean waitForResponseReceived(final int timeout) throws InterruptedException { + return responseReceived.tryAcquire(timeout, TimeUnit.SECONDS); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java new file mode 100644 index 0000000..c858293 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.clients; + +import java.io.*; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.HttpsURLConnection; +import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; +import org.gxf.soapbridge.application.services.SigningService; +import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.configuration.properties.SoapEndpointConfiguration; +import org.gxf.soapbridge.kafka.senders.ProxyResponseKafkaSender; +import org.gxf.soapbridge.soap.exceptions.ProxyServerException; +import org.gxf.soapbridge.soap.exceptions.UnableToCreateHttpsURLConnectionException; +import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage; +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 send SOAP messages to the Platform. */ +@Component +public class SoapClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(SoapClient.class); + + /** Message sender to send messages to a queue. */ + @Autowired private ProxyResponseKafkaSender proxyReponseSender; + + @Autowired private SoapConfigurationProperties soapConfiguration; + + /** Factory which assist in creating {@link HttpsURLConnection} instances. */ + @Autowired private HttpsUrlConnectionFactory httpsUrlConnectionFactory; + + /** Service used to sign the content of a message. */ + @Autowired private SigningService signingService; + + /** + * Send a request to the Platform. + * + * @param connectionId The connectionId for this connection. + * @param context The part of the URL indicating the SOAP web-service. + * @param commonName The common name (organisation identification). + * @param soapPayload The SOAP message to send to the platform. + * @return True if the request has been sent and the response has been received and written to a + * queue, false otherwise. + */ + public boolean sendRequest( + final String connectionId, + final String context, + final String commonName, + final String soapPayload) { + + HttpsURLConnection connection = null; + + try { + // Try to create a connection. + connection = createConnection(context, soapPayload, commonName); + if (connection == null) { + LOGGER.warn("Could not create connection for sending SOAP request."); + return false; + } + + // Send the SOAP payload to the server. + sendRequest(connection, soapPayload); + // Read the response. + LOGGER.debug("SOAP request sent, trying to read SOAP response..."); + final String soapResponse = readResponse(connection); + LOGGER.debug("SOAP response: {}", soapResponse); + // Always disconnect the connection. + connection.disconnect(); + + // Create proxy-server response message. + final ProxyServerResponseMessage responseMessage = + createProxyServerResponseMessage(connectionId, soapResponse); + + // Send queue message. + proxyReponseSender.send(responseMessage); + + return true; + } catch (final Exception e) { + if (connection != null) { + connection.disconnect(); + } + LOGGER.error("Unexpected exception while sending SOAP request", e); + return false; + } + } + + private HttpsURLConnection createConnection( + final String context, final String soapPayload, final String commonName) + throws UnableToCreateHttpsURLConnectionException { + final String contentLength = String.format("%d", soapPayload.length()); + + final SoapEndpointConfiguration callEndpoint = soapConfiguration.getCallEndpoint(); + + final String uri = callEndpoint.getUri().concat(context); + LOGGER.info("Preparing to open connection for WEBAPP_REQUEST using URI: {}", uri); + return httpsUrlConnectionFactory.createConnection( + uri, callEndpoint.getHostAndPort(), contentLength, commonName); + } + + private void sendRequest(final HttpsURLConnection connection, final String soapPayLoad) + throws IOException { + try (final OutputStream outputStream = connection.getOutputStream(); + final OutputStreamWriter outputStreamWriter = + new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { + outputStreamWriter.write(soapPayLoad); + outputStreamWriter.flush(); + } catch (final IOException e) { + LOGGER.debug("Rethrow IOException while sending SOAP request."); + throw e; + } + } + + private String readResponse(final HttpsURLConnection connection) throws IOException { + // Use a BufferedReader and an InputStreamReader configured with UTF-8 + // character encoding. This will ensure that the response from the + // Platform is read correctly. + try (final InputStream inputStream = getInputStream(connection); + final InputStreamReader inputStreamReader = + new InputStreamReader(inputStream, StandardCharsets.UTF_8); + final BufferedReader reader = new BufferedReader(inputStreamReader)) { + final StringBuilder response = new StringBuilder(); + String line = reader.readLine(); + while (line != null) { + response.append(line); + line = reader.readLine(); + } + return response.toString(); + } catch (final IOException e) { + LOGGER.debug("Rethrow IOException while reading SOAP response"); + throw e; + } + } + + private InputStream getInputStream(final HttpsURLConnection connection) throws IOException { + final InputStream inputStream; + + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + inputStream = connection.getInputStream(); + } else { + inputStream = connection.getErrorStream(); + } + + return inputStream; + } + + private ProxyServerResponseMessage createProxyServerResponseMessage( + final String connectionId, final String soapResponse) throws ProxyServerException { + final ProxyServerResponseMessage responseMessage = + new ProxyServerResponseMessage(connectionId, soapResponse); + final String signature = signingService.signContent(responseMessage.constructString()); + responseMessage.setSignature(signature); + return responseMessage; + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java new file mode 100644 index 0000000..a6590da --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.endpoints; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.*; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.Rdn; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.*; +import org.gxf.soapbridge.application.services.ConnectionCacheService; +import org.gxf.soapbridge.application.services.SigningService; +import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.kafka.senders.ProxyRequestKafkaSender; +import org.gxf.soapbridge.soap.clients.Connection; +import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; +import org.gxf.soapbridge.soap.exceptions.ProxyServerException; +import org.gxf.soapbridge.soap.valueobjects.ClientCertificate; +import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; +import org.springframework.stereotype.Component; +import org.springframework.web.HttpRequestHandler; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +/** + * This {@link @Component} class is the endpoint for incoming SOAP requests from client + * applications. + */ +@Component +public class SoapEndpoint implements HttpRequestHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(SoapEndpoint.class); + + private static final String SOAP_HEADER_KEY_APPLICATION_NAME = "ApplicationName"; + private static final String SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION = + "OrganisationIdentification"; + private static final String SOAP_HEADER_KEY_USER_NAME = "UserName"; + private static final List SOAP_HEADER_KEYS = + List.of( + SOAP_HEADER_KEY_APPLICATION_NAME, + SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION, + SOAP_HEADER_KEY_USER_NAME); + + private static final String URL_PROXY_SERVER = "/proxy-server"; + + private static final int INVALID_CUSTOM_TIME_OUT = -1; + public static final String X_509_CERTIFICATE_REQUEST_ATTRIBUTE = + "jakarta.servlet.request.X509Certificate"; + + /** Service used to cache incoming connections from client applications. */ + @Autowired private ConnectionCacheService connectionCacheService; + + @Autowired private SoapConfigurationProperties soapConfiguration; + + /** Message sender which can send a webapp request message to ActiveMQ. */ + @Autowired private ProxyRequestKafkaSender proxyRequestsSender; + + /** Service used to sign the content of a message. */ + @Autowired private SigningService signingService; + + /** Map of time outs for specific functions. */ + private final Map customTimeOutsMap = new HashMap<>(); + + @PostConstruct + public void init() { + final String[] split = soapConfiguration.getCustomTimeouts().split(","); + for (int i = 0; i < split.length; i += 2) { + final String key = split[i]; + final Integer value = Integer.valueOf(split[i + 1]); + LOGGER.debug("Adding custom time out with key: {} and value: {}", key, value); + customTimeOutsMap.put(key, value); + } + LOGGER.debug("Added {} custom time outs to the map", customTimeOutsMap.size()); + } + + /** Handles incoming SOAP requests. */ + @Override + public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { + + // For debugging, print all headers and parameters. + LOGGER.debug("Start of SoapEndpoint.handleRequest()"); + printHeaderValues(request); + printParameterValues(request); + + // Get the context, which should be an OSGP SOAP end-point or a + // NOTIFICATION SOAP end-point. + final String context = getContextForRequestType(request); + LOGGER.debug("Context: {}", context); + + // Try to read the SOAP request. + final String soapPayload = readSoapPayload(request); + if (soapPayload == null) { + LOGGER.error("Unable to read SOAP request, returning 500."); + createErrorResponse(response); + return; + } + + String organisationName = null; + if (request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME) + instanceof final SecurityContext securityContext) { + if (securityContext.getAuthentication().getPrincipal() instanceof final User organisation) { + organisationName = organisation.getUsername(); + } + } + if (organisationName == null) { + LOGGER.error("Unable to find client certificate, returning 500."); + createErrorResponse(response); + return; + } + + // Cache the incoming connection. + final Connection newConnection = connectionCacheService.cacheConnection(); + final String connectionId = newConnection.getConnectionId(); + + // Create a queue message and sign it. + final ProxyServerRequestMessage requestMessage = + new ProxyServerRequestMessage(connectionId, organisationName, context, soapPayload); + try { + final String signature = signingService.signContent(requestMessage.constructString()); + requestMessage.setSignature(signature); + } catch (final ProxyServerException e) { + LOGGER.error("Unable to sign message or set security key", e); + createErrorResponse(response); + connectionCacheService.removeConnection(connectionId); + return; + } + + final Integer customTimeOut = shouldUseCustomTimeOut(soapPayload); + final int timeOut; + if (customTimeOut == INVALID_CUSTOM_TIME_OUT) { + timeOut = soapConfiguration.getTimeOut(); + LOGGER.debug("Using default time out: {} seconds", timeOut); + } else { + LOGGER.debug("Using custom time out: {} seconds", customTimeOut); + timeOut = customTimeOut; + } + + try { + proxyRequestsSender.send(requestMessage); + + final boolean responseReceived = newConnection.waitForResponseReceived(timeOut); + if (!responseReceived) { + LOGGER.info("No response received within the specified time out of {} seconds", timeOut); + createErrorResponse(response); + connectionCacheService.removeConnection(connectionId); + return; + } + } catch (final Exception e) { + LOGGER.info("Error while waiting for response", e); + createErrorResponse(response); + connectionCacheService.removeConnection(connectionId); + return; + } + + final String soap = readResponse(connectionId); + if (soap == null) { + LOGGER.error("Unable to read SOAP response: null"); + createErrorResponse(response); + } else { + LOGGER.debug("Request handled, trying to send response..."); + createSuccessFulResponse(response, soap); + } + + LOGGER.debug( + "End of SoapEndpoint.handleRequest() --> incoming request handled and response returned."); + } + + public String[] sander() { + return new String[] {"a", "poipoi", "frats"}; + } + + private void printHeaderValues(final HttpServletRequest request) { + if (LOGGER.isDebugEnabled()) { + for (final Enumeration headerNames = request.getHeaderNames(); + headerNames.hasMoreElements(); ) { + final String headerName = headerNames.nextElement(); + final String headerValue = request.getHeader(headerName); + LOGGER.debug(" header name: {} header value: {}", headerName, headerValue); + } + } + } + + private void printParameterValues(final HttpServletRequest request) { + if (LOGGER.isDebugEnabled()) { + for (final Enumeration parameterNames = request.getParameterNames(); + parameterNames.hasMoreElements(); ) { + final String parameterName = parameterNames.nextElement(); + final String[] parameterValues = request.getParameterValues(parameterName); + String str = ""; + for (final String parameterValue : parameterValues) { + str = str.concat(parameterValue).concat(" "); + } + LOGGER.debug(" parameter name: {} parameter value: {}", parameterName, str); + } + } + } + + private String getContextForRequestType(final HttpServletRequest request) { + return request.getRequestURI().replace(URL_PROXY_SERVER, ""); + } + + private String readSoapPayload(final HttpServletRequest request) { + final StringBuilder stringBuilder = new StringBuilder(); + String line; + String soapPayload = null; + try { + final BufferedReader reader = request.getReader(); + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + soapPayload = stringBuilder.toString(); + LOGGER.debug(" payload: {}", soapPayload); + } catch (final Exception e) { + LOGGER.error("Unexpected error while reading request body", e); + } + return soapPayload; + } + + private Map getSoapHeaderValues( + final String soapPayload, final List soapHeaderKeys) { + final Map values = new HashMap<>(); + try { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + factory.setNamespaceAware(false); + final InputStream inputStream = + new ByteArrayInputStream(soapPayload.getBytes(StandardCharsets.UTF_8)); + // Try to find the desired XML elements in the document. + final Document document = factory.newDocumentBuilder().parse(inputStream); + for (final String soapHeaderKey : soapHeaderKeys) { + final String value = evaluateXPathExpression(document, soapHeaderKey); + values.put(soapHeaderKey, value); + } + inputStream.close(); + } catch (final Exception e) { + LOGGER.error("Exception", e); + } + return values; + } + + /** + * Search an XML element using an XPath expression. + * + * @param document The XML document. + * @param element The name of the desired XML element. + * @return The content of the XML element, or null if the element is not found. + * @throws XPathExpressionException In case the expression fails to compile or evaluate, an + * exception will be thrown. + */ + private String evaluateXPathExpression(final Document document, final String element) + throws XPathExpressionException { + final String expression = String.format("//*[contains(local-name(), '%s')]", element); + + final XPathFactory xFactory = XPathFactory.newInstance(); + final XPath xPath = xFactory.newXPath(); + + final XPathExpression xPathExpression = xPath.compile(expression); + final NodeList nodeList = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET); + + if (nodeList != null && nodeList.getLength() > 0) { + return nodeList.item(0).getTextContent(); + } else { + return null; + } + } + + private ClientCertificate tryToFindClientCertificate( + final HttpServletRequest request, final String soapPayload) { + final X509Certificate[] x509Certificates = getX509CertificatesFromServlet(request); + ClientCertificate clientCertificate = null; + + if (x509Certificates.length == 0) { + LOGGER.error(" HTTPServletRequest's attribute was an empty array of X509Certificates."); + } else if (x509Certificates.length == 1) { + LOGGER.debug(" HTTPServletRequest's attribute was array of X509Certificates of length 1."); + + // Get the client certificate. + clientCertificate = getClientCertificate(x509Certificates[0]); + } else { + LOGGER.debug( + " HTTPServletRequest's attribute was array of X509Certificates of length {}.", + x509Certificates.length); + + final Map soapHeaderValues = + getSoapHeaderValues(soapPayload, SOAP_HEADER_KEYS); + LOGGER.debug( + " SOAP Header ApplicationName: {}", + soapHeaderValues.get(SOAP_HEADER_KEY_APPLICATION_NAME)); + LOGGER.debug( + " SOAP Header OrganisationIdentification: {}", + soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); + LOGGER.debug(" SOAP Header UserName: {}", soapHeaderValues.get(SOAP_HEADER_KEY_USER_NAME)); + + // Try to extract the client certificate for the organization + // identification. + clientCertificate = + getClientCertificateByOrganisationIdentification( + x509Certificates, soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); + } + + return clientCertificate; + } + + private X509Certificate[] getX509CertificatesFromServlet(final HttpServletRequest request) { + final Object x509CertificateAttribute = + request.getAttribute(X_509_CERTIFICATE_REQUEST_ATTRIBUTE); + LOGGER.debug(X_509_CERTIFICATE_REQUEST_ATTRIBUTE + ": {}", x509CertificateAttribute); + if (x509CertificateAttribute instanceof X509Certificate[]) { + LOGGER.debug(" x509CertificateAttribute instanceof X509Certificate[]"); + return (X509Certificate[]) x509CertificateAttribute; + } else { + return new X509Certificate[0]; + } + } + + private ClientCertificate getClientCertificateByOrganisationIdentification( + final X509Certificate[] array, final String organisationCommonName) { + + for (final X509Certificate x509Certificate : array) { + final Principal principal = x509Certificate.getSubjectDN(); + LOGGER.debug(" principal: {}", principal); + final String subjectDn = principal.getName(); + LOGGER.debug(" subjectDn: {}", subjectDn); + try { + final String commonName = getCommonName(subjectDn); + if (commonName.equals(organisationCommonName)) { + LOGGER.debug( + "Found client certificate for right organisation {}", organisationCommonName); + return new ClientCertificate(x509Certificate, commonName); + } + } catch (final NamingException e) { + LOGGER.info("Failed to extract CommonName from this ClientCertificate", e); + } + } + return null; + } + + private ClientCertificate getClientCertificate(final X509Certificate x509Certificate) { + ClientCertificate clientCertificate = null; + + final Principal principal = x509Certificate.getSubjectDN(); + LOGGER.debug(" principal: {}", principal); + final String subjectDn = principal.getName(); + LOGGER.debug(" subjectDn: {}", subjectDn); + try { + final String commonName = getCommonName(subjectDn); + clientCertificate = new ClientCertificate(x509Certificate, commonName); + } catch (final NamingException e) { + LOGGER.info("Failed to extract CommonName from ClientCertificate", e); + } + + return clientCertificate; + } + + private String getCommonName(final String subjectDn) throws NamingException { + final Rdn rdn = new Rdn(subjectDn); + LOGGER.debug(" rdn: {}", rdn); + final Attributes attributes = rdn.toAttributes(); + LOGGER.debug(" attributes: {}", attributes); + final Attribute attribute = attributes.get("cn"); + LOGGER.debug(" attribute: {}", attribute); + final String commonName = (String) attribute.get(); + LOGGER.debug(" common name: {}", commonName); + return commonName; + } + + private Integer shouldUseCustomTimeOut(final String soapPayload) { + final Set keys = customTimeOutsMap.keySet(); + for (final String key : keys) { + if (soapPayload.contains(key)) { + return customTimeOutsMap.get(key); + } + } + return INVALID_CUSTOM_TIME_OUT; + } + + private String readResponse(final String connectionId) throws ServletException { + final String soap; + try { + final Connection connection = connectionCacheService.findConnection(connectionId); + soap = connection.getSoapResponse(); + connectionCacheService.removeConnection(connectionId); + } catch (final ConnectionNotFoundInCacheException e) { + LOGGER.error("Unexpected error while trying to find a cached connection", e); + throw new ServletException("Unable to obtain response"); + } + return soap; + } + + private void createErrorResponse(final HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + private void createSuccessFulResponse(final HttpServletResponse response, final String soap) + throws IOException { + LOGGER.debug("Start - creating successful response"); + response.setStatus(HttpServletResponse.SC_OK); + response.addHeader("SOAP-ACTION", ""); + response.addHeader("Keep-Alive", "timeout=5, max=100"); + response.addHeader("Accept", "text/xml"); + response.addHeader("Connection", "Keep-Alive"); + response.setContentType("text/xml; charset=" + StandardCharsets.UTF_8.name()); + response.getWriter().write(soap); + LOGGER.debug("End - creating successful response"); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java new file mode 100644 index 0000000..5c7d346 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.exceptions; + +public class ConnectionNotFoundInCacheException extends ProxyServerException { + + /** Serial Version UID. */ + private static final long serialVersionUID = -858760086093512799L; + + public ConnectionNotFoundInCacheException(final String message) { + super(message); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java new file mode 100644 index 0000000..2bb17a9 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.exceptions; + +/** Base type for exceptions for proxy server component. */ +public class ProxyServerException extends Exception { + + /** Serial Version UID. */ + private static final long serialVersionUID = -8696835428244659385L; + + public ProxyServerException() { + super(); + } + + public ProxyServerException(final String message) { + super(message); + } + + public ProxyServerException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java new file mode 100644 index 0000000..7fb3077 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.exceptions; + +public class UnableToCreateHttpsURLConnectionException extends ProxyServerException { + + /** Serial Version UID. */ + private static final long serialVersionUID = -8807766325167125880L; + + public UnableToCreateHttpsURLConnectionException(final String message) { + super(message); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java new file mode 100644 index 0000000..b50ff46 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.exceptions; + +public class UnableToCreateKeyManagersException extends ProxyServerException { + + /** Serial Version UID. */ + private static final long serialVersionUID = -100586751704652623L; + + public UnableToCreateKeyManagersException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java new file mode 100644 index 0000000..7695973 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.exceptions; + +public class UnableToCreateTrustManagersException extends ProxyServerException { + + /** Serial Version UID. */ + private static final long serialVersionUID = -855694158211466200L; + + public UnableToCreateTrustManagersException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java b/application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java new file mode 100644 index 0000000..5984f62 --- /dev/null +++ b/application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.valueobjects; + +import java.security.cert.X509Certificate; + +public class ClientCertificate { + + private final X509Certificate x509Certificate; + + private final String commonName; + + public ClientCertificate(final X509Certificate x509Certificate, final String commonName) { + this.x509Certificate = x509Certificate; + this.commonName = commonName; + } + + public X509Certificate getX509Certificate() { + return x509Certificate; + } + + public String getCommonName() { + return commonName; + } +} diff --git a/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt b/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt index 2706b03..93685e6 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/SoapBridgeApplication.kt @@ -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 diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt index 03a4c53..b03f77d 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt @@ -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.configuration diff --git a/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt similarity index 93% rename from components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt rename to application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt index fd7ce79..5b9c1ef 100644 --- a/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SecurityConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt @@ -1,6 +1,8 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.application.properties +package org.gxf.soapbridge.configuration.properties import mu.KotlinLogging import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt similarity index 83% rename from components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt rename to application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt index ba81758..dd062d5 100644 --- a/components/soap/src/main/kotlin/org/gxf/soapbridge/application/properties/SoapConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt @@ -1,6 +1,8 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.application.properties +package org.gxf.soapbridge.configuration.properties import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt b/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt similarity index 68% rename from components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt rename to application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt index 71f7471..07c9e64 100644 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/exceptions/ProxyMessageException.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt @@ -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.messaging.exceptions diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt similarity index 69% rename from components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt rename to application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt index f32d77e..d0975dd 100644 --- a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt @@ -1,12 +1,14 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.kafka.listeners import io.micrometer.observation.annotation.Observed import mu.KotlinLogging import org.apache.kafka.clients.consumer.ConsumerRecord -import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage -import org.gxf.soapbridge.services.ProxyRequestHandler +import org.gxf.soapbridge.application.services.PlatformCommunicationService +import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage import org.springframework.kafka.annotation.KafkaListener import org.springframework.kafka.annotation.RetryableTopic import org.springframework.retry.annotation.Backoff @@ -14,7 +16,7 @@ import org.springframework.stereotype.Component import java.net.SocketTimeoutException @Component -class ProxyRequestKafkaListener(private val proxyRequestHandler: ProxyRequestHandler) { +class ProxyRequestKafkaListener(private val platformCommunicationService: PlatformCommunicationService) { private val logger = KotlinLogging.logger { } @Observed(name = "requests.consumed") @@ -27,6 +29,6 @@ class ProxyRequestKafkaListener(private val proxyRequestHandler: ProxyRequestHan fun consume(record: ConsumerRecord) { logger.info("Received message") val requestMessage = ProxyServerRequestMessage.createInstanceFromString(record.value()) - proxyRequestHandler.handleIncomingRequest(requestMessage) + platformCommunicationService.handleIncomingRequest(requestMessage) } } diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt similarity index 71% rename from components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt rename to application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt index 115a396..89062b2 100644 --- a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt @@ -1,12 +1,14 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.kafka.listeners import io.micrometer.observation.annotation.Observed import mu.KotlinLogging import org.apache.kafka.clients.consumer.ConsumerRecord -import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage -import org.gxf.soapbridge.services.ProxyResponseHandler +import org.gxf.soapbridge.application.services.ClientCommunicationService +import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage import org.springframework.kafka.annotation.KafkaListener import org.springframework.kafka.annotation.RetryableTopic import org.springframework.retry.annotation.Backoff @@ -15,7 +17,7 @@ import java.net.SocketTimeoutException @Component class ProxyResponseKafkaListener( - private val proxyResponseHandler: ProxyResponseHandler + private val clientCommunicationService: ClientCommunicationService ) { private val logger = KotlinLogging.logger { } @@ -29,6 +31,6 @@ class ProxyResponseKafkaListener( fun consume(record: ConsumerRecord) { logger.info("Received response") val responseMessage = ProxyServerResponseMessage.createInstanceFromString(record.value()) - proxyResponseHandler.handleIncomingResponse(responseMessage) + clientCommunicationService.handleIncomingResponse(responseMessage) } } diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt similarity index 76% rename from components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt rename to application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt index 6d3eb80..2eb613f 100644 --- a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt @@ -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.kafka.properties diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt similarity index 71% rename from components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt rename to application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt index cbe45f4..36d3e2c 100644 --- a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt @@ -1,11 +1,12 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.kafka.senders import mu.KotlinLogging import org.gxf.soapbridge.kafka.properties.TopicsConfigurationProperties -import org.gxf.soapbridge.messaging.ProxyRequestsMessageSender -import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage +import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component @@ -13,12 +14,12 @@ import org.springframework.stereotype.Component class ProxyRequestKafkaSender( private val kafkaTemplate: KafkaTemplate, topicConfiguration: TopicsConfigurationProperties -) : ProxyRequestsMessageSender { +) { private val logger = KotlinLogging.logger {} private val topic = topicConfiguration.outgoing.requests - override fun send(requestMessage: ProxyServerRequestMessage) { + fun send(requestMessage: ProxyServerRequestMessage) { logger.debug("SOAP payload: ${requestMessage.soapPayload} to $topic") kafkaTemplate.send(topic, requestMessage.constructSignedString()) } diff --git a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt similarity index 71% rename from components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt rename to application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt index 1304501..3a1e978 100644 --- a/components/kafka/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt @@ -1,11 +1,12 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.kafka.senders import mu.KotlinLogging import org.gxf.soapbridge.kafka.properties.TopicsConfigurationProperties -import org.gxf.soapbridge.messaging.ProxyResponsesMessageSender -import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage +import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage import org.springframework.kafka.core.KafkaTemplate import org.springframework.stereotype.Component @@ -13,12 +14,12 @@ import org.springframework.stereotype.Component class ProxyResponseKafkaSender( private val kafkaTemplate: KafkaTemplate, topicConfiguration: TopicsConfigurationProperties -) : ProxyResponsesMessageSender { +) { private val logger = KotlinLogging.logger {} private val topic = topicConfiguration.outgoing.responses - override fun send(responseMessage: ProxyServerResponseMessage) { + fun send(responseMessage: ProxyServerResponseMessage) { logger.debug("SOAP payload: ${responseMessage.soapResponse} to $topic") kafkaTemplate.send(topic, responseMessage.constructSignedString()) } diff --git a/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt index 67806a2..89d00f9 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt @@ -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.observability diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt similarity index 84% rename from components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt rename to application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt index 1a9a832..4ca6752 100644 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerBaseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt @@ -1,6 +1,8 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.messaging.messages +package org.gxf.soapbridge.valueobjects import java.util.* diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt similarity index 95% rename from components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt rename to application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt index 4aa1c9a..a222361 100644 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerRequestMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt @@ -1,6 +1,8 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.messaging.messages +package org.gxf.soapbridge.valueobjects import mu.KotlinLogging import org.gxf.soapbridge.messaging.exceptions.ProxyMessageException diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt similarity index 92% rename from components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt rename to application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt index 234444a..e57f04b 100644 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/messages/ProxyServerResponseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt @@ -1,6 +1,8 @@ -// Copyright 2023 Alliander N.V. +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.messaging.messages +package org.gxf.soapbridge.valueobjects import mu.KotlinLogging import org.gxf.soapbridge.messaging.exceptions.ProxyMessageException diff --git a/application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java b/application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java new file mode 100644 index 0000000..5faa1a5 --- /dev/null +++ b/application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.application.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class RandomStringServiceTests { + + @Test + void test1() { + final String randomString = RandomStringFactory.generateRandomString(); + + Assertions.assertNotNull( + randomString, "It is expected that the generated random string not is null"); + Assertions.assertEquals( + 16, + randomString.length(), + "It is expected that the generated random string is of length 16"); + } + + @Test + void test2() { + final String randomString = RandomStringFactory.generateRandomString(42); + + Assertions.assertNotNull( + randomString, "It is expected that the generated random string not is null"); + Assertions.assertEquals( + 42, + randomString.length(), + "It is expected that the generated random string is of length 42"); + } +} diff --git a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java new file mode 100644 index 0000000..6e1c605 --- /dev/null +++ b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: Copyright Contributors to the GXF project +// +// SPDX-License-Identifier: Apache-2.0 +package org.gxf.soapbridge.soap.clients; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.HttpsURLConnection; +import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; +import org.gxf.soapbridge.application.services.SigningService; +import org.gxf.soapbridge.configuration.properties.HostnameVerificationStrategy; +import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties; +import org.gxf.soapbridge.configuration.properties.SoapEndpointConfiguration; +import org.gxf.soapbridge.kafka.senders.ProxyResponseKafkaSender; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SoapClientTest { + + @Mock ProxyResponseKafkaSender proxyResponseKafkaSender; + @Mock HttpsUrlConnectionFactory httpsUrlConnectionFactory; + @Mock SigningService signingService; + + byte[] testContent = "test content".getBytes(StandardCharsets.UTF_8); + + @Spy + SoapConfigurationProperties soapConfigurationProperties = + new SoapConfigurationProperties( + HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES, + 45, + "", + new SoapEndpointConfiguration("localhost", 443, "https")); + + @InjectMocks SoapClient soapClient; + + @Test + void shouldSendSoapRequestAndJmsResponse() throws Exception { + // arrange + final HttpsURLConnection connection = setupConnectionMock(); + Mockito.when( + httpsUrlConnectionFactory.createConnection( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString())) + .thenReturn(connection); + + // act + soapClient.sendRequest("connectionId", "context", "commonName", "payload"); + + // assert + Mockito.verify(connection).disconnect(); + } + + @Test + void shoudDisconnectWhenSoapRequestFails() throws Exception { + // arrange + final HttpsURLConnection connection = setupFailingConnectionMock(); + Mockito.when( + httpsUrlConnectionFactory.createConnection( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString())) + .thenReturn(connection); + + // act + soapClient.sendRequest("connectionId", "context", "commonName", "payload"); + + // assert + Mockito.verify(connection).disconnect(); + Mockito.verifyNoInteractions(proxyResponseKafkaSender); + } + + private HttpsURLConnection setupConnectionMock() throws Exception { + final HttpsURLConnection connection = Mockito.mock(HttpsURLConnection.class); + final InputStream inputStream = new ByteArrayInputStream(testContent); + Mockito.when(connection.getOutputStream()).thenReturn(Mockito.mock(OutputStream.class)); + Mockito.when(connection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + Mockito.when(connection.getInputStream()).thenReturn(inputStream); + return connection; + } + + private HttpsURLConnection setupFailingConnectionMock() throws Exception { + final HttpsURLConnection connection = Mockito.mock(HttpsURLConnection.class); + Mockito.when(connection.getOutputStream()).thenThrow(ConnectException.class); + return connection; + } +} diff --git a/components/core/build.gradle.kts b/components/core/build.gradle.kts deleted file mode 100644 index ce4a92e..0000000 --- a/components/core/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Alliander N.V. - -dependencies { - implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") - - testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation("org.junit.jupiter:junit-jupiter-engine") - testImplementation("org.junit.jupiter:junit-jupiter-params") -} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt deleted file mode 100644 index f1ad634..0000000 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyRequestsMessageSender.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Alliander N.V. - -package org.gxf.soapbridge.messaging - -import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage - -interface ProxyRequestsMessageSender { - fun send(requestMessage: ProxyServerRequestMessage) -} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt deleted file mode 100644 index cdc04e7..0000000 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/messaging/ProxyResponsesMessageSender.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Alliander N.V. - -package org.gxf.soapbridge.messaging - -import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage - -interface ProxyResponsesMessageSender { - fun send(responseMessage: ProxyServerResponseMessage) -} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt deleted file mode 100644 index d22f5f7..0000000 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyRequestHandler.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Alliander N.V. - -package org.gxf.soapbridge.services - -import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage - -interface ProxyRequestHandler { - fun handleIncomingRequest(proxyServerRequestMessage: ProxyServerRequestMessage) -} diff --git a/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt b/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt deleted file mode 100644 index f14b0cf..0000000 --- a/components/core/src/main/kotlin/org/gxf/soapbridge/services/ProxyResponseHandler.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2023 Alliander N.V. - -package org.gxf.soapbridge.services - -import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage - -interface ProxyResponseHandler { - fun handleIncomingResponse(proxyServerResponseMessage: ProxyServerResponseMessage) -} diff --git a/components/kafka/build.gradle.kts b/components/kafka/build.gradle.kts deleted file mode 100644 index 485cce7..0000000 --- a/components/kafka/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2023 Alliander N.V. - -dependencies { - implementation("org.springframework.boot:spring-boot-autoconfigure") - implementation("org.springframework.boot:spring-boot-starter-logging") - implementation("org.springframework:spring-aop") - implementation("org.springframework.kafka:spring-kafka") - implementation("com.microsoft.azure:msal4j:1.13.10") - implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") - implementation(project(":components:core")) - - testImplementation("org.springframework:spring-test") - - testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation("org.junit.jupiter:junit-jupiter-engine") - testImplementation("org.junit.jupiter:junit-jupiter-params") -} diff --git a/components/soap/build.gradle.kts b/components/soap/build.gradle.kts deleted file mode 100644 index f52db11..0000000 --- a/components/soap/build.gradle.kts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 Alliander N.V. - -dependencies { - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("org.springframework.boot:spring-boot-starter-security") - implementation("org.springframework.boot:spring-boot-starter-logging") - implementation(kotlin("reflect")) - implementation("org.apache.httpcomponents:httpclient:4.5.13") - implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") - implementation(project(":components:core")) - - runtimeOnly("io.micrometer:micrometer-registry-prometheus") - annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") - - testImplementation("org.springframework:spring-test") - testImplementation("org.mockito:mockito-junit-jupiter") - testImplementation("org.junit.jupiter:junit-jupiter-api") - testImplementation("org.junit.jupiter:junit-jupiter-engine") - testImplementation("org.junit.jupiter:junit-jupiter-params") -} - -testing { - suites { - val integrationTest by registering(JvmTestSuite::class) { - useJUnitJupiter() - dependencies { - implementation(project()) - implementation("org.springframework.boot:spring-boot-starter-test") - implementation("org.springframework.kafka:spring-kafka-test") - implementation("org.testcontainers:kafka:1.17.6") - } - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java b/components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java deleted file mode 100644 index 6c3a65f..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023 Alliander N.V. - -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; - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java deleted file mode 100644 index 9182d33..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HostnameVerifierFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2023 Alliander N.V. - -package org.gxf.soapbridge.application.factories; - -import org.apache.http.conn.ssl.DefaultHostnameVerifier; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; -import org.gxf.soapbridge.soap.exceptions.ProxyServerException; -import org.springframework.stereotype.Component; - -import javax.net.ssl.HostnameVerifier; - -import static org.gxf.soapbridge.application.properties.HostnameVerificationStrategy.ALLOW_ALL_HOSTNAMES; -import static org.gxf.soapbridge.application.properties.HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES; - -@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!"); - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java deleted file mode 100644 index 04486ba..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.factories; - -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; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import java.io.IOException; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -/** - * 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()); - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java deleted file mode 100644 index 828754f..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.factories; - -import org.gxf.soapbridge.application.properties.SecurityConfigurationProperties; -import org.gxf.soapbridge.application.properties.StoreConfigurationProperties; -import org.gxf.soapbridge.application.services.SslContextCacheService; -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; - -import javax.net.ssl.*; -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.KeyStore; - -/** - * 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); - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java deleted file mode 100644 index 4816892..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.services; - -import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage; -import org.gxf.soapbridge.services.ProxyResponseHandler; -import org.gxf.soapbridge.soap.clients.Connection; -import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -/** - * Service which handles SOAP responses from OSGP. The SOAP response will be set for the connection - * which correlates with the connection-id. - */ -@Service -public class ClientCommunicationService implements ProxyResponseHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(ClientCommunicationService.class); - - /** - * Service used to cache incoming connections from client applications. - */ - @Autowired - private ConnectionCacheService connectionCacheService; - - /** - * Service used to sign and/or verify the content of queue messages. - */ - @Autowired - private SigningService signingService; - - /** - * Process an incoming queue message. The content of the message has to be verified by the - * {@link SigningService}. Then a response from OSGP will set for the pending connection from a - * client. - * - * @param proxyServerResponseMessage The incoming queue message to process. - */ - @Override - public void handleIncomingResponse(final ProxyServerResponseMessage proxyServerResponseMessage) { - final boolean isValid = signingService.verifyContent( - proxyServerResponseMessage.constructString(), proxyServerResponseMessage.getSignature()); - if (!isValid) { - LOGGER.error("ProxyServerResponseMessage failed to pass security check."); - return; - } - - try { - final Connection connection = - connectionCacheService.findConnection(proxyServerResponseMessage.getConnectionId()); - if (connection != null) { - if (isValid) { - LOGGER.debug("Connection valid, set SOAP response"); - connection.setResponse(proxyServerResponseMessage.getSoapResponse()); - } else { - connection.setResponse("Security check has failed."); - } - } else { - LOGGER.error("Cached connection is null"); - } - } catch (final ConnectionNotFoundInCacheException e) { - LOGGER.error("ConnectionNotFoundInCacheException", e); - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java deleted file mode 100644 index 0c699a5..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.services; - -import org.gxf.soapbridge.application.utils.RandomStringFactory; -import org.gxf.soapbridge.soap.clients.Connection; -import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -import java.util.concurrent.ConcurrentHashMap; - -/** - * This {@link @Service} class caches connections from client applications. - */ -@Service -public class ConnectionCacheService { - - private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionCacheService.class); - - /** - * Map used to cache connections. The key is a random string generated by - * {@link RandomStringFactory}. The value is a {@link Connection} instance. - */ - private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - - /** - * Creates a connection and puts it in the cache. - * - * @return the created Connection - */ - public Connection cacheConnection() { - final Connection connection = new Connection(); - final String connectionId = connection.getConnectionId(); - LOGGER.debug("Caching connection with connectionId: {}", connectionId); - cache.put(connectionId, connection); - return connection; - } - - /** - * Get a {@link Connection} instance from the {@link ConnectionCacheService#cache}. - * - * @param connectionId The key for the {@link Connection} instance obtained by calling - * {@link ConnectionCacheService#cacheConnection()}. - * @return A {@link Connection} instance. - * @throws ConnectionNotFoundInCacheException In case the connection is not present in the - * {@link ConnectionCacheService#cache}. - */ - public Connection findConnection(final String connectionId) - throws ConnectionNotFoundInCacheException { - LOGGER.debug("Trying to find connection with connectionId: {}", connectionId); - final Connection connection = cache.get(connectionId); - if (connection == null) { - throw new ConnectionNotFoundInCacheException( - String.format("Unable to find connection for connectionId: %s", connectionId)); - } - return connection; - } - - /** - * Determine if the response is available for a particular {@link Connection} instance. - * - * @param connectionId The key for the {@link Connection} instance obtained by calling - * {@link ConnectionCacheService#cacheConnection()}. - * @return True in case the response from the Platform has resolved and is available, false - * otherwise. - * @throws ConnectionNotFoundInCacheException In case the connection is not present in the - * {@link ConnectionCacheService#cache}. - */ - public boolean hasResponseResolved(final String connectionId) - throws ConnectionNotFoundInCacheException { - final Connection connection = cache.get(connectionId); - if (connection != null) { - LOGGER.debug( - "hasResponseResolved called with connectionId: {}, this.cache.size(): {}", - connectionId, - cache.size()); - return connection.isResponseResolved(); - } else { - throw new ConnectionNotFoundInCacheException( - String.format("Unable to find connection for connectionId: %s", connectionId)); - } - } - - /** - * Removes a {@link Connection} instance from the {@link ConnectionCacheService#cache}. - * - * @param connectionId The key for the {@link Connection} instance obtained by calling - * {@link ConnectionCacheService#cacheConnection()}. - */ - public void removeConnection(final String connectionId) { - LOGGER.debug("Removing connection with connectionId: {}", connectionId); - cache.remove(connectionId); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java deleted file mode 100644 index 60ae07d..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.services; - -import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage; -import org.gxf.soapbridge.services.ProxyRequestHandler; -import org.gxf.soapbridge.soap.clients.SoapClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -/** - * Service which can send SOAP requests to OSGP. - */ -@Service -public class PlatformCommunicationService implements ProxyRequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(PlatformCommunicationService.class); - - /** - * SOAP client used to sent request messages to OSGP. - */ - @Autowired - private SoapClient soapClient; - - /** - * Service used to sign and/or verify the content of queue messages. - */ - @Autowired - private SigningService signingService; - - /** - * Process an incoming queue message. The content of the message has to be verified by the - * {@link SigningService}. Then a SOAP message can be sent to OSGP using {@link SoapClient}. - * - * @param proxyServerRequestMessage The incoming queue message to process. - */ - @Override - public void handleIncomingRequest( - final ProxyServerRequestMessage proxyServerRequestMessage) { - - final String proxyServerRequestMessageAsString = proxyServerRequestMessage.constructString(); - final String securityKey = proxyServerRequestMessage.getSignature(); - - final boolean isValid = - signingService.verifyContent(proxyServerRequestMessageAsString, securityKey); - if (!isValid) { - LOGGER.error("ProxyServerRequestMessage failed to pass security check."); - return; - } - - final String connectionId = proxyServerRequestMessage.getConnectionId(); - final String context = proxyServerRequestMessage.getContext(); - final String commonName = proxyServerRequestMessage.getCommonName(); - final String soapPayload = proxyServerRequestMessage.getSoapPayload(); - - final boolean result = - soapClient.sendRequest( - connectionId, context, commonName, soapPayload); - if (!result) { - LOGGER.error("Unsuccessful at sending request to platform."); - } else { - LOGGER.debug("Successfully sent response message to queue"); - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java deleted file mode 100644 index ed2241a..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/services/SigningService.java +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.services; - -import org.gxf.soapbridge.application.properties.SecurityConfigurationProperties; -import org.gxf.soapbridge.application.properties.SigningConfigurationProperties; -import org.gxf.soapbridge.soap.exceptions.ProxyServerException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.Signature; -import java.util.HexFormat; - -/** - * This {@link @Service} class can generate a signature for a given content, and verify the content - * using the signature. - */ -@Service -public class SigningService { - private final SigningConfigurationProperties signingConfiguration; - - private static final Logger LOGGER = LoggerFactory.getLogger(SigningService.class); - - private SigningService(final SecurityConfigurationProperties securityConfiguration) { - signingConfiguration = securityConfiguration.getSigning(); - } - - /** - * Using the given content, create a signature. The signature is converted from byte array to - * hexadecimal string. - * - * @param content The content to sign. - * @return The signature which can be used later to verify if the content is intact and unaltered. - * @throws ProxyServerException thrown when an error occurs during signing. - */ - public String signContent(final String content) throws ProxyServerException { - final byte[] bytes = content.getBytes(StandardCharsets.UTF_8); - try { - final byte[] signature = createSignature(bytes); - LOGGER.debug("signature.length: {}", signature.length); - return HexFormat.of().formatHex(signature); - } catch (final GeneralSecurityException e) { - throw new ProxyServerException( - "Unexpected GeneralSecurityException when trying to sign the content", e); - } - } - - /** - * For the given content and security key, verify if the content is intact and unaltered. - * - * @param content The content to verify. - * @param securityKey The signature a.k.a. security key which was created using - * {@link SigningService#signContent(String)}. - * @return True when the verification succeeds. - */ - public boolean verifyContent(final String content, final String securityKey) { - final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8); - final byte[] securityKeyBytes = HexFormat.of().parseHex(securityKey); - LOGGER.debug("securityKeyBytes.length: {}", securityKeyBytes.length); - try { - return validateSignature(contentBytes, securityKeyBytes); - } catch (final GeneralSecurityException e) { - LOGGER.error( - "Unexpected GeneralSecurityException when trying to verify the content using the security key", - e); - return false; - } - } - - private byte[] createSignature(final byte[] message) throws GeneralSecurityException { - final Signature signatureBuilder = Signature.getInstance(signingConfiguration.getSignature(), signingConfiguration.getProvider()); - signatureBuilder.initSign(signingConfiguration.getSignKey()); - signatureBuilder.update(message); - return signatureBuilder.sign(); - } - - private boolean validateSignature(final byte[] message, final byte[] securityKey) - throws GeneralSecurityException { - final Signature signatureBuilder = Signature.getInstance(signingConfiguration.getSignature(), signingConfiguration.getProvider()); - signatureBuilder.initVerify(signingConfiguration.getVerifyKey()); - signatureBuilder.update(message); - return signatureBuilder.verify(securityKey); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java b/components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java deleted file mode 100644 index 8f8a5a3..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.services; - -import org.gxf.soapbridge.application.factories.SslContextFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import java.util.concurrent.ConcurrentHashMap; - -/** - * This {@link @Service} class encapsulates the creation of a suitable {@link SSLContext} instance - * to use with a HTTPS connection, like {@link HttpsURLConnection} for example. Further, the created - * instance is cached using a {@link ConcurrentHashMap} in order to maintain performance even when - * many calls are handled simultaneously. The actual creation of {@link SSLContext} instances is - * delegated to {@link SslContextFactory} class. - */ -@Service -public class SslContextCacheService { - - private static final Logger LOGGER = LoggerFactory.getLogger(SslContextCacheService.class); - - /** - * Map used to cache {@link SSLContext} instances. The key is the common name for an organization, - * which is equal to the organization identification. - */ - private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - - /** - * Factory which assists in creating {@link SSLContext} instances. - */ - @Autowired - private SslContextFactory sslContextFactory; - - /** - * Creates a new {@link SSLContext} instance and caches it, or fetches an existing instance from - * the cache. - * - * @return A {@link SSLContext} instance. - */ - public SSLContext getSslContext() { - final String key = "SSLContextWithoutCommonName"; - if (cache.containsKey(key)) { - LOGGER.debug("Returning SSL Context from cache for key: {}", key); - return cache.get(key); - } else { - LOGGER.debug( - "Creating new SSL Context and putting the instance in the cache for key: {}", key); - final SSLContext sslContext = sslContextFactory.createSslContext(); - cache.put(key, sslContext); - return sslContext; - } - } - - /** - * Creates a new {@link SSLContext} instance for the given common name and caches it, or fetches - * an existing instance from the cache. - * - * @param commonName The common name which is used to look up a Personal Information Exchange - * (*.pfx) file that is used as key store. - * @return A {@link SSLContext} instance. - */ - public SSLContext getSslContextForCommonName(final String commonName) { - if (cache.containsKey(commonName)) { - LOGGER.debug("Returning SSL Context from cache for common name: {}", commonName); - return cache.get(commonName); - } else { - LOGGER.debug( - "Creating new SSL Context and putting the instance in the cache for common name: {}", - commonName); - final SSLContext sslContext = sslContextFactory.createSslContext(commonName); - cache.put(commonName, sslContext); - return sslContext; - } - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java b/components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java deleted file mode 100644 index e5a3e7a..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.utils; - -import java.security.SecureRandom; - -/** - * This class can generate random string values. - */ -public class RandomStringFactory { - - /** - * The random used to generate random strings. - */ - private static final SecureRandom random = new SecureRandom(); - - /** - * Default length of the generated random strings. - */ - private static final int DEFAULT_LENGTH = 16; - - private RandomStringFactory() { - // Only static. - } - - /** - * Generate a random string. - * - * @return A string of length {@link RandomStringFactory#defaultLength} - */ - public static String generateRandomString() { - return randomString(DEFAULT_LENGTH); - } - - /** - * Generate a random string. - * - * @param length The length of the random string to generate. - * @return A string of the given length. - */ - public static String generateRandomString(final int length) { - return randomString(length); - } - - private static String randomString(final int length) { - final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - final StringBuilder buf = new StringBuilder(); - for (int i = 0; i < length; i++) { - buf.append(chars.charAt(random.nextInt(chars.length()))); - } - return buf.toString(); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java deleted file mode 100644 index 4780037..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.clients; - -import org.gxf.soapbridge.application.services.ConnectionCacheService; -import org.gxf.soapbridge.application.utils.RandomStringFactory; - -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -public class Connection { - - /** - * Default length for the connection id, which is the key for the - * {@link ConnectionCacheService#cache}. - */ - private static final int CONNECTION_ID_LENGTH = 32; - - private volatile boolean responseResolved; - - private String soapResponse; - - private String connectionId; - - private final Semaphore responseReceived; - - public Connection() { - responseResolved = false; - responseReceived = new Semaphore(0); - connectionId = RandomStringFactory.generateRandomString(CONNECTION_ID_LENGTH); - } - - public boolean isResponseResolved() { - return responseResolved; - } - - public void setResponse(final String soapResponse) { - responseResolved = true; - this.soapResponse = soapResponse; - responseReceived(); - } - - public String getSoapResponse() { - return soapResponse; - } - - public void setConnectionId(final String connectionId) { - this.connectionId = connectionId; - } - - public String getConnectionId() { - return connectionId; - } - - /* - * Indicates the response for this connection has been received. - */ - public void responseReceived() { - responseReceived.release(); - } - - /* - * Waits for a response on this connection. - * - * @timeout The number of seconds to wait for a response. - * - * @returns true, if the response was received within @timeout seconds, - * false otherwise. - */ - public boolean waitForResponseReceived(final int timeout) throws InterruptedException { - return responseReceived.tryAcquire(timeout, TimeUnit.SECONDS); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java deleted file mode 100644 index 7f92f83..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.clients; - -import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; -import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; -import org.gxf.soapbridge.application.properties.SoapEndpointConfiguration; -import org.gxf.soapbridge.application.services.SigningService; -import org.gxf.soapbridge.messaging.ProxyResponsesMessageSender; -import org.gxf.soapbridge.messaging.messages.ProxyServerResponseMessage; -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 javax.net.ssl.HttpsURLConnection; -import java.io.*; -import java.net.HttpURLConnection; -import java.nio.charset.StandardCharsets; - -/** - * This {@link @Component} class can send SOAP messages to the Platform. - */ -@Component -public class SoapClient { - - private static final Logger LOGGER = LoggerFactory.getLogger(SoapClient.class); - - /** - * Message sender to send messages to a queue. - */ - @Autowired - private ProxyResponsesMessageSender callResponsesMessageSender; - - @Autowired - private SoapConfigurationProperties soapConfiguration; - - /** - * Factory which assist in creating {@link HttpsURLConnection} instances. - */ - @Autowired - private HttpsUrlConnectionFactory httpsUrlConnectionFactory; - - /** - * Service used to sign the content of a message. - */ - @Autowired - private SigningService signingService; - - /** - * Send a request to the Platform. - * - * @param connectionId The connectionId for this connection. - * @param context The part of the URL indicating the SOAP web-service. - * @param commonName The common name (organisation identification). - * @param soapPayload The SOAP message to send to the platform. - * @return True if the request has been sent and the response has been received and written to a - * queue, false otherwise. - */ - public boolean sendRequest( - final String connectionId, - final String context, - final String commonName, - final String soapPayload) { - - HttpsURLConnection connection = null; - - try { - // Try to create a connection. - connection = createConnection(context, soapPayload, commonName); - if (connection == null) { - LOGGER.warn("Could not create connection for sending SOAP request."); - return false; - } - - // Send the SOAP payload to the server. - sendRequest(connection, soapPayload); - // Read the response. - LOGGER.debug("SOAP request sent, trying to read SOAP response..."); - final String soapResponse = readResponse(connection); - LOGGER.debug("SOAP response: {}", soapResponse); - // Always disconnect the connection. - connection.disconnect(); - - // Create proxy-server response message. - final ProxyServerResponseMessage responseMessage = - createProxyServerResponseMessage(connectionId, soapResponse); - - // Send queue message. - callResponsesMessageSender.send(responseMessage); - - return true; - } catch (final Exception e) { - if (connection != null) { - connection.disconnect(); - } - LOGGER.error("Unexpected exception while sending SOAP request", e); - return false; - } - } - - private HttpsURLConnection createConnection( - final String context, - final String soapPayload, - final String commonName) - throws UnableToCreateHttpsURLConnectionException { - final String contentLength = String.format("%d", soapPayload.length()); - - final SoapEndpointConfiguration callEndpoint = soapConfiguration.getCallEndpoint(); - - final String uri = callEndpoint.getUri().concat(context); - LOGGER.info("Preparing to open connection for WEBAPP_REQUEST using URI: {}", uri); - return httpsUrlConnectionFactory.createConnection( - uri, callEndpoint.getHostAndPort(), contentLength, commonName); - } - - private void sendRequest(final HttpsURLConnection connection, final String soapPayLoad) - throws IOException { - try (final OutputStream outputStream = connection.getOutputStream(); - final OutputStreamWriter outputStreamWriter = - new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { - outputStreamWriter.write(soapPayLoad); - outputStreamWriter.flush(); - } catch (final IOException e) { - LOGGER.debug("Rethrow IOException while sending SOAP request."); - throw e; - } - } - - private String readResponse(final HttpsURLConnection connection) throws IOException { - // Use a BufferedReader and an InputStreamReader configured with UTF-8 - // character encoding. This will ensure that the response from the - // Platform is read correctly. - try (final InputStream inputStream = getInputStream(connection); - final InputStreamReader inputStreamReader = - new InputStreamReader(inputStream, StandardCharsets.UTF_8); - final BufferedReader reader = new BufferedReader(inputStreamReader)) { - final StringBuilder response = new StringBuilder(); - String line = reader.readLine(); - while (line != null) { - response.append(line); - line = reader.readLine(); - } - return response.toString(); - } catch (final IOException e) { - LOGGER.debug("Rethrow IOException while reading SOAP response"); - throw e; - } - } - - private InputStream getInputStream(final HttpsURLConnection connection) throws IOException { - final InputStream inputStream; - - if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { - inputStream = connection.getInputStream(); - } else { - inputStream = connection.getErrorStream(); - } - - return inputStream; - } - - private ProxyServerResponseMessage createProxyServerResponseMessage( - final String connectionId, final String soapResponse) throws ProxyServerException { - final ProxyServerResponseMessage responseMessage = - new ProxyServerResponseMessage(connectionId, soapResponse); - final String signature = signingService.signContent(responseMessage.constructString()); - responseMessage.setSignature(signature); - return responseMessage; - } - -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java deleted file mode 100644 index 7638797..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ /dev/null @@ -1,440 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.endpoints; - -import jakarta.annotation.PostConstruct; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; -import org.gxf.soapbridge.application.services.ConnectionCacheService; -import org.gxf.soapbridge.application.services.SigningService; -import org.gxf.soapbridge.messaging.ProxyRequestsMessageSender; -import org.gxf.soapbridge.messaging.messages.ProxyServerRequestMessage; -import org.gxf.soapbridge.soap.clients.Connection; -import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; -import org.gxf.soapbridge.soap.exceptions.ProxyServerException; -import org.gxf.soapbridge.soap.valueobjects.ClientCertificate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; -import org.springframework.stereotype.Component; -import org.springframework.web.HttpRequestHandler; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; - -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.Rdn; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.*; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.util.*; - -/** - * This {@link @Component} class is the endpoint for incoming SOAP requests from client applications. - */ -@Component -public class SoapEndpoint implements HttpRequestHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(SoapEndpoint.class); - - private static final String SOAP_HEADER_KEY_APPLICATION_NAME = "ApplicationName"; - private static final String SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION = "OrganisationIdentification"; - private static final String SOAP_HEADER_KEY_USER_NAME = "UserName"; - private static final List SOAP_HEADER_KEYS = List.of( - SOAP_HEADER_KEY_APPLICATION_NAME, - SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION, - SOAP_HEADER_KEY_USER_NAME); - - private static final String URL_PROXY_SERVER = "/proxy-server"; - - private static final int INVALID_CUSTOM_TIME_OUT = -1; - public static final String X_509_CERTIFICATE_REQUEST_ATTRIBUTE = "jakarta.servlet.request.X509Certificate"; - - /** - * Service used to cache incoming connections from client applications. - */ - @Autowired - private ConnectionCacheService connectionCacheService; - - @Autowired - private SoapConfigurationProperties soapConfiguration; - - /** - * Message sender which can send a webapp request message to ActiveMQ. - */ - @Autowired - private ProxyRequestsMessageSender proxyRequestsMessageSender; - - /** - * Service used to sign the content of a message. - */ - @Autowired - private SigningService signingService; - - /** - * Map of time outs for specific functions. - */ - private final Map customTimeOutsMap = new HashMap<>(); - - @PostConstruct - public void init() { - final String[] split = soapConfiguration.getCustomTimeouts().split(","); - for (int i = 0; i < split.length; i += 2) { - final String key = split[i]; - final Integer value = Integer.valueOf(split[i + 1]); - LOGGER.debug("Adding custom time out with key: {} and value: {}", key, value); - customTimeOutsMap.put(key, value); - } - LOGGER.debug("Added {} custom time outs to the map", customTimeOutsMap.size()); - } - - /** - * Handles incoming SOAP requests. - */ - @Override - public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) - throws ServletException, IOException { - - // For debugging, print all headers and parameters. - LOGGER.debug("Start of SoapEndpoint.handleRequest()"); - printHeaderValues(request); - printParameterValues(request); - - // Get the context, which should be an OSGP SOAP end-point or a - // NOTIFICATION SOAP end-point. - final String context = getContextForRequestType(request); - LOGGER.debug("Context: {}", context); - - // Try to read the SOAP request. - final String soapPayload = readSoapPayload(request); - if (soapPayload == null) { - LOGGER.error("Unable to read SOAP request, returning 500."); - createErrorResponse(response); - return; - } - - String organisationName = null; - if (request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME) instanceof final SecurityContext securityContext) { - if (securityContext.getAuthentication().getPrincipal() instanceof final User organisation) { - organisationName = organisation.getUsername(); - } - } - if (organisationName == null) { - LOGGER.error("Unable to find client certificate, returning 500."); - createErrorResponse(response); - return; - } - - // Cache the incoming connection. - final Connection newConnection = connectionCacheService.cacheConnection(); - final String connectionId = newConnection.getConnectionId(); - - // Create a queue message and sign it. - final ProxyServerRequestMessage requestMessage = - new ProxyServerRequestMessage( - connectionId, - organisationName, - context, - soapPayload); - try { - final String signature = signingService.signContent(requestMessage.constructString()); - requestMessage.setSignature(signature); - } catch (final ProxyServerException e) { - LOGGER.error("Unable to sign message or set security key", e); - createErrorResponse(response); - connectionCacheService.removeConnection(connectionId); - return; - } - - final Integer customTimeOut = shouldUseCustomTimeOut(soapPayload); - final int timeOut; - if (customTimeOut == INVALID_CUSTOM_TIME_OUT) { - timeOut = soapConfiguration.getTimeOut(); - LOGGER.debug("Using default time out: {} seconds", timeOut); - } else { - LOGGER.debug("Using custom time out: {} seconds", customTimeOut); - timeOut = customTimeOut; - } - - try { - proxyRequestsMessageSender.send(requestMessage); - - final boolean responseReceived = newConnection.waitForResponseReceived(timeOut); - if (!responseReceived) { - LOGGER.info("No response received within the specified time out of {} seconds", timeOut); - createErrorResponse(response); - connectionCacheService.removeConnection(connectionId); - return; - } - } catch (final Exception e) { - LOGGER.info("Error while waiting for response", e); - createErrorResponse(response); - connectionCacheService.removeConnection(connectionId); - return; - } - - final String soap = readResponse(connectionId); - if (soap == null) { - LOGGER.error("Unable to read SOAP response: null"); - createErrorResponse(response); - } else { - LOGGER.debug("Request handled, trying to send response..."); - createSuccessFulResponse(response, soap); - } - - LOGGER.debug( - "End of SoapEndpoint.handleRequest() --> incoming request handled and response returned."); - } - - public String[] sander() { - return new String[]{"a", "poipoi", "frats"}; - } - - private void printHeaderValues(final HttpServletRequest request) { - if (LOGGER.isDebugEnabled()) { - for (final Enumeration headerNames = request.getHeaderNames(); - headerNames.hasMoreElements(); ) { - final String headerName = headerNames.nextElement(); - final String headerValue = request.getHeader(headerName); - LOGGER.debug(" header name: {} header value: {}", headerName, headerValue); - } - } - } - - private void printParameterValues(final HttpServletRequest request) { - if (LOGGER.isDebugEnabled()) { - for (final Enumeration parameterNames = request.getParameterNames(); - parameterNames.hasMoreElements(); ) { - final String parameterName = parameterNames.nextElement(); - final String[] parameterValues = request.getParameterValues(parameterName); - String str = ""; - for (final String parameterValue : parameterValues) { - str = str.concat(parameterValue).concat(" "); - } - LOGGER.debug(" parameter name: {} parameter value: {}", parameterName, str); - } - } - } - - private String getContextForRequestType( - final HttpServletRequest request) { - return request.getRequestURI().replace(URL_PROXY_SERVER, ""); - } - - private String readSoapPayload(final HttpServletRequest request) { - final StringBuilder stringBuilder = new StringBuilder(); - String line; - String soapPayload = null; - try { - final BufferedReader reader = request.getReader(); - while ((line = reader.readLine()) != null) { - stringBuilder.append(line); - } - soapPayload = stringBuilder.toString(); - LOGGER.debug(" payload: {}", soapPayload); - } catch (final Exception e) { - LOGGER.error("Unexpected error while reading request body", e); - } - return soapPayload; - } - - private Map getSoapHeaderValues( - final String soapPayload, final List soapHeaderKeys) { - final Map values = new HashMap<>(); - try { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - factory.setNamespaceAware(false); - final InputStream inputStream = - new ByteArrayInputStream(soapPayload.getBytes(StandardCharsets.UTF_8)); - // Try to find the desired XML elements in the document. - final Document document = factory.newDocumentBuilder().parse(inputStream); - for (final String soapHeaderKey : soapHeaderKeys) { - final String value = evaluateXPathExpression(document, soapHeaderKey); - values.put(soapHeaderKey, value); - } - inputStream.close(); - } catch (final Exception e) { - LOGGER.error("Exception", e); - } - return values; - } - - /** - * Search an XML element using an XPath expression. - * - * @param document The XML document. - * @param element The name of the desired XML element. - * @return The content of the XML element, or null if the element is not found. - * @throws XPathExpressionException In case the expression fails to compile or evaluate, an - * exception will be thrown. - */ - private String evaluateXPathExpression(final Document document, final String element) - throws XPathExpressionException { - final String expression = String.format("//*[contains(local-name(), '%s')]", element); - - final XPathFactory xFactory = XPathFactory.newInstance(); - final XPath xPath = xFactory.newXPath(); - - final XPathExpression xPathExpression = xPath.compile(expression); - final NodeList nodeList = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET); - - if (nodeList != null && nodeList.getLength() > 0) { - return nodeList.item(0).getTextContent(); - } else { - return null; - } - } - - private ClientCertificate tryToFindClientCertificate( - final HttpServletRequest request, final String soapPayload) { - final X509Certificate[] x509Certificates = getX509CertificatesFromServlet(request); - ClientCertificate clientCertificate = null; - - if (x509Certificates.length == 0) { - LOGGER.error(" HTTPServletRequest's attribute was an empty array of X509Certificates."); - } else if (x509Certificates.length == 1) { - LOGGER.debug(" HTTPServletRequest's attribute was array of X509Certificates of length 1."); - - // Get the client certificate. - clientCertificate = getClientCertificate(x509Certificates[0]); - } else { - LOGGER.debug( - " HTTPServletRequest's attribute was array of X509Certificates of length {}.", - x509Certificates.length); - - final Map soapHeaderValues = - getSoapHeaderValues(soapPayload, SOAP_HEADER_KEYS); - LOGGER.debug( - " SOAP Header ApplicationName: {}", - soapHeaderValues.get(SOAP_HEADER_KEY_APPLICATION_NAME)); - LOGGER.debug( - " SOAP Header OrganisationIdentification: {}", - soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); - LOGGER.debug(" SOAP Header UserName: {}", soapHeaderValues.get(SOAP_HEADER_KEY_USER_NAME)); - - // Try to extract the client certificate for the organization - // identification. - clientCertificate = - getClientCertificateByOrganisationIdentification( - x509Certificates, soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); - } - - return clientCertificate; - } - - private X509Certificate[] getX509CertificatesFromServlet(final HttpServletRequest request) { - final Object x509CertificateAttribute = request.getAttribute(X_509_CERTIFICATE_REQUEST_ATTRIBUTE); - LOGGER.debug(X_509_CERTIFICATE_REQUEST_ATTRIBUTE + ": {}", x509CertificateAttribute); - if (x509CertificateAttribute instanceof X509Certificate[]) { - LOGGER.debug(" x509CertificateAttribute instanceof X509Certificate[]"); - return (X509Certificate[]) x509CertificateAttribute; - } else { - return new X509Certificate[0]; - } - } - - private ClientCertificate getClientCertificateByOrganisationIdentification( - final X509Certificate[] array, final String organisationCommonName) { - - for (final X509Certificate x509Certificate : array) { - final Principal principal = x509Certificate.getSubjectDN(); - LOGGER.debug(" principal: {}", principal); - final String subjectDn = principal.getName(); - LOGGER.debug(" subjectDn: {}", subjectDn); - try { - final String commonName = getCommonName(subjectDn); - if (commonName.equals(organisationCommonName)) { - LOGGER.debug( - "Found client certificate for right organisation {}", organisationCommonName); - return new ClientCertificate(x509Certificate, commonName); - } - } catch (final NamingException e) { - LOGGER.info("Failed to extract CommonName from this ClientCertificate", e); - } - } - return null; - } - - private ClientCertificate getClientCertificate(final X509Certificate x509Certificate) { - ClientCertificate clientCertificate = null; - - final Principal principal = x509Certificate.getSubjectDN(); - LOGGER.debug(" principal: {}", principal); - final String subjectDn = principal.getName(); - LOGGER.debug(" subjectDn: {}", subjectDn); - try { - final String commonName = getCommonName(subjectDn); - clientCertificate = new ClientCertificate(x509Certificate, commonName); - } catch (final NamingException e) { - LOGGER.info("Failed to extract CommonName from ClientCertificate", e); - } - - return clientCertificate; - } - - private String getCommonName(final String subjectDn) throws NamingException { - final Rdn rdn = new Rdn(subjectDn); - LOGGER.debug(" rdn: {}", rdn); - final Attributes attributes = rdn.toAttributes(); - LOGGER.debug(" attributes: {}", attributes); - final Attribute attribute = attributes.get("cn"); - LOGGER.debug(" attribute: {}", attribute); - final String commonName = (String) attribute.get(); - LOGGER.debug(" common name: {}", commonName); - return commonName; - } - - private Integer shouldUseCustomTimeOut(final String soapPayload) { - final Set keys = customTimeOutsMap.keySet(); - for (final String key : keys) { - if (soapPayload.contains(key)) { - return customTimeOutsMap.get(key); - } - } - return INVALID_CUSTOM_TIME_OUT; - } - - private String readResponse(final String connectionId) throws ServletException { - final String soap; - try { - final Connection connection = connectionCacheService.findConnection(connectionId); - soap = connection.getSoapResponse(); - connectionCacheService.removeConnection(connectionId); - } catch (final ConnectionNotFoundInCacheException e) { - LOGGER.error("Unexpected error while trying to find a cached connection", e); - throw new ServletException("Unable to obtain response"); - } - return soap; - } - - private void createErrorResponse(final HttpServletResponse response) { - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - - private void createSuccessFulResponse(final HttpServletResponse response, final String soap) - throws IOException { - LOGGER.debug("Start - creating successful response"); - response.setStatus(HttpServletResponse.SC_OK); - response.addHeader("SOAP-ACTION", ""); - response.addHeader("Keep-Alive", "timeout=5, max=100"); - response.addHeader("Accept", "text/xml"); - response.addHeader("Connection", "Keep-Alive"); - response.setContentType("text/xml; charset=" + StandardCharsets.UTF_8.name()); - response.getWriter().write(soap); - LOGGER.debug("End - creating successful response"); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java deleted file mode 100644 index fe8701a..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.exceptions; - -public class ConnectionNotFoundInCacheException extends ProxyServerException { - - /** - * Serial Version UID. - */ - private static final long serialVersionUID = -858760086093512799L; - - public ConnectionNotFoundInCacheException(final String message) { - super(message); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java deleted file mode 100644 index 33d801c..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.exceptions; - -/** - * Base type for exceptions for proxy server component. - */ -public class ProxyServerException extends Exception { - - /** - * Serial Version UID. - */ - private static final long serialVersionUID = -8696835428244659385L; - - public ProxyServerException() { - super(); - } - - public ProxyServerException(final String message) { - super(message); - } - - public ProxyServerException(final String message, final Throwable t) { - super(message, t); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java deleted file mode 100644 index 5ab543e..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.exceptions; - -public class UnableToCreateHttpsURLConnectionException extends ProxyServerException { - - /** - * Serial Version UID. - */ - private static final long serialVersionUID = -8807766325167125880L; - - public UnableToCreateHttpsURLConnectionException(final String message) { - super(message); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java deleted file mode 100644 index a497607..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.exceptions; - -public class UnableToCreateKeyManagersException extends ProxyServerException { - - /** - * Serial Version UID. - */ - private static final long serialVersionUID = -100586751704652623L; - - public UnableToCreateKeyManagersException(final String message, final Throwable t) { - super(message, t); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java deleted file mode 100644 index 0c54802..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.exceptions; - -public class UnableToCreateTrustManagersException extends ProxyServerException { - - /** - * Serial Version UID. - */ - private static final long serialVersionUID = -855694158211466200L; - - public UnableToCreateTrustManagersException(final String message, final Throwable t) { - super(message, t); - } -} diff --git a/components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java b/components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java deleted file mode 100644 index 8643c2d..0000000 --- a/components/soap/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.valueobjects; - -import java.security.cert.X509Certificate; - -public class ClientCertificate { - - private final X509Certificate x509Certificate; - - private final String commonName; - - public ClientCertificate(final X509Certificate x509Certificate, final String commonName) { - this.x509Certificate = x509Certificate; - this.commonName = commonName; - } - - public X509Certificate getX509Certificate() { - return x509Certificate; - } - - public String getCommonName() { - return commonName; - } -} diff --git a/components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java b/components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java deleted file mode 100644 index d834d4e..0000000 --- a/components/soap/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.application.utils; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class RandomStringServiceTests { - - @Test - void test1() { - final String randomString = RandomStringFactory.generateRandomString(); - - assertNotNull(randomString, "It is expected that the generated random string not is null"); - assertEquals( - 16, - randomString.length(), - "It is expected that the generated random string is of length 16"); - } - - @Test - void test2() { - final String randomString = RandomStringFactory.generateRandomString(42); - - assertNotNull(randomString, "It is expected that the generated random string not is null"); - assertEquals( - 42, - randomString.length(), - "It is expected that the generated random string is of length 42"); - } -} diff --git a/components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java b/components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java deleted file mode 100644 index ed952d3..0000000 --- a/components/soap/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2023 Alliander N.V. -package org.gxf.soapbridge.soap.clients; - -import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; -import org.gxf.soapbridge.application.properties.HostnameVerificationStrategy; -import org.gxf.soapbridge.application.properties.SoapConfigurationProperties; -import org.gxf.soapbridge.application.properties.SoapEndpointConfiguration; -import org.gxf.soapbridge.application.services.SigningService; -import org.gxf.soapbridge.messaging.ProxyResponsesMessageSender; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import javax.net.ssl.HttpsURLConnection; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ConnectException; -import java.net.HttpURLConnection; -import java.nio.charset.StandardCharsets; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class SoapClientTest { - - @Mock - ProxyResponsesMessageSender proxyResponsesMessageSender; - @Mock - HttpsUrlConnectionFactory httpsUrlConnectionFactory; - @Mock - SigningService signingService; - - byte[] testContent = "test content".getBytes(StandardCharsets.UTF_8); - - @Spy - SoapConfigurationProperties soapConfigurationProperties = new SoapConfigurationProperties( - HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES, - 45, - "", - new SoapEndpointConfiguration( - "localhost", 443, "https" - ) - ); - - @InjectMocks - SoapClient soapClient; - - @Test - void shouldSendSoapRequestAndJmsResponse() throws Exception { - // arrange - final HttpsURLConnection connection = setupConnectionMock(); - when(httpsUrlConnectionFactory.createConnection( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(connection); - - // act - soapClient.sendRequest( - "connectionId", - "context", - "commonName", - "payload"); - - // assert - verify(connection).disconnect(); - } - - @Test - void shoudDisconnectWhenSoapRequestFails() throws Exception { - // arrange - final HttpsURLConnection connection = setupFailingConnectionMock(); - when(httpsUrlConnectionFactory.createConnection( - anyString(), anyString(), anyString(), anyString())) - .thenReturn(connection); - - // act - soapClient.sendRequest( - "connectionId", - "context", - "commonName", - "payload"); - - // assert - verify(connection).disconnect(); - verifyNoInteractions(proxyResponsesMessageSender); - } - - private HttpsURLConnection setupConnectionMock() throws Exception { - final HttpsURLConnection connection = mock(HttpsURLConnection.class); - final InputStream inputStream = new ByteArrayInputStream(testContent); - when(connection.getOutputStream()).thenReturn(mock(OutputStream.class)); - when(connection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); - when(connection.getInputStream()).thenReturn(inputStream); - return connection; - } - - private HttpsURLConnection setupFailingConnectionMock() throws Exception { - final HttpsURLConnection connection = mock(HttpsURLConnection.class); - when(connection.getOutputStream()).thenThrow(ConnectException.class); - return connection; - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 91fd055..7b2730b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,3 @@ rootProject.name = "gxf-soap-bridge" include("application") -include("components:core") -include("components:kafka") -include("components:soap") From c131a369a54d25d1b37e3b610e899011501a0428 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Tue, 14 Nov 2023 15:24:41 +0100 Subject: [PATCH 03/46] FDP-94: Updated Sonar settings Signed-off-by: Sander Verbruggen --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 517ea15..1c5acba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,8 +19,8 @@ version = System.getenv("GITHUB_REF_NAME")?.replace("/", "-")?.lowercase() ?: "d sonarqube { properties { property("sonar.host.url", "https://sonarcloud.io") - property("sonar.projectKey", "SSS-gxf-soap-bridge") - property("sonar.organization", "digitalisering") + property("sonar.projectKey", "OSGP-gxf-soap-bridge") + property("sonar.organization", "gxf") } } tasks.sonar From eb0b79ac2a27ee4e1e99941dccca5576aa3d00af Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Tue, 14 Nov 2023 15:31:37 +0100 Subject: [PATCH 04/46] FDP-94: made shell scripts executable Signed-off-by: Sander Verbruggen --- application/src/integrationTest/resources/generate_certs.sh | 0 gradlew | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 application/src/integrationTest/resources/generate_certs.sh mode change 100644 => 100755 gradlew diff --git a/application/src/integrationTest/resources/generate_certs.sh b/application/src/integrationTest/resources/generate_certs.sh old mode 100644 new mode 100755 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 160195aba1ddb08c3206161d840f709dba95078d Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Tue, 14 Nov 2023 16:43:31 +0100 Subject: [PATCH 05/46] FDP-94: Updated Sonar settings Signed-off-by: Sander Verbruggen --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1c5acba..4d0049e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ version = System.getenv("GITHUB_REF_NAME")?.replace("/", "-")?.lowercase() ?: "d sonarqube { properties { property("sonar.host.url", "https://sonarcloud.io") - property("sonar.projectKey", "OSGP-gxf-soap-bridge") + property("sonar.projectKey", "gxf-soap-bridge") property("sonar.organization", "gxf") } } From cb9894719240cfe473feb0ef40224130d6800621 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 09:01:29 +0100 Subject: [PATCH 06/46] FDP-94: Processed Sonar comments Signed-off-by: Sander Verbruggen --- .../application/services/SigningService.java | 2 +- .../soap/endpoints/SoapEndpoint.java | 218 ++---------------- 2 files changed, 26 insertions(+), 194 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java b/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java index f45d569..8c275cc 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/SigningService.java @@ -24,7 +24,7 @@ public class SigningService { private static final Logger LOGGER = LoggerFactory.getLogger(SigningService.class); - private SigningService(final SecurityConfigurationProperties securityConfiguration) { + public SigningService(final SecurityConfigurationProperties securityConfiguration) { signingConfiguration = securityConfiguration.getSigning(); } diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index a6590da..92da6a8 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -3,25 +3,16 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.endpoints; +import static org.springframework.security.web.context.RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME; + import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.security.Principal; -import java.security.cert.X509Certificate; import java.util.*; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.Rdn; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.xpath.*; import org.gxf.soapbridge.application.services.ConnectionCacheService; import org.gxf.soapbridge.application.services.SigningService; import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties; @@ -29,18 +20,13 @@ import org.gxf.soapbridge.soap.clients.Connection; import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; import org.gxf.soapbridge.soap.exceptions.ProxyServerException; -import org.gxf.soapbridge.soap.valueobjects.ClientCertificate; import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.userdetails.User; -import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.stereotype.Component; import org.springframework.web.HttpRequestHandler; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; /** * This {@link @Component} class is the endpoint for incoming SOAP requests from client @@ -51,36 +37,35 @@ public class SoapEndpoint implements HttpRequestHandler { private static final Logger LOGGER = LoggerFactory.getLogger(SoapEndpoint.class); - private static final String SOAP_HEADER_KEY_APPLICATION_NAME = "ApplicationName"; - private static final String SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION = - "OrganisationIdentification"; - private static final String SOAP_HEADER_KEY_USER_NAME = "UserName"; - private static final List SOAP_HEADER_KEYS = - List.of( - SOAP_HEADER_KEY_APPLICATION_NAME, - SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION, - SOAP_HEADER_KEY_USER_NAME); - private static final String URL_PROXY_SERVER = "/proxy-server"; private static final int INVALID_CUSTOM_TIME_OUT = -1; - public static final String X_509_CERTIFICATE_REQUEST_ATTRIBUTE = - "jakarta.servlet.request.X509Certificate"; /** Service used to cache incoming connections from client applications. */ - @Autowired private ConnectionCacheService connectionCacheService; + private final ConnectionCacheService connectionCacheService; - @Autowired private SoapConfigurationProperties soapConfiguration; + private final SoapConfigurationProperties soapConfiguration; /** Message sender which can send a webapp request message to ActiveMQ. */ - @Autowired private ProxyRequestKafkaSender proxyRequestsSender; + private final ProxyRequestKafkaSender proxyRequestsSender; /** Service used to sign the content of a message. */ - @Autowired private SigningService signingService; + private final SigningService signingService; - /** Map of time outs for specific functions. */ + /** Map of time-outs for specific functions. */ private final Map customTimeOutsMap = new HashMap<>(); + public SoapEndpoint( + final ConnectionCacheService connectionCacheService, + final SoapConfigurationProperties soapConfiguration, + final ProxyRequestKafkaSender proxyRequestsSender, + final SigningService signingService) { + this.connectionCacheService = connectionCacheService; + this.soapConfiguration = soapConfiguration; + this.proxyRequestsSender = proxyRequestsSender; + this.signingService = signingService; + } + @PostConstruct public void init() { final String[] split = soapConfiguration.getCustomTimeouts().split(","); @@ -117,11 +102,10 @@ public void handleRequest(final HttpServletRequest request, final HttpServletRes } String organisationName = null; - if (request.getAttribute(RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME) - instanceof final SecurityContext securityContext) { - if (securityContext.getAuthentication().getPrincipal() instanceof final User organisation) { - organisationName = organisation.getUsername(); - } + if (request.getAttribute(DEFAULT_REQUEST_ATTR_NAME) + instanceof final SecurityContext securityContext + && securityContext.getAuthentication().getPrincipal() instanceof final User organisation) { + organisationName = organisation.getUsername(); } if (organisationName == null) { LOGGER.error("Unable to find client certificate, returning 500."); @@ -166,10 +150,11 @@ public void handleRequest(final HttpServletRequest request, final HttpServletRes connectionCacheService.removeConnection(connectionId); return; } - } catch (final Exception e) { - LOGGER.info("Error while waiting for response", e); + } catch (final InterruptedException e) { + LOGGER.error("Error while waiting for response", e); createErrorResponse(response); connectionCacheService.removeConnection(connectionId); + Thread.currentThread().interrupt(); return; } @@ -186,10 +171,6 @@ public void handleRequest(final HttpServletRequest request, final HttpServletRes "End of SoapEndpoint.handleRequest() --> incoming request handled and response returned."); } - public String[] sander() { - return new String[] {"a", "poipoi", "frats"}; - } - private void printHeaderValues(final HttpServletRequest request) { if (LOGGER.isDebugEnabled()) { for (final Enumeration headerNames = request.getHeaderNames(); @@ -237,155 +218,6 @@ private String readSoapPayload(final HttpServletRequest request) { return soapPayload; } - private Map getSoapHeaderValues( - final String soapPayload, final List soapHeaderKeys) { - final Map values = new HashMap<>(); - try { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - factory.setNamespaceAware(false); - final InputStream inputStream = - new ByteArrayInputStream(soapPayload.getBytes(StandardCharsets.UTF_8)); - // Try to find the desired XML elements in the document. - final Document document = factory.newDocumentBuilder().parse(inputStream); - for (final String soapHeaderKey : soapHeaderKeys) { - final String value = evaluateXPathExpression(document, soapHeaderKey); - values.put(soapHeaderKey, value); - } - inputStream.close(); - } catch (final Exception e) { - LOGGER.error("Exception", e); - } - return values; - } - - /** - * Search an XML element using an XPath expression. - * - * @param document The XML document. - * @param element The name of the desired XML element. - * @return The content of the XML element, or null if the element is not found. - * @throws XPathExpressionException In case the expression fails to compile or evaluate, an - * exception will be thrown. - */ - private String evaluateXPathExpression(final Document document, final String element) - throws XPathExpressionException { - final String expression = String.format("//*[contains(local-name(), '%s')]", element); - - final XPathFactory xFactory = XPathFactory.newInstance(); - final XPath xPath = xFactory.newXPath(); - - final XPathExpression xPathExpression = xPath.compile(expression); - final NodeList nodeList = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET); - - if (nodeList != null && nodeList.getLength() > 0) { - return nodeList.item(0).getTextContent(); - } else { - return null; - } - } - - private ClientCertificate tryToFindClientCertificate( - final HttpServletRequest request, final String soapPayload) { - final X509Certificate[] x509Certificates = getX509CertificatesFromServlet(request); - ClientCertificate clientCertificate = null; - - if (x509Certificates.length == 0) { - LOGGER.error(" HTTPServletRequest's attribute was an empty array of X509Certificates."); - } else if (x509Certificates.length == 1) { - LOGGER.debug(" HTTPServletRequest's attribute was array of X509Certificates of length 1."); - - // Get the client certificate. - clientCertificate = getClientCertificate(x509Certificates[0]); - } else { - LOGGER.debug( - " HTTPServletRequest's attribute was array of X509Certificates of length {}.", - x509Certificates.length); - - final Map soapHeaderValues = - getSoapHeaderValues(soapPayload, SOAP_HEADER_KEYS); - LOGGER.debug( - " SOAP Header ApplicationName: {}", - soapHeaderValues.get(SOAP_HEADER_KEY_APPLICATION_NAME)); - LOGGER.debug( - " SOAP Header OrganisationIdentification: {}", - soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); - LOGGER.debug(" SOAP Header UserName: {}", soapHeaderValues.get(SOAP_HEADER_KEY_USER_NAME)); - - // Try to extract the client certificate for the organization - // identification. - clientCertificate = - getClientCertificateByOrganisationIdentification( - x509Certificates, soapHeaderValues.get(SOAP_HEADER_KEY_ORGANISATION_IDENTIFICATION)); - } - - return clientCertificate; - } - - private X509Certificate[] getX509CertificatesFromServlet(final HttpServletRequest request) { - final Object x509CertificateAttribute = - request.getAttribute(X_509_CERTIFICATE_REQUEST_ATTRIBUTE); - LOGGER.debug(X_509_CERTIFICATE_REQUEST_ATTRIBUTE + ": {}", x509CertificateAttribute); - if (x509CertificateAttribute instanceof X509Certificate[]) { - LOGGER.debug(" x509CertificateAttribute instanceof X509Certificate[]"); - return (X509Certificate[]) x509CertificateAttribute; - } else { - return new X509Certificate[0]; - } - } - - private ClientCertificate getClientCertificateByOrganisationIdentification( - final X509Certificate[] array, final String organisationCommonName) { - - for (final X509Certificate x509Certificate : array) { - final Principal principal = x509Certificate.getSubjectDN(); - LOGGER.debug(" principal: {}", principal); - final String subjectDn = principal.getName(); - LOGGER.debug(" subjectDn: {}", subjectDn); - try { - final String commonName = getCommonName(subjectDn); - if (commonName.equals(organisationCommonName)) { - LOGGER.debug( - "Found client certificate for right organisation {}", organisationCommonName); - return new ClientCertificate(x509Certificate, commonName); - } - } catch (final NamingException e) { - LOGGER.info("Failed to extract CommonName from this ClientCertificate", e); - } - } - return null; - } - - private ClientCertificate getClientCertificate(final X509Certificate x509Certificate) { - ClientCertificate clientCertificate = null; - - final Principal principal = x509Certificate.getSubjectDN(); - LOGGER.debug(" principal: {}", principal); - final String subjectDn = principal.getName(); - LOGGER.debug(" subjectDn: {}", subjectDn); - try { - final String commonName = getCommonName(subjectDn); - clientCertificate = new ClientCertificate(x509Certificate, commonName); - } catch (final NamingException e) { - LOGGER.info("Failed to extract CommonName from ClientCertificate", e); - } - - return clientCertificate; - } - - private String getCommonName(final String subjectDn) throws NamingException { - final Rdn rdn = new Rdn(subjectDn); - LOGGER.debug(" rdn: {}", rdn); - final Attributes attributes = rdn.toAttributes(); - LOGGER.debug(" attributes: {}", attributes); - final Attribute attribute = attributes.get("cn"); - LOGGER.debug(" attribute: {}", attribute); - final String commonName = (String) attribute.get(); - LOGGER.debug(" common name: {}", commonName); - return commonName; - } - private Integer shouldUseCustomTimeOut(final String soapPayload) { final Set keys = customTimeOutsMap.keySet(); for (final String key : keys) { From d2c04a5f7aeaf025702fc3c99042dea513944881 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 10:08:21 +0100 Subject: [PATCH 07/46] FDP-94: Sonar fix? Signed-off-by: Sander Verbruggen --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4d0049e..4056063 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ version = System.getenv("GITHUB_REF_NAME")?.replace("/", "-")?.lowercase() ?: "d sonarqube { properties { property("sonar.host.url", "https://sonarcloud.io") - property("sonar.projectKey", "gxf-soap-bridge") + property("sonar.projectKey", "OGSP_gxf-soap-bridge") property("sonar.organization", "gxf") } } From 57b6f83deb5c425a8678d7593e340bf066fa34b2 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 10:24:06 +0100 Subject: [PATCH 08/46] FDP-94: Spring Boot update Signed-off-by: Sander Verbruggen --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4056063..5b38790 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ import io.spring.gradle.dependencymanagement.internal.dsl.StandardDependencyMana import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "3.1.4" apply false + id("org.springframework.boot") version "3.1.5" apply false id("io.spring.dependency-management") version "1.1.3" apply false kotlin("jvm") version "1.9.10" apply false kotlin("plugin.spring") version "1.9.10" apply false From 412fffb96a4262c1fa433f18ca6d343a2c66465c Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 10:25:42 +0100 Subject: [PATCH 09/46] FDP-94: Code cleanup Signed-off-by: Sander Verbruggen --- .../soapbridge/SoapBridgeApplicationTests.kt | 2 ++ .../configuration/SoapConfiguration.java | 3 ++- .../factories/HttpsUrlConnectionFactory.java | 23 +++++++++------- .../factories/SslContextFactory.java | 7 +++-- .../services/ClientCommunicationService.java | 20 +++++++------- .../services/ConnectionCacheService.java | 25 ------------------ .../PlatformCommunicationService.java | 11 +++++--- .../services/SslContextCacheService.java | 7 +++-- .../utils/RandomStringFactory.java | 2 +- .../soapbridge/soap/clients/Connection.java | 16 ++---------- .../soapbridge/soap/clients/SoapClient.java | 20 ++++++++++---- .../soap/endpoints/SoapEndpoint.java | 4 ++- .../soap/valueobjects/ClientCertificate.java | 26 ------------------- .../SecurityConfigurationProperties.kt | 6 ++--- .../exceptions/ProxyMessageException.kt | 2 +- .../valueobjects/ProxyServerRequestMessage.kt | 4 +-- .../ProxyServerResponseMessage.kt | 2 +- .../soap/clients/SoapClientTest.java | 2 +- 18 files changed, 76 insertions(+), 106 deletions(-) delete mode 100644 application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt index b550b51..66e040c 100644 --- a/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/SoapBridgeApplicationTests.kt @@ -4,6 +4,7 @@ package org.gxf.soapbridge +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest import org.springframework.kafka.test.context.EmbeddedKafka @@ -14,6 +15,7 @@ class SoapBridgeApplicationTests { @Test fun contextLoads() { + assertThat(true).`as` { "Application context loads" }.isTrue() } } diff --git a/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java b/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java index 0773d7d..ac1279b 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java +++ b/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.gxf.soapbridge.soap.endpoints.SoapEndpoint; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.AbstractHandlerMapping; @@ -18,7 +19,7 @@ public SoapConfiguration(final SoapEndpoint soapEndpoint) { } @Override - protected Object getHandlerInternal(final HttpServletRequest request) { + protected Object getHandlerInternal(@NotNull final HttpServletRequest request) { return soapEndpoint; } } diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java index 7d7a5e2..fdda8e2 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -13,7 +13,6 @@ 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; @@ -24,9 +23,16 @@ 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; + private final SslContextCacheService sslContextCacheService; - @Autowired private HostnameVerifierFactory hostnameVerifierFactory; + private final HostnameVerifierFactory hostnameVerifierFactory; + + public HttpsUrlConnectionFactory( + final SslContextCacheService sslContextCacheService, + final HostnameVerifierFactory hostnameVerifierFactory) { + this.sslContextCacheService = sslContextCacheService; + this.hostnameVerifierFactory = hostnameVerifierFactory; + } /** * Create an {@link HttpsURLConnection} instance for the given arguments. @@ -48,11 +54,11 @@ public HttpsURLConnection createConnection( throws UnableToCreateHttpsURLConnectionException { try { // Get SSLContext instance. - SSLContext sslContext = null; - if (StringUtils.isEmpty(commonName)) { - sslContext = sslContextCacheService.getSslContext(); - } else { + final SSLContext sslContext; + if (StringUtils.hasText(commonName)) { sslContext = sslContextCacheService.getSslContextForCommonName(commonName); + } else { + sslContext = sslContextCacheService.getSslContext(); } // Check SSLContext instance. if (sslContext == null) { @@ -92,8 +98,7 @@ public HttpsURLConnection createConnection( return connection; } catch (final IOException | ProxyServerException e) { - LOGGER.error("Creating connection failed.", e); - throw new UnableToCreateHttpsURLConnectionException(e.getMessage()); + throw new UnableToCreateHttpsURLConnectionException("Creating connection failed.", e); } } } diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java index c762554..3820072 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java @@ -14,7 +14,6 @@ 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. */ @@ -30,7 +29,7 @@ public class SslContextFactory { */ private static final String SSL_CONTEXT_PROTOCOL = "TLS"; - @Autowired private SecurityConfigurationProperties securityConfiguration; + private final SecurityConfigurationProperties securityConfiguration; /** * Using the trust store, a {@link TrustManager} instance is created. This instance is created @@ -48,6 +47,10 @@ public class SslContextFactory { */ private TrustManager[] trustManagersForHttpsWithClientCertificate; + public SslContextFactory(final SecurityConfigurationProperties securityConfiguration) { + this.securityConfiguration = securityConfiguration; + } + /** * Create an {@link SSLContext} instance. * diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java index d9c7e38..8cbb5b2 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java @@ -8,7 +8,6 @@ import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** @@ -21,10 +20,16 @@ public class ClientCommunicationService { private static final Logger LOGGER = LoggerFactory.getLogger(ClientCommunicationService.class); /** Service used to cache incoming connections from client applications. */ - @Autowired private ConnectionCacheService connectionCacheService; + private final ConnectionCacheService connectionCacheService; /** Service used to sign and/or verify the content of queue messages. */ - @Autowired private SigningService signingService; + private final SigningService signingService; + + public ClientCommunicationService( + final ConnectionCacheService connectionCacheService, final SigningService signingService) { + this.connectionCacheService = connectionCacheService; + this.signingService = signingService; + } /** * Process an incoming queue message. The content of the message has to be verified by the {@link @@ -37,10 +42,6 @@ public void handleIncomingResponse(final ProxyServerResponseMessage proxyServerR signingService.verifyContent( proxyServerResponseMessage.constructString(), proxyServerResponseMessage.getSignature()); - if (!isValid) { - LOGGER.error("ProxyServerResponseMessage failed to pass security check."); - return; - } try { final Connection connection = @@ -48,9 +49,10 @@ public void handleIncomingResponse(final ProxyServerResponseMessage proxyServerR if (connection != null) { if (isValid) { LOGGER.debug("Connection valid, set SOAP response"); - connection.setResponse(proxyServerResponseMessage.getSoapResponse()); + connection.setSoapResponse(proxyServerResponseMessage.getSoapResponse()); } else { - connection.setResponse("Security check has failed."); + LOGGER.error("ProxyServerResponseMessage failed to pass security check."); + connection.setSoapResponse("Security check has failed."); } } else { LOGGER.error("Cached connection is null"); diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java index 44de523..dc4c856 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java @@ -56,31 +56,6 @@ public Connection findConnection(final String connectionId) return connection; } - /** - * Determine if the response is available for a particular {@link Connection} instance. - * - * @param connectionId The key for the {@link Connection} instance obtained by calling {@link - * ConnectionCacheService#cacheConnection()}. - * @return True in case the response from the Platform has resolved and is available, false - * otherwise. - * @throws ConnectionNotFoundInCacheException In case the connection is not present in the {@link - * ConnectionCacheService#cache}. - */ - public boolean hasResponseResolved(final String connectionId) - throws ConnectionNotFoundInCacheException { - final Connection connection = cache.get(connectionId); - if (connection != null) { - LOGGER.debug( - "hasResponseResolved called with connectionId: {}, this.cache.size(): {}", - connectionId, - cache.size()); - return connection.isResponseResolved(); - } else { - throw new ConnectionNotFoundInCacheException( - String.format("Unable to find connection for connectionId: %s", connectionId)); - } - } - /** * Removes a {@link Connection} instance from the {@link ConnectionCacheService#cache}. * diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java index c3eb513..a8fe5f1 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java @@ -7,7 +7,6 @@ import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** Service which can send SOAP requests to OSGP. */ @@ -17,10 +16,16 @@ public class PlatformCommunicationService { private static final Logger LOGGER = LoggerFactory.getLogger(PlatformCommunicationService.class); /** SOAP client used to sent request messages to OSGP. */ - @Autowired private SoapClient soapClient; + private final SoapClient soapClient; /** Service used to sign and/or verify the content of queue messages. */ - @Autowired private SigningService signingService; + private final SigningService signingService; + + public PlatformCommunicationService( + final SoapClient soapClient, final SigningService signingService) { + this.soapClient = soapClient; + this.signingService = signingService; + } /** * Process an incoming queue message. The content of the message has to be verified by the {@link diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java b/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java index 14995c6..e3c088d 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/SslContextCacheService.java @@ -9,7 +9,6 @@ import org.gxf.soapbridge.application.factories.SslContextFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** @@ -31,7 +30,11 @@ public class SslContextCacheService { private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); /** Factory which assists in creating {@link SSLContext} instances. */ - @Autowired private SslContextFactory sslContextFactory; + private final SslContextFactory sslContextFactory; + + public SslContextCacheService(final SslContextFactory sslContextFactory) { + this.sslContextFactory = sslContextFactory; + } /** * Creates a new {@link SSLContext} instance and caches it, or fetches an existing instance from diff --git a/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java b/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java index 581bb78..be964c0 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java @@ -21,7 +21,7 @@ private RandomStringFactory() { /** * Generate a random string. * - * @return A string of length {@link RandomStringFactory#defaultLength} + * @return A string of length {@link RandomStringFactory#DEFAULT_LENGTH} */ public static String generateRandomString() { return randomString(DEFAULT_LENGTH); diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java index 61e42ad..0c04114 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java @@ -16,26 +16,18 @@ public class Connection { */ private static final int CONNECTION_ID_LENGTH = 32; - private volatile boolean responseResolved; - private String soapResponse; - private String connectionId; + private final String connectionId; private final Semaphore responseReceived; public Connection() { - responseResolved = false; responseReceived = new Semaphore(0); connectionId = RandomStringFactory.generateRandomString(CONNECTION_ID_LENGTH); } - public boolean isResponseResolved() { - return responseResolved; - } - - public void setResponse(final String soapResponse) { - responseResolved = true; + public void setSoapResponse(final String soapResponse) { this.soapResponse = soapResponse; responseReceived(); } @@ -44,10 +36,6 @@ public String getSoapResponse() { return soapResponse; } - public void setConnectionId(final String connectionId) { - this.connectionId = connectionId; - } - public String getConnectionId() { return connectionId; } diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java index c858293..0abb21c 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java @@ -17,7 +17,6 @@ import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage; 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 send SOAP messages to the Platform. */ @@ -27,15 +26,26 @@ public class SoapClient { private static final Logger LOGGER = LoggerFactory.getLogger(SoapClient.class); /** Message sender to send messages to a queue. */ - @Autowired private ProxyResponseKafkaSender proxyReponseSender; + private final ProxyResponseKafkaSender proxyReponseSender; - @Autowired private SoapConfigurationProperties soapConfiguration; + private final SoapConfigurationProperties soapConfiguration; /** Factory which assist in creating {@link HttpsURLConnection} instances. */ - @Autowired private HttpsUrlConnectionFactory httpsUrlConnectionFactory; + private final HttpsUrlConnectionFactory httpsUrlConnectionFactory; /** Service used to sign the content of a message. */ - @Autowired private SigningService signingService; + private final SigningService signingService; + + public SoapClient( + final ProxyResponseKafkaSender proxyReponseSender, + final SoapConfigurationProperties soapConfiguration, + final HttpsUrlConnectionFactory httpsUrlConnectionFactory, + final SigningService signingService) { + this.proxyReponseSender = proxyReponseSender; + this.soapConfiguration = soapConfiguration; + this.httpsUrlConnectionFactory = httpsUrlConnectionFactory; + this.signingService = signingService; + } /** * Send a request to the Platform. diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 92da6a8..7e04d32 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -21,6 +21,7 @@ import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; import org.gxf.soapbridge.soap.exceptions.ProxyServerException; import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.context.SecurityContext; @@ -80,7 +81,8 @@ public void init() { /** Handles incoming SOAP requests. */ @Override - public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) + public void handleRequest( + @NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response) throws ServletException, IOException { // For debugging, print all headers and parameters. diff --git a/application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java b/application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java deleted file mode 100644 index 5984f62..0000000 --- a/application/src/main/java/org/gxf/soapbridge/soap/valueobjects/ClientCertificate.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.soap.valueobjects; - -import java.security.cert.X509Certificate; - -public class ClientCertificate { - - private final X509Certificate x509Certificate; - - private final String commonName; - - public ClientCertificate(final X509Certificate x509Certificate, final String commonName) { - this.x509Certificate = x509Certificate; - this.commonName = commonName; - } - - public X509Certificate getX509Certificate() { - return x509Certificate; - } - - public String getCommonName() { - return commonName; - } -} diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt index 5b9c1ef..33b6cf0 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt @@ -30,9 +30,9 @@ class StoreConfigurationProperties( class SigningConfigurationProperties( - val keyType: String, - val signKeyFile: String, - val verifyKeyFile: String, + keyType: String, + signKeyFile: String, + verifyKeyFile: String, /** Indicates which provider is used for signing and verification. */ val provider: String, /** Indicates which signature is used for signing and verification. */ diff --git a/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt b/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt index 07c9e64..12af51d 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.messaging.exceptions +package org.gxf.soapbridge.exceptions class ProxyMessageException : Exception { diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt index a222361..d7d5f84 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt @@ -5,7 +5,7 @@ package org.gxf.soapbridge.valueobjects import mu.KotlinLogging -import org.gxf.soapbridge.messaging.exceptions.ProxyMessageException +import org.gxf.soapbridge.exceptions.ProxyMessageException import java.util.* @@ -44,7 +44,7 @@ class ProxyServerRequestMessage( "Invalid number of tokens, not trying to create ProxyServerRequestMessage." ) } - if (LOGGER.isDebugEnabled()) { + if (LOGGER.isDebugEnabled) { printValues(numTokens, split) } val connectionId = split[0] diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt index e57f04b..c6bda1f 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt @@ -5,7 +5,7 @@ package org.gxf.soapbridge.valueobjects import mu.KotlinLogging -import org.gxf.soapbridge.messaging.exceptions.ProxyMessageException +import org.gxf.soapbridge.exceptions.ProxyMessageException import java.util.* class ProxyServerResponseMessage(connectionId: String, val soapResponse: String) : diff --git a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java index 6e1c605..268a00a 100644 --- a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java +++ b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java @@ -28,7 +28,7 @@ class SoapClientTest { @Mock HttpsUrlConnectionFactory httpsUrlConnectionFactory; @Mock SigningService signingService; - byte[] testContent = "test content".getBytes(StandardCharsets.UTF_8); + private final byte[] testContent = "test content".getBytes(StandardCharsets.UTF_8); @Spy SoapConfigurationProperties soapConfigurationProperties = From 9a0dcf025e77f0220a6522416be15421756cd9c2 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 10:25:56 +0100 Subject: [PATCH 10/46] FDP-94: Code cleanup Signed-off-by: Sander Verbruggen --- .../application/factories/HttpsUrlConnectionFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java index fdda8e2..087f5e9 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -28,8 +28,8 @@ public class HttpsUrlConnectionFactory { private final HostnameVerifierFactory hostnameVerifierFactory; public HttpsUrlConnectionFactory( - final SslContextCacheService sslContextCacheService, - final HostnameVerifierFactory hostnameVerifierFactory) { + final SslContextCacheService sslContextCacheService, + final HostnameVerifierFactory hostnameVerifierFactory) { this.sslContextCacheService = sslContextCacheService; this.hostnameVerifierFactory = hostnameVerifierFactory; } From 78849bf30857b7533b2547329738394bf8fa4286 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 10:26:59 +0100 Subject: [PATCH 11/46] FDP-94: Sonar fix? Signed-off-by: Sander Verbruggen --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5b38790..f563e5b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ version = System.getenv("GITHUB_REF_NAME")?.replace("/", "-")?.lowercase() ?: "d sonarqube { properties { property("sonar.host.url", "https://sonarcloud.io") - property("sonar.projectKey", "OGSP_gxf-soap-bridge") + property("sonar.projectKey", "gxf-soap-bridge") property("sonar.organization", "gxf") } } From 4d3653918a2ea8b4be4e01066cad0095f577b1b3 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 11:04:32 +0100 Subject: [PATCH 12/46] FDP-94: Sanitize "user input" Signed-off-by: Sander Verbruggen --- .../soap/endpoints/SoapEndpoint.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 7e04d32..8bd8ebb 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.stream.Collectors; import org.gxf.soapbridge.application.services.ConnectionCacheService; import org.gxf.soapbridge.application.services.SigningService; import org.gxf.soapbridge.configuration.properties.SoapConfigurationProperties; @@ -87,8 +88,8 @@ public void handleRequest( // For debugging, print all headers and parameters. LOGGER.debug("Start of SoapEndpoint.handleRequest()"); - printHeaderValues(request); - printParameterValues(request); + logHeaderValues(request); + logParameterValues(request); // Get the context, which should be an OSGP SOAP end-point or a // NOTIFICATION SOAP end-point. @@ -173,7 +174,7 @@ public void handleRequest( "End of SoapEndpoint.handleRequest() --> incoming request handled and response returned."); } - private void printHeaderValues(final HttpServletRequest request) { + private void logHeaderValues(final HttpServletRequest request) { if (LOGGER.isDebugEnabled()) { for (final Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements(); ) { @@ -184,21 +185,24 @@ private void printHeaderValues(final HttpServletRequest request) { } } - private void printParameterValues(final HttpServletRequest request) { + private void logParameterValues(final HttpServletRequest request) { if (LOGGER.isDebugEnabled()) { for (final Enumeration parameterNames = request.getParameterNames(); parameterNames.hasMoreElements(); ) { final String parameterName = parameterNames.nextElement(); - final String[] parameterValues = request.getParameterValues(parameterName); - String str = ""; - for (final String parameterValue : parameterValues) { - str = str.concat(parameterValue).concat(" "); - } - LOGGER.debug(" parameter name: {} parameter value: {}", parameterName, str); + final String valuesString = + Arrays.stream(request.getParameterValues(parameterName)) + .map(this::sanitize) + .collect(Collectors.joining(" ")); + LOGGER.debug(" parameter name: {} parameter value(s): {}", parameterName, valuesString); } } } + private String sanitize(final String value) { + return value.replace('\n', '_').replace('\r', '_').replace('\t', '_'); + } + private String getContextForRequestType(final HttpServletRequest request) { return request.getRequestURI().replace(URL_PROXY_SERVER, ""); } From d57414836915b7cc0c259e89b2843646588c7ee8 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 11:07:37 +0100 Subject: [PATCH 13/46] FDP-94: fix Signed-off-by: Sander Verbruggen --- .../UnableToCreateHttpsURLConnectionException.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java index 7fb3077..28d5342 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateHttpsURLConnectionException.java @@ -3,12 +3,14 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.exceptions; +import java.io.Serial; + public class UnableToCreateHttpsURLConnectionException extends ProxyServerException { - /** Serial Version UID. */ - private static final long serialVersionUID = -8807766325167125880L; + @Serial private static final long serialVersionUID = -8807766325167125880L; - public UnableToCreateHttpsURLConnectionException(final String message) { - super(message); + public UnableToCreateHttpsURLConnectionException( + final String message, final Throwable throwable) { + super(message, throwable); } } From 28712bcae9832db183acaabdab44b078b9314920 Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 11:13:09 +0100 Subject: [PATCH 14/46] FDP-94: Sonar fix? Signed-off-by: Sander Verbruggen --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f563e5b..5c06ab5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ version = System.getenv("GITHUB_REF_NAME")?.replace("/", "-")?.lowercase() ?: "d sonarqube { properties { property("sonar.host.url", "https://sonarcloud.io") - property("sonar.projectKey", "gxf-soap-bridge") + property("sonar.projectKey", "OSGP_gxf-soap-bridge") property("sonar.organization", "gxf") } } From 0f0ed4fe7c445e9bfad17a9b515645d317df543f Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Wed, 15 Nov 2023 12:58:50 +0100 Subject: [PATCH 15/46] FDP-94: gradle/sonar fix Signed-off-by: Sander Verbruggen --- .github/workflows/gradle.yml | 30 +++++++++++++++++++----------- build.gradle.kts | 2 ++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 403c8df..c1b4a2f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,16 +1,19 @@ -# Copyright 2023 Alliander N.V. +# SPDX-FileCopyrightText: Contributors to the GXF project +# +# SPDX-License-Identifier: Apache-2.0 -name: Gradle Pipeline +name: Build Pipeline on: push: - branches: [ "main" , "develop" ] + branches: [ "main" ] tags: [ "v**" ] pull_request: - branches: [ "main", "develop" ] + branches: [ "main" ] jobs: build: + timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,17 +24,22 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Build with Gradle - uses: gradle/gradle-build-action@v2.8.1 + - name: Setup Gradle to generate and submit dependency graphs + uses: gradle/gradle-build-action@v2.9.0 with: - arguments: build integrationTest bootBuildImage sonar + dependency-graph: generate-and-submit + - name: Build with Gradle + run: ./gradlew build integrationTest bootBuildImage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run sonar analysis with Gradle + run: ./gradlew testCodeCoverageReport integrationTestCodeCoverageReport sonar env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - name: Publish Docker image - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || github.ref_type == 'tag' - uses: gradle/gradle-build-action@v2.8.1 - with: - arguments: bootBuildImage -PpublishImage + if: github.ref == 'refs/heads/main' || github.ref_type == 'tag' + run: ./gradlew bootBuildImage -PpublishImage env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/build.gradle.kts b/build.gradle.kts index 5c06ab5..758b59e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,8 @@ subprojects { apply(plugin = "io.spring.dependency-management") apply(plugin = "eclipse") apply(plugin = "org.jetbrains.kotlin.plugin.jpa") + apply(plugin = "jacoco") + apply(plugin = "jacoco-report-aggregation") group = "org.gxf.soap-bridge" version = rootProject.version From 81e96167fad4a2dc4e7de57aa506368883dbaffd Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:21:38 +0100 Subject: [PATCH 16/46] FDP-94: Use UUID instead of random string Signed-off-by: Jasper Kamerling --- .../services/ConnectionCacheService.java | 1 - .../utils/RandomStringFactory.java | 48 ------------------- .../soapbridge/soap/clients/Connection.java | 10 +--- .../utils/RandomStringServiceTests.java | 34 ------------- docker-compose.yaml | 11 ----- 5 files changed, 2 insertions(+), 102 deletions(-) delete mode 100644 application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java delete mode 100644 application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java index dc4c856..77e672b 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java @@ -4,7 +4,6 @@ package org.gxf.soapbridge.application.services; import java.util.concurrent.ConcurrentHashMap; -import org.gxf.soapbridge.application.utils.RandomStringFactory; import org.gxf.soapbridge.soap.clients.Connection; import org.gxf.soapbridge.soap.exceptions.ConnectionNotFoundInCacheException; import org.slf4j.Logger; diff --git a/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java b/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java deleted file mode 100644 index be964c0..0000000 --- a/application/src/main/java/org/gxf/soapbridge/application/utils/RandomStringFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.application.utils; - -import java.security.SecureRandom; - -/** This class can generate random string values. */ -public class RandomStringFactory { - - /** The random used to generate random strings. */ - private static final SecureRandom random = new SecureRandom(); - - /** Default length of the generated random strings. */ - private static final int DEFAULT_LENGTH = 16; - - private RandomStringFactory() { - // Only static. - } - - /** - * Generate a random string. - * - * @return A string of length {@link RandomStringFactory#DEFAULT_LENGTH} - */ - public static String generateRandomString() { - return randomString(DEFAULT_LENGTH); - } - - /** - * Generate a random string. - * - * @param length The length of the random string to generate. - * @return A string of the given length. - */ - public static String generateRandomString(final int length) { - return randomString(length); - } - - private static String randomString(final int length) { - final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - final StringBuilder buf = new StringBuilder(); - for (int i = 0; i < length; i++) { - buf.append(chars.charAt(random.nextInt(chars.length()))); - } - return buf.toString(); - } -} diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java index 0c04114..cc57c6f 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java @@ -3,19 +3,13 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.clients; +import java.util.UUID; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.gxf.soapbridge.application.services.ConnectionCacheService; -import org.gxf.soapbridge.application.utils.RandomStringFactory; public class Connection { - /** - * Default length for the connection id, which is the key for the {@link - * ConnectionCacheService#cache}. - */ - private static final int CONNECTION_ID_LENGTH = 32; - private String soapResponse; private final String connectionId; @@ -24,7 +18,7 @@ public class Connection { public Connection() { responseReceived = new Semaphore(0); - connectionId = RandomStringFactory.generateRandomString(CONNECTION_ID_LENGTH); + connectionId = UUID.randomUUID().toString(); } public void setSoapResponse(final String soapResponse) { diff --git a/application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java b/application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java deleted file mode 100644 index 5faa1a5..0000000 --- a/application/src/test/java/org/gxf/soapbridge/application/utils/RandomStringServiceTests.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 -package org.gxf.soapbridge.application.utils; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class RandomStringServiceTests { - - @Test - void test1() { - final String randomString = RandomStringFactory.generateRandomString(); - - Assertions.assertNotNull( - randomString, "It is expected that the generated random string not is null"); - Assertions.assertEquals( - 16, - randomString.length(), - "It is expected that the generated random string is of length 16"); - } - - @Test - void test2() { - final String randomString = RandomStringFactory.generateRandomString(42); - - Assertions.assertNotNull( - randomString, "It is expected that the generated random string not is null"); - Assertions.assertEquals( - 42, - randomString.length(), - "It is expected that the generated random string is of length 42"); - } -} diff --git a/docker-compose.yaml b/docker-compose.yaml index e9652be..3c98f55 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,14 +22,3 @@ services: KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_CREATE_TOPICS: "avroTopic:1:1" - schema-registry: - image: confluentinc/cp-schema-registry:7.3.0 - depends_on: - - zookeeper - - kafka - ports: - - "8081:8081" - environment: - SCHEMA_REGISTRY_HOST_NAME: schema-registry - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka:29092' - SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 From fcc36d68dc6576ce3ad7f9166e10d6adff6a5546 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:23:39 +0100 Subject: [PATCH 17/46] FDP-94: Update exceptions Signed-off-by: Jasper Kamerling --- .../soap/exceptions/ConnectionNotFoundInCacheException.java | 4 +++- .../soapbridge/soap/exceptions/ProxyServerException.java | 4 +++- .../soap/exceptions/UnableToCreateKeyManagersException.java | 3 +++ .../exceptions/UnableToCreateTrustManagersException.java | 3 +++ .../org/gxf/soapbridge/exceptions/ProxyMessageException.kt | 6 +----- .../soapbridge/valueobjects/ProxyServerRequestMessage.kt | 2 +- .../soapbridge/valueobjects/ProxyServerResponseMessage.kt | 2 +- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java index 5c7d346..d3604fe 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ConnectionNotFoundInCacheException.java @@ -3,10 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.exceptions; +import java.io.Serial; + public class ConnectionNotFoundInCacheException extends ProxyServerException { /** Serial Version UID. */ - private static final long serialVersionUID = -858760086093512799L; + @Serial private static final long serialVersionUID = -858760086093512799L; public ConnectionNotFoundInCacheException(final String message) { super(message); diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java index 2bb17a9..b3ec6d1 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java @@ -3,11 +3,13 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.exceptions; +import java.io.Serial; + /** Base type for exceptions for proxy server component. */ public class ProxyServerException extends Exception { /** Serial Version UID. */ - private static final long serialVersionUID = -8696835428244659385L; + @Serial private static final long serialVersionUID = -8696835428244659385L; public ProxyServerException() { super(); diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java index b50ff46..c9d071d 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateKeyManagersException.java @@ -3,9 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.exceptions; +import java.io.Serial; + public class UnableToCreateKeyManagersException extends ProxyServerException { /** Serial Version UID. */ + @Serial private static final long serialVersionUID = -100586751704652623L; public UnableToCreateKeyManagersException(final String message, final Throwable t) { diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java index 7695973..747caed 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/UnableToCreateTrustManagersException.java @@ -3,9 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 package org.gxf.soapbridge.soap.exceptions; +import java.io.Serial; + public class UnableToCreateTrustManagersException extends ProxyServerException { /** Serial Version UID. */ + @Serial private static final long serialVersionUID = -855694158211466200L; public UnableToCreateTrustManagersException(final String message, final Throwable t) { diff --git a/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt b/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt index 12af51d..6a70b37 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/exceptions/ProxyMessageException.kt @@ -5,8 +5,4 @@ package org.gxf.soapbridge.exceptions -class ProxyMessageException : Exception { - constructor() : super() - constructor(message: String?) : super(message) - constructor(message: String?, t: Throwable?) : super(message, t) -} +class ProxyMessageException(message: String?) : Exception(message) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt index d7d5f84..93ef1f8 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt @@ -31,7 +31,7 @@ class ProxyServerRequestMessage( * * @param string The input string. * @return A ProxyServerRequestMessage instance. - * @throws ProxyServerException + * @throws ProxyMessageException */ @Throws(ProxyMessageException::class) fun createInstanceFromString(string: String): ProxyServerRequestMessage { diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt index c6bda1f..92c513c 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt @@ -26,7 +26,7 @@ class ProxyServerResponseMessage(connectionId: String, val soapResponse: String) * * @param string The input string. * @return A ProxyServerResponseMessage instance. - * @throws ProxyServerException + * @throws ProxyMessageException */ @Throws(ProxyMessageException::class) fun createInstanceFromString(string: String): ProxyServerResponseMessage { From 8e8732f66c2768419c76c37404c132fc8639adc5 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:24:00 +0100 Subject: [PATCH 18/46] FDP-94: cleanup SecurityConfiguration Signed-off-by: Jasper Kamerling --- .../configuration/SecurityConfiguration.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt index b03f77d..5a63b01 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/SecurityConfiguration.kt @@ -13,31 +13,28 @@ import org.springframework.security.web.SecurityFilterChain @Configuration class SecurityConfiguration { + @Bean - @Throws(Exception::class) - fun filterChain(http: HttpSecurity): SecurityFilterChain { + fun filterChain(http: HttpSecurity): SecurityFilterChain = http.authorizeHttpRequests { it .anyRequest().authenticated() - } - http.x509 { + }.x509 { it .subjectPrincipalRegex("CN=(.*?)(?:,|$)") .userDetailsService(userDetailsService()) - } - http.csrf { it.disable() } - return http.build() - } + }.csrf { it.disable() } + .build() + /** * Uses the CN of the client certificate as the username for Springs Principal object */ @Bean - fun userDetailsService(): UserDetailsService { - return UserDetailsService { username -> + fun userDetailsService(): UserDetailsService = + UserDetailsService { username -> return@UserDetailsService User( username, "", emptyList() ) } - } } From bd91c557ebdd2a5e0744467c9ca50320f4a97646 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:24:27 +0100 Subject: [PATCH 19/46] FDP-94: Correct timeout spelling Signed-off-by: Jasper Kamerling --- .../soap/endpoints/SoapEndpoint.java | 18 +++++++++--------- .../properties/SoapConfigurationProperties.kt | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 8bd8ebb..85af9d2 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -74,10 +74,10 @@ public void init() { for (int i = 0; i < split.length; i += 2) { final String key = split[i]; final Integer value = Integer.valueOf(split[i + 1]); - LOGGER.debug("Adding custom time out with key: {} and value: {}", key, value); + LOGGER.debug("Adding custom timeout with key: {} and value: {}", key, value); customTimeOutsMap.put(key, value); } - LOGGER.debug("Added {} custom time outs to the map", customTimeOutsMap.size()); + LOGGER.debug("Added {} custom timeouts to the map", customTimeOutsMap.size()); } /** Handles incoming SOAP requests. */ @@ -134,21 +134,21 @@ public void handleRequest( } final Integer customTimeOut = shouldUseCustomTimeOut(soapPayload); - final int timeOut; + final int timeout; if (customTimeOut == INVALID_CUSTOM_TIME_OUT) { - timeOut = soapConfiguration.getTimeOut(); - LOGGER.debug("Using default time out: {} seconds", timeOut); + timeout = soapConfiguration.getTimeout(); + LOGGER.debug("Using default timeout: {} seconds", timeout); } else { - LOGGER.debug("Using custom time out: {} seconds", customTimeOut); - timeOut = customTimeOut; + LOGGER.debug("Using custom timeout: {} seconds", customTimeOut); + timeout = customTimeOut; } try { proxyRequestsSender.send(requestMessage); - final boolean responseReceived = newConnection.waitForResponseReceived(timeOut); + final boolean responseReceived = newConnection.waitForResponseReceived(timeout); if (!responseReceived) { - LOGGER.info("No response received within the specified time out of {} seconds", timeOut); + LOGGER.info("No response received within the specified timeout of {} seconds", timeout); createErrorResponse(response); connectionCacheService.removeConnection(connectionId); return; diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt index dd062d5..b3597d6 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt @@ -13,9 +13,9 @@ class SoapConfigurationProperties( * Maximum number of seconds this {@link SoapEndpoint} will wait for a response from the other end * before terminating the connection with the client application. */ - val timeOut: Int, + val timeout: Int, /** - * Time outs for specific functions. + * Timeouts for specific functions. */ val customTimeouts: String, val callEndpoint: SoapEndpointConfiguration, From 52f8730ae2bdf66b81093733ecaf886bfbe50e6e Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:25:22 +0100 Subject: [PATCH 20/46] FDP-94: Update listener configuration Signed-off-by: Jasper Kamerling --- .../soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt | 6 ------ .../kafka/listeners/ProxyResponseKafkaListener.kt | 6 ------ 2 files changed, 12 deletions(-) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt index d0975dd..b9760a5 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt @@ -19,13 +19,7 @@ import java.net.SocketTimeoutException class ProxyRequestKafkaListener(private val platformCommunicationService: PlatformCommunicationService) { private val logger = KotlinLogging.logger { } - @Observed(name = "requests.consumed") @KafkaListener(topics = ["\${topics.incoming.requests}"], id = "gxf-request-consumer") - @RetryableTopic( - backoff = Backoff(value = 3000L), - attempts = "2", - include = [SocketTimeoutException::class] - ) fun consume(record: ConsumerRecord) { logger.info("Received message") val requestMessage = ProxyServerRequestMessage.createInstanceFromString(record.value()) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt index 89062b2..5cc18d8 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt @@ -21,13 +21,7 @@ class ProxyResponseKafkaListener( ) { private val logger = KotlinLogging.logger { } - @Observed(name = "responses.consumed") @KafkaListener(topics = ["\${topics.incoming.responses}"], id = "gxf-response-consumer") - @RetryableTopic( - backoff = Backoff(value = 3000L), - attempts = "2", - include = [SocketTimeoutException::class] - ) fun consume(record: ConsumerRecord) { logger.info("Received response") val responseMessage = ProxyServerResponseMessage.createInstanceFromString(record.value()) From 2a8a721b28f7a47c3e8fe261c5ddd3623bd82d43 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:25:32 +0100 Subject: [PATCH 21/46] FDP-94: Update Gradle Signed-off-by: Jasper Kamerling --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e1bef7e..e411586 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 9fd9ea3cbff921fa2719cd25a1dba6c319b4bc7f Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:25:51 +0100 Subject: [PATCH 22/46] FDP-94: Remove unused ObservabilityConfiguration.kt Signed-off-by: Jasper Kamerling --- .../ObservabilityConfiguration.kt | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt diff --git a/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt deleted file mode 100644 index 89d00f9..0000000 --- a/application/src/main/kotlin/org/gxf/soapbridge/observability/ObservabilityConfiguration.kt +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: Copyright Contributors to the GXF project -// -// SPDX-License-Identifier: Apache-2.0 - -package org.gxf.soapbridge.observability - -import io.micrometer.observation.ObservationRegistry -import io.micrometer.observation.aop.ObservedAspect -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - - -@Configuration(proxyBeanMethods = false) -internal class ObservabilityConfiguration { - - @Bean - fun observedAspect(observationRegistry: ObservationRegistry): ObservedAspect { - return ObservedAspect(observationRegistry) - } -} From e07500074ae425625de362139bef61a15d4b9f87 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:28:08 +0100 Subject: [PATCH 23/46] FDP-94: Update logging Signed-off-by: Jasper Kamerling --- .../application/services/ClientCommunicationService.java | 2 +- .../org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt | 2 +- .../gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt | 2 +- .../gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java index 8cbb5b2..29fb25f 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java @@ -55,7 +55,7 @@ public void handleIncomingResponse(final ProxyServerResponseMessage proxyServerR connection.setSoapResponse("Security check has failed."); } } else { - LOGGER.error("Cached connection is null"); + LOGGER.error("No connection found in cache for id."); } } catch (final ConnectionNotFoundInCacheException e) { LOGGER.error("ConnectionNotFoundInCacheException", e); diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt index 36d3e2c..9c620a1 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt @@ -20,7 +20,7 @@ class ProxyRequestKafkaSender( private val topic = topicConfiguration.outgoing.requests fun send(requestMessage: ProxyServerRequestMessage) { - logger.debug("SOAP payload: ${requestMessage.soapPayload} to $topic") + logger.debug { "SOAP payload: ${requestMessage.soapPayload} to $topic" } kafkaTemplate.send(topic, requestMessage.constructSignedString()) } } diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt index 3a1e978..d97a38e 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt @@ -20,7 +20,7 @@ class ProxyResponseKafkaSender( private val topic = topicConfiguration.outgoing.responses fun send(responseMessage: ProxyServerResponseMessage) { - logger.debug("SOAP payload: ${responseMessage.soapResponse} to $topic") + logger.debug { "SOAP payload: ${responseMessage.soapResponse} to $topic" } kafkaTemplate.send(topic, responseMessage.constructSignedString()) } } diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt index 92c513c..a6ad773 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerResponseMessage.kt @@ -32,7 +32,7 @@ class ProxyServerResponseMessage(connectionId: String, val soapResponse: String) fun createInstanceFromString(string: String): ProxyServerResponseMessage { val split = string.split(SEPARATOR) val numTokens = split.size - logger.debug("split.length: {}", numTokens) + logger.debug { "split.length: ${numTokens}" } if (numTokens < 3) { throw ProxyMessageException( "Invalid number of tokens, don't try to create ProxyServerResponseMessage" From 0ca49e0cda656210b4c348a055d599e716ebdea3 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:38:12 +0100 Subject: [PATCH 24/46] FDP-94: Rename SoapConfiguration Signed-off-by: Jasper Kamerling --- .../{SoapConfiguration.java => SoapEndpointMapping.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename application/src/main/java/org/gxf/soapbridge/application/configuration/{SoapConfiguration.java => SoapEndpointMapping.java} (83%) diff --git a/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java b/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapEndpointMapping.java similarity index 83% rename from application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java rename to application/src/main/java/org/gxf/soapbridge/application/configuration/SoapEndpointMapping.java index ac1279b..81136a8 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapConfiguration.java +++ b/application/src/main/java/org/gxf/soapbridge/application/configuration/SoapEndpointMapping.java @@ -11,10 +11,10 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping; @Component -public class SoapConfiguration extends AbstractHandlerMapping { +public class SoapEndpointMapping extends AbstractHandlerMapping { private final SoapEndpoint soapEndpoint; - public SoapConfiguration(final SoapEndpoint soapEndpoint) { + public SoapEndpointMapping(final SoapEndpoint soapEndpoint) { this.soapEndpoint = soapEndpoint; } From 71a9c7cade0de9c22667767e5103d6270de248a9 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:41:49 +0100 Subject: [PATCH 25/46] FDP-94: Remove test code Signed-off-by: Jasper Kamerling --- .../org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt index 93ef1f8..253a554 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt @@ -36,7 +36,6 @@ class ProxyServerRequestMessage( @Throws(ProxyMessageException::class) fun createInstanceFromString(string: String): ProxyServerRequestMessage { val split = string.split(SEPARATOR) - "een of andere string".lines() val numTokens = split.size LOGGER.debug("split.length: {}", numTokens) if (numTokens < 4 || numTokens > 5) { From 8b1bb9910c0a704f67c817ebc8bbe47c00de2a0a Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 10:45:22 +0100 Subject: [PATCH 26/46] FDP-94: Update logging Signed-off-by: Jasper Kamerling --- .../gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt index 253a554..a7c07b0 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerRequestMessage.kt @@ -37,7 +37,7 @@ class ProxyServerRequestMessage( fun createInstanceFromString(string: String): ProxyServerRequestMessage { val split = string.split(SEPARATOR) val numTokens = split.size - LOGGER.debug("split.length: {}", numTokens) + LOGGER.debug { "split.length: ${numTokens}" } if (numTokens < 4 || numTokens > 5) { throw ProxyMessageException( "Invalid number of tokens, not trying to create ProxyServerRequestMessage." From a75a94ab184f2fcee7b33b0cb821b9c9f77829ea Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Thu, 16 Nov 2023 12:36:06 +0100 Subject: [PATCH 27/46] FDP-94: Clean unused imports Signed-off-by: Jasper Kamerling --- .../main/java/org/gxf/soapbridge/soap/clients/Connection.java | 1 - .../soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt | 4 ---- .../soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt | 4 ---- 3 files changed, 9 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java index cc57c6f..797b694 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java @@ -6,7 +6,6 @@ import java.util.UUID; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import org.gxf.soapbridge.application.services.ConnectionCacheService; public class Connection { diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt index b9760a5..322f1cb 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt @@ -4,16 +4,12 @@ package org.gxf.soapbridge.kafka.listeners -import io.micrometer.observation.annotation.Observed import mu.KotlinLogging import org.apache.kafka.clients.consumer.ConsumerRecord import org.gxf.soapbridge.application.services.PlatformCommunicationService import org.gxf.soapbridge.valueobjects.ProxyServerRequestMessage import org.springframework.kafka.annotation.KafkaListener -import org.springframework.kafka.annotation.RetryableTopic -import org.springframework.retry.annotation.Backoff import org.springframework.stereotype.Component -import java.net.SocketTimeoutException @Component class ProxyRequestKafkaListener(private val platformCommunicationService: PlatformCommunicationService) { diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt index 5cc18d8..381f60b 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt @@ -4,16 +4,12 @@ package org.gxf.soapbridge.kafka.listeners -import io.micrometer.observation.annotation.Observed import mu.KotlinLogging import org.apache.kafka.clients.consumer.ConsumerRecord import org.gxf.soapbridge.application.services.ClientCommunicationService import org.gxf.soapbridge.valueobjects.ProxyServerResponseMessage import org.springframework.kafka.annotation.KafkaListener -import org.springframework.kafka.annotation.RetryableTopic -import org.springframework.retry.annotation.Backoff import org.springframework.stereotype.Component -import java.net.SocketTimeoutException @Component class ProxyResponseKafkaListener( From 205730fbfd28775a17d3bfefb2331a0564815440 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 08:52:13 +0100 Subject: [PATCH 28/46] FDP-94: Cleanup Signed-off-by: Jasper Kamerling --- .../kotlin/org/gxf/soapbridge/EndToEndTest.kt | 2 -- .../application/services/ConnectionCacheService.java | 3 +-- .../java/org/gxf/soapbridge/soap/clients/Connection.java | 8 ++++---- .../soapbridge/soap/exceptions/ProxyServerException.java | 4 ---- .../gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt | 2 +- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt index 55c8182..de2a29e 100644 --- a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt @@ -9,7 +9,6 @@ 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.configuration.properties.SoapConfigurationProperties import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -30,7 +29,6 @@ import java.net.http.HttpClient class EndToEndTest( @LocalServerPort private val soapPort: Int, @Autowired private val sslContextFactory: SslContextFactory, - @Autowired private val soapConfigProperties: SoapConfigurationProperties ) { private val proxyUrl = "https://localhost:$soapPort/proxy-server" private val methodPath = "/someSoapMethod" diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java index 77e672b..3c2cda2 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ConnectionCacheService.java @@ -17,8 +17,7 @@ public class ConnectionCacheService { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionCacheService.class); /** - * Map used to cache connections. The key is a random string generated by {@link - * RandomStringFactory}. The value is a {@link Connection} instance. + * Map used to cache connections. The key is an uuid. The value is a {@link Connection} instance. */ private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java index 797b694..1dcaf96 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/Connection.java @@ -33,19 +33,19 @@ public String getConnectionId() { return connectionId; } - /* + /** * Indicates the response for this connection has been received. */ public void responseReceived() { responseReceived.release(); } - /* + /** * Waits for a response on this connection. * - * @timeout The number of seconds to wait for a response. + * @param timeout The number of seconds to wait for a response. * - * @returns true, if the response was received within @timeout seconds, + * @return true, if the response was received within @timeout seconds, * false otherwise. */ public boolean waitForResponseReceived(final int timeout) throws InterruptedException { diff --git a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java index b3ec6d1..409af69 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/exceptions/ProxyServerException.java @@ -11,10 +11,6 @@ public class ProxyServerException extends Exception { /** Serial Version UID. */ @Serial private static final long serialVersionUID = -8696835428244659385L; - public ProxyServerException() { - super(); - } - public ProxyServerException(final String message) { super(message); } diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt index 4ca6752..e6b16d0 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt @@ -4,7 +4,7 @@ package org.gxf.soapbridge.valueobjects -import java.util.* +import java.util.Base64 /** Base class for proxy-server messages. */ From 3208ee1038aa1b8bb2b267468893236854e2e802 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 09:55:12 +0100 Subject: [PATCH 29/46] FDP-94: Fix typo Signed-off-by: Jasper Kamerling --- .../main/java/org/gxf/soapbridge/soap/clients/SoapClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java index 0abb21c..d111d6e 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java @@ -37,11 +37,11 @@ public class SoapClient { private final SigningService signingService; public SoapClient( - final ProxyResponseKafkaSender proxyReponseSender, + final ProxyResponseKafkaSender proxyResponseSender, final SoapConfigurationProperties soapConfiguration, final HttpsUrlConnectionFactory httpsUrlConnectionFactory, final SigningService signingService) { - this.proxyReponseSender = proxyReponseSender; + this.proxyReponseSender = proxyResponseSender; this.soapConfiguration = soapConfiguration; this.httpsUrlConnectionFactory = httpsUrlConnectionFactory; this.signingService = signingService; From b6f9d710fe871de3baca77d5a465bf0db57a9b6f Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 10:06:56 +0100 Subject: [PATCH 30/46] FDP-94: Improve logging Signed-off-by: Jasper Kamerling --- .../soapbridge/application/factories/SslContextFactory.java | 4 ++-- .../java/org/gxf/soapbridge/soap/clients/SoapClient.java | 2 +- .../org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java | 2 +- .../soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt | 2 +- .../kafka/listeners/ProxyResponseKafkaListener.kt | 6 ++---- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java index 3820072..84993ba 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java @@ -77,7 +77,7 @@ public SSLContext createSslContext(final String commonName) { // 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"); + LOGGER.debug("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); @@ -98,7 +98,7 @@ public SSLContext createSslContext(final String commonName) { // 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"); + LOGGER.debug("Created SSL context using trust manager and key manager for HTTPS"); return sslContext; } catch (final Exception e) { LOGGER.error( diff --git a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java index d111d6e..b4f0716 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/clients/SoapClient.java @@ -107,7 +107,7 @@ private HttpsURLConnection createConnection( final SoapEndpointConfiguration callEndpoint = soapConfiguration.getCallEndpoint(); final String uri = callEndpoint.getUri().concat(context); - LOGGER.info("Preparing to open connection for WEBAPP_REQUEST using URI: {}", uri); + LOGGER.debug("Preparing to open connection for WEBAPP_REQUEST using URI: {}", uri); return httpsUrlConnectionFactory.createConnection( uri, callEndpoint.getHostAndPort(), contentLength, commonName); } diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 85af9d2..f9a870c 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -148,7 +148,7 @@ public void handleRequest( final boolean responseReceived = newConnection.waitForResponseReceived(timeout); if (!responseReceived) { - LOGGER.info("No response received within the specified timeout of {} seconds", timeout); + LOGGER.error("No response received within the specified timeout of {} seconds", timeout); createErrorResponse(response); connectionCacheService.removeConnection(connectionId); return; diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt index 322f1cb..2ad3a92 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt @@ -17,7 +17,7 @@ class ProxyRequestKafkaListener(private val platformCommunicationService: Platfo @KafkaListener(topics = ["\${topics.incoming.requests}"], id = "gxf-request-consumer") fun consume(record: ConsumerRecord) { - logger.info("Received message") + logger.debug { "Received request: ${record.key()}, ${record.value()}" } val requestMessage = ProxyServerRequestMessage.createInstanceFromString(record.value()) platformCommunicationService.handleIncomingRequest(requestMessage) } diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt index 381f60b..95191bb 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt @@ -12,14 +12,12 @@ import org.springframework.kafka.annotation.KafkaListener import org.springframework.stereotype.Component @Component -class ProxyResponseKafkaListener( - private val clientCommunicationService: ClientCommunicationService -) { +class ProxyResponseKafkaListener(private val clientCommunicationService: ClientCommunicationService) { private val logger = KotlinLogging.logger { } @KafkaListener(topics = ["\${topics.incoming.responses}"], id = "gxf-response-consumer") fun consume(record: ConsumerRecord) { - logger.info("Received response") + logger.debug { "Received response: ${record.key()}, ${record.value()}" } val responseMessage = ProxyServerResponseMessage.createInstanceFromString(record.value()) clientCommunicationService.handleIncomingResponse(responseMessage) } From e7aa4292b2027114853d289bea7b30b3ab537a76 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 10:07:19 +0100 Subject: [PATCH 31/46] FDP-94: Set host name verifier on connection Signed-off-by: Jasper Kamerling --- .../application/factories/HttpsUrlConnectionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java index 087f5e9..b25c9e0 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -71,8 +71,8 @@ public HttpsURLConnection createConnection( return null; } // Create connection. - HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifierFactory.getHostnameVerifier()); final HttpsURLConnection connection = (HttpsURLConnection) new URL(uri).openConnection(); + connection.setHostnameVerifier(hostnameVerifierFactory.getHostnameVerifier()); connection.setSSLSocketFactory(sslContext.getSocketFactory()); connection.setDoInput(true); connection.setDoOutput(true); From a684d93dcfd19dd20db0477ee3c678863cef55a5 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 10:12:01 +0100 Subject: [PATCH 32/46] FDP-94: Move non production properties to dev Signed-off-by: Jasper Kamerling --- .../src/main/resources/application-dev.yml | 57 ++++++++++++++++--- .../src/main/resources/application.yaml | 40 ------------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/application/src/main/resources/application-dev.yml b/application/src/main/resources/application-dev.yml index 98d3b87..9921998 100644 --- a/application/src/main/resources/application-dev.yml +++ b/application/src/main/resources/application-dev.yml @@ -1,10 +1,51 @@ logging: - level: - org: - gxf: - soapbridge: DEBUG + level: + org: + gxf: + soapbridge: DEBUG + spring: - kafka: - bootstrap-servers: localhost:9092 - consumer: - group-id: gxf-proxy + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: gxf-proxy + +proxy-server: + network-zone: IT + +security: + key-store: + location: /etc/ssl/certs + password: 1234 + type: pkcs12 + trust-store: + location: /etc/ssl/certs/trust.jks + password: 123456 + type: jks + signing: + key-type: RSA + sign-key-file: /etc/ssl/certs/proxy-server/sign-key.der + verify-key-file: /etc/ssl/certs/proxy-server/verify-key.der + provider: SunRsaSign + signature: SHA256withRSA + +topics: + calls: + requests: proxy-server-calls-requests + responses: proxy-server-calls-responses + notifications: + requests: proxy-server-notification-requests + responses: proxy-server-notification-responses + +soap: + notification: + host: localhost + port: 443 + protocol: https + platform: + host: localhost + port: 443 + protocol: https + hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES + time-out: 45 + custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index 0ddc75a..4f84fa6 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -3,43 +3,3 @@ management: web: exposure: include: prometheus - -proxy-server: - network-zone: IT - -security: - key-store: - location: /etc/ssl/certs - password: 1234 - type: pkcs12 - trust-store: - location: /etc/ssl/certs/trust.jks - password: 123456 - type: jks - signing: - key-type: RSA - sign-key-file: /etc/ssl/certs/proxy-server/sign-key.der - verify-key-file: /etc/ssl/certs/proxy-server/verify-key.der - provider: SunRsaSign - signature: SHA256withRSA - -topics: - calls: - requests: proxy-server-calls-requests - responses: proxy-server-calls-responses - notifications: - requests: proxy-server-notification-requests - responses: proxy-server-notification-responses - -soap: - notification: - host: localhost - port: 443 - protocol: https - platform: - host: localhost - port: 443 - protocol: https - hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES - time-out: 45 - custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 From a431a7becb7655ec11cb9c28b4082b9fe320f29f Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 10:13:02 +0100 Subject: [PATCH 33/46] FDP-94: Add todo's Signed-off-by: Jasper Kamerling --- .../application/factories/HttpsUrlConnectionFactory.java | 1 + .../java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java | 1 + .../configuration/properties/SoapConfigurationProperties.kt | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java index b25c9e0..4d4de32 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -76,6 +76,7 @@ public HttpsURLConnection createConnection( connection.setSSLSocketFactory(sslContext.getSocketFactory()); connection.setDoInput(true); connection.setDoOutput(true); + // TODO use constants for properties and method connection.setRequestMethod("POST"); connection.setRequestProperty( "Accept-Encoding", "text/xml;charset=" + StandardCharsets.UTF_8.name()); diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index f9a870c..5618160 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -255,6 +255,7 @@ private void createSuccessFulResponse(final HttpServletResponse response, final throws IOException { LOGGER.debug("Start - creating successful response"); response.setStatus(HttpServletResponse.SC_OK); + // TODO use constants for headers response.addHeader("SOAP-ACTION", ""); response.addHeader("Keep-Alive", "timeout=5, max=100"); response.addHeader("Accept", "text/xml"); diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt index b3597d6..90b5a9a 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt @@ -30,7 +30,7 @@ class SoapEndpointConfiguration( port: Int, protocol: String ) { - + // TODO Use java.net.URI class val hostAndPort = "$host:$port" val uri = "$protocol://${hostAndPort}" } From 6437e4389e47c7569341b58cb4b4678637c88ff5 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 10:37:08 +0100 Subject: [PATCH 34/46] FDP-94: Add azure oauth dependency Signed-off-by: Jasper Kamerling --- application/build.gradle.kts | 1 + build.gradle.kts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/application/build.gradle.kts b/application/build.gradle.kts index 9b58bcf..3c53a58 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-logging") implementation("org.springframework.kafka:spring-kafka") + implementation("com.gxf.utilities:kafka-azure-oauth:0.2") implementation("com.microsoft.azure:msal4j:1.13.10") implementation("org.apache.httpcomponents:httpclient:4.5.13") implementation(kotlin("reflect")) diff --git a/build.gradle.kts b/build.gradle.kts index 758b59e..5f2617f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,6 +39,14 @@ subprojects { repositories { mavenCentral() + maven { + name = "GXFGithubPackages" + url = uri("https://maven.pkg.github.com/osgp/*") + credentials { + username = project.findProperty("github.username") as String? ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("github.token") as String? ?: System.getenv("GITHUB_TOKEN") + } + } } extensions.configure { From d73a090ba93bed92c56f2008bef3003ce3611fc5 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 12:04:55 +0100 Subject: [PATCH 35/46] FDP-94: Make keystore optional Signed-off-by: Jasper Kamerling --- .../configuration/properties/SecurityConfigurationProperties.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt index 33b6cf0..2213040 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SecurityConfigurationProperties.kt @@ -17,7 +17,7 @@ import java.security.spec.X509EncodedKeySpec @ConfigurationProperties("security") class SecurityConfigurationProperties( - val keyStore: StoreConfigurationProperties, + val keyStore: StoreConfigurationProperties?, val trustStore: StoreConfigurationProperties, val signing: SigningConfigurationProperties ) From dda0e994fb0831b8aca1bab927a6e1b004d93366 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 12:05:56 +0100 Subject: [PATCH 36/46] FDP-94: Update properties Signed-off-by: Jasper Kamerling --- .../resources/application.yaml | 3 -- .../src/main/resources/application-dev.yml | 35 ++++++++----------- .../src/main/resources/application.yaml | 3 ++ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/application/src/integrationTest/resources/application.yaml b/application/src/integrationTest/resources/application.yaml index 8c4b888..496f5b5 100644 --- a/application/src/integrationTest/resources/application.yaml +++ b/application/src/integrationTest/resources/application.yaml @@ -21,9 +21,6 @@ wiremock: notification-proxy: url: "https://localhost:9001" -proxy-server: - network-zone: BOTH - security: key-store: location: src/integrationTest/resources/organisations/ diff --git a/application/src/main/resources/application-dev.yml b/application/src/main/resources/application-dev.yml index 9921998..99ecf0c 100644 --- a/application/src/main/resources/application-dev.yml +++ b/application/src/main/resources/application-dev.yml @@ -10,9 +10,6 @@ spring: consumer: group-id: gxf-proxy -proxy-server: - network-zone: IT - security: key-store: location: /etc/ssl/certs @@ -30,22 +27,20 @@ security: signature: SHA256withRSA topics: - calls: - requests: proxy-server-calls-requests - responses: proxy-server-calls-responses - notifications: - requests: proxy-server-notification-requests - responses: proxy-server-notification-responses + calls: + requests: proxy-server-calls-requests + responses: proxy-server-calls-responses + notifications: + requests: proxy-server-notification-requests + responses: proxy-server-notification-responses soap: - notification: - host: localhost - port: 443 - protocol: https - platform: - host: localhost - port: 443 - protocol: https - hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES - time-out: 45 - custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 + notification: + host: localhost + port: 443 + protocol: https + platform: + host: localhost + port: 443 + protocol: https + time-out: 45 diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index 4f84fa6..7f15a0d 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -3,3 +3,6 @@ management: web: exposure: include: prometheus + +soap: + hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES \ No newline at end of file From 6303c0f04927d366ef3f59ecaad72ccf3fdab272 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 12:10:05 +0100 Subject: [PATCH 37/46] FDP-94: Add todo Signed-off-by: Jasper Kamerling --- .../gxf/soapbridge/application/factories/SslContextFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java index 84993ba..04fb91e 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/SslContextFactory.java @@ -132,6 +132,7 @@ 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. + // TODO improve null safety final StoreConfigurationProperties keyStore = securityConfiguration.getKeyStore(); final String pathToKeyStore = String.format("%s/%s.pfx", keyStore.getLocation(), commonName); LOGGER.debug("Opening key store, pathToKeyStore: {}", pathToKeyStore); From b571d13a8ac8238ac01195fa7c8f80328dbe25b6 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 13:43:53 +0100 Subject: [PATCH 38/46] FDP-94: Set backoff to 2 retries Signed-off-by: Jasper Kamerling --- .../integrationTest/resources/application.yaml | 6 ------ .../configuration/kafka/KafkaConfiguration.kt | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 application/src/main/kotlin/org/gxf/soapbridge/configuration/kafka/KafkaConfiguration.kt diff --git a/application/src/integrationTest/resources/application.yaml b/application/src/integrationTest/resources/application.yaml index 496f5b5..a015445 100644 --- a/application/src/integrationTest/resources/application.yaml +++ b/application/src/integrationTest/resources/application.yaml @@ -15,12 +15,6 @@ server: trust-store-password: 123456 trust-store-type: PKCS12 -wiremock: - call-proxy: - url: "https://localhost:9000" - notification-proxy: - url: "https://localhost:9001" - security: key-store: location: src/integrationTest/resources/organisations/ diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/kafka/KafkaConfiguration.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/kafka/KafkaConfiguration.kt new file mode 100644 index 0000000..8075fd1 --- /dev/null +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/kafka/KafkaConfiguration.kt @@ -0,0 +1,18 @@ +package org.gxf.soapbridge.configuration.kafka + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.listener.DefaultErrorHandler +import org.springframework.util.backoff.FixedBackOff + +@Configuration +class KafkaConfiguration { + + /** + * Retry messages two times before giving up on the message + */ + @Bean + fun errorHandler(): DefaultErrorHandler { + return DefaultErrorHandler(FixedBackOff(0, 2L)) + } +} \ No newline at end of file From a794107b12a0d8ade03e2ab783e3f34c1a2b1990 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 13:44:07 +0100 Subject: [PATCH 39/46] FDP-94: Set timeout on integration test Signed-off-by: Jasper Kamerling --- .../integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt index de2a29e..c1a2325 100644 --- a/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt +++ b/application/src/integrationTest/kotlin/org/gxf/soapbridge/EndToEndTest.kt @@ -21,6 +21,7 @@ import org.springframework.http.client.reactive.JdkClientHttpConnector import org.springframework.kafka.test.context.EmbeddedKafka import org.springframework.web.reactive.function.client.WebClient import java.net.http.HttpClient +import java.time.Duration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -46,7 +47,7 @@ class EndToEndTest( } @Test - fun testRequestResponse(applicationContext: ApplicationContext) { + fun testRequestResponse() { // Arrange an SSL context for organisation "testClient" using its client certificate val sslContextForOrganisation = sslContextFactory.createSslContext("testClient") val httpClient = HttpClient.newBuilder() @@ -60,6 +61,7 @@ class EndToEndTest( val responseBody = webClient.post().uri(callUrl) .bodyValue(soapBody) .exchangeToMono { it.bodyToMono(String::class.java) } + .timeout(Duration.ofSeconds(10)) .block() // Assert From 575caabe8dcb6b859f2acccc0058146d573cdf14 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 13:44:13 +0100 Subject: [PATCH 40/46] FDP-94: Add todo Signed-off-by: Jasper Kamerling --- .../org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt index e6b16d0..364de6a 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/valueobjects/ProxyServerBaseMessage.kt @@ -16,6 +16,7 @@ abstract class ProxyServerBaseMessage(val connectionId: String) { /** Constructs a string separated by '~' from the fields of this instance. */ fun constructString() = getFieldsForMessage().joinToString(SEPARATOR, postfix = SEPARATOR) + // TODO possibly convert to jackson mapping /** Constructs a string separated by '~' from the fields of this instance followed by the signature. */ fun constructSignedString() = constructString() + signature From be6d1b0e8821df19f615697bbe18cd0afde82fee Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 13:56:49 +0100 Subject: [PATCH 41/46] FDP-94: Use map for custom timeouts Signed-off-by: Jasper Kamerling --- .../integrationTest/resources/application.yaml | 4 +++- .../soapbridge/soap/endpoints/SoapEndpoint.java | 15 ++------------- .../properties/SoapConfigurationProperties.kt | 2 +- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/application/src/integrationTest/resources/application.yaml b/application/src/integrationTest/resources/application.yaml index a015445..6865a9b 100644 --- a/application/src/integrationTest/resources/application.yaml +++ b/application/src/integrationTest/resources/application.yaml @@ -47,7 +47,9 @@ soap: protocol: https hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES time-out: 45 - custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 + custom-timeouts: + SetScheduleRequest: 180 + GetStatusRequest: 120 logging: level: diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 5618160..674ea2d 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -55,7 +55,7 @@ public class SoapEndpoint implements HttpRequestHandler { private final SigningService signingService; /** Map of time-outs for specific functions. */ - private final Map customTimeOutsMap = new HashMap<>(); + private final Map customTimeOutsMap; public SoapEndpoint( final ConnectionCacheService connectionCacheService, @@ -66,18 +66,7 @@ public SoapEndpoint( this.soapConfiguration = soapConfiguration; this.proxyRequestsSender = proxyRequestsSender; this.signingService = signingService; - } - - @PostConstruct - public void init() { - final String[] split = soapConfiguration.getCustomTimeouts().split(","); - for (int i = 0; i < split.length; i += 2) { - final String key = split[i]; - final Integer value = Integer.valueOf(split[i + 1]); - LOGGER.debug("Adding custom timeout with key: {} and value: {}", key, value); - customTimeOutsMap.put(key, value); - } - LOGGER.debug("Added {} custom timeouts to the map", customTimeOutsMap.size()); + this.customTimeOutsMap = soapConfiguration.getCustomTimeouts(); } /** Handles incoming SOAP requests. */ diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt index 90b5a9a..f47c8eb 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt @@ -17,7 +17,7 @@ class SoapConfigurationProperties( /** * Timeouts for specific functions. */ - val customTimeouts: String, + val customTimeouts: Map, val callEndpoint: SoapEndpointConfiguration, ) From 2e8cffc329a30f5b6ff98e0cd7901e6845863032 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 14:28:49 +0100 Subject: [PATCH 42/46] FDP-94: Use empty map Signed-off-by: Jasper Kamerling --- .../configuration/properties/SoapConfigurationProperties.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt index f47c8eb..ebe4675 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/configuration/properties/SoapConfigurationProperties.kt @@ -17,7 +17,7 @@ class SoapConfigurationProperties( /** * Timeouts for specific functions. */ - val customTimeouts: Map, + val customTimeouts: Map = emptyMap(), val callEndpoint: SoapEndpointConfiguration, ) From aaba60310d03f56fc0ba3d14b4833afe18ff842d Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Fri, 17 Nov 2023 14:29:27 +0100 Subject: [PATCH 43/46] FDP-94: Add Kafka listener concurrency configuration Signed-off-by: Jasper Kamerling --- .../resources/application.yaml | 18 ++++++++----- .../listeners/ProxyRequestKafkaListener.kt | 6 ++++- .../listeners/ProxyResponseKafkaListener.kt | 6 ++++- .../TopicsConfigurationProperties.kt | 15 ++++++----- .../kafka/senders/ProxyRequestKafkaSender.kt | 2 +- .../kafka/senders/ProxyResponseKafkaSender.kt | 2 +- .../src/main/resources/application-dev.yml | 26 ++++++++++--------- .../src/main/resources/application.yaml | 9 ++++++- .../soap/clients/SoapClientTest.java | 3 ++- 9 files changed, 57 insertions(+), 30 deletions(-) diff --git a/application/src/integrationTest/resources/application.yaml b/application/src/integrationTest/resources/application.yaml index 6865a9b..c46fea9 100644 --- a/application/src/integrationTest/resources/application.yaml +++ b/application/src/integrationTest/resources/application.yaml @@ -31,14 +31,20 @@ security: provider: SunRsaSign signature: SHA256withRSA -topics: -# short circuit configuration: topics are linked back to the same proxy instance +kafka: + # short circuit configuration: topics are linked back to the same proxy instance outgoing: - requests: requests - responses: responses + requests: + topic: requests + responses: + topic: responses incoming: - requests: requests - responses: responses + requests: + topic: requests + concurrency: 1 + responses: + topic: responses + concurrency: 1 soap: call-endpoint: diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt index 2ad3a92..a5aafa7 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyRequestKafkaListener.kt @@ -15,7 +15,11 @@ import org.springframework.stereotype.Component class ProxyRequestKafkaListener(private val platformCommunicationService: PlatformCommunicationService) { private val logger = KotlinLogging.logger { } - @KafkaListener(topics = ["\${topics.incoming.requests}"], id = "gxf-request-consumer") + @KafkaListener( + id = "gxf-request-consumer", + topics = ["\${kafka.incoming.requests.topic}"], + concurrency = "\${kafka.incoming.requests.concurrency}" + ) fun consume(record: ConsumerRecord) { logger.debug { "Received request: ${record.key()}, ${record.value()}" } val requestMessage = ProxyServerRequestMessage.createInstanceFromString(record.value()) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt index 95191bb..fe2d3e7 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/listeners/ProxyResponseKafkaListener.kt @@ -15,7 +15,11 @@ import org.springframework.stereotype.Component class ProxyResponseKafkaListener(private val clientCommunicationService: ClientCommunicationService) { private val logger = KotlinLogging.logger { } - @KafkaListener(topics = ["\${topics.incoming.responses}"], id = "gxf-response-consumer") + @KafkaListener( + id = "gxf-response-consumer", + topics = ["\${kafka.incoming.responses.topic}"], + concurrency = "\${kafka.incoming.responses.concurrency}" + ) fun consume(record: ConsumerRecord) { logger.debug { "Received response: ${record.key()}, ${record.value()}" } val responseMessage = ProxyServerResponseMessage.createInstanceFromString(record.value()) diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt index 2eb613f..1561a83 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/properties/TopicsConfigurationProperties.kt @@ -6,13 +6,16 @@ package org.gxf.soapbridge.kafka.properties import org.springframework.boot.context.properties.ConfigurationProperties -@ConfigurationProperties("topics") +@ConfigurationProperties("kafka") class TopicsConfigurationProperties( - val outgoing: RequestResponseTopics, - val incoming: RequestResponseTopics + val outgoing: OutgoingTopicsConfiguration, ) -class RequestResponseTopics( - val requests: String, - val responses: String +class OutgoingTopicsConfiguration( + val requests: OutgoingTopic, + val responses: OutgoingTopic ) + +class OutgoingTopic( + val topic: String +) \ No newline at end of file diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt index 9c620a1..96fc475 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyRequestKafkaSender.kt @@ -17,7 +17,7 @@ class ProxyRequestKafkaSender( ) { private val logger = KotlinLogging.logger {} - private val topic = topicConfiguration.outgoing.requests + private val topic = topicConfiguration.outgoing.requests.topic fun send(requestMessage: ProxyServerRequestMessage) { logger.debug { "SOAP payload: ${requestMessage.soapPayload} to $topic" } diff --git a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt index d97a38e..0841fd2 100644 --- a/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt +++ b/application/src/main/kotlin/org/gxf/soapbridge/kafka/senders/ProxyResponseKafkaSender.kt @@ -17,7 +17,7 @@ class ProxyResponseKafkaSender( ) { private val logger = KotlinLogging.logger {} - private val topic = topicConfiguration.outgoing.responses + private val topic = topicConfiguration.outgoing.responses.topic fun send(responseMessage: ProxyServerResponseMessage) { logger.debug { "SOAP payload: ${responseMessage.soapResponse} to $topic" } diff --git a/application/src/main/resources/application-dev.yml b/application/src/main/resources/application-dev.yml index 99ecf0c..427a305 100644 --- a/application/src/main/resources/application-dev.yml +++ b/application/src/main/resources/application-dev.yml @@ -26,20 +26,22 @@ security: provider: SunRsaSign signature: SHA256withRSA -topics: - calls: - requests: proxy-server-calls-requests - responses: proxy-server-calls-responses - notifications: - requests: proxy-server-notification-requests - responses: proxy-server-notification-responses +kafka: + outgoing: + requests: + topic: proxy-server-calls-requests + responses: + topic: proxy-server-calls-responses + incoming: + requests: + topic: proxy-server-notification-requests + concurrency: 1 + responses: + topic: proxy-server-notification-responses + concurrency: 1 soap: - notification: - host: localhost - port: 443 - protocol: https - platform: + call-endpoint: host: localhost port: 443 protocol: https diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index 7f15a0d..ba729f1 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -5,4 +5,11 @@ management: include: prometheus soap: - hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES \ No newline at end of file + hostname-verification-strategy: BROWSER_COMPATIBLE_HOSTNAMES + +kafka: + incoming: + requests: + concurrency: 1 + responses: + concurrency: 1 \ No newline at end of file diff --git a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java index 268a00a..20d4b89 100644 --- a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java +++ b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java @@ -9,6 +9,7 @@ import java.net.ConnectException; import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import javax.net.ssl.HttpsURLConnection; import org.gxf.soapbridge.application.factories.HttpsUrlConnectionFactory; import org.gxf.soapbridge.application.services.SigningService; @@ -35,7 +36,7 @@ class SoapClientTest { new SoapConfigurationProperties( HostnameVerificationStrategy.BROWSER_COMPATIBLE_HOSTNAMES, 45, - "", + new HashMap<>(), new SoapEndpointConfiguration("localhost", 443, "https")); @InjectMocks SoapClient soapClient; From 61b26eafa7828f7a1a227a2dadf8f06615610b2f Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Mon, 20 Nov 2023 10:01:26 +0100 Subject: [PATCH 44/46] FDP-94: Remove unknown/unused HTTP header Signed-off-by: Sander Verbruggen --- .../application/factories/HttpsUrlConnectionFactory.java | 1 - .../java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java index 4d4de32..f90b874 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -83,7 +83,6 @@ public HttpsURLConnection createConnection( 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"); diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 674ea2d..0cc31b5 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -5,7 +5,6 @@ import static org.springframework.security.web.context.RequestAttributeSecurityContextRepository.DEFAULT_REQUEST_ATTR_NAME; -import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -66,7 +65,7 @@ public SoapEndpoint( this.soapConfiguration = soapConfiguration; this.proxyRequestsSender = proxyRequestsSender; this.signingService = signingService; - this.customTimeOutsMap = soapConfiguration.getCustomTimeouts(); + customTimeOutsMap = soapConfiguration.getCustomTimeouts(); } /** Handles incoming SOAP requests. */ @@ -245,7 +244,6 @@ private void createSuccessFulResponse(final HttpServletResponse response, final LOGGER.debug("Start - creating successful response"); response.setStatus(HttpServletResponse.SC_OK); // TODO use constants for headers - response.addHeader("SOAP-ACTION", ""); response.addHeader("Keep-Alive", "timeout=5, max=100"); response.addHeader("Accept", "text/xml"); response.addHeader("Connection", "Keep-Alive"); From 04b7e43b51221451c0df69acd3c88e05395b889e Mon Sep 17 00:00:00 2001 From: Sander Verbruggen Date: Mon, 20 Nov 2023 10:23:37 +0100 Subject: [PATCH 45/46] FDP-94: Replaced stringified HTTP headers with constants Signed-off-by: Sander Verbruggen --- .../factories/HttpsUrlConnectionFactory.java | 16 ++++++++-------- .../soapbridge/soap/endpoints/SoapEndpoint.java | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java index f90b874..f6d81c8 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java +++ b/application/src/main/java/org/gxf/soapbridge/application/factories/HttpsUrlConnectionFactory.java @@ -13,6 +13,7 @@ import org.gxf.soapbridge.soap.exceptions.UnableToCreateHttpsURLConnectionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -76,18 +77,17 @@ public HttpsURLConnection createConnection( connection.setSSLSocketFactory(sslContext.getSocketFactory()); connection.setDoInput(true); connection.setDoOutput(true); - // TODO use constants for properties and method connection.setRequestMethod("POST"); connection.setRequestProperty( - "Accept-Encoding", "text/xml;charset=" + StandardCharsets.UTF_8.name()); - connection.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.name()); + HttpHeaders.ACCEPT_ENCODING, "text/xml;charset=" + StandardCharsets.UTF_8.name()); + connection.setRequestProperty(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name()); connection.setRequestProperty( - "Content-Type", "text/xml;charset=" + StandardCharsets.UTF_8.name()); - connection.setRequestProperty("Content-Length", contentLength); - connection.setRequestProperty("Host", host); - connection.setRequestProperty("Connection", "Keep-Alive"); + HttpHeaders.CONTENT_TYPE, "text/xml;charset=" + StandardCharsets.UTF_8.name()); + connection.setRequestProperty(HttpHeaders.CONTENT_LENGTH, contentLength); + connection.setRequestProperty(HttpHeaders.HOST, host); + connection.setRequestProperty(HttpHeaders.CONNECTION, "Keep-Alive"); connection.setRequestProperty( - "User-Agent", + HttpHeaders.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: {}", diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index 0cc31b5..c4163eb 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; @@ -243,10 +244,9 @@ private void createSuccessFulResponse(final HttpServletResponse response, final throws IOException { LOGGER.debug("Start - creating successful response"); response.setStatus(HttpServletResponse.SC_OK); - // TODO use constants for headers response.addHeader("Keep-Alive", "timeout=5, max=100"); - response.addHeader("Accept", "text/xml"); - response.addHeader("Connection", "Keep-Alive"); + response.addHeader(HttpHeaders.ACCEPT, "text/xml"); + response.addHeader(HttpHeaders.CONNECTION, "Keep-Alive"); response.setContentType("text/xml; charset=" + StandardCharsets.UTF_8.name()); response.getWriter().write(soap); LOGGER.debug("End - creating successful response"); From 17efe2224c0b16c94f0f4a5295427bdc6c4c3d81 Mon Sep 17 00:00:00 2001 From: Jasper Kamerling Date: Mon, 20 Nov 2023 17:31:04 +0100 Subject: [PATCH 46/46] FDP-94: Remove jms and gxf Signed-off-by: Jasper Kamerling --- .../services/ClientCommunicationService.java | 4 +- .../PlatformCommunicationService.java | 6 +- .../soap/endpoints/SoapEndpoint.java | 2 +- .../src/main/resources/proxy-server.yml | 140 ------------------ .../soap/clients/SoapClientTest.java | 2 +- 5 files changed, 7 insertions(+), 147 deletions(-) delete mode 100644 application/src/main/resources/proxy-server.yml diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java index 29fb25f..91a423f 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/ClientCommunicationService.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service; /** - * Service which handles SOAP responses from OSGP. The SOAP response will be set for the connection + * Service which handles SOAP responses from GXF. The SOAP response will be set for the connection * which correlates with the connection-id. */ @Service @@ -33,7 +33,7 @@ public ClientCommunicationService( /** * Process an incoming queue message. The content of the message has to be verified by the {@link - * SigningService}. Then a response from OSGP will set for the pending connection from a client. + * SigningService}. Then a response from GXF will set for the pending connection from a client. * * @param proxyServerResponseMessage The incoming queue message to process. */ diff --git a/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java b/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java index a8fe5f1..7b3e027 100644 --- a/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java +++ b/application/src/main/java/org/gxf/soapbridge/application/services/PlatformCommunicationService.java @@ -9,13 +9,13 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -/** Service which can send SOAP requests to OSGP. */ +/** Service which can send SOAP requests to GXF. */ @Service public class PlatformCommunicationService { private static final Logger LOGGER = LoggerFactory.getLogger(PlatformCommunicationService.class); - /** SOAP client used to sent request messages to OSGP. */ + /** SOAP client used to sent request messages to GXF. */ private final SoapClient soapClient; /** Service used to sign and/or verify the content of queue messages. */ @@ -29,7 +29,7 @@ public PlatformCommunicationService( /** * Process an incoming queue message. The content of the message has to be verified by the {@link - * SigningService}. Then a SOAP message can be sent to OSGP using {@link SoapClient}. + * SigningService}. Then a SOAP message can be sent to GXF using {@link SoapClient}. * * @param proxyServerRequestMessage The incoming queue message to process. */ diff --git a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java index c4163eb..88dfe75 100644 --- a/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java +++ b/application/src/main/java/org/gxf/soapbridge/soap/endpoints/SoapEndpoint.java @@ -80,7 +80,7 @@ public void handleRequest( logHeaderValues(request); logParameterValues(request); - // Get the context, which should be an OSGP SOAP end-point or a + // Get the context, which should be an GXF SOAP end-point or a // NOTIFICATION SOAP end-point. final String context = getContextForRequestType(request); LOGGER.debug("Context: {}", context); diff --git a/application/src/main/resources/proxy-server.yml b/application/src/main/resources/proxy-server.yml deleted file mode 100644 index 1533737..0000000 --- a/application/src/main/resources/proxy-server.yml +++ /dev/null @@ -1,140 +0,0 @@ -jms: - proxy: - server: - back: - 'off': - multiplier: 2 - initial: - redelivery: - delay: 60000 - maximum: - redeliveries: 3 - redelivery: - delay: 300000 - notification: - back: - 'off': - multiplier: 2 - initial: - redelivery: - delay: 60000 - maximum: - redeliveries: 3 - redelivery: - delay: 300000 - redelivery: - delay: 60000 - requests: - concurrent: - consumers: 10 - delivery: - persistent: false - explicit: - qos: - enabled: true - max: - concurrent: - consumers: 10 - receive: - timeout: 10 - time: - to: - live: 3600000 - responses: - concurrent: - consumers: 10 - delivery: - persistent: false - explicit: - qos: - enabled: true - max: - concurrent: - consumers: 10 - receive: - timeout: 10 - time: - to: - live: 3600000 - use: - exponential: - back: - 'off': true - redelivery: - delay: 60000 - requests: - concurrent: - consumers: 10 - delivery: - persistent: false - explicit: - qos: - enabled: true - max: - concurrent: - consumers: 10 - receive: - timeout: 10 - time: - to: - live: 3600000 - responses: - concurrent: - consumers: 10 - delivery: - persistent: false - explicit: - qos: - enabled: true - max: - concurrent: - consumers: 10 - receive: - timeout: 10 - time: - to: - live: 3600000 - use: - exponential: - back: - 'off': true - -topics: - calls: - requests: proxy-server-webapp-requests - responses: proxy-server-webapp-responses - notification: - requests: proxy-server-notification-requests - responses: proxy-server-notification-responses - -proxy-server: - custom-timeouts: SetScheduleRequest,180,GetStatusRequest,120 - network-zone: BOTH - notification: - host: localhost - port: 443 - protocol: https - platform: - host: localhost - port: 443 - protocol: https - security: - key-store: - location: /etc/ssl/certs - password: 1234 - type: pkcs12 - trust-store: - location: /etc/ssl/certs/trust.jks - password: 123456 - signing: - key-type: RSA - sign-key: /etc/ssl/certs/proxy-server/sign-key.der - verify-key: /etc/ssl/certs/proxy-server/verify-key.der - provider: SunRsaSign - signature: SHA256withRSA - time-out: 45 -soap: - client: - hostname: - verification: - strategy: BROWSER_COMPATIBLE_HOSTNAMES diff --git a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java index 20d4b89..0340a52 100644 --- a/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java +++ b/application/src/test/java/org/gxf/soapbridge/soap/clients/SoapClientTest.java @@ -42,7 +42,7 @@ class SoapClientTest { @InjectMocks SoapClient soapClient; @Test - void shouldSendSoapRequestAndJmsResponse() throws Exception { + void shouldSendSoapRequestAndKafkaResponse() throws Exception { // arrange final HttpsURLConnection connection = setupConnectionMock(); Mockito.when(