Skip to content

Commit

Permalink
Allow stubbing & recording of forward proxy https traffic
Browse files Browse the repository at this point in the history
This allows WireMock to act as a forward (browser) proxy for HTTPS as well as
HTTP origins whilst stubbing & recording.

Step towards implementing wiremock#401.
  • Loading branch information
Mahoney committed May 25, 2020
1 parent 57d932b commit 37fb1ab
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 44 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ To build both JARs (thin and standalone):

The built JAR will be placed under ``java8/build/libs``.

Developing on IntelliJ IDEA
---------------------------

IntelliJ can't import the gradle build script correctly automatically, so run
```bash
./gradlew -c release-settings.gradle :java8:idea
```

Make sure you have no `.idea` directory, the plugin generates old style .ipr,
.iml & .iws metadata files.

You may have to then set up your project SDK to point at your Java 8
installation.

Then edit the module settings. Remove the "null" Source & Test source folders
from all modules. Add `wiremock` as a module dependency to Java 7 & Java 8.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.io.NetworkTrafficListener;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.util.ssl.SslContextFactory;

public class Jetty94HttpServer extends JettyHttpServer {
Expand All @@ -27,39 +33,45 @@ protected MultipartRequestConfigurer buildMultipartRequestConfigurer() {
}

@Override
protected ServerConnector createHttpsConnector(Server server, String bindAddress, HttpsSettings httpsSettings, JettySettings jettySettings, NetworkTrafficListener listener) {
SslContextFactory.Server http2SslContextFactory = buildHttp2SslContextFactory(httpsSettings);

HttpConfiguration httpConfig = createHttpConfig(jettySettings);
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(httpsSettings.port());
httpConfig.setSendXPoweredBy(false);
httpConfig.setSendServerVersion(false);
httpConfig.addCustomizer(new SecureRequestCustomizer());
protected ServerConnector createHttpConnector(String bindAddress, int port, JettySettings jettySettings, NetworkTrafficListener listener) {

HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig);

ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
ConnectionFactories connectionFactories = buildConnectionFactories(jettySettings, 0);
return createServerConnector(
bindAddress,
jettySettings,
port,
listener,
// http needs to be the first (the default)
connectionFactories.http,
// alpn & h2 are included so that HTTPS forward proxying can find them
connectionFactories.alpn,
connectionFactories.h2
);
}

SslConnectionFactory ssl = new SslConnectionFactory(http2SslContextFactory, alpn.getProtocol());
@Override
protected ServerConnector createHttpsConnector(Server server, String bindAddress, HttpsSettings httpsSettings, JettySettings jettySettings, NetworkTrafficListener listener) {

ConnectionFactory[] connectionFactories = new ConnectionFactory[] {
ssl,
alpn,
h2,
http
};
ConnectionFactories connectionFactories = buildConnectionFactories(jettySettings, httpsSettings.port());
SslConnectionFactory ssl = sslConnectionFactory(httpsSettings);

return createServerConnector(
bindAddress,
jettySettings,
httpsSettings.port(),
listener,
connectionFactories
ssl,
connectionFactories.alpn,
connectionFactories.h2,
connectionFactories.http
);
}

private SslConnectionFactory sslConnectionFactory(HttpsSettings httpsSettings) {
SslContextFactory.Server http2SslContextFactory = buildHttp2SslContextFactory(httpsSettings);
return new SslConnectionFactory(http2SslContextFactory, "alpn");
}

private SslContextFactory.Server buildHttp2SslContextFactory(HttpsSettings httpsSettings) {
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();

Expand All @@ -75,4 +87,56 @@ private SslContextFactory.Server buildHttp2SslContextFactory(HttpsSettings https
sslContextFactory.setProvider("Conscrypt");
return sslContextFactory;
}

@Override
protected HttpConfiguration createHttpConfig(JettySettings jettySettings) {
HttpConfiguration httpConfig = super.createHttpConfig(jettySettings);
httpConfig.setSendXPoweredBy(false);
httpConfig.setSendServerVersion(false);
httpConfig.addCustomizer(new SecureRequestCustomizer());
return httpConfig;
}

@Override
protected HandlerCollection createHandler(
Options options,
AdminRequestHandler adminRequestHandler,
StubRequestHandler stubRequestHandler
) {
HandlerCollection handler = super.createHandler(options, adminRequestHandler, stubRequestHandler);

ManInTheMiddleSslConnectHandler manInTheMiddleSslConnectHandler = new ManInTheMiddleSslConnectHandler(
sslConnectionFactory(options.httpsSettings())
);

handler.addHandler(manInTheMiddleSslConnectHandler);

return handler;
}

private ConnectionFactories buildConnectionFactories(
JettySettings jettySettings,
int securePort
) {
HttpConfiguration httpConfig = createHttpConfig(jettySettings);
httpConfig.setSecurePort(securePort);

HttpConnectionFactory http = new HttpConnectionFactory(httpConfig);
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig);
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();

return new ConnectionFactories(http, h2, alpn);
}

private static class ConnectionFactories {
private final HttpConnectionFactory http;
private final HTTP2ServerConnectionFactory h2;
private final ALPNServerConnectionFactory alpn;

private ConnectionFactories(HttpConnectionFactory http, HTTP2ServerConnectionFactory h2, ALPNServerConnectionFactory alpn) {
this.http = http;
this.h2 = h2;
this.alpn = alpn;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.github.tomakehurst.wiremock.jetty94;

import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static org.eclipse.jetty.http.HttpMethod.CONNECT;

/**
* A Handler for the HTTP CONNECT method that, instead of opening up a
* TCP tunnel between the downstream and upstream sockets,
* 1) captures the
* and
* 2) turns the connection into an SSL connection allowing this server to handle
* it.
*
*
*/
class ManInTheMiddleSslConnectHandler extends AbstractHandler {

private final SslConnectionFactory sslConnectionFactory;

ManInTheMiddleSslConnectHandler(SslConnectionFactory sslConnectionFactory) {
this.sslConnectionFactory = sslConnectionFactory;
}

@Override
protected void doStart() throws Exception {
super.doStart();
sslConnectionFactory.start();
}

@Override
protected void doStop() throws Exception {
super.doStop();
sslConnectionFactory.stop();
}

@Override
public void handle(
String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
if (CONNECT.is(request.getMethod())) {
baseRequest.setHandled(true);
handleConnect(baseRequest, response);
}
}

private void handleConnect(
Request baseRequest,
HttpServletResponse response
) throws IOException {
sendConnectResponse(response);
final HttpConnection transport = (HttpConnection) baseRequest.getHttpChannel().getHttpTransport();
EndPoint endpoint = transport.getEndPoint();
Connection connection = sslConnectionFactory.newConnection(transport.getConnector(), endpoint);
endpoint.setConnection(connection);
connection.onOpen();
}

private void sendConnectResponse(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2011 Thomas Akehurst
*
* Licensed 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 com.github.tomakehurst.wiremock;

import com.github.tomakehurst.wiremock.common.SingleRootFileSource;
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
import com.github.tomakehurst.wiremock.testsupport.TestFiles;
import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;

import java.io.File;
import java.io.IOException;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked;
import static com.github.tomakehurst.wiremock.core.WireMockApp.FILES_ROOT;
import static com.github.tomakehurst.wiremock.core.WireMockApp.MAPPINGS_ROOT;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

public class Http2BrowserProxyAcceptanceTest {

private static final String CERTIFICATE_NOT_TRUSTED_BY_TEST_CLIENT = TestFiles.KEY_STORE_PATH;

@ClassRule
public static WireMockClassRule target = new WireMockClassRule(wireMockConfig()
.httpDisabled(true)
.keystorePath(CERTIFICATE_NOT_TRUSTED_BY_TEST_CLIENT)
.dynamicHttpsPort()
);

@Rule
public WireMockClassRule instanceRule = target;

private WireMockServer proxy;
private WireMockTestClient testClient;

@Before
public void addAResourceToProxy() {
testClient = new WireMockTestClient(target.httpsPort());

proxy = new WireMockServer(wireMockConfig()
.dynamicPort()
.fileSource(new SingleRootFileSource(setupTempFileRoot()))
.enableBrowserProxying(true));
proxy.start();
}

@After
public void stopServer() {
if (proxy.isRunning()) {
proxy.stop();
}
}

@Test
public void canProxyHttpsInBrowserProxyMode() throws Exception {
target.stubFor(get(urlEqualTo("/whatever")).willReturn(aResponse().withBody("Got it")));

assertThat(testClient.getViaProxy(target.url("/whatever"), proxy.port()).content(), is("Got it"));
}

@Test
public void canStubHttpsInBrowserProxyMode() throws Exception {
target.stubFor(get(urlEqualTo("/stubbed")).willReturn(aResponse().withBody("Should Not Be Returned")));
proxy.stubFor(get(urlEqualTo("/stubbed")).willReturn(aResponse().withBody("Stubbed Value")));
target.stubFor(get(urlEqualTo("/not_stubbed")).willReturn(aResponse().withBody("Should be served from target")));

assertThat(testClient.getViaProxy(target.url("/stubbed"), proxy.port()).content(), is("Stubbed Value"));
assertThat(testClient.getViaProxy(target.url("/not_stubbed"), proxy.port()).content(), is("Should be served from target"));
}

@Test
public void canRecordHttpsInBrowserProxyMode() throws Exception {

// given
proxy.startRecording(target.baseUrl());
String recordedEndpoint = target.url("/record_me");

// and
target.stubFor(get(urlEqualTo("/record_me")).willReturn(aResponse().withBody("Target response")));

// then
assertThat(testClient.getViaProxy(recordedEndpoint, proxy.port()).content(), is("Target response"));

// when
proxy.stopRecording();

// and
target.stop();

// then
assertThat(testClient.getViaProxy(recordedEndpoint, proxy.port()).content(), is("Target response"));
}

private static File setupTempFileRoot() {
try {
File root = java.nio.file.Files.createTempDirectory("wiremock").toFile();
new File(root, MAPPINGS_ROOT).mkdirs();
new File(root, FILES_ROOT).mkdirs();
return root;
} catch (IOException e) {
return throwUnchecked(e, File.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.NetworkTrafficListener;
import org.eclipse.jetty.proxy.ConnectHandler;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.handler.HandlerWrapper;
Expand Down Expand Up @@ -129,8 +128,6 @@ protected HandlerCollection createHandler(Options options, AdminRequestHandler a
addGZipHandler(mockServiceContext, handlers);
}

handlers.addHandler(new ConnectHandler());

return handlers;
}

Expand Down
Loading

0 comments on commit 37fb1ab

Please sign in to comment.