diff --git a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd index 953b839..673be7d 100644 --- a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd +++ b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd @@ -24,6 +24,9 @@ + + + diff --git a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy index 91b851a..0bef47f 100644 --- a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy +++ b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy @@ -16,8 +16,10 @@ import pl.touk.mockserver.client.Util import pl.touk.mockserver.server.HttpMockServer import spock.lang.Shared import spock.lang.Specification +import spock.lang.Unroll import javax.net.ssl.SSLContext +import javax.net.ssl.SSLHandshakeException import java.security.KeyStore class MockServerHttpsTest extends Specification { @@ -27,14 +29,20 @@ class MockServerHttpsTest extends Specification { HttpMockServer httpMockServer @Shared - SSLContext sslContext = SSLContexts.custom() + SSLContext noClientAuthSslContext = SSLContexts.custom() .loadTrustMaterial(trustStore()) .build() @Shared - CloseableHttpClient client = HttpClients.custom() - .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) - .setSslcontext(sslContext) + SSLContext trustedCertificateSslContext = SSLContexts.custom() + .loadKeyMaterial(trustedCertificateKeystore(), 'changeit'.toCharArray()) + .loadTrustMaterial(trustStore()) + .build() + + @Shared + SSLContext untrustedCertificateSslContext = SSLContexts.custom() + .loadKeyMaterial(untrustedCertificateKeystore(), 'changeit'.toCharArray()) + .loadTrustMaterial(trustStore()) .build() def setup() { @@ -64,17 +72,90 @@ class MockServerHttpsTest extends Specification { when: HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) - CloseableHttpResponse response = client.execute(restPost) + CloseableHttpResponse response = client(noClientAuthSslContext).execute(restPost) then: GPathResult restPostResponse = Util.extractXmlResponse(response) restPostResponse.name() == 'goodResponse-request' - and: - remoteMockServer.removeMock('testHttps')?.size() == 1 + } + + def 'should handle HTTPS server with client auth' () { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path, + truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path, + truststorePassword: 'changeit', + requireClientAuth: true + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client(trustedCertificateSslContext).execute(restPost) + then: + GPathResult restPostResponse = Util.extractXmlResponse(response) + restPostResponse.name() == 'goodResponse-request' + } + + @Unroll + def 'should handle HTTPS server with wrong client auth' () { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path, + truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path, + truststorePassword: 'changeit', + requireClientAuth: true + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + client(sslContext).execute(restPost) + then: + thrown(SSLHandshakeException) + where: + sslContext << [noClientAuthSslContext, untrustedCertificateSslContext] + } + + private CloseableHttpClient client(SSLContext sslContext) { + return HttpClients.custom() + .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) + .setSslcontext(sslContext) + .build() + } + + private KeyStore trustedCertificateKeystore() { + return loadKeystore('trusted.jks') + } + + private KeyStore untrustedCertificateKeystore() { + return loadKeystore('untrusted.jks') } private KeyStore trustStore() { - KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType) - truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource('truststore.jks').path), "changeit".toCharArray()); + return loadKeystore('truststore.jks') + } + + private KeyStore loadKeystore(String fileName) { + KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType) + truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource(fileName).path), "changeit".toCharArray()); return truststore } } diff --git a/mockserver-tests/src/test/resources/trusted.jks b/mockserver-tests/src/test/resources/trusted.jks new file mode 100644 index 0000000..e6fa704 Binary files /dev/null and b/mockserver-tests/src/test/resources/trusted.jks differ diff --git a/mockserver-tests/src/test/resources/truststore.jks b/mockserver-tests/src/test/resources/truststore.jks new file mode 100644 index 0000000..27a8332 Binary files /dev/null and b/mockserver-tests/src/test/resources/truststore.jks differ diff --git a/mockserver-tests/src/test/resources/untrusted.jks b/mockserver-tests/src/test/resources/untrusted.jks new file mode 100644 index 0000000..ca94b45 Binary files /dev/null and b/mockserver-tests/src/test/resources/untrusted.jks differ diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy index 9121c6b..0ab3ca2 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy @@ -2,15 +2,16 @@ package pl.touk.mockserver.server import com.sun.net.httpserver.HttpHandler import com.sun.net.httpserver.HttpServer -import com.sun.net.httpserver.HttpsConfigurator import com.sun.net.httpserver.HttpsServer import groovy.transform.PackageScope import groovy.util.logging.Slf4j import pl.touk.mockserver.api.common.Https +import javax.net.ssl.KeyManager import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory import java.security.KeyStore import java.security.SecureRandom import java.util.concurrent.Executor @@ -36,7 +37,7 @@ class HttpServerWrapper { private HttpServer buildServer(InetSocketAddress addr, Https https) { if (https) { HttpsServer httpsServer = HttpsServer.create(addr, 0) - httpsServer.httpsConfigurator = new HttpsConfigurator(buildSslContext(https)) + httpsServer.httpsConfigurator = new HttpsConfig(buildSslContext(https), https) return httpsServer } else { return HttpServer.create(addr, 0) @@ -44,14 +45,32 @@ class HttpServerWrapper { } private SSLContext buildSslContext(Https https) { + KeyManager[] keyManagers = buildKeyManager(https) + TrustManager[] trustManagers = buildTrustManager(https) + + SSLContext ssl = SSLContext.getInstance('TLSv1') + ssl.init(keyManagers, trustManagers, new SecureRandom()) + return ssl + } + + private KeyManager[] buildKeyManager(Https https) { KeyStore keyStore = KeyStore.getInstance(KeyStore.defaultType) keyStore.load(new FileInputStream(https.keystorePath), https.keystorePassword.toCharArray()) KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.defaultAlgorithm) kmf.init(keyStore, https.keyPassword.toCharArray()) + return kmf.keyManagers + } - SSLContext ssl = SSLContext.getInstance('TLSv1') - ssl.init(kmf.keyManagers, [] as TrustManager[], new SecureRandom()) - return ssl + private TrustManager[] buildTrustManager(Https https) { + if (https.requireClientAuth) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.defaultType) + trustStore.load(new FileInputStream(https.truststorePath), https.truststorePassword.toCharArray()) + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.defaultAlgorithm) + tmf.init(trustStore) + return tmf.trustManagers + } else { + return [] + } } void createContext(String context, HttpHandler handler) { diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy new file mode 100644 index 0000000..68b5550 --- /dev/null +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy @@ -0,0 +1,28 @@ +package pl.touk.mockserver.server + +import com.sun.net.httpserver.HttpsConfigurator +import com.sun.net.httpserver.HttpsParameters +import groovy.transform.CompileStatic +import pl.touk.mockserver.api.common.Https + +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLParameters + +@CompileStatic +class HttpsConfig extends HttpsConfigurator { + private final Https https + + HttpsConfig(SSLContext sslContext, Https https) { + super(sslContext) + this.https = https + } + + @Override + void configure(HttpsParameters httpsParameters) { + SSLContext sslContext = getSSLContext() + SSLParameters sslParameters = sslContext.defaultSSLParameters + sslParameters.needClientAuth = https.requireClientAuth + httpsParameters.needClientAuth = https.requireClientAuth + httpsParameters.SSLParameters = sslParameters + } +}