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
+ }
+}