diff --git a/src/client/build.gradle.kts b/src/client/build.gradle.kts index a26fd7e..a4b6159 100644 --- a/src/client/build.gradle.kts +++ b/src/client/build.gradle.kts @@ -7,6 +7,9 @@ dependencies { api(project(":rest")) testImplementation(libs.bundles.testImplementation) + testImplementation(libs.org.wiremock.wiremock) + implementation(libs.org.assertj.assertjCore) + } diff --git a/src/client/src/main/java/org/niis/xrd4j/client/util/ClientUtil.java b/src/client/src/main/java/org/niis/xrd4j/client/util/ClientUtil.java index 0dd63b6..b5a0400 100644 --- a/src/client/src/main/java/org/niis/xrd4j/client/util/ClientUtil.java +++ b/src/client/src/main/java/org/niis/xrd4j/client/util/ClientUtil.java @@ -69,6 +69,7 @@ private ClientUtil() { */ public static void doTrustToCertificates() throws XRd4JException { // Create a trust manager that does not validate certificate chains + @SuppressWarnings("java:S4830") // Sonar rule: "Server certificates should be verified during SSL/TLS connections" TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @@ -91,7 +92,7 @@ public void checkClientTrusted(X509Certificate[] certs, String authType) throws try { // Install the all-trusting trust manager - SSLContext sc = SSLContext.getInstance("SSL"); + SSLContext sc = SSLContext.getInstance("TLSv1.2"); sc.init(null, trustAllCerts, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HostnameVerifier hv = (String urlHostName, SSLSession session) -> { diff --git a/src/client/src/test/java/org/niis/xrd4j/client/util/ClientUtilTest.java b/src/client/src/test/java/org/niis/xrd4j/client/util/ClientUtilTest.java new file mode 100644 index 0000000..75cd42d --- /dev/null +++ b/src/client/src/test/java/org/niis/xrd4j/client/util/ClientUtilTest.java @@ -0,0 +1,95 @@ +/* + * The MIT License + * Copyright © 2018 Nordic Institute for Interoperability Solutions (NIIS) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.niis.xrd4j.client.util; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocketFactory; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static jakarta.servlet.http.HttpServletResponse.SC_OK; +import static org.assertj.core.api.Assertions.assertThat; + +@WireMockTest(httpsEnabled = true) +class ClientUtilTest { + + private SSLSocketFactory defaultSSLSocketFactory; + private HostnameVerifier defaultHostnameVerifier; + + @BeforeEach + void setUp() { + defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); + defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); + } + + @AfterEach + void tearDown() { + HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); + HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier); + } + + + @Test + void certificateNotTrusted(WireMockRuntimeInfo wm) throws Exception { + stubFor(get("/").willReturn(ok())); + + URL url = new URL(wm.getHttpsBaseUrl()); + var connection = (HttpURLConnection) url.openConnection(); + + Assertions.assertThatThrownBy(connection::getResponseCode) + .isInstanceOf(SSLHandshakeException.class) + .hasMessageStartingWith("PKIX path building failed"); + + } + + @Test + void doTrustToCertificates(WireMockRuntimeInfo wm) throws Exception { + stubFor(get("/").willReturn(ok())); + + ClientUtil.doTrustToCertificates(); + + var responseCode = getResponseCode(wm.getHttpsBaseUrl()); + + assertThat(responseCode).isEqualTo(SC_OK); + } + + private int getResponseCode(String url) throws IOException { + var connection = (HttpURLConnection) new URL(url).openConnection(); + return connection.getResponseCode(); + } + +} diff --git a/src/common/src/main/java/org/niis/xrd4j/common/security/SymmetricEncrypter.java b/src/common/src/main/java/org/niis/xrd4j/common/security/SymmetricEncrypter.java index ccdc6e8..5549609 100644 --- a/src/common/src/main/java/org/niis/xrd4j/common/security/SymmetricEncrypter.java +++ b/src/common/src/main/java/org/niis/xrd4j/common/security/SymmetricEncrypter.java @@ -89,6 +89,7 @@ public SymmetricEncrypter(Key key, byte[] iv, String transformation) { * @throws BadPaddingException if there's an error */ @Override + @SuppressWarnings("java:S3329") protected byte[] encrypt(byte[] plaintext) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { Cipher cipher = Cipher.getInstance(this.transformation); diff --git a/src/gradle/libs.versions.toml b/src/gradle/libs.versions.toml index 9cc9b19..6373897 100644 --- a/src/gradle/libs.versions.toml +++ b/src/gradle/libs.versions.toml @@ -25,6 +25,7 @@ org-apache-tomcat-embed-core = { group = "org.apache.tomcat.embed", name = "tomc org-assertj-assertjCore = { module = "org.assertj:assertj-core", version.ref = "assertj" } org-xmlunit-xmlunitAssertj3 = { module = "org.xmlunit:xmlunit-assertj3", version.ref = "xmlunit" } org-xmlunit-xmlunitPlaceholders = { module = "org.xmlunit:xmlunit-placeholders", version.ref = "xmlunit" } +org-wiremock-wiremock = { module = "org.wiremock:wiremock", version = "3.9.1" } licenseGradlePlugin = { module = "gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin", version = "0.16.1" } dependencyCheckGradlePlugin = { module = "org.owasp:dependency-check-gradle", version = "10.0.4" } diff --git a/src/server/src/main/java/org/niis/xrd4j/server/AbstractAdapterServlet.java b/src/server/src/main/java/org/niis/xrd4j/server/AbstractAdapterServlet.java index 7c07158..6f497b3 100644 --- a/src/server/src/main/java/org/niis/xrd4j/server/AbstractAdapterServlet.java +++ b/src/server/src/main/java/org/niis/xrd4j/server/AbstractAdapterServlet.java @@ -36,6 +36,7 @@ import org.niis.xrd4j.server.utils.AdapterUtils; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -133,12 +134,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) if (contentTypeMatches(requestContentType, Constants.TEXT_XML)) { // Regular SOAP message without attachments LOGGER.info("Request's content type is \"{}\".", Constants.TEXT_XML); - soapRequest = SOAPHelper.toSOAP(request.getInputStream()); + soapRequest = SOAPHelper.toSOAP(getInputStream(request)); } else if (contentTypeMatches(requestContentType, Constants.MULTIPART_RELATED)) { // SOAP message with attachments LOGGER.info("Request's content type is \"{}\".", Constants.MULTIPART_RELATED); MimeHeaders mh = AdapterUtils.getHeaders(request); - soapRequest = SOAPHelper.toSOAP(request.getInputStream(), mh); + soapRequest = SOAPHelper.toSOAP(getInputStream(request), mh); LOGGER.trace(AdapterUtils.getAttachmentsInfo(soapRequest)); } else { // Invalid content type -> message is not processed @@ -149,7 +150,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) // Conversion has failed if soapRequest is null. Return SOAP Fault. if (soapRequest == null) { LOGGER.warn("Unable to deserialize the request to SOAP. SOAP Fault is returned."); - LOGGER.trace("Incoming message : \"{}\"", request.getInputStream().toString()); + LOGGER.trace("Incoming message : \"{}\"", getInputStream(request)); ErrorMessage errorMessage = new ErrorMessage(FAULT_CODE_CLIENT, errString, "", ""); soapResponse = this.errorToSOAP(errorMessage, null); } @@ -174,6 +175,15 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) writeResponse(soapResponse, response); } + private ServletInputStream getInputStream(HttpServletRequest request) { + try { + return request.getInputStream(); + } catch (IOException e) { + LOGGER.error("Error getting InputStream from request", e); + return null; + } + } + private boolean contentTypeMatches(String contentType, String expected) { return contentType != null && contentType.toLowerCase().startsWith(expected); }