Add https support

This commit is contained in:
Piotr Fus 2018-02-26 17:48:11 +01:00 committed by Dominik Przybysz
parent f8e0cc44f9
commit 0727ced422
17 changed files with 383 additions and 83 deletions

View file

@ -64,6 +64,21 @@ testRest {
path='testEndpoint'
name='testRest'
}
testHttps {
soap=false
port=10443
path='testHttps'
name='testHttps'
method='GET'
https={
keystorePath='/tmp/keystore.jks'
keystorePassword='keystorePass'
keyPassword='keyPass'
truststorePath='/tmp/truststore.jks'
truststorePassword='truststorePass'
requireClientAuth=true
}
}
```
### Build with docker
@ -96,7 +111,15 @@ remoteMockServer.addMock(new AddMock(
statusCode: ...,
method: ...,
responseHeaders: ...,
schema: ...
schema: ...,
https: new Https(
keystorePath: '/tmp/keystore.jks',
keystorePassword: 'keystorePass',
keyPassword: 'keyPass',
truststorePath: '/tmp/truststore.jks',
truststorePassword: 'truststorePass',
requireClientAuth: true
)
))
```
@ -117,6 +140,14 @@ Send POST request to localhost:<PORT>/serverControl
<responseHeaders>...</responseHeaders>
<schema>...</schema>
<imports alias="..." fullClassName="..."/>
<https>
<keystorePath>/tmp/keystore.jks</keystorePath>
<keystorePassword>keystorePass</keystorePassword>
<keyPassword>keyPass</keyPassword>
<truststorePath>/tmp/truststore.jks</truststorePath>
<truststorePassword>truststorePass</truststorePassword>
<requireClientAuth>true</requireClientAuth>
</https>
</addMock>
```
@ -133,6 +164,18 @@ Send POST request to localhost:<PORT>/serverControl
- responseHeaders - groovyClosure as string which must evaluate to Map which will be added to response headers, default { _ -> \[:] }
- schema - path to xsd schema file on mockserver classpath; default empty, so no vallidation of request is performed; if validation fails then response has got status 400 and response is raw message from validator
- imports - list of imports for closures (each import is separate tag); `alias` is the name of `fullClassName` available in closure; `fullClassName` must be available on classpath of mock server
- https - HTTPS configuration
#### HTTPS configuration
- keystorePath - path to keystore in JKS format, keystore should contains only one privateKeyEntry
- keystorePassword - keystore password
- keyPassword - key password
- truststorePath - path to truststore in JKS format
- truststorePassword - truststore password
- requireClientAuth - whether client auth is required (two-way SSL)
**HTTP** and **HTTPS** should be started on separated ports.
### Closures request properties

View file

@ -18,5 +18,16 @@
<xs:attribute name="alias" type="xs:string"/>
<xs:attribute name="fullClassName" type="xs:string"/>
</xs:complexType>
<xs:complexType name="https">
<xs:sequence>
<xs:element name="keystorePath" type="xs:string" />
<xs:element name="keystorePassword" type="xs:string" />
<xs:element name="keyPassword" type="xs:string" />
<xs:element name="truststorePath" type="xs:string" />
<xs:element name="truststorePassword" type="xs:string" />
<xs:element name="requireClientAuth" type="xs:boolean" />
</xs:sequence>
</xs:complexType>
</xs:schema>

View file

@ -20,6 +20,7 @@
<xs:element name="soap" type="xs:boolean" minOccurs="0"/>
<xs:element name="statusCode" type="xs:int" minOccurs="0"/>
<xs:element name="method" type="common:method" minOccurs="0"/>
<xs:element name="https" type="common:https" minOccurs="0" />
<xs:element name="responseHeaders" type="xs:string" minOccurs="0"/>
<xs:element name="schema" type="xs:string" minOccurs="0"/>
<xs:element name="imports" type="common:importAlias" minOccurs="0" maxOccurs="unbounded"/>

View file

@ -44,6 +44,10 @@
<artifactId>mockserver-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>info.solidsoft.spock</groupId>
<artifactId>spock-global-unroll</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,152 @@
package pl.touk.mockserver.tests
import groovy.util.slurpersupport.GPathResult
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpPost
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.conn.ssl.SSLContexts
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClients
import pl.touk.mockserver.api.common.Https
import pl.touk.mockserver.api.request.AddMock
import pl.touk.mockserver.client.RemoteMockServer
import pl.touk.mockserver.client.Util
import pl.touk.mockserver.server.HttpMockServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLHandshakeException
import java.security.KeyStore
class MockServerHttpsTest extends Specification {
RemoteMockServer remoteMockServer = new RemoteMockServer('localhost', 19000)
@AutoCleanup('stop')
HttpMockServer httpMockServer = new HttpMockServer(19000)
@Shared
SSLContext noClientAuthSslContext = SSLContexts.custom()
.loadTrustMaterial(trustStore())
.build()
@Shared
SSLContext trustedCertificateSslContext = SSLContexts.custom()
.loadKeyMaterial(trustedCertificateKeystore(), 'changeit'.toCharArray())
.loadTrustMaterial(trustStore())
.build()
@Shared
SSLContext untrustedCertificateSslContext = SSLContexts.custom()
.loadKeyMaterial(untrustedCertificateKeystore(), 'changeit'.toCharArray())
.loadTrustMaterial(trustStore())
.build()
def 'should handle HTTPS server' () {
given:
remoteMockServer.addMock(new AddMock(
name: 'testHttps',
path: 'testEndpoint',
port: 10443,
predicate: '''{req -> req.xml.name() == 'request'}''',
response: '''{req -> "<goodResponse-${req.xml.name()}/>"}''',
https: new Https(
keyPassword: 'changeit',
keystorePassword: 'changeit',
keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path
),
soap: false
))
when:
HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint')
restPost.entity = new StringEntity('<request/>', ContentType.create("text/xml", "UTF-8"))
CloseableHttpResponse response = client(noClientAuthSslContext).execute(restPost)
then:
GPathResult restPostResponse = Util.extractXmlResponse(response)
restPostResponse.name() == 'goodResponse-request'
}
def 'should handle HTTPS server with client auth' () {
given:
remoteMockServer.addMock(new AddMock(
name: 'testHttps',
path: 'testEndpoint',
port: 10443,
predicate: '''{req -> req.xml.name() == 'request'}''',
response: '''{req -> "<goodResponse-${req.xml.name()}/>"}''',
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('<request/>', ContentType.create("text/xml", "UTF-8"))
CloseableHttpResponse response = client(trustedCertificateSslContext).execute(restPost)
then:
GPathResult restPostResponse = Util.extractXmlResponse(response)
restPostResponse.name() == 'goodResponse-request'
}
def 'should handle HTTPS server with wrong client auth' () {
given:
remoteMockServer.addMock(new AddMock(
name: 'testHttps',
path: 'testEndpoint',
port: 10443,
predicate: '''{req -> req.xml.name() == 'request'}''',
response: '''{req -> "<goodResponse-${req.xml.name()}/>"}''',
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('<request/>', 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() {
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
}
}

View file

@ -27,14 +27,15 @@ import pl.touk.mockserver.client.MockDoesNotExist
import pl.touk.mockserver.client.RemoteMockServer
import pl.touk.mockserver.client.Util
import pl.touk.mockserver.server.HttpMockServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class MockServerIntegrationTest extends Specification {
RemoteMockServer remoteMockServer
@AutoCleanup('stop')
HttpMockServer httpMockServer
@Shared
@ -45,10 +46,6 @@ class MockServerIntegrationTest extends Specification {
remoteMockServer = new RemoteMockServer('localhost', 9000)
}
def cleanup() {
httpMockServer.stop()
}
def "should add working rest mock on endpoint"() {
expect:
remoteMockServer.addMock(new AddMock(
@ -244,7 +241,6 @@ class MockServerIntegrationTest extends Specification {
soapPostResponse.Body.'goodResponseSoap-request'.size() == 1
}
@Unroll
def "should dispatch rest mocks when second on #name"() {
given:
remoteMockServer.addMock(new AddMock(
@ -283,7 +279,6 @@ class MockServerIntegrationTest extends Specification {
9998 | 'test2' | 'another port and path'
}
@Unroll
def "should dispatch rest mock with response code"() {
given:
remoteMockServer.addMock(new AddMock(
@ -857,7 +852,6 @@ class MockServerIntegrationTest extends Specification {
mockEvents2[0].response.statusCode == 202
}
@Unroll
def "should return mock report with #mockEvents events when deleting mock with flag skip mock = #skipReport"() {
expect:
remoteMockServer.addMock(new AddMock(
@ -885,7 +879,6 @@ class MockServerIntegrationTest extends Specification {
true | 0
}
@Unroll
def "should reject mock when it has System.exit in closure"() {
when:
remoteMockServer.addMock(new AddMock(
@ -1133,7 +1126,6 @@ class MockServerIntegrationTest extends Specification {
remoteMockServer.removeMock('testRest')?.size() == 1
}
@Unroll
def 'should handle leading slash'() {
given:
String name = "testRest-${UUID.randomUUID().toString()}"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -10,15 +10,15 @@ import java.util.concurrent.CopyOnWriteArrayList
@Slf4j
@PackageScope
class ContextExecutor {
private final HttpServerWraper httpServerWraper
private final HttpServerWrapper httpServerWrapper
final String path
private final List<Mock> mocks
ContextExecutor(HttpServerWraper httpServerWraper, Mock initialMock) {
this.httpServerWraper = httpServerWraper
ContextExecutor(HttpServerWrapper httpServerWrapper, Mock initialMock) {
this.httpServerWrapper = httpServerWrapper
this.path = "/${initialMock.path}"
this.mocks = new CopyOnWriteArrayList<>([initialMock])
httpServerWraper.createContext(path) {
httpServerWrapper.createContext(path) {
HttpExchange ex ->
try {
applyMocks(ex)

View file

@ -2,6 +2,7 @@ package pl.touk.mockserver.server
import com.sun.net.httpserver.HttpExchange
import groovy.util.logging.Slf4j
import pl.touk.mockserver.api.common.Https
import pl.touk.mockserver.api.common.ImportAlias
import pl.touk.mockserver.api.common.Method
import pl.touk.mockserver.api.request.AddMock
@ -30,8 +31,8 @@ import static pl.touk.mockserver.server.Util.createResponse
@Slf4j
class HttpMockServer {
private final HttpServerWraper httpServerWraper
private final Map<Integer, HttpServerWraper> childServers = new ConcurrentHashMap<>()
private final HttpServerWrapper httpServerWrapper
private final Map<Integer, HttpServerWrapper> childServers = new ConcurrentHashMap<>()
private final Set<String> mockNames = new CopyOnWriteArraySet<>()
private final ConfigObject configuration = new ConfigObject()
private final Executor executor
@ -41,13 +42,13 @@ class HttpMockServer {
HttpMockServer(int port = 9999, ConfigObject initialConfiguration = new ConfigObject(), int threads = 10) {
executor = Executors.newFixedThreadPool(threads)
httpServerWraper = new HttpServerWraper(port, executor)
httpServerWrapper = new HttpServerWrapper(port, executor)
initialConfiguration.values()?.each { ConfigObject co ->
addMock(co)
}
httpServerWraper.createContext('/serverControl', {
httpServerWrapper.createContext('/serverControl', {
HttpExchange ex ->
try {
if (ex.requestMethod == 'GET') {
@ -108,7 +109,7 @@ class HttpMockServer {
throw new RuntimeException('mock already registered')
}
Mock mock = mockFromRequest(request)
HttpServerWraper child = getOrCreateChildServer(mock.port)
HttpServerWrapper child = getOrCreateChildServer(mock.port, mock.https)
child.addMock(mock)
saveConfiguration(request)
mockNames << name
@ -121,7 +122,7 @@ class HttpMockServer {
throw new RuntimeException('mock already registered')
}
Mock mock = mockFromConfig(co)
HttpServerWraper child = getOrCreateChildServer(mock.port)
HttpServerWrapper child = getOrCreateChildServer(mock.port, mock.https)
child.addMock(mock)
configuration.put(name, co)
mockNames << name
@ -156,6 +157,7 @@ class HttpMockServer {
mock.responseHeaders = request.responseHeaders
mock.schema = request.schema
mock.preserveHistory = request.preserveHistory != false
mock.https = request.https
return mock
}
@ -170,13 +172,23 @@ class HttpMockServer {
mock.responseHeaders = co.responseHeaders ?: null
mock.schema = co.schema ?: null
mock.preserveHistory = co.preserveHistory != false
if (co.https) {
mock.https = new Https(
keystorePath: co.https.keystorePath ?: null,
keystorePassword: co.https.keystorePassword,
keyPassword: co.https.keyPassword,
truststorePath: co.https.truststorePath,
truststorePassword: co.https.truststorePassword,
requireClientAuth: co.https?.requireClientAuth?.asBoolean() ?: false
)
}
return mock
}
private HttpServerWraper getOrCreateChildServer(int mockPort) {
HttpServerWraper child = childServers[mockPort]
private HttpServerWrapper getOrCreateChildServer(int mockPort, Https https) {
HttpServerWrapper child = childServers[mockPort]
if (!child) {
child = new HttpServerWraper(mockPort, executor)
child = new HttpServerWrapper(mockPort, executor, https)
childServers.put(mockPort, child)
}
return child
@ -244,6 +256,6 @@ class HttpMockServer {
void stop() {
childServers.values().each { it.stop() }
httpServerWraper.stop()
httpServerWrapper.stop()
}
}

View file

@ -1,58 +0,0 @@
package pl.touk.mockserver.server
import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer
import groovy.transform.PackageScope
import groovy.util.logging.Slf4j
import java.util.concurrent.Executor
@Slf4j
@PackageScope
class HttpServerWraper {
private final HttpServer httpServer
final int port
private List<ContextExecutor> executors = []
HttpServerWraper(int port, Executor executor) {
this.port = port
InetSocketAddress addr = new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), port)
httpServer = HttpServer.create(addr, 0)
httpServer.executor = executor
log.info("Http server starting on port $port...")
httpServer.start()
log.info('Http server is started')
}
void createContext(String context, HttpHandler handler) {
httpServer.createContext(context, handler)
}
void addMock(Mock mock) {
ContextExecutor executor = executors.find { it.path == mock.path }
if (executor) {
executor.addMock(mock)
} else {
executors << new ContextExecutor(this, mock)
}
log.info("Added mock ${mock.name}")
}
void stop() {
executors.each { httpServer.removeContext(it.contextPath) }
httpServer.stop(0)
}
List<MockEvent> removeMock(String name) {
return executors.collect { it.removeMock(name) }.flatten() as List<MockEvent>
}
List<MockEvent> peekMock(String name) {
return executors.collect { it.peekMock(name) }.flatten() as List<MockEvent>
}
List<Mock> getMocks() {
return executors.collect { it.mocks }.flatten() as List<Mock>
}
}

View file

@ -0,0 +1,106 @@
package pl.touk.mockserver.server
import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer
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
@Slf4j
@PackageScope
class HttpServerWrapper {
private final HttpServer httpServer
final int port
private List<ContextExecutor> executors = []
HttpServerWrapper(int port, Executor executor, Https https = null) {
this.port = port
InetSocketAddress addr = new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), port)
httpServer = buildServer(addr, https)
httpServer.executor = executor
log.info("Http server starting on port $port...")
httpServer.start()
log.info('Http server is started')
}
private HttpServer buildServer(InetSocketAddress addr, Https https) {
if (https) {
HttpsServer httpsServer = HttpsServer.create(addr, 0)
httpsServer.httpsConfigurator = new HttpsConfig(buildSslContext(https), https)
return httpsServer
} else {
return HttpServer.create(addr, 0)
}
}
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('jks')
keyStore.load(new FileInputStream(https.keystorePath), https.keystorePassword.toCharArray())
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.defaultAlgorithm)
kmf.init(keyStore, https.keyPassword.toCharArray())
return kmf.keyManagers
}
private TrustManager[] buildTrustManager(Https https) {
if (https.requireClientAuth) {
KeyStore trustStore = KeyStore.getInstance('jks')
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) {
httpServer.createContext(context, handler)
}
void addMock(Mock mock) {
ContextExecutor executor = executors.find { it.path == mock.path }
if (executor) {
executor.addMock(mock)
} else {
executors << new ContextExecutor(this, mock)
}
log.info("Added mock ${mock.name}")
}
void stop() {
executors.each { httpServer.removeContext(it.contextPath) }
httpServer.stop(0)
}
List<MockEvent> removeMock(String name) {
return executors.collect { it.removeMock(name) }.flatten() as List<MockEvent>
}
List<MockEvent> peekMock(String name) {
return executors.collect { it.peekMock(name) }.flatten() as List<MockEvent>
}
List<Mock> getMocks() {
return executors.collect { it.mocks }.flatten() as List<Mock>
}
}

View file

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

View file

@ -5,6 +5,7 @@ import groovy.transform.PackageScope
import groovy.util.logging.Slf4j
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ImportCustomizer
import pl.touk.mockserver.api.common.Https
import pl.touk.mockserver.api.common.Method
import javax.xml.XMLConstants
@ -35,6 +36,7 @@ class Mock implements Comparable<Mock> {
private Validator validator
Map<String, String> imports = [:]
boolean preserveHistory = true
Https https
Mock(String name, String path, int port) {
if (!(name)) {

View file

@ -36,6 +36,7 @@
<jmh.version>1.11.2</jmh.version>
<maven-release-plugin.version>2.5.2</maven-release-plugin.version>
<gmavenplus-plugin.version>1.4</gmavenplus-plugin.version>
<spock-global-unroll.version>0.5.1</spock-global-unroll.version>
</properties>
<scm>
@ -99,6 +100,12 @@
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.solidsoft.spock</groupId>
<artifactId>spock-global-unroll</artifactId>
<version>${spock-global-unroll.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>