diff --git a/.github/workflows/ECC.yml b/.github/workflows/ECC.yml index 221e6b39..4d594b00 100644 --- a/.github/workflows/ECC.yml +++ b/.github/workflows/ECC.yml @@ -15,10 +15,10 @@ jobs: DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '11' @@ -40,7 +40,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -74,7 +74,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -108,7 +108,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -150,7 +150,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -184,7 +184,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -218,7 +218,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -252,7 +252,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -286,7 +286,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -320,7 +320,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -354,7 +354,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh @@ -388,7 +388,7 @@ jobs: TRUSTSTORE_PASSWORD_DOCKER: ${{secrets.TRUSTSTORE_PASSWORD_DOCKER}} steps: - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run env setup run: ./ci/setupEnv.sh diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 7d4ecac4..62f6eff2 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -15,9 +15,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' diff --git a/.github/workflows/maven_release.yml b/.github/workflows/maven_release.yml index 99f8799e..d29da578 100644 --- a/.github/workflows/maven_release.yml +++ b/.github/workflows/maven_release.yml @@ -15,9 +15,9 @@ jobs: if: "!contains(github.event.head_commit.message, '[maven-release-plugin]')" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4418b5bd..4c89a4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.14.9] - 2024-07-11 + +### Added + + - CORS configuration (for compatibility with UI) + +### Changed + + - Updated firewall.properties + - Multipart message library upgraded to 1.0.18 + - Websocket library upgraded to 1.0.18 + - Fix WSS SSL support + - Upgrade GHA to use Node.js 20 + + ## [1.14.8] - 2024-02-14 ### Added diff --git a/README.md b/README.md index cde20b9f..0a502ed5 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,35 @@ allowUrlEncodedPeriod=true ``` *IMPORTANT:* If you're not an expert, the strong advice is to keep values at their default values. If you decide to change values, pay special attention to allowHeaderNames and allowHeaderValues, since those set values are exclusive and considered as only values that should be present in the header. +## CORS Configuration + +In order to communicate with UI, CORS (Cross-Origin Resource Sharing) settings should be configured in `application.properties` file. This allows you to specify which origins, methods, and headers are permitted when making cross-origin requests to your application. + +``` +application.cors.allowed.origins= +application.cors.allowed.methods= +application.cors.allowed.headers= +``` + + - `application.cors.allowed.origins`: Specifies the allowed origins. If empty, all origins (*) are allowed. + - `application.cors.allowed.methods`: Specifies the allowed HTTP methods. If empty, all methods (*) are allowed. + - `application.cors.allowed.header`s: Specifies the allowed headers. If empty, all headers (*) are allowed. + + Example configuration: + + ``` + # Allow specific origins +application.cors.allowed.origins=https://example.com,https://another-example.com + +# Allow specific HTTP methods +application.cors.allowed.methods=GET,POST,PUT,DELETE + +# Allow specific headers +application.cors.allowed.headers= + ``` + + + ## How to Test The reachability could be verified using the following endpoints: * **http://{IP_ADDRESS}:{HTTP_PUBLIC_PORT}/about/version** diff --git a/ci/docker/be-dataapp_resources/application-docker.properties b/ci/docker/be-dataapp_resources/application-docker.properties index c618c505..39f3291d 100644 --- a/ci/docker/be-dataapp_resources/application-docker.properties +++ b/ci/docker/be-dataapp_resources/application-docker.properties @@ -61,3 +61,11 @@ application.security.password=$2a$10$MQ5grDaIqDpBjMlG78PFduv.AMRe9cs0CNm/V4cgUub #checkSum verification - true | false application.verifyCheckSum=false + +#CORS configuration +#Allow specific origins +application.cors.allowed.origins= +#Allow specific HTTP methods +application.cors.allowed.methods= +#Allow specific headers +application.cors.allowed.headers= diff --git a/ci/docker/be-dataapp_resources/firewall.properties b/ci/docker/be-dataapp_resources/firewall.properties index 6d92b820..35c3d700 100644 --- a/ci/docker/be-dataapp_resources/firewall.properties +++ b/ci/docker/be-dataapp_resources/firewall.properties @@ -3,7 +3,7 @@ allowedHeaderNames= #Set which values in header names should have the exact value and allowed (if want to allow any values keep it empty) allowedHeaderValues= #Set which HTTP methods should be allowed (if want to allow all header names, keep it empty) -allowedMethods=GET,POST +allowedMethods=GET,POST,OPTIONS #Set if a backslash "\" or a URL encoded backslash "%5C" should be allowed in the path or not allowBackSlash=true #Set if a slash "/" that is URL encoded "%2F" should be allowed in the path or not diff --git a/ci/docker/ecc_resources_consumer/application-docker.properties b/ci/docker/ecc_resources_consumer/application-docker.properties index 6a4b3bcd..82f024b7 100644 --- a/ci/docker/ecc_resources_consumer/application-docker.properties +++ b/ci/docker/ecc_resources_consumer/application-docker.properties @@ -196,5 +196,13 @@ spring.h2.console.enabled=true spring.datasource.username=sa spring.datasource.password=file_password password +#CORS configuration +#Allow specific origins +application.cors.allowed.origins= +#Allow specific HTTP methods +application.cors.allowed.methods= +#Allow specific headers +application.cors.allowed.headers= + #For logging the response over WSS set to DEBUG, else leave empty #logging.level.it.eng.idsa.businesslogic.processor.receiver= diff --git a/ci/docker/ecc_resources_consumer/firewall.properties b/ci/docker/ecc_resources_consumer/firewall.properties index 6d92b820..35c3d700 100644 --- a/ci/docker/ecc_resources_consumer/firewall.properties +++ b/ci/docker/ecc_resources_consumer/firewall.properties @@ -3,7 +3,7 @@ allowedHeaderNames= #Set which values in header names should have the exact value and allowed (if want to allow any values keep it empty) allowedHeaderValues= #Set which HTTP methods should be allowed (if want to allow all header names, keep it empty) -allowedMethods=GET,POST +allowedMethods=GET,POST,OPTIONS #Set if a backslash "\" or a URL encoded backslash "%5C" should be allowed in the path or not allowBackSlash=true #Set if a slash "/" that is URL encoded "%2F" should be allowed in the path or not diff --git a/ci/docker/ecc_resources_provider/application-docker.properties b/ci/docker/ecc_resources_provider/application-docker.properties index 8fd190ba..c034758c 100644 --- a/ci/docker/ecc_resources_provider/application-docker.properties +++ b/ci/docker/ecc_resources_provider/application-docker.properties @@ -199,5 +199,13 @@ spring.h2.console.enabled=true spring.datasource.username=sa spring.datasource.password=file_password password +#CORS configuration +#Allow specific origins +application.cors.allowed.origins= +#Allow specific HTTP methods +application.cors.allowed.methods= +#Allow specific headers +application.cors.allowed.headers= + #For logging the response over WSS set to DEBUG, else leave empty #logging.level.it.eng.idsa.businesslogic.processor.receiver= diff --git a/ci/docker/ecc_resources_provider/firewall.properties b/ci/docker/ecc_resources_provider/firewall.properties index 6d92b820..35c3d700 100644 --- a/ci/docker/ecc_resources_provider/firewall.properties +++ b/ci/docker/ecc_resources_provider/firewall.properties @@ -3,7 +3,7 @@ allowedHeaderNames= #Set which values in header names should have the exact value and allowed (if want to allow any values keep it empty) allowedHeaderValues= #Set which HTTP methods should be allowed (if want to allow all header names, keep it empty) -allowedMethods=GET,POST +allowedMethods=GET,POST,OPTIONS #Set if a backslash "\" or a URL encoded backslash "%5C" should be allowed in the path or not allowBackSlash=true #Set if a slash "/" that is URL encoded "%2F" should be allowed in the path or not diff --git a/ci/docker/test-cases/https-https-form/HTTPS-HTTPS-form-API.json b/ci/docker/test-cases/https-https-form/HTTPS-HTTPS-form-API.json index 612eed3a..c6febbc6 100644 --- a/ci/docker/test-cases/https-https-form/HTTPS-HTTPS-form-API.json +++ b/ci/docker/test-cases/https-https-form/HTTPS-HTTPS-form-API.json @@ -108,7 +108,7 @@ } ] }, - "method": "OPTIONS", + "method": "PUT", "header": [ { "key": "fizz", diff --git a/pom.xml b/pom.xml index d7505762..a72b5853 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 3.19.0 9.0.76 ${basedir} - 1.0.17 + 1.0.18 0.5.2 0.8.8 diff --git a/src/main/java/it/eng/idsa/businesslogic/configuration/EccFirewall.java b/src/main/java/it/eng/idsa/businesslogic/configuration/EccFirewall.java index 17f9ac4f..d125ef22 100644 --- a/src/main/java/it/eng/idsa/businesslogic/configuration/EccFirewall.java +++ b/src/main/java/it/eng/idsa/businesslogic/configuration/EccFirewall.java @@ -38,11 +38,11 @@ public class EccFirewall { private boolean allowUrlEncodedPercent; @Value("${allowUrlEncodedPeriod}") private boolean allowUrlEncodedPeriod; - + @Bean @ConditionalOnProperty(name = "application.firewall.isEnabled", havingValue = "true", matchIfMissing = false) RequestRejectedHandler requestRejectedHandler() { - return new HttpStatusRequestRejectedHandler(HttpStatus.METHOD_NOT_ALLOWED.value()); + return new HttpStatusRequestRejectedHandler(HttpStatus.METHOD_NOT_ALLOWED.value()); } @Bean @@ -53,13 +53,19 @@ public StrictHttpFirewall httpFirewall() { if (!CollectionUtils.isEmpty(allowedHeaderNames)) { Predicate headerNamePredicate = createAllowedHeaderNamesPredicate(allowedHeaderNames); firewall.setAllowedHeaderNames(headerNamePredicate); + } else { + firewall.setAllowedHeaderNames((header) -> true); } if (!allowedHeaderValues.isEmpty()) { Predicate headerNameValuePredicate = createAllowedHeaderValuesPredicate(allowedHeaderValues); firewall.setAllowedHeaderValues(headerNameValuePredicate); + } else { + firewall.setAllowedHeaderValues((header) -> true); } if (!CollectionUtils.isEmpty(allowedMethods)) { firewall.setAllowedHttpMethods(allowedMethods); + } else { + firewall.setUnsafeAllowAnyHttpMethod(true); } firewall.setAllowBackSlash(allowBackSlash); firewall.setAllowUrlEncodedSlash(allowUrlEncodedSlash); @@ -70,7 +76,7 @@ public StrictHttpFirewall httpFirewall() { return firewall; } - + private Predicate createAllowedHeaderNamesPredicate(List allowedHeaderNames) { return allowedHeaderNames.stream().collect(Collectors.toSet())::contains; } diff --git a/src/main/java/it/eng/idsa/businesslogic/configuration/SecurityConfig.java b/src/main/java/it/eng/idsa/businesslogic/configuration/SecurityConfig.java index 5e7ad9d5..c24023d9 100644 --- a/src/main/java/it/eng/idsa/businesslogic/configuration/SecurityConfig.java +++ b/src/main/java/it/eng/idsa/businesslogic/configuration/SecurityConfig.java @@ -1,6 +1,10 @@ package it.eng.idsa.businesslogic.configuration; - + +import java.util.Arrays; + +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; @@ -12,6 +16,9 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import it.eng.idsa.businesslogic.util.TrueConnectorConstants; @@ -25,34 +32,35 @@ @EnableWebSecurity @EnableScheduling public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${application.cors.allowed.origins:}") + private String allowedOrigins; + + @Value("${application.cors.allowed.methods:}") + private String allowedMethods; + + @Value("${application.cors.allowed.headers:}") + private String allowedHeaders; + @Autowired private UserDetailsService inMemoryUserCrudService; - + @Override - public void configure(WebSecurity web) throws Exception { + public void configure(WebSecurity web) throws Exception { // allow Swagger UI to be displayed without asking credentials in browser - web.ignoring().antMatchers("/swagger-ui.html", "/swagger-ui/**"); - } - + web.ignoring().antMatchers("/swagger-ui.html", "/swagger-ui/**"); + } + @Override protected void configure(HttpSecurity http) throws Exception { - http - .userDetailsService(inMemoryUserCrudService) - .cors() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .authorizeRequests() - .antMatchers("/about/**").permitAll() - .antMatchers("/error").permitAll() - .antMatchers("/").permitAll() - .antMatchers("/api/**").hasRole(TrueConnectorConstants.API_USER_ROLE) - .anyRequest().authenticated() - .and() - .csrf().disable() - .httpBasic() - .authenticationEntryPoint(authenticationEntryPoint()); + http.cors().and() + .csrf().disable() + .httpBasic().authenticationEntryPoint(authenticationEntryPoint()).and() + .userDetailsService(inMemoryUserCrudService).sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests() + .antMatchers("/about/**").permitAll().antMatchers("/error").permitAll().antMatchers("/").permitAll() + .antMatchers("/api/**").hasRole(TrueConnectorConstants.API_USER_ROLE).anyRequest().authenticated(); + http.headers().xssProtection(); } @@ -62,4 +70,31 @@ public AuthenticationEntryPoint authenticationEntryPoint() { entryPoint.setRealmName("api realm"); return entryPoint; } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + if (StringUtils.isBlank(allowedOrigins)) { + configuration.addAllowedOrigin("*"); + } else { + configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); + } + + if (StringUtils.isBlank(allowedMethods)) { + configuration.addAllowedMethod("*"); + } else { + configuration.setAllowedMethods(Arrays.asList(allowedMethods.split(","))); + } + + if (StringUtils.isBlank(allowedHeaders)) { + configuration.addAllowedHeader("*"); + } else { + configuration.setAllowedHeaders(Arrays.asList(allowedHeaders.split(","))); + } + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } \ No newline at end of file diff --git a/src/main/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSender.java b/src/main/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSender.java index 2546bcf6..c79d3a77 100644 --- a/src/main/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSender.java +++ b/src/main/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSender.java @@ -5,12 +5,9 @@ import java.io.IOException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.concurrent.ExecutionException; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; import javax.validation.constraints.NotNull; import org.apache.http.ParseException; @@ -21,6 +18,7 @@ import org.asynchttpclient.ws.WebSocketUpgradeHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import de.fraunhofer.iais.eis.Message; @@ -30,27 +28,31 @@ import it.eng.idsa.multipart.builder.MultipartMessageBuilder; import it.eng.idsa.multipart.domain.MultipartMessage; import it.eng.idsa.multipart.processor.MultipartMessageProcessor; +import it.eng.idsa.businesslogic.service.impl.TLSProvider; /** - * @author Antonio Scatoloni + * Author: Antonio Scatoloni */ @Component public class MessageWebSocketOverHttpSender { private static final Logger logger = LoggerFactory.getLogger(MessageWebSocketOverHttpSender.class); -// private WebSocketClientConfiguration webSocketClientConfiguration; private RejectionMessageService rejectionMessageService; private FileStreamingBean fileStreamingBean; private ResponseMessageBufferClient responseMessageBufferClient; private InputStreamSocketListenerClient inputStreamSocketListenerWebSocketClient; + private TLSProvider tlsProvider; + public MessageWebSocketOverHttpSender(RejectionMessageService rejectionMessageService, FileStreamingBean fileStreamingBean, - ResponseMessageBufferClient responseMessageBufferClient, InputStreamSocketListenerClient inputStreamSocketListenerWebSocketClient) { - this.rejectionMessageService = rejectionMessageService; - this.fileStreamingBean = fileStreamingBean; - this.responseMessageBufferClient = responseMessageBufferClient; - this.inputStreamSocketListenerWebSocketClient = inputStreamSocketListenerWebSocketClient; + ResponseMessageBufferClient responseMessageBufferClient, InputStreamSocketListenerClient inputStreamSocketListenerWebSocketClient, + TLSProvider tlsProvider) { + this.rejectionMessageService = rejectionMessageService; + this.fileStreamingBean = fileStreamingBean; + this.responseMessageBufferClient = responseMessageBufferClient; + this.inputStreamSocketListenerWebSocketClient = inputStreamSocketListenerWebSocketClient; + this.tlsProvider = tlsProvider; } public String sendMultipartMessageWebSocketOverHttps(String webSocketHost, Integer webSocketPort, String header, String payload) @@ -73,7 +75,6 @@ public String sendMultipartMessageWebSocketOverHttps(String webSocketHost, Integ return doSendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, webSocketPath, header, payload, message); } - private String doSendMultipartMessageWebSocketOverHttps(String webSocketHost, Integer webSocketPort, String webSocketPath, String header, String payload, Message message) throws ParseException, IOException, KeyManagementException, NoSuchAlgorithmException, InterruptedException, ExecutionException { @@ -81,15 +82,11 @@ private String doSendMultipartMessageWebSocketOverHttps(String webSocketHost, In .withHeaderContent(header) .withPayloadContent(payload) .build(); - //TODO Use this implementation with includeHttpHeaders set to false, but in future implementations these headers may be mandatory String multipartMessageString = MultipartMessageProcessor.multipartMessagetoString(multipartMessage, false, Boolean.TRUE); - -// FileStreamingBean fileStreamingBean = webSocketClientConfiguration.fileStreamingWebSocket(); + WebSocket wsClient = createWebSocketClient(webSocketHost, webSocketPort, webSocketPath, message); - // Try to connect to the Server. Wait until you are not connected to the server. fileStreamingBean.setup(wsClient); fileStreamingBean.sendMultipartMessage(multipartMessageString); - // We don't have status of the response (is it 200 OK or not). We have only the content of the response. String responseMessage = new String(responseMessageBufferClient.remove()); closeWSClient(wsClient, message); logger.info("Response is received"); @@ -105,7 +102,6 @@ private WebSocket createWebSocketClient(String webSocketHost, Integer webSocketP final SslEngineFactory ssl = getSslEngineFactory(); DefaultAsyncHttpClientConfig clientConfig = new DefaultAsyncHttpClientConfig.Builder() - .setDisableHttpsEndpointIdentificationAlgorithm(true) .setUseOpenSsl(true) .setSslEngineFactory(ssl) .build(); @@ -129,33 +125,12 @@ private WebSocket createWebSocketClient(String webSocketHost, Integer webSocketP @NotNull private SslEngineFactory getSslEngineFactory() throws NoSuchAlgorithmException, KeyManagementException { - final TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, - String authType) throws CertificateException { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, - String authType) throws CertificateException { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new java.security.cert.X509Certificate[0]; - } - } - }; - // Install the all-trusting trust manager - final SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + SSLContext sslContext = SSLContext.getInstance("TLSv1.3"); + sslContext.init(tlsProvider.getKeyManagers(), tlsProvider.getTrustManagers(), new java.security.SecureRandom()); return new JsseSslEngineFactory(sslContext); } - private void closeWSClient(WebSocket wsClient, Message message) { - // Send the close frame 1000 (CLOSE), "Shutdown"; in this method we also close the wsClient. try { wsClient.sendCloseFrame(1000, "Shutdown"); } catch (Exception e) { @@ -164,5 +139,4 @@ private void closeWSClient(WebSocket wsClient, Message message) { rejectionMessageService.sendRejectionMessage(message, RejectionReason.INTERNAL_RECIPIENT_ERROR); } } - } diff --git a/src/main/resources/application-RECEIVER.properties b/src/main/resources/application-RECEIVER.properties index 1c4669ab..2e51e0eb 100644 --- a/src/main/resources/application-RECEIVER.properties +++ b/src/main/resources/application-RECEIVER.properties @@ -212,5 +212,13 @@ springdoc.packagesToScan=it.eng.idsa.businesslogic.web.rest springdoc.pathsToMatch=/** springdoc.writer-with-default-pretty-printer=true +# CORS configuration +# Allow specific origins +application.cors.allowed.origins= +# Allow specific HTTP methods +application.cors.allowed.methods= +# Allow specific headers +application.cors.allowed.headers= + #For logging the response over WSS set to DEBUG, else leave empty #logging.level.it.eng.idsa.businesslogic.processor.receiver= \ No newline at end of file diff --git a/src/main/resources/application-SENDER.properties b/src/main/resources/application-SENDER.properties index c52254dd..2426bd15 100644 --- a/src/main/resources/application-SENDER.properties +++ b/src/main/resources/application-SENDER.properties @@ -214,5 +214,13 @@ springdoc.packagesToScan=it.eng.idsa.businesslogic.web.rest springdoc.pathsToMatch=/** springdoc.writer-with-default-pretty-printer=true +# CORS configuration +# Allow specific origins +application.cors.allowed.origins= +# Allow specific HTTP methods +application.cors.allowed.methods= +# Allow specific headers +application.cors.allowed.headers= + #For logging the response over WSS set to DEBUG, else leave empty #logging.level.it.eng.idsa.businesslogic.processor.sender.websocket.client= \ No newline at end of file diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index aaf39c5f..ee4ecaa8 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -179,5 +179,13 @@ application.selfdescription.filelocation=/ application.selfdescription.inboundModelVersion=4.0.0,4.1.0,4.1.2,4.2.0,4.2.1,4.2.2,4.2.3,4.2.4,4.2.5,4.2.6,4.2.7 application.selfdescription.defaultEndpoint= +# CORS configuration +# Allow specific origins +application.cors.allowed.origins= +# Allow specific HTTP methods +application.cors.allowed.methods= +# Allow specific headers +application.cors.allowed.headers= + #For logging the response over WSS set to DEBUG, else leave empty #logging.level.it.eng.idsa.businesslogic.processor.receiver= diff --git a/src/main/resources/firewall.properties b/src/main/resources/firewall.properties index 6d92b820..35c3d700 100644 --- a/src/main/resources/firewall.properties +++ b/src/main/resources/firewall.properties @@ -3,7 +3,7 @@ allowedHeaderNames= #Set which values in header names should have the exact value and allowed (if want to allow any values keep it empty) allowedHeaderValues= #Set which HTTP methods should be allowed (if want to allow all header names, keep it empty) -allowedMethods=GET,POST +allowedMethods=GET,POST,OPTIONS #Set if a backslash "\" or a URL encoded backslash "%5C" should be allowed in the path or not allowBackSlash=true #Set if a slash "/" that is URL encoded "%2F" should be allowed in the path or not diff --git a/src/test/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSenderTest.java b/src/test/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSenderTest.java index f34226bf..ebf3ffac 100644 --- a/src/test/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSenderTest.java +++ b/src/test/java/it/eng/idsa/businesslogic/processor/sender/websocket/client/MessageWebSocketOverHttpSenderTest.java @@ -2,14 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Enumeration; import java.util.concurrent.ExecutionException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + import org.apache.http.ParseException; import org.asynchttpclient.ws.WebSocket; import org.junit.jupiter.api.AfterEach; @@ -19,7 +30,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.test.util.ReflectionTestUtils; @@ -27,108 +37,142 @@ import it.eng.idsa.businesslogic.processor.receiver.websocket.server.HttpWebSocketMessagingServletA; import it.eng.idsa.businesslogic.processor.receiver.websocket.server.HttpWebSocketServerBean; import it.eng.idsa.businesslogic.service.RejectionMessageService; +import it.eng.idsa.businesslogic.service.impl.TLSProvider; import it.eng.idsa.multipart.util.UtilMessageService; public class MessageWebSocketOverHttpSenderTest { - @InjectMocks - private MessageWebSocketOverHttpSender sender; + @InjectMocks + private MessageWebSocketOverHttpSender sender; - @Mock - private RejectionMessageService rejectionMessageService; - @Mock + @Mock + private RejectionMessageService rejectionMessageService; + @Mock private FileStreamingBean fileStreamingBean; - - private ResponseMessageBufferClient responseMessageBufferClient; - private InputStreamSocketListenerClient inputStreamSocketListenerClient; - - private ResourceLoader resourceLoader; - @Mock - private Resource resourceKeyStore; - - private HttpWebSocketServerBean server; - - private String webSocketHost = "localhost"; - private Integer webSocketPort = 1234; - private String header; - private String payload = "PAYLOAD"; - - @BeforeEach - public void setup() { - MockitoAnnotations.openMocks(this); - responseMessageBufferClient = new ResponseMessageBufferClient(); - ReflectionTestUtils.setField(responseMessageBufferClient, "responseMessageIsReceived", true, boolean.class); - ReflectionTestUtils.setField(responseMessageBufferClient, "responseMessage", "RESPONSE".getBytes(StandardCharsets.UTF_8)); - - inputStreamSocketListenerClient = new InputStreamSocketListenerClient(); + @Mock + private TLSProvider tlsProvider; + + private ResponseMessageBufferClient responseMessageBufferClient; + private InputStreamSocketListenerClient inputStreamSocketListenerClient; + + private ResourceLoader resourceLoader; + + private HttpWebSocketServerBean server; + + private String webSocketHost = "localhost"; + private Integer webSocketPort = 1234; + private String header; + private String payload = "PAYLOAD"; + + @BeforeEach + public void setup() throws Exception { + MockitoAnnotations.openMocks(this); + responseMessageBufferClient = new ResponseMessageBufferClient(); + ReflectionTestUtils.setField(responseMessageBufferClient, "responseMessageIsReceived", true, boolean.class); + ReflectionTestUtils.setField(responseMessageBufferClient, "responseMessage", "RESPONSE".getBytes(StandardCharsets.UTF_8)); + + inputStreamSocketListenerClient = new InputStreamSocketListenerClient(); + + header = UtilMessageService.getMessageAsString(UtilMessageService.getArtifactRequestMessage()); + + ReflectionTestUtils.setField(sender, "responseMessageBufferClient", responseMessageBufferClient, ResponseMessageBufferClient.class); + ReflectionTestUtils.setField(sender, "inputStreamSocketListenerWebSocketClient", inputStreamSocketListenerClient, InputStreamSocketListenerClient.class); + + server = new HttpWebSocketServerBean(); + resourceLoader = new DefaultResourceLoader(); + ReflectionTestUtils.setField(server, "resourceLoader", resourceLoader, ResourceLoader.class); + ReflectionTestUtils.setField(server, "keyStoreLocation", "classpath:ssl-server.jks", String.class); + ReflectionTestUtils.setField(server, "keyStoreType", "JKS", String.class); + ReflectionTestUtils.setField(server, "keyStorePassword", "changeit", String.class); + + server.setPort(webSocketPort); + server.setMessagingServlet(HttpWebSocketMessagingServletA.class); + server.createServer(); + + // Mocking TLSProvider methods + KeyStore mockKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + mockKeyStore.load(null, null); + Certificate mockCertificate = mock(Certificate.class); + X509Certificate mockX509Certificate = mock(X509Certificate.class); + KeyManager[] mockKeyManagers = new KeyManager[0]; + TrustManager[] mockTrustManagers = new TrustManager[] { createTrustManager() }; + Enumeration mockAliases = mock(Enumeration.class); - header = UtilMessageService.getMessageAsString(UtilMessageService.getArtifactRequestMessage()); - - ReflectionTestUtils.setField(sender, "responseMessageBufferClient", responseMessageBufferClient, ResponseMessageBufferClient.class); - ReflectionTestUtils.setField(sender, "inputStreamSocketListenerWebSocketClient", inputStreamSocketListenerClient, InputStreamSocketListenerClient.class); - - server = new HttpWebSocketServerBean(); - resourceLoader = new DefaultResourceLoader(); - ReflectionTestUtils.setField(server, "resourceLoader", resourceLoader, ResourceLoader.class); - ReflectionTestUtils.setField(server, "keyStoreLocation", "classpath:ssl-server.jks", String.class); - ReflectionTestUtils.setField(server, "keyStoreType", "JKS", String.class); - ReflectionTestUtils.setField(server, "keyStorePassword", "changeit", String.class); - - server.setPort(webSocketPort); - server.setMessagingServlet(HttpWebSocketMessagingServletA.class); - server.createServer(); - } - - @AfterEach - public void destroy() throws Exception { - if(server != null) { - server.onDestroy(); - } - } - - @Test - public void sendMultipartMessageWebSocketOverHttps1() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { - - String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, header, payload); - - assertEquals("RESPONSE", response); - verify(fileStreamingBean).setup(any(WebSocket.class)); + when(tlsProvider.getTrustManagerKeyStore()).thenReturn(mockKeyStore); + when(tlsProvider.getTruststoreAliases()).thenReturn(mockAliases); + when(tlsProvider.getTLSKeystoreCertificate()).thenReturn(mockCertificate); + when(tlsProvider.getKeyManagers()).thenReturn(mockKeyManagers); + when(tlsProvider.getTrustManagers()).thenReturn(mockTrustManagers); + when(tlsProvider.getCertificateTLS()).thenReturn(mockX509Certificate); + + ReflectionTestUtils.setField(sender, "tlsProvider", tlsProvider, TLSProvider.class); + } + + @AfterEach + public void destroy() throws Exception { + if(server != null) { + server.onDestroy(); + } + } + + @Test + public void sendMultipartMessageWebSocketOverHttps1() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { + + String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, header, payload); + + assertEquals("RESPONSE", response); + verify(fileStreamingBean).setup(any(WebSocket.class)); verify(fileStreamingBean).sendMultipartMessage(any(String.class)); - } - - @Test - public void sendMultipartMessageWebSocketOverHttps2() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { - Message message = UtilMessageService.getArtifactRequestMessage(); - - String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, header, payload, message); - - assertEquals("RESPONSE", response); - verify(fileStreamingBean).setup(any(WebSocket.class)); + } + + @Test + public void sendMultipartMessageWebSocketOverHttps2() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { + Message message = UtilMessageService.getArtifactRequestMessage(); + + String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, header, payload, message); + + assertEquals("RESPONSE", response); + verify(fileStreamingBean).setup(any(WebSocket.class)); verify(fileStreamingBean).sendMultipartMessage(any(String.class)); - } - - @Test - public void sendMultipartMessageWebSocketOverHttps3() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { - String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, null, header, payload); - - assertEquals("RESPONSE", response); - verify(fileStreamingBean).setup(any(WebSocket.class)); + } + + @Test + public void sendMultipartMessageWebSocketOverHttps3() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { + String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, null, header, payload); + + assertEquals("RESPONSE", response); + verify(fileStreamingBean).setup(any(WebSocket.class)); verify(fileStreamingBean).sendMultipartMessage(any(String.class)); - } - - @Test - public void sendMultipartMessageWebSocketOverHttps4() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { - Message message = UtilMessageService.getArtifactRequestMessage(); - String webSocketPath = null; + } + + @Test + public void sendMultipartMessageWebSocketOverHttps4() throws KeyManagementException, ParseException, NoSuchAlgorithmException, IOException, InterruptedException, ExecutionException { + Message message = UtilMessageService.getArtifactRequestMessage(); + String webSocketPath = null; - String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, webSocketPath, header, payload, message); - - assertEquals("RESPONSE", response); - verify(fileStreamingBean).setup(any(WebSocket.class)); + String response = sender.sendMultipartMessageWebSocketOverHttps(webSocketHost, webSocketPort, webSocketPath, header, payload, message); + + assertEquals("RESPONSE", response); + verify(fileStreamingBean).setup(any(WebSocket.class)); verify(fileStreamingBean).sendMultipartMessage(any(String.class)); - } - -} + } + private X509TrustManager createTrustManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // No-op for testing + } + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + // No-op for testing + } + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } +}