/* * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. * See LICENSE in the project root for license information. */ package com.linkedin.flashback.smartproxy; import com.linkedin.flashback.SceneAccessLayer; import com.linkedin.flashback.matchrules.MatchRule; import com.linkedin.flashback.scene.Scene; import com.linkedin.flashback.scene.SceneMode; import com.linkedin.flashback.smartproxy.proxycontroller.RecordController; import com.linkedin.flashback.smartproxy.proxycontroller.ReplayController; import com.linkedin.mitm.model.CertificateAuthority; import com.linkedin.mitm.model.Protocol; import com.linkedin.mitm.proxy.ProxyServer; import com.linkedin.mitm.proxy.connectionflow.steps.ConnectionFlowStep; import com.linkedin.mitm.proxy.dataflow.ProxyModeController; import com.linkedin.mitm.proxy.dataflow.ProxyModeControllerFactory; import com.linkedin.mitm.proxy.factory.ConnectionFlowFactory; import io.netty.handler.codec.http.HttpRequest; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.util.List; import org.apache.log4j.Logger; /** * Flashback runner which bootstrap based on configs we pass in. * It also allows to change scene and match rules dynamically. * Note: this class is not thread safe. * * i.e. * How to record: * <pre> * {@code * SceneConfiguration sceneConfiguration = new SceneConfiguration("/root", SceneMode.RECORD,"file name"); * try (FlashbackRunner flashbackRunner = new FlashbackRunner.Builder() * .rootCertificatePath("gaap.p12").rootCertificateFileInputStream(fis).rootCertificatePassphrase("changeit") * .certificateAuthority(certificateAuthority) * .mode(SceneMode.RECORD) * .sceneAccessLayer(new SceneAccessLayer(SceneFactory.create(sceneConfiguration), MatchRuleUtils.matchEntireRequest())) * .build()) { * flashbackRunner.start(); * String url = "https://www.example.org/"; * HttpHost host = new HttpHost("localhost", 5555); * HttpClient client = HttpClientBuilder.create().setProxy(host).build(); * HttpResponse httpResponse = client.execute(request); * } * } * </pre> * * How to replay: * <pre> * {@code * SceneConfiguration sceneConfiguration = new SceneConfiguration("/root", SceneMode.PLAYBACK,"file name"); * try (FlashbackRunner flashbackRunner = new FlashbackRunner.Builder() * .rootCertificatePath("gaap.p12").rootCertificatePassphrase("changeit") * .certificateAuthority(certificateAuthority) * .mode(SceneMode.PLAYBACK) * .sceneAccessLayer(new SceneAccessLayer(SceneFactory.create(sceneConfiguration), MatchRuleUtils.matchEntireRequest())) * .build()) { * flashbackRunner.start(); * String url = "https://www.example.org/"; * HttpHost host = new HttpHost("localhost", 5555); * HttpClient client = HttpClientBuilder.create().setProxy(host).build(); * * //Change Scene * flashbackRunner.setScene(SceneFactory.create(new SceneConfiguration("/root", SceneMode.PLAYBACK,"new file name"))); * url = "https://www.google.com/"; * request = new HttpGet(url); * HttpResponse httpResponse = client.execute(request); * } * } * </pre> * * @author shfeng * */ public class FlashbackRunner implements AutoCloseable { private static final String MODULE = FlashbackRunner.class.getName(); private static final Logger LOG = Logger.getLogger(MODULE); private final ProxyServer _proxyServer; private final SceneAccessLayer _sceneAccessLayer; private boolean _running; private FlashbackRunner(final Builder builder) { _sceneAccessLayer = builder._sceneAccessLayer; if (builder._sceneMode == SceneMode.RECORD || builder._sceneMode == SceneMode.SEQUENTIAL_RECORD) { _proxyServer = createProxyServerInRecordMode(builder); } else { _proxyServer = createProxyServerInReplayMode(builder); } } public void start() throws InterruptedException { _proxyServer.start(); _running = true; } public void stop() { if (!_running) { throw new IllegalStateException("Flashback proxy server is already stopped"); } _sceneAccessLayer.flush(); _proxyServer.stop(); _running = false; } public void setScene(Scene scene) { _sceneAccessLayer.flush(); _sceneAccessLayer.setScene(scene); } public void setMatchRule(MatchRule matchRule) { _sceneAccessLayer.setMatchRule(matchRule); } @Override public void close() { if (_running) { stop(); } } /** * Create proxy server in replay mode */ private ProxyServer createProxyServerInReplayMode(Builder builder) { ProxyModeControllerFactory proxyModeControllerFactory = new ProxyModeControllerFactory() { @Override public ProxyModeController create(HttpRequest httpRequest) { return new ReplayController(_sceneAccessLayer, httpRequest); } }; //Create Http connection flow for replay mode List<ConnectionFlowStep> httpReplayConnectionFlow = ConnectionFlowFactory.createClientOnlyHttpConnectionFlow(); ProxyServer.Builder proxyServerBuilder = new ProxyServer.Builder().proxyModeControllerFactory(proxyModeControllerFactory) .connectionFlow(Protocol.HTTP, httpReplayConnectionFlow).host(builder._host).port(builder._port); if (requiresHttps(builder)) { //Create Https connection flow for replay mode List<ConnectionFlowStep> httpsReplayConnectionFlow = ConnectionFlowFactory .createClientOnlyHttpsConnectionFlow(builder._rootCertificateInputStream, builder._rootCertificatePassphrase, builder._certificateAuthority); proxyServerBuilder.connectionFlow(Protocol.HTTPS, httpsReplayConnectionFlow); } return proxyServerBuilder.build(); } /** * Create proxy server in record mode */ private ProxyServer createProxyServerInRecordMode(Builder builder) { ProxyModeControllerFactory proxyModeControllerFactory = new ProxyModeControllerFactory() { @Override public ProxyModeController create(HttpRequest httpRequest) { return new RecordController(_sceneAccessLayer, httpRequest); } }; //Create Http connection flow for record mode List<ConnectionFlowStep> httpConnectionFlow = ConnectionFlowFactory.createFullHttpConnectionFlow(); ProxyServer.Builder proxyServerBuilder = new ProxyServer.Builder().proxyModeControllerFactory(proxyModeControllerFactory) .connectionFlow(Protocol.HTTP, httpConnectionFlow).host(builder._host).port(builder._port); if (requiresHttps(builder)) { //Create Https connection flow for record mode List<ConnectionFlowStep> httpsConnectionFlow = ConnectionFlowFactory .createFullHttpsConnectionFlow(builder._rootCertificateInputStream, builder._rootCertificatePassphrase, builder._certificateAuthority); proxyServerBuilder.connectionFlow(Protocol.HTTPS, httpsConnectionFlow); } return proxyServerBuilder.build(); } private boolean requiresHttps(Builder builder) { if (builder._rootCertificateInputStream == null || builder._rootCertificatePassphrase == null || builder._certificateAuthority == null) { LOG.warn("With current setup, it can't intercept HTTPS request."); return false; } return true; } public static class Builder { private String _host = "127.0.0.1"; private int _port = 5555; private SceneMode _sceneMode = SceneMode.PLAYBACK; private InputStream _rootCertificateInputStream; private String _rootCertificatePassphrase; private CertificateAuthority _certificateAuthority; private SceneAccessLayer _sceneAccessLayer; /** * @param port proxy port number * Default: 5555 */ public Builder port(int port) { _port = port; return this; } /** * @param host proxy host address * Default: localhost */ public Builder host(String host) { _host = host; return this; } /** * @param sceneMode scene mode * Default: Playback */ public Builder mode(SceneMode sceneMode) { _sceneMode = sceneMode; return this; } /** * @param path root certificate path. * It's only required for Https */ public Builder rootCertificatePath(String path) throws FileNotFoundException { _rootCertificateInputStream = new FileInputStream(path); return this; } /** * */ public Builder rootCertificateInputStream(InputStream inputstream) { _rootCertificateInputStream = inputstream; return this; } /** * @param rootCertificatePassphrase root certificate passsphrase * It's only required for Https */ public Builder rootCertificatePassphrase(String rootCertificatePassphrase) { _rootCertificatePassphrase = rootCertificatePassphrase; return this; } /** * @param certificateAuthority Certificate authority information, which is required to generate server certificates * It's only required for Https */ public Builder certificateAuthority(CertificateAuthority certificateAuthority) { _certificateAuthority = certificateAuthority; return this; } /** * @param sceneAccessLayer Access layer to record/replay scenes. */ public Builder sceneAccessLayer(SceneAccessLayer sceneAccessLayer) { _sceneAccessLayer = sceneAccessLayer; return this; } public FlashbackRunner build() { validate(); return new FlashbackRunner(this); } private void validate() { if (_sceneAccessLayer == null) { throw new IllegalStateException("scene access layer can't be null"); } } } }