diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockBackEndServerForMultiSSLProfiles.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockBackEndServerForMultiSSLProfiles.java new file mode 100644 index 0000000000..92886f35d0 --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockBackEndServerForMultiSSLProfiles.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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. + */ +package org.wso2.micro.gateway.tests.common; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import io.netty.handler.codec.http.HttpHeaderNames; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wso2.apimgt.gateway.cli.constants.TokenManagementConstants; + +import javax.net.ssl.*; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.io.FileInputStream; + +/** + * Mock HTTP server for testing Open API tests. + */ +public class MockBackEndServerForMultiSSLProfiles extends Thread { + + private static final Logger log = LoggerFactory.getLogger(MockHttpServer.class); + private HttpsServer httpServer; + private HttpsServer incorrectHttpServer; + private static int backEndServerPort; + + public static void main(String[] args) { + + MockBackEndServerForMultiSSLProfiles mockBackEndServer = new MockBackEndServerForMultiSSLProfiles(backEndServerPort); + mockBackEndServer.start(); + } + + public MockBackEndServerForMultiSSLProfiles(int port) { + + backEndServerPort = port; + } + + public void run() { + + if (backEndServerPort < 0) { + throw new RuntimeException("Server port is not defined"); + } + try { + httpServer = HttpsServer.create(new InetSocketAddress(backEndServerPort), 0); + incorrectHttpServer = HttpsServer.create(new InetSocketAddress(backEndServerPort + 1), 0); + + httpServer.setHttpsConfigurator(new HttpsConfigurator( + getSslContext("keyStores/dynamicSSL/KS.p12", "keyStores/dynamicSSL/TS.p12")) { + public void configure(HttpsParameters params) { + + try { + // initialise the SSL context + SSLContext sslContext = SSLContext.getDefault(); + SSLEngine engine = sslContext.createSSLEngine(); + params.setNeedClientAuth(false); + params.setCipherSuites(engine.getEnabledCipherSuites()); + params.setProtocols(engine.getEnabledProtocols()); + // get the default parameters + SSLParameters defaultSSLParameters = sslContext + .getDefaultSSLParameters(); + params.setSSLParameters(defaultSSLParameters); + } catch (Exception ex) { + log.error("Failed to create HTTPS port"); + } + } + }); + + incorrectHttpServer.setHttpsConfigurator(new HttpsConfigurator( + getSslContext("keyStores/dynamicSSL/keystore.p12", "keyStores/dynamicSSL/truststore.p12")) { + public void configure(HttpsParameters params) { + + try { + // initialise the SSL context + SSLContext sslContext = SSLContext.getDefault(); + SSLEngine engine = sslContext.createSSLEngine(); + params.setNeedClientAuth(false); + params.setCipherSuites(engine.getEnabledCipherSuites()); + params.setProtocols(engine.getEnabledProtocols()); + // get the default parameters + SSLParameters defaultSSLParameters = sslContext + .getDefaultSSLParameters(); + params.setSSLParameters(defaultSSLParameters); + } catch (Exception ex) { + log.error("Failed to create HTTPS port"); + } + } + }); + String base = "/v1"; + httpServer.createContext(base + "/pet/findByStatus", exchange -> { + + byte[] response = ResponseConstants.responseBodyV1.getBytes(); + exchange.getResponseHeaders().set(HttpHeaderNames.CONTENT_TYPE.toString(), + TokenManagementConstants.CONTENT_TYPE_APPLICATION_JSON); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + }); + incorrectHttpServer.createContext(base + "/pet/2", exchange -> { + byte[] response = ResponseConstants.getPetResponse.getBytes(); + exchange.getResponseHeaders().set(HttpHeaderNames.CONTENT_TYPE.toString(), + TokenManagementConstants.CONTENT_TYPE_APPLICATION_JSON); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + }); + incorrectHttpServer.createContext(base + "/pet/", exchange -> { + byte[] response = ResponseConstants.getPetResponse.getBytes(); + exchange.getResponseHeaders().set(HttpHeaderNames.CONTENT_TYPE.toString(), + TokenManagementConstants.CONTENT_TYPE_APPLICATION_JSON); + exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + }); + + httpServer.start(); + incorrectHttpServer.start(); + } catch (Exception e) { + log.error("Error occurred while setting up mock server", e); + } + } + + public void stopIt() { + + httpServer.stop(0); + incorrectHttpServer.stop(0); + } + + private SSLContext getSslContext(String keystorePath, String truststorePath) throws Exception { + final char[] password = "ballerina".toCharArray(); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + String keyPath = getClass().getClassLoader() + .getResource(keystorePath).getPath(); + final InputStream is = new FileInputStream(keyPath); + keyStore.load(is, password); + + KeyStore trustStore = KeyStore.getInstance("PKCS12"); + String trustPath = getClass().getClassLoader() + .getResource(truststorePath).getPath(); + final InputStream tsIn = new FileInputStream(trustPath); + trustStore.load(tsIn, password); + + String kmAlg = KeyManagerFactory.getDefaultAlgorithm(); + String tmAlg = TrustManagerFactory.getDefaultAlgorithm(); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmAlg); + kmf.init(keyStore, password); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmAlg); + tmf.init(trustStore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return sslContext; + } + +} + diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/multiSSLProfiles/MultiSSLProfilesTestCase.java b/tests/src/test/java/org/wso2/micro/gateway/tests/multiSSLProfiles/MultiSSLProfilesTestCase.java new file mode 100644 index 0000000000..61ddf2b13d --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/multiSSLProfiles/MultiSSLProfilesTestCase.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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. + */ +package org.wso2.micro.gateway.tests.multiSSLProfiles; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.micro.gateway.tests.common.BaseTestCase; +import org.wso2.micro.gateway.tests.common.model.ApplicationDTO; +import org.wso2.micro.gateway.tests.util.HttpClientRequest; +import org.wso2.micro.gateway.tests.util.TestConstant; +import org.wso2.micro.gateway.tests.util.TokenUtil; + +import java.util.HashMap; +import java.util.Map; + +public class MultiSSLProfilesTestCase extends BaseTestCase { + protected String jwtTokenProd; + protected String context = "v1"; + @BeforeClass() + public void start() throws Exception { + + String project = "MultiSSLProfileProject"; + //Define application info + + ApplicationDTO application = new ApplicationDTO(); + application.setName("jwtApp"); + application.setTier("Unlimited"); + application.setId((int) (Math.random() * 1000)); + jwtTokenProd = TokenUtil.getBasicJWT(application, new JSONObject(), TestConstant.KEY_TYPE_PRODUCTION, 3600); + + //generate apis with CLI and start the micro gateway server + super.init(project, new String[] { "multiSSLProfiles/multi_ssl_profiles.yaml"}, null, + "confs/multi-ssl-profiles.conf"); + } + + @Test(description = "Test invoking a backend with a correct truststore and keystore") + public void testCorrectTrustStoreAndKeyStore() throws Exception { + Map headers = new HashMap<>(); + headers.put(HttpHeaderNames.AUTHORIZATION.toString(), "Bearer " + jwtTokenProd); + // invokes the 2381, which contains the correct truststore and keystore + org.wso2.micro.gateway.tests.util.HttpResponse response = HttpClientRequest + .doGet(getServiceURLHttp("petstore/v1/pet/findByStatus"), headers); + Assert.assertNotNull(response); + Assert.assertEquals(response.getResponseCode(), 200); + } + + @Test(description = "Test invoking a backend with an incorrect truststore and keystore") + public void testIncorrectTrustStoreAndKeyStore() throws Exception { + Map headers = new HashMap<>(); + headers.put(HttpHeaderNames.AUTHORIZATION.toString(), "Bearer " + jwtTokenProd); + // invokes the backend at port 2382, which contains an incorrect truststore and keystore + org.wso2.micro.gateway.tests.util.HttpResponse response = HttpClientRequest + .doGet(getServiceURLHttp("/petstore/v1/pet/10"), headers); + Assert.assertNull(response); + } + + @AfterClass + public void stop() throws Exception { + //Stop all the mock servers + super.finalize(); + } + +} diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/prepare/PreRequisites.java b/tests/src/test/java/org/wso2/micro/gateway/tests/prepare/PreRequisites.java index 282dd64024..00c35727dc 100644 --- a/tests/src/test/java/org/wso2/micro/gateway/tests/prepare/PreRequisites.java +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/prepare/PreRequisites.java @@ -5,12 +5,14 @@ import org.testng.Assert; import org.wso2.micro.gateway.tests.common.JMSPublisher; import org.wso2.micro.gateway.tests.common.MockBackEndServer; +import org.wso2.micro.gateway.tests.common.MockBackEndServerForMultiSSLProfiles; import org.wso2.micro.gateway.tests.context.Utils; import java.io.File; public class PreRequisites { private MockBackEndServer mockBackEndServer; + private MockBackEndServerForMultiSSLProfiles mockBackEndServerForMultiSSL; @BeforeSuite private void initializeMessageBroker() throws Exception { JMSPublisher jmsPublisher = new JMSPublisher(); @@ -27,10 +29,20 @@ public void startMockBackendServer() { mockBackEndServer.start(); } + @BeforeSuite + public void startMockBackendServerForMultiSSL() { + int port = 2381; + boolean isOpen = Utils.isPortOpen(port); + Assert.assertFalse(isOpen, "Port: " + port + " already in use."); + mockBackEndServerForMultiSSL = new MockBackEndServerForMultiSSLProfiles(port); + mockBackEndServerForMultiSSL.start(); + } + @AfterSuite public void stop() throws Exception { //Stop all the mock servers mockBackEndServer.stopIt(); + mockBackEndServerForMultiSSL.stopIt(); } @BeforeSuite diff --git a/tests/src/test/resources/confs/multi-ssl-profiles.conf b/tests/src/test/resources/confs/multi-ssl-profiles.conf new file mode 100644 index 0000000000..0c8dc2400d --- /dev/null +++ b/tests/src/test/resources/confs/multi-ssl-profiles.conf @@ -0,0 +1,34 @@ +[listenerConfig] +httpPort=9590 +httpsPort=9595 +tokenListenerPort=9596 + +[[httpClients.sslConfig]] +hostname = "localhost" +port = 2380 +# SSL Protocol to be used +protocolName = "TLS" +# List of ciphers to be used +ciphers="" +# SSL/TLS protocols to be enabled +protocolVersions = "TLSv1.2,TLSv1.1" +# Internal keystore +keyStorePath = "${mgw-runtime.home}/runtime/bre/security/ballerinaKeystore.p12" +keyStorePassword = "ballerina" +# Truststore +trustStorePath = "${mgw-runtime.home}/runtime/bre/security/ballerinaTruststore.p12" +trustStorePassword = "ballerina" + +[[httpClients.sslConfig]] +# SSL Protocol to be used +protocolName = "TLS" +# List of ciphers to be used +ciphers="" +# SSL/TLS protocols to be enabled +protocolVersions = "TLSv1.2,TLSv1.1" +# Internal keystore +keyStorePath = "${mgw-runtime.home}/runtime/bre/security/ballerinaKeystore.p12" +keyStorePassword = "ballerina" +# Truststore +trustStorePath = "${mgw-runtime.home}/runtime/bre/security/ballerinaTruststore.p12" +trustStorePassword = "ballerina" diff --git a/tests/src/test/resources/keyStores/dynamicSSL/KS.p12 b/tests/src/test/resources/keyStores/dynamicSSL/KS.p12 new file mode 100644 index 0000000000..14d6a2b8ec Binary files /dev/null and b/tests/src/test/resources/keyStores/dynamicSSL/KS.p12 differ diff --git a/tests/src/test/resources/keyStores/dynamicSSL/TS.p12 b/tests/src/test/resources/keyStores/dynamicSSL/TS.p12 new file mode 100644 index 0000000000..70ff5f2ced Binary files /dev/null and b/tests/src/test/resources/keyStores/dynamicSSL/TS.p12 differ diff --git a/tests/src/test/resources/keyStores/dynamicSSL/keystore.p12 b/tests/src/test/resources/keyStores/dynamicSSL/keystore.p12 new file mode 100644 index 0000000000..a88ec95603 Binary files /dev/null and b/tests/src/test/resources/keyStores/dynamicSSL/keystore.p12 differ diff --git a/tests/src/test/resources/keyStores/dynamicSSL/truststore.p12 b/tests/src/test/resources/keyStores/dynamicSSL/truststore.p12 new file mode 100644 index 0000000000..0075e85f29 Binary files /dev/null and b/tests/src/test/resources/keyStores/dynamicSSL/truststore.p12 differ diff --git a/tests/src/test/resources/openAPIs/multiSSLProfiles/multi_ssl_profiles.yaml b/tests/src/test/resources/openAPIs/multiSSLProfiles/multi_ssl_profiles.yaml new file mode 100644 index 0000000000..f20a8a6f92 --- /dev/null +++ b/tests/src/test/resources/openAPIs/multiSSLProfiles/multi_ssl_profiles.yaml @@ -0,0 +1,178 @@ +--- +openapi: 3.0.0 +servers: + - url: https://petstore.swagger.io/v2 + - url: http://petstore.swagger.io/v2 +info: + description: 'This is a sample server Petstore server. You can find out more about + Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For + this sample, you can use the api key `special-key` to test the authorization filters.' + version: 1.0.0 + title: Swagger Petstore New + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: http://swagger.io +x-wso2-basePath: /petstore/v1 +x-wso2-production-endpoints: + urls: + - https://localhost:2381/v1 +paths: + "/pet/findByStatus": + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + x-wso2-production-endpoints: + urls: + - https://localhost:2381/v1 + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + "$ref": "#/components/schemas/Pet" + application/json: + schema: + type: array + items: + "$ref": "#/components/schemas/Pet" + '400': + description: Invalid status value + "/pet/{petId}": + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + x-wso2-production-endpoints: + urls: + - https://localhost:2382/v1 + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + "$ref": "#/components/schemas/Pet" + application/json: + schema: + "$ref": "#/components/schemas/Pet" + '400': + description: Invalid ID supplied + '404': + description: Pet not found +components: + schemas: + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + "$ref": "#/components/schemas/Category" + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + "$ref": "#/components/schemas/Tag" + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + requestBodies: + Pet: + content: + application/json: + schema: + "$ref": "#/components/schemas/Pet" + application/xml: + schema: + "$ref": "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true \ No newline at end of file diff --git a/tests/src/test/resources/testng.xml b/tests/src/test/resources/testng.xml index 87e2dc8843..151b8358ba 100644 --- a/tests/src/test/resources/testng.xml +++ b/tests/src/test/resources/testng.xml @@ -45,6 +45,7 @@ +