├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── build.gradle ├── defaultEnvironment.gradle ├── flashback-admin └── src │ └── main │ └── java │ └── com │ └── linkedin │ └── flashback │ └── FlashbackAdminResource.java ├── flashback-core-impl └── src │ ├── main │ └── java │ │ └── com │ │ └── linkedin │ │ └── flashback │ │ ├── SceneAccessLayer.java │ │ ├── decorator │ │ └── compression │ │ │ ├── AbstractCompressor.java │ │ │ ├── AbstractDecompressor.java │ │ │ ├── DeflateCompressor.java │ │ │ ├── DeflateDecompressor.java │ │ │ ├── GzipCompressor.java │ │ │ └── GzipDecompressor.java │ │ ├── factory │ │ ├── RecordedHttpBodyFactory.java │ │ └── SceneFactory.java │ │ ├── http │ │ └── HttpUtilities.java │ │ ├── matchrules │ │ ├── BaseMatchRule.java │ │ ├── CompositeMatchRule.java │ │ ├── DummyMatchRule.java │ │ ├── MatchBody.java │ │ ├── MatchBodyPostParameters.java │ │ ├── MatchBodyWithAnyBoundary.java │ │ ├── MatchCaseInsensitiveMethod.java │ │ ├── MatchHeaders.java │ │ ├── MatchMethod.java │ │ ├── MatchRule.java │ │ ├── MatchRuleBlacklistTransform.java │ │ ├── MatchRuleIdentityTransform.java │ │ ├── MatchRuleMapTransform.java │ │ ├── MatchRuleUtils.java │ │ ├── MatchRuleWhitelistTransform.java │ │ ├── MatchUri.java │ │ ├── MatchUriWithQueryTransform.java │ │ └── NamedMatchRule.java │ │ ├── scene │ │ ├── DummyScene.java │ │ ├── Scene.java │ │ ├── SceneConfiguration.java │ │ └── SceneMode.java │ │ ├── serializable │ │ ├── RecordedByteHttpBody.java │ │ ├── RecordedEncodedHttpBody.java │ │ ├── RecordedHttpBody.java │ │ ├── RecordedHttpExchange.java │ │ ├── RecordedHttpMessage.java │ │ ├── RecordedHttpRequest.java │ │ ├── RecordedHttpResponse.java │ │ └── RecordedStringHttpBody.java │ │ └── serialization │ │ ├── SceneDeserializer.java │ │ ├── SceneReader.java │ │ ├── SceneSerializationConstant.java │ │ ├── SceneSerializer.java │ │ └── SceneWriter.java │ └── test │ └── java │ └── com │ └── linkedin │ └── flashback │ ├── SceneAccessLayerTest.java │ ├── decorator │ └── compression │ │ ├── DeflateCompressorTest.java │ │ └── GzipCompressorTest.java │ ├── factory │ ├── RecordedHttpBodyFactoryTest.java │ └── SceneFactoryTest.java │ ├── http │ └── HttpUtilitiesTest.java │ ├── matchrules │ ├── BaseMatchRuleTest.java │ ├── CompositeMatchRuleTest.java │ ├── MatchBodyPostParametersTest.java │ ├── MatchBodyTest.java │ ├── MatchBodyWithAnyBoundaryTest.java │ ├── MatchCaseInsensitiveMethodTest.java │ ├── MatchHeadersTest.java │ ├── MatchMethodTest.java │ ├── MatchRuleUtilsTest.java │ └── MatchUriTest.java │ ├── serializable │ ├── RecordedEncodedHttpBodyTest.java │ ├── RecordedHttpMessageTest.java │ └── RecordedStringHttpBodyTest.java │ └── serialization │ ├── MockDataGenerator.java │ ├── SceneDeserializerTest.java │ └── SceneSerializerTest.java ├── flashback-netty └── src │ ├── main │ └── java │ │ └── com │ │ └── linkedin │ │ └── flashback │ │ └── netty │ │ ├── builder │ │ ├── RecordedHttpMessageBuilder.java │ │ ├── RecordedHttpRequestBuilder.java │ │ └── RecordedHttpResponseBuilder.java │ │ └── mapper │ │ └── NettyHttpResponseMapper.java │ └── test │ └── java │ └── com │ └── linkedin │ └── flashback │ └── netty │ ├── builder │ ├── RecordedHttpMessageBuilderTest.java │ ├── RecordedHttpRequestBuilderTest.java │ └── RecordedHttpResponseBuilderTest.java │ └── mapper │ └── NettyHttpResponseMapperTest.java ├── flashback-smartproxy └── src │ ├── main │ └── java │ │ └── com │ │ └── linkedin │ │ └── flashback │ │ └── smartproxy │ │ ├── FlashbackRunner.java │ │ ├── proxycontroller │ │ ├── RecordController.java │ │ └── ReplayController.java │ │ └── utils │ │ └── NoMatchResponseGenerator.java │ └── test │ ├── java │ └── com │ │ └── linkedin │ │ └── flashback │ │ └── smartproxy │ │ └── FlashbackRunnerTest.java │ └── resources │ └── flashback │ └── scene │ ├── http │ └── setCookie ├── flashback-test-util └── src │ └── main │ └── java │ └── com │ └── linkedin │ └── flashback │ └── test │ └── FlashbackBaseTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Integration-test.jpg ├── Production-environment.jpg ├── Recording.jpg ├── Use-case-need-dynamic-changes.jpg ├── non-java-high-level-diagram.png └── non-java-service-interaction.png ├── mitm └── src │ └── main │ └── java │ └── com │ └── linkedin │ └── mitm │ ├── factory │ ├── CertificateKeyStoreFactory.java │ ├── KeyPairFactory.java │ └── RSASha1KeyPairFactory.java │ ├── model │ ├── CertificateAuthority.java │ ├── CertificateValidPeriod.java │ └── Protocol.java │ ├── proxy │ ├── ProxyInitializer.java │ ├── ProxyServer.java │ ├── channel │ │ ├── ChannelHandlerDelegate.java │ │ ├── ChannelMediator.java │ │ ├── ClientChannelHandler.java │ │ ├── Flushable.java │ │ ├── ServerChannelHandler.java │ │ └── protocol │ │ │ ├── HttpChannelHandlerDelegate.java │ │ │ └── HttpsChannelHandlerDelegate.java │ ├── connectionflow │ │ ├── ConnectionFlowProcessor.java │ │ └── steps │ │ │ ├── AcceptTCPConnectionFromClient.java │ │ │ ├── ConnectionFlowStep.java │ │ │ ├── EstablishTCPConnectionToServer.java │ │ │ ├── HandshakeWithClient.java │ │ │ ├── HandshakeWithServer.java │ │ │ ├── ResumeReadingFromClient.java │ │ │ └── StopReadingFromClient.java │ ├── dataflow │ │ ├── NormalProxyModeController.java │ │ ├── NormalProxyModeControllerFactory.java │ │ ├── ProxyModeController.java │ │ └── ProxyModeControllerFactory.java │ └── factory │ │ ├── ConnectionFlowFactory.java │ │ ├── ErrorResponseFactory.java │ │ ├── HandlerDelegateFactory.java │ │ └── NamedThreadFactory.java │ ├── services │ ├── AbstractX509CertificateService.java │ ├── CACertificateService.java │ ├── CertificateService.java │ ├── IdentityCertificateService.java │ ├── RandomNumberGenerator.java │ └── SSLContextGenerator.java │ └── store │ ├── JKSTrustStoreReader.java │ ├── KeyStoreReader.java │ ├── KeyStoreWriter.java │ └── PKC12KeyStoreReadWriter.java ├── settings.gradle └── startAdminServer.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | logs/ 4 | .idea/ 5 | */out/* 6 | *.iml 7 | *.ipr 8 | *.iws -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Agreement 2 | ====================== 3 | 4 | As a contributor, you represent that the code you submit is your 5 | original work or that of your employer (in which case you represent you 6 | have the right to bind your employer). By submitting code, you (and, if 7 | applicable, your employer) are licensing the submitted code to LinkedIn 8 | and the open source community subject to the BSD 2-Clause license. 9 | 10 | Responsible Disclosure of Security Vulnerabilities 11 | ================================================== 12 | 13 | Please do not file reports on Github for security issues. 14 | Please review the guidelines on at (link to more info). 15 | Reports should be encrypted using PGP (link to PGP key) and sent to 16 | security@linkedin.com preferably with the title "Github linkedin/ - ". 17 | 18 | Tips for Getting Your Pull Request Accepted 19 | =========================================== 20 | 1. Make sure all new features are tested and the tests pass. 21 | 2. Bug fixes must include a test case demonstrating the error that it fixes. 22 | 3. Open an issue first and seek advice for your change before submitting 23 | a pull request. Large features which have never been discussed are 24 | unlikely to be accepted. **You have been warned.** 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright 2016, LinkedIn Corporation. 4 | All Rights Reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2016 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the BSD 2-Clause License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | 8 | This product includes software from the following 3rd parties 9 | and their copyright notices are as follows: 10 | 11 | 12 | TestNG 13 | http://testng.org 14 | Copyright 2004 Cedric Beust 15 | License: Apache 2.0 16 | 17 | Apache Commons Lang 18 | http://www.apache.org/ 19 | Copyright 2001-2015 The Apache Software Foundation 20 | License: Apache 2.0 21 | 22 | Bouncy Castle PKIX 23 | http://www.bouncycastle.org/ 24 | Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc 25 | License: Bouncy Castle Licence 26 | 27 | Bouncy Castle PKIX 28 | http://www.bouncycastle.org/ 29 | Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc 30 | License: Bouncy Castle Licence 31 | 32 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 33 | Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 41 | 42 | Apache Commons CLI 43 | http://www.apache.org/ 44 | Copyright 2001-2009 The Apache Software Foundation 45 | License: Apache 2.0 46 | 47 | EasyMock 48 | http://easymock.org 49 | Copyright 2001-2014 EasyMock contributors 50 | License: Apache 2.0 51 | 52 | Guava 53 | https://github.com/google/guava 54 | 55 | License: Apache 2.0 56 | 57 | Apache HttpClient 58 | http://www.apache.org/ 59 | Copyright 2001-2015 The Apache Software Foundation 60 | License: Apache 2.0 61 | 62 | Jackson JSON processor 63 | http://fasterxml.com 64 | 65 | License: Apache 2.0 66 | 67 | Apache Log4j API 68 | http://www.apache.org/ 69 | Copyright 1999-2014 Apache Software Foundation 70 | License: Apache 2.0 71 | 72 | Netty 73 | http://netty.io/ 74 | 75 | License: Apache 2.0 76 | -------------------------------------------------------------------------------- /defaultEnvironment.gradle: -------------------------------------------------------------------------------- 1 | //Build logic specific to OS environment, outside LinkedIn 2 | //OS builds will use this build logic 3 | logger.lifecycle("Configuring using default environment (defaultEnvironment.gradle)...") 4 | 5 | subprojects { 6 | repositories { 7 | mavenCentral() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/decorator/compression/AbstractCompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | 12 | 13 | /** 14 | * Abstract compressor class 15 | * 16 | * @author shfeng 17 | */ 18 | public abstract class AbstractCompressor { 19 | 20 | public byte[] compress(byte[] encodedBytes) 21 | throws IOException { 22 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 23 | try (OutputStream stream = getOutputStream(out)) { 24 | stream.write(encodedBytes); 25 | stream.flush(); 26 | } 27 | return out.toByteArray(); 28 | } 29 | 30 | abstract protected OutputStream getOutputStream(OutputStream output) 31 | throws IOException; 32 | } 33 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/decorator/compression/AbstractDecompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | 13 | 14 | /** 15 | * Abstract decompressor class 16 | * 17 | * @author dvinegra 18 | */ 19 | public abstract class AbstractDecompressor { 20 | 21 | public byte[] decompress(byte[] encodedBytes) 22 | throws IOException { 23 | byte[] decompressedBytes; 24 | try (InputStream stream = getInputStream(new ByteArrayInputStream(encodedBytes)); 25 | ByteArrayOutputStream out = new ByteArrayOutputStream()) { 26 | int bytesRead; 27 | byte[] decodedData = new byte[1024]; 28 | while ((bytesRead = stream.read(decodedData)) != -1) { 29 | out.write(decodedData, 0, bytesRead); 30 | } 31 | out.flush(); 32 | decompressedBytes = out.toByteArray(); 33 | } 34 | return decompressedBytes; 35 | } 36 | 37 | abstract protected InputStream getInputStream(InputStream input) 38 | throws IOException; 39 | } 40 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/decorator/compression/DeflateCompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | import java.util.zip.DeflaterOutputStream; 11 | 12 | 13 | /** 14 | * Compressor that compress bytes to deflate format 15 | * 16 | * @author shfeng 17 | */ 18 | public class DeflateCompressor extends AbstractCompressor { 19 | @Override 20 | protected OutputStream getOutputStream(OutputStream output) 21 | throws IOException { 22 | return new DeflaterOutputStream(output); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/decorator/compression/DeflateDecompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.zip.InflaterInputStream; 11 | 12 | 13 | /** 14 | * Decompressor that decompresses bytes in deflate format 15 | * 16 | * @author dvinegra 17 | */ 18 | public class DeflateDecompressor extends AbstractDecompressor { 19 | @Override 20 | protected InputStream getInputStream(InputStream input) 21 | throws IOException { 22 | return new InflaterInputStream(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/decorator/compression/GzipCompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | import java.util.zip.GZIPOutputStream; 11 | 12 | 13 | /** 14 | * Compressor that compress bytes to gzip format 15 | * 16 | * @author shfeng 17 | */ 18 | public class GzipCompressor extends AbstractCompressor { 19 | @Override 20 | protected OutputStream getOutputStream(OutputStream output) 21 | throws IOException { 22 | return new GZIPOutputStream(output); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/decorator/compression/GzipDecompressor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.zip.GZIPInputStream; 11 | 12 | 13 | /** 14 | * Decompressor that decompresses bytes in gzip format 15 | * 16 | * @author dvinegra 17 | */ 18 | public class GzipDecompressor extends AbstractDecompressor { 19 | @Override 20 | protected InputStream getInputStream(InputStream input) 21 | throws IOException { 22 | return new GZIPInputStream(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/factory/RecordedHttpBodyFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.factory; 7 | 8 | import com.linkedin.flashback.http.HttpUtilities; 9 | import com.linkedin.flashback.serializable.RecordedByteHttpBody; 10 | import com.linkedin.flashback.serializable.RecordedEncodedHttpBody; 11 | import com.linkedin.flashback.serializable.RecordedHttpBody; 12 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import org.apache.commons.io.IOUtils; 16 | 17 | 18 | /** 19 | * Implementation of Http body factory class 20 | * @author shfeng 21 | * @author dvinegra 22 | */ 23 | public final class RecordedHttpBodyFactory { 24 | 25 | private RecordedHttpBodyFactory() { 26 | } 27 | 28 | /** 29 | * Given content type, charset, construct concrete {@link com.linkedin.flashback.serializable.RecordedByteHttpBody} 30 | * @param contentType http body content type 31 | * @param contentEncoding http body content encoding 32 | * @param inputStream input stream from http request/response 33 | * @param charset charset 34 | * @return concrete {@link com.linkedin.flashback.serializable.RecordedHttpBody} 35 | * 36 | * */ 37 | public static RecordedHttpBody create(String contentType, String contentEncoding, final InputStream inputStream, 38 | final String charset) 39 | throws IOException { 40 | 41 | if (HttpUtilities.isCompressedContentEncoding(contentEncoding)) { 42 | return new RecordedEncodedHttpBody(IOUtils.toByteArray(inputStream), contentEncoding, charset, 43 | contentType); 44 | } else if (HttpUtilities.isTextContentType(contentType)) { 45 | return new RecordedStringHttpBody(IOUtils.toString(inputStream, charset)); 46 | } else { 47 | return new RecordedByteHttpBody(IOUtils.toByteArray(inputStream)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/factory/SceneFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.factory; 7 | 8 | import com.linkedin.flashback.scene.Scene; 9 | import com.linkedin.flashback.scene.SceneConfiguration; 10 | import com.linkedin.flashback.scene.SceneMode; 11 | import com.linkedin.flashback.serializable.RecordedHttpExchange; 12 | import com.linkedin.flashback.serialization.SceneReader; 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | 18 | /** 19 | * Scene factory that create scene based on configuration 20 | * 1. In playback mode, if found existing scene from file, then playback this scene. 21 | * 2. In record mode, if found existing scene from the file, then update existing scene. 22 | * 3. In playback mode, if not found existing scene from the file, then throw exception 23 | * 4. In record mode, if not found existing scene from the file, then create a new one using configuration 24 | * 25 | * @author shfeng 26 | * 27 | */ 28 | public final class SceneFactory { 29 | private static final SceneReader SCENE_READER = new SceneReader(); 30 | 31 | private SceneFactory() { 32 | } 33 | 34 | public static Scene create(SceneConfiguration sceneConfiguration) 35 | throws IOException, IllegalStateException { 36 | //The reason that pass static variables is that it would be easier to write unit-test 37 | return create(sceneConfiguration, SCENE_READER); 38 | } 39 | 40 | /** 41 | * Helper method that will be used for unit test 42 | * */ 43 | static Scene create(SceneConfiguration sceneConfiguration, SceneReader sceneReader) 44 | throws IOException { 45 | Scene sceneResult = null; 46 | Scene sceneFromLocal = sceneReader.readScene(sceneConfiguration.getSceneRoot(), sceneConfiguration.getSceneName()); 47 | if (sceneFromLocal == null) { 48 | if (sceneConfiguration.getSceneMode() == SceneMode.PLAYBACK 49 | || sceneConfiguration.getSceneMode() == SceneMode.SEQUENTIAL_PLAYBACK) { 50 | throw new IllegalStateException(String.format("No Scene is found at %s/%s", sceneConfiguration.getSceneRoot(), 51 | sceneConfiguration.getSceneName())); 52 | } else { 53 | sceneResult = new Scene(sceneConfiguration); 54 | } 55 | } else { 56 | List recordedHttpExchanges = sceneFromLocal.getRecordedHttpExchangeList(); 57 | // In sequential record mode, start with an empty scene 58 | if (sceneConfiguration.getSceneMode() == SceneMode.SEQUENTIAL_RECORD) { 59 | recordedHttpExchanges = new ArrayList<>(); 60 | } 61 | sceneResult = new Scene(sceneConfiguration.getSceneName(), sceneConfiguration.getSceneMode(), 62 | sceneConfiguration.getSceneRoot(), recordedHttpExchanges); 63 | } 64 | 65 | return sceneResult; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/http/HttpUtilities.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.http; 7 | 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URLDecoder; 10 | import java.net.URLEncoder; 11 | import java.util.LinkedHashMap; 12 | import java.util.Map; 13 | import java.util.regex.Pattern; 14 | import org.apache.commons.lang3.StringUtils; 15 | 16 | 17 | /** 18 | * Common utilities and constants pertaining to HTTP headers 19 | * 20 | * @author dvinegra 21 | */ 22 | public final class HttpUtilities { 23 | 24 | private static final String IS_TEXT_REGEX_CONSTANT = "^text/|application/(json|javascript|(\\w+\\+)?xml)"; 25 | private static final String FORM_URL_ENCODED_CONSTANT = "application/x-www-form-urlencoded"; 26 | public final static String GZIP_CONSTANT = "gzip"; 27 | public final static String DEFLATE_CONSTANT = "deflate"; 28 | public final static String UTF8_CONSTANT = "UTF-8"; 29 | public final static String HTTP_SCHEME = "http"; 30 | public final static String HTTPS_SCHEME = "https"; 31 | public final static int HTTP_DEFAULT_PORT = 80; 32 | public final static int HTTPS_DEFAULT_PORT = 443; 33 | 34 | private HttpUtilities() { 35 | } 36 | 37 | /** 38 | * Check if content is text content type 39 | * @param contentType http body content type 40 | * 41 | * */ 42 | static public boolean isTextContentType(String contentType) { 43 | return contentType != null && (Pattern.compile(IS_TEXT_REGEX_CONSTANT).matcher(contentType).find() 44 | || isFormURLEncodedContentType(contentType)); 45 | } 46 | 47 | /** 48 | * Check if content is application/x-www-form-url-encoded (POST parameters) 49 | * @param contentType http body content type 50 | * 51 | */ 52 | static public boolean isFormURLEncodedContentType(String contentType) { 53 | return FORM_URL_ENCODED_CONSTANT.equals(contentType); 54 | } 55 | 56 | /** 57 | * Check if content is encoded in a compression format 58 | * @param encodingName http body content encoding 59 | * 60 | * */ 61 | static public boolean isCompressedContentEncoding(String encodingName) { 62 | return GZIP_CONSTANT.equals(encodingName) || DEFLATE_CONSTANT.equals(encodingName); 63 | } 64 | 65 | /** 66 | * Converts a URL / POST parameter string to an ordered map of key / value pairs 67 | * @param paramsString the URL-encoded '&' delimited string of key / value pairs 68 | * @return a LinkedHashMap representing the decoded parameters 69 | */ 70 | static public Map stringToUrlParams(String paramsString, String charset) 71 | throws UnsupportedEncodingException { 72 | Map params = new LinkedHashMap<>(); 73 | if (!StringUtils.isBlank(paramsString)) { 74 | for (String param : paramsString.split("&")) { 75 | String[] keyValue = param.split("="); 76 | assert (keyValue.length > 0); 77 | if (keyValue.length == 1) { 78 | params.put(URLDecoder.decode(keyValue[0], charset), ""); 79 | } else { 80 | params.put(URLDecoder.decode(keyValue[0], charset), URLDecoder.decode(keyValue[1], charset)); 81 | } 82 | } 83 | } 84 | return params; 85 | } 86 | 87 | /** 88 | * Converts an ordered map of key / value pairs to a URL parameter string 89 | * @param params the map of key / value pairs 90 | * @return a string representation of the key / value pairs, delimited by '&' 91 | */ 92 | static public String urlParamsToString(Map params, String charset) 93 | throws UnsupportedEncodingException { 94 | if (params.size() == 0) { 95 | return ""; 96 | } 97 | StringBuilder resultBuilder = new StringBuilder(); 98 | for (Map.Entry entry : params.entrySet()) { 99 | String encodedName = URLEncoder.encode(entry.getKey(), charset); 100 | String encodedValue = URLEncoder.encode(entry.getValue(), charset); 101 | resultBuilder.append(encodedName).append("=").append(encodedValue).append("&"); 102 | } 103 | resultBuilder.deleteCharAt(resultBuilder.length() - 1); 104 | return resultBuilder.toString(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/BaseMatchRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import org.apache.commons.lang3.builder.EqualsBuilder; 9 | import org.apache.commons.lang3.builder.HashCodeBuilder; 10 | 11 | 12 | /** 13 | * Abstract base {@link com.linkedin.flashback.matchrules.MatchRule} class 14 | * This class is required because derived class will be used as lookup key in 15 | * {@link CompositeMatchRule} 16 | * 17 | * @author shfeng 18 | */ 19 | public abstract class BaseMatchRule implements MatchRule { 20 | @Override 21 | public int hashCode() { 22 | return HashCodeBuilder.reflectionHashCode(this); 23 | } 24 | 25 | @Override 26 | public boolean equals(Object obj) { 27 | return EqualsBuilder.reflectionEquals(this, obj); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/CompositeMatchRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.google.common.base.Predicate; 9 | import com.google.common.collect.Iterables; 10 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | 15 | /** 16 | * Customized match rule which can contain any Match rule combinations. 17 | * @author shfeng 18 | * @author dvinegra 19 | */ 20 | public class CompositeMatchRule implements MatchRule { 21 | private Set _matchRules = new HashSet<>(); 22 | 23 | public void addRule(MatchRule matchRule) { 24 | _matchRules.add(matchRule); 25 | } 26 | 27 | public void addAll(Set rules) { 28 | _matchRules.addAll(rules); 29 | } 30 | 31 | @Override 32 | public boolean test(final RecordedHttpRequest incomingRequest, final RecordedHttpRequest expectedRequest) { 33 | return Iterables.all(_matchRules, new Predicate() { 34 | @Override 35 | public boolean apply(MatchRule rule) { 36 | return rule.test(incomingRequest, expectedRequest); 37 | } 38 | }); 39 | } 40 | 41 | @Override 42 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 43 | StringBuilder resultBuilder = new StringBuilder(); 44 | _matchRules.stream().forEach((rule) -> { 45 | if (!rule.test(incomingRequest, expectedRequest)) { 46 | resultBuilder.append(rule.getMatchFailureDescriptionForRequests(incomingRequest, expectedRequest)).append("\n"); 47 | } 48 | }); 49 | return resultBuilder.toString(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/DummyMatchRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | 10 | 11 | /** 12 | * Create a dummy match rule for proxy server to start with, then 13 | * the user will need to set their specific rule as need. 14 | */ 15 | public class DummyMatchRule extends BaseMatchRule { 16 | static final String MATCH_RULE_IS_NOT_VALID = "match rule is not valid"; 17 | 18 | @Override 19 | public boolean test(RecordedHttpRequest recordedHttpRequest, RecordedHttpRequest recordedHttpRequest2) { 20 | throw new IllegalStateException(MATCH_RULE_IS_NOT_VALID); 21 | } 22 | 23 | @Override 24 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 25 | throw new IllegalStateException(MATCH_RULE_IS_NOT_VALID); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedByteHttpBody; 9 | import com.linkedin.flashback.serializable.RecordedEncodedHttpBody; 10 | import com.linkedin.flashback.serializable.RecordedHttpBody; 11 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 12 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 13 | import java.io.IOException; 14 | import java.util.Arrays; 15 | 16 | 17 | /** 18 | * Match rule to match two RecordedHttpBody 19 | * @author shfeng 20 | */ 21 | public class MatchBody extends BaseMatchRule { 22 | @Override 23 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 24 | String charSet1 = incomingRequest.getCharset(); 25 | String charSet2 = expectedRequest.getCharset(); 26 | RecordedHttpBody incomingBody = incomingRequest.getHttpBody(); 27 | if (incomingBody == null) { 28 | incomingBody = new RecordedByteHttpBody(new byte[0]); 29 | } 30 | RecordedHttpBody expectedBody = expectedRequest.getHttpBody(); 31 | if (expectedBody == null) { 32 | expectedBody = new RecordedByteHttpBody(new byte[0]); 33 | } 34 | 35 | try { 36 | return Arrays.equals(incomingBody.getContent(charSet1), expectedBody.getContent(charSet2)); 37 | } catch (IOException e) { 38 | //TODO: PLACEHOLDER, error handling will be in separate RB. 39 | throw new RuntimeException("Failed to convert to byte arrays", e); 40 | } 41 | } 42 | 43 | @Override 44 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 45 | RecordedHttpBody incomingBody = getBodyFromRequest(incomingRequest); 46 | RecordedHttpBody expectedBody = getBodyFromRequest(expectedRequest); 47 | if (incomingBody instanceof RecordedStringHttpBody && expectedBody instanceof RecordedStringHttpBody) { 48 | return String.format("HTTP Body Mismatch%nIncoming Body: %s%nExpected Body: %s%n", 49 | ((RecordedStringHttpBody) incomingBody).getContent(), 50 | ((RecordedStringHttpBody) expectedBody).getContent()); 51 | } else { 52 | return "HTTP Body Mismatch (binary bodies differ)"; 53 | } 54 | } 55 | 56 | private RecordedHttpBody getBodyFromRequest(RecordedHttpRequest request) { 57 | RecordedHttpBody body = request.getHttpBody(); 58 | if (body instanceof RecordedEncodedHttpBody) { 59 | return ((RecordedEncodedHttpBody) body).getDecodedBody(); 60 | } else { 61 | return body; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchBodyPostParameters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.http.HttpUtilities; 9 | import com.linkedin.flashback.serializable.RecordedEncodedHttpBody; 10 | import com.linkedin.flashback.serializable.RecordedHttpBody; 11 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 12 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 13 | import java.io.UnsupportedEncodingException; 14 | import java.util.Map; 15 | import org.apache.log4j.Logger; 16 | 17 | 18 | /** 19 | * Match the form-urlencoded POST parameters in the request body 20 | * @author dvinegra 21 | */ 22 | public class MatchBodyPostParameters extends MatchBody { 23 | 24 | private static final Logger logger = Logger.getLogger("MatchBodyPostParameters"); 25 | 26 | private final MatchRuleMapTransform _transform; 27 | 28 | public MatchBodyPostParameters() { 29 | this(null); 30 | } 31 | 32 | public MatchBodyPostParameters(MatchRuleMapTransform transform) { 33 | if (transform != null) { 34 | _transform = transform; 35 | } else { 36 | _transform = new MatchRuleIdentityTransform(); 37 | } 38 | } 39 | 40 | @Override 41 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 42 | if (HttpUtilities.isFormURLEncodedContentType(incomingRequest.getContentType()) && HttpUtilities 43 | .isFormURLEncodedContentType(expectedRequest.getContentType())) { 44 | try { 45 | Map incomingParams = getPostParametersFromRequest(incomingRequest); 46 | Map expectedParams = getPostParametersFromRequest(expectedRequest); 47 | return testParameterEquivalency(incomingParams, expectedParams); 48 | } catch (UnsupportedEncodingException e) { 49 | logger.error("Caught exception " + e + " while decoding POST parameters"); 50 | } 51 | } 52 | return super.test(incomingRequest, expectedRequest); 53 | } 54 | 55 | private Map getPostParametersFromRequest(RecordedHttpRequest request) 56 | throws UnsupportedEncodingException { 57 | RecordedHttpBody body = request.getHttpBody(); 58 | if (body instanceof RecordedEncodedHttpBody) { 59 | body = ((RecordedEncodedHttpBody) body).getDecodedBody(); 60 | } 61 | assert (body instanceof RecordedStringHttpBody); 62 | String content = ((RecordedStringHttpBody) body).getContent(); 63 | return HttpUtilities.stringToUrlParams(content, request.getCharset()); 64 | } 65 | 66 | /** 67 | * Tests whether the maps have the same key/value pairs in the same order 68 | */ 69 | private boolean testParameterEquivalency(Map incomingParams, Map expectedParams) { 70 | return _transform.transform(incomingParams).toString().equals(_transform.transform(expectedParams).toString()); 71 | } 72 | 73 | @Override 74 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 75 | StringBuilder resultBuilder = new StringBuilder("HTTP Body Parameters Mismatch"); 76 | if (_transform instanceof MatchRuleBlacklistTransform) { 77 | resultBuilder.append(" (with Blacklist)"); 78 | } else if (_transform instanceof MatchRuleWhitelistTransform) { 79 | resultBuilder.append(" (with Whitelist)"); 80 | } 81 | try { 82 | Map incomingParams = getPostParametersFromRequest(incomingRequest); 83 | Map expectedParams = getPostParametersFromRequest(expectedRequest); 84 | resultBuilder.append("%n") 85 | .append(String.format("Incoming Parameters: %s%n", _transform.transform(incomingParams))) 86 | .append(String.format("Expected Parameters: %s%n", _transform.transform(expectedParams))); 87 | } catch (UnsupportedEncodingException e) { 88 | logger.error("Caught exception " + e + " while decoding POST parameters"); 89 | } 90 | return resultBuilder.toString(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchBodyWithAnyBoundary.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.flashback.matchrules; 2 | 3 | import com.google.common.net.HttpHeaders; 4 | import com.linkedin.flashback.serializable.RecordedByteHttpBody; 5 | import com.linkedin.flashback.serializable.RecordedEncodedHttpBody; 6 | import com.linkedin.flashback.serializable.RecordedHttpBody; 7 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 8 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 9 | import java.io.IOException; 10 | import java.util.Arrays; 11 | 12 | 13 | /** 14 | * Match rule to match two RecordedHttpBody where boundary values differ 15 | * but all other content remains same. 16 | * 17 | * @author kagale 18 | */ 19 | public class MatchBodyWithAnyBoundary extends BaseMatchRule { 20 | 21 | private static final String BOUNDARY = "boundary="; 22 | private static final String MULTIPART = "multipart"; 23 | private static final String EMPTY = ""; 24 | private static final String SEMI_COLON = ";"; 25 | 26 | @Override 27 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 28 | String charSet1 = incomingRequest.getCharset(); 29 | String charSet2 = expectedRequest.getCharset(); 30 | RecordedHttpBody incomingBody = incomingRequest.getHttpBody(); 31 | if (incomingBody == null) { 32 | incomingBody = new RecordedByteHttpBody(new byte[0]); 33 | } 34 | RecordedHttpBody expectedBody = expectedRequest.getHttpBody(); 35 | if (expectedBody == null) { 36 | expectedBody = new RecordedByteHttpBody(new byte[0]); 37 | } 38 | 39 | try { 40 | // Go inside only for requests with multipart data. 41 | if (incomingRequest.getHeaders().containsKey(HttpHeaders.CONTENT_TYPE) && expectedRequest.getHeaders().containsKey(HttpHeaders.CONTENT_TYPE) 42 | && incomingRequest.getHeaders().get(HttpHeaders.CONTENT_TYPE).iterator().next().contains(MULTIPART) 43 | && expectedRequest.getHeaders().get(HttpHeaders.CONTENT_TYPE).iterator().next().contains(MULTIPART)) { 44 | // Get boundary values. 45 | String currentRequestBoundaryValue = Arrays.stream(incomingRequest.getHeaders().get(HttpHeaders.CONTENT_TYPE).iterator().next() 46 | .split(SEMI_COLON)) 47 | .filter((attr) -> attr.contains(BOUNDARY)) 48 | .findFirst() 49 | .orElse(EMPTY) 50 | .replaceFirst(BOUNDARY, EMPTY) 51 | .trim(); 52 | String recordedRequestBoundaryValue = Arrays.stream(expectedRequest.getHeaders().get(HttpHeaders.CONTENT_TYPE).iterator().next() 53 | .split(SEMI_COLON)) 54 | .filter((attr) -> attr.contains(BOUNDARY)) 55 | .findFirst() 56 | .orElse(EMPTY) 57 | .replaceFirst(BOUNDARY, EMPTY) 58 | .trim(); 59 | 60 | // Prepare request for matching 61 | // i.e., by overriding the boundary value 62 | if (!currentRequestBoundaryValue.equals(recordedRequestBoundaryValue)) { 63 | // Override the boundary value of request body. 64 | String recordedBody = new String(expectedBody.getContent(charSet2)).replaceAll(recordedRequestBoundaryValue, 65 | currentRequestBoundaryValue); 66 | expectedBody = new RecordedByteHttpBody(recordedBody.getBytes(charSet2)); 67 | } 68 | } 69 | return Arrays.equals(incomingBody.getContent(charSet1), expectedBody.getContent(charSet2)); 70 | } catch (IOException e) { 71 | throw new RuntimeException("Failed to convert to byte arrays", e); 72 | } 73 | } 74 | 75 | @Override 76 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 77 | RecordedHttpBody incomingBody = getBodyFromRequest(incomingRequest); 78 | RecordedHttpBody expectedBody = getBodyFromRequest(expectedRequest); 79 | if (incomingBody instanceof RecordedStringHttpBody && expectedBody instanceof RecordedStringHttpBody) { 80 | return String.format("HTTP Body Mismatch\nIncoming Body: %s\nExpected Body: %s\n", 81 | ((RecordedStringHttpBody) incomingBody).getContent(), 82 | ((RecordedStringHttpBody) expectedBody).getContent()); 83 | } else { 84 | return "HTTP Body Mismatch (binary bodies differ)"; 85 | } 86 | } 87 | 88 | private RecordedHttpBody getBodyFromRequest(RecordedHttpRequest request) { 89 | RecordedHttpBody body = request.getHttpBody(); 90 | if (body instanceof RecordedEncodedHttpBody) { 91 | return ((RecordedEncodedHttpBody) body).getDecodedBody(); 92 | } else { 93 | return body; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchCaseInsensitiveMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | 10 | 11 | /** 12 | * In some edge case, client might need match two request method which is not case sensitive 13 | * 14 | * @author shfeng 15 | */ 16 | public class MatchCaseInsensitiveMethod extends BaseMatchRule { 17 | @Override 18 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 19 | return incomingRequest.getMethod().equalsIgnoreCase(expectedRequest.getMethod()); 20 | } 21 | 22 | @Override 23 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 24 | return String.format("HTTP Method Mismatch%nIncoming Method: %s%nExpected Method: %s%n", 25 | incomingRequest.getMethod(), 26 | expectedRequest.getMethod()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchHeaders.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.google.common.collect.Multimap; 9 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 10 | 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | 16 | /** 17 | * Match rule to match http headers 18 | * @author shfeng. 19 | */ 20 | public class MatchHeaders extends BaseMatchRule { 21 | 22 | private final MatchRuleMapTransform _transform; 23 | 24 | public MatchHeaders() { 25 | this(null); 26 | } 27 | 28 | public MatchHeaders(MatchRuleMapTransform transform) { 29 | if (transform != null) { 30 | _transform = transform; 31 | } else { 32 | _transform = new MatchRuleIdentityTransform(); 33 | } 34 | } 35 | 36 | @Override 37 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 38 | return _transform.transform(multimapToCommaSeparatedMap(incomingRequest.getHeaders())) 39 | .equals(_transform.transform(multimapToCommaSeparatedMap(expectedRequest.getHeaders()))); 40 | } 41 | 42 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 43 | StringBuilder resultBuilder = new StringBuilder("HTTP Headers Mismatch"); 44 | if (_transform instanceof MatchRuleBlacklistTransform) { 45 | resultBuilder.append(" (with Blacklist)"); 46 | } else if (_transform instanceof MatchRuleWhitelistTransform) { 47 | resultBuilder.append(" (with Whitelist)"); 48 | } 49 | resultBuilder.append("%n") 50 | .append(String.format("Incoming Headers: %s%n", _transform.transform(multimapToCommaSeparatedMap(incomingRequest.getHeaders())))) 51 | .append(String.format("Expected Headers: %s%n", _transform.transform(multimapToCommaSeparatedMap(expectedRequest.getHeaders())))); 52 | return resultBuilder.toString(); 53 | } 54 | 55 | private static Map multimapToCommaSeparatedMap(Multimap multimap) { 56 | Map> mapOfCollections = multimap.asMap(); 57 | HashMap map = new HashMap<>(); 58 | for (Map.Entry> entry : mapOfCollections.entrySet()) { 59 | map.put(entry.getKey(), String.join(",", entry.getValue())); 60 | } 61 | return map; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | 10 | 11 | /** 12 | * Match rule to match http method 13 | * @author shfeng 14 | */ 15 | public class MatchMethod extends BaseMatchRule { 16 | @Override 17 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 18 | return incomingRequest.getMethod().equals(expectedRequest.getMethod()); 19 | } 20 | 21 | @Override 22 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 23 | return String.format("HTTP Method Mismatch%nIncoming Method: %s%nExpected Method: %s%n", 24 | incomingRequest.getMethod(), 25 | expectedRequest.getMethod()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | import java.util.function.BiPredicate; 10 | 11 | 12 | /** 13 | * Interface for Match Rule 14 | * @author shfeng 15 | */ 16 | public interface MatchRule extends BiPredicate { 17 | 18 | /** 19 | * Returns a description for the match failure for a pair of requests 20 | * @return the match failure description string 21 | */ 22 | String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest); 23 | } 24 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchRuleBlacklistTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import java.util.Collections; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | 14 | 15 | /** 16 | * Transform that returns a map containing only the keys absent from the blacklist 17 | * @author dvinegra 18 | */ 19 | public class MatchRuleBlacklistTransform implements MatchRuleMapTransform { 20 | 21 | private final Set _blackList; 22 | 23 | public MatchRuleBlacklistTransform(Set blackList) { 24 | if (blackList != null) { 25 | _blackList = blackList; 26 | } else { 27 | _blackList = Collections.EMPTY_SET; 28 | } 29 | } 30 | 31 | @Override 32 | public Map transform(Map map) { 33 | return map.keySet().stream().filter(k -> !_blackList.contains(k)) 34 | .collect(Collectors.toMap(k -> k, k -> map.get(k), (k, v) -> { 35 | throw new RuntimeException("Duplicate key " + k); 36 | }, LinkedHashMap::new)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchRuleIdentityTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | 11 | 12 | /** 13 | * Transform that returns a copy of the existing map 14 | * @author dvinegra 15 | */ 16 | public class MatchRuleIdentityTransform implements MatchRuleMapTransform { 17 | 18 | @Override 19 | public Map transform(Map map) { 20 | return new LinkedHashMap<>(map); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchRuleMapTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import java.util.Map; 9 | 10 | 11 | /** 12 | * Interface to perform a transformation on a Map, which is a common operation of several Match Rules 13 | * @author dvinegra 14 | */ 15 | public interface MatchRuleMapTransform { 16 | 17 | Map transform(Map map); 18 | } 19 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchRuleUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import java.util.Set; 9 | 10 | 11 | /** 12 | * Class that provides convenience methods to create match rules based on common combinations 13 | * @author dvinegra 14 | */ 15 | public final class MatchRuleUtils { 16 | 17 | private MatchRuleUtils() { 18 | } 19 | 20 | /** 21 | * @return rule to match request URI, Method, Headers, and Body 22 | */ 23 | public static MatchRule matchEntireRequest() { 24 | CompositeMatchRule compositeMatchRule = new CompositeMatchRule(); 25 | compositeMatchRule.addRule(new MatchMethod()); 26 | compositeMatchRule.addRule(new MatchUri()); 27 | compositeMatchRule.addRule(new MatchHeaders()); 28 | compositeMatchRule.addRule(new MatchBody()); 29 | return compositeMatchRule; 30 | } 31 | 32 | /** 33 | * @return rule to match request URI and Method 34 | */ 35 | public static MatchRule matchMethodUri() { 36 | CompositeMatchRule compositeMatchRule = new CompositeMatchRule(); 37 | compositeMatchRule.addRule(new MatchMethod()); 38 | compositeMatchRule.addRule(new MatchUri()); 39 | return compositeMatchRule; 40 | } 41 | 42 | /** 43 | * @return rule to match request URI, Body and Method 44 | */ 45 | public static MatchRule matchMethodBodyUri() { 46 | CompositeMatchRule compositeMatchRule = new CompositeMatchRule(); 47 | compositeMatchRule.addRule(new MatchBody()); 48 | compositeMatchRule.addRule(new MatchMethod()); 49 | compositeMatchRule.addRule(new MatchUri()); 50 | return compositeMatchRule; 51 | } 52 | 53 | /** 54 | * @return rule to match request Method, URI and Body with any boundary 55 | */ 56 | public static MatchRule matchMethodUriBodyWithAnyBoundary() { 57 | CompositeMatchRule compositeMatchRule = new CompositeMatchRule(); 58 | compositeMatchRule.addRule(new MatchMethod()); 59 | compositeMatchRule.addRule(new MatchUri()); 60 | compositeMatchRule.addRule(new MatchBodyWithAnyBoundary()); 61 | return compositeMatchRule; 62 | } 63 | 64 | /** 65 | * @return rule to match request URI, whitelisting the specified query parameters 66 | */ 67 | public static MatchRule matchUriWithQueryWhitelist(Set whiteList) { 68 | return new MatchUriWithQueryTransform(new MatchRuleWhitelistTransform(whiteList)); 69 | } 70 | 71 | /** 72 | * @return rule to match request URI, blacklisting the specified query parameters 73 | */ 74 | public static MatchRule matchUriWithQueryBlacklist(Set blackList) { 75 | return new MatchUriWithQueryTransform(new MatchRuleBlacklistTransform(blackList)); 76 | } 77 | 78 | /** 79 | * @return rule to match the request headers contained in the whitelist 80 | */ 81 | public static MatchRule matchHeadersWithWhitelist(Set whiteList) { 82 | return new MatchHeaders(new MatchRuleWhitelistTransform(whiteList)); 83 | } 84 | 85 | /** 86 | * @return rule to match the request headers, except for those in the blacklist 87 | */ 88 | public static MatchRule matchHeadersWithBlacklist(Set blackList) { 89 | return new MatchHeaders(new MatchRuleBlacklistTransform(blackList)); 90 | } 91 | 92 | /** 93 | * @return rule to match the request POST body parameters contained in the whitelist 94 | */ 95 | public static MatchRule matchBodyPostParametersWithWhitelist(Set whiteList) { 96 | return new MatchBodyPostParameters(new MatchRuleWhitelistTransform(whiteList)); 97 | } 98 | 99 | /** 100 | * @return rule to match the request POST body parameters, except for those in the blacklist 101 | */ 102 | public static MatchRule matchBodyPostParametersWithBlacklist(Set blackList) { 103 | return new MatchBodyPostParameters(new MatchRuleBlacklistTransform(blackList)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchRuleWhitelistTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import java.util.*; 9 | import java.util.stream.Collectors; 10 | 11 | 12 | /** 13 | * Transform that returns a map containing only the keys in the whitelist 14 | * @author dvinegra 15 | */ 16 | public class MatchRuleWhitelistTransform implements MatchRuleMapTransform { 17 | 18 | private final Set _whiteList; 19 | 20 | public MatchRuleWhitelistTransform(Set whiteList) { 21 | if (whiteList != null) { 22 | _whiteList = whiteList; 23 | } else { 24 | _whiteList = Collections.EMPTY_SET; 25 | } 26 | } 27 | 28 | @Override 29 | public Map transform(Map map) { 30 | return map.keySet().stream().filter(_whiteList::contains) 31 | .collect(Collectors.toMap(k -> k, k -> map.get(k), (k, v) -> { 32 | throw new RuntimeException("Duplicate key " + k); 33 | }, LinkedHashMap::new)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchUri.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.http.HttpUtilities; 9 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | import org.apache.log4j.Logger; 13 | 14 | 15 | /** 16 | * Match rule to match Http uri 17 | * @author shfeng 18 | */ 19 | public class MatchUri extends BaseMatchRule { 20 | 21 | private static final String MODULE = MatchUri.class.getName(); 22 | private static final Logger LOGGER = Logger.getLogger(MODULE); 23 | 24 | @Override 25 | public boolean test(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 26 | return testUriEquivalency(incomingRequest.getUri(), expectedRequest.getUri()); 27 | } 28 | 29 | @Override 30 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 31 | return String.format("URI Mismatch%nIncoming URI: %s%nExpected URI: %s%n", 32 | incomingRequest.getUri(), 33 | expectedRequest.getUri()); 34 | } 35 | 36 | protected boolean testUriEquivalency(URI incomingUri, URI expectedUri) { 37 | try { 38 | URI incomingCanonicalizedUri = getCanonicalizedUri(incomingUri); 39 | URI expectedCanonicalizedUri = getCanonicalizedUri(expectedUri); 40 | return incomingCanonicalizedUri.equals(expectedCanonicalizedUri); 41 | } catch (URISyntaxException e) { 42 | LOGGER.error("Caught exception " + e + " while constructing modified URI"); 43 | } 44 | return incomingUri.equals(expectedUri); 45 | } 46 | 47 | private int getPortForScheme(String scheme) { 48 | if (HttpUtilities.HTTP_SCHEME.equalsIgnoreCase(scheme)) { 49 | return HttpUtilities.HTTP_DEFAULT_PORT; 50 | } else if (HttpUtilities.HTTPS_SCHEME.equalsIgnoreCase(scheme)) { 51 | return HttpUtilities.HTTPS_DEFAULT_PORT; 52 | } 53 | return -1; 54 | } 55 | 56 | /** 57 | * Returns a canonicalized version of the Uri, accounting for the default port for the scheme 58 | * @param uri 59 | * @return a canonicalized URI with the default port for the scheme (http or https) explicitly added if no port is specified 60 | * @throws java.net.URISyntaxException 61 | */ 62 | private URI getCanonicalizedUri(URI uri) 63 | throws URISyntaxException { 64 | int defaultPort = getPortForScheme(uri.getScheme()); 65 | if (uri.getPort() == -1 && defaultPort != -1) { 66 | return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), defaultPort, uri.getPath(), uri.getQuery(), uri.getFragment()); 67 | } 68 | return uri; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/MatchUriWithQueryTransform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.http.HttpUtilities; 9 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 10 | import java.io.UnsupportedEncodingException; 11 | import java.net.URI; 12 | import java.net.URISyntaxException; 13 | import java.util.Map; 14 | import org.apache.log4j.Logger; 15 | 16 | 17 | /** 18 | * Match the URI, modifying the query parameters as specified by the transform 19 | * This only applies to query parameters of the top-level URI 20 | * @author dvinegra 21 | */ 22 | public class MatchUriWithQueryTransform extends MatchUri { 23 | 24 | private static final Logger LOGGER = Logger.getLogger("MatchUriWithQueryTransform"); 25 | 26 | private final MatchRuleMapTransform _transform; 27 | 28 | public MatchUriWithQueryTransform(MatchRuleMapTransform transform) { 29 | if (transform != null) { 30 | _transform = transform; 31 | } else { 32 | _transform = new MatchRuleIdentityTransform(); 33 | } 34 | } 35 | 36 | @Override 37 | protected boolean testUriEquivalency(URI incomingUri, URI expectedUri) { 38 | try { 39 | URI modifiedIncomingUri = getModifiedUri(incomingUri); 40 | URI modifiedExpectedUri = getModifiedUri(expectedUri); 41 | return super.testUriEquivalency(modifiedIncomingUri, modifiedExpectedUri); 42 | } catch (URISyntaxException | UnsupportedEncodingException e) { 43 | LOGGER.error("Caught exception " + e + " while constructing modified URI"); 44 | } 45 | return false; 46 | } 47 | 48 | @Override 49 | public String getMatchFailureDescriptionForRequests(RecordedHttpRequest incomingRequest, RecordedHttpRequest expectedRequest) { 50 | StringBuilder resultBuilder = new StringBuilder("URI Mismatch"); 51 | if (_transform instanceof MatchRuleBlacklistTransform) { 52 | resultBuilder.append(" (with Query Blacklist)"); 53 | } else if (_transform instanceof MatchRuleWhitelistTransform) { 54 | resultBuilder.append(" (with Query Whitelist)"); 55 | } 56 | try { 57 | URI modifiedIncomingUri = getModifiedUri(incomingRequest.getUri()); 58 | URI modifiedExpectedUri = getModifiedUri(expectedRequest.getUri()); 59 | resultBuilder.append("%n") 60 | .append(String.format("Incoming URI: %s%n", modifiedIncomingUri)) 61 | .append(String.format("Expected URI: %s%n", modifiedExpectedUri)); 62 | } catch (URISyntaxException | UnsupportedEncodingException e) { 63 | LOGGER.error("Caught exception " + e + " while constructing modified URI"); 64 | } 65 | return resultBuilder.toString(); 66 | } 67 | 68 | private URI getModifiedUri(URI uri) 69 | throws UnsupportedEncodingException, URISyntaxException { 70 | Map incomingParams = HttpUtilities.stringToUrlParams(uri.getRawQuery(), HttpUtilities.UTF8_CONSTANT); 71 | String modifiedIncomingQuery = HttpUtilities.urlParamsToString(_transform.transform(incomingParams), HttpUtilities.UTF8_CONSTANT); 72 | return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), modifiedIncomingQuery, uri.getFragment()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/matchrules/NamedMatchRule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | /** 9 | * List out of pre-defined match rules. It's mainly used for non-java 10 | * application because flashback doesn't support non-java match rules 11 | * now. 12 | * 13 | * @author shfeng 14 | */ 15 | public enum NamedMatchRule { 16 | MATCH_ENTIRE_REQUEST("matchEntireRequest"), 17 | MATCH_METHOD_URI("matchMethodUri"), 18 | MATCH_METHOD_BODY_URI("matchMethodBodyUri"), 19 | MATCH_METHOD_URI_BODY_WITHOUT_BOUNDARY("matchMethodUriBodyWithAnyBoundary"); 20 | private final String _text; 21 | 22 | NamedMatchRule(String text) { 23 | _text = text; 24 | } 25 | 26 | public static MatchRule fromString(String predefinedMatchRule) { 27 | if (MATCH_ENTIRE_REQUEST._text.equalsIgnoreCase(predefinedMatchRule)) { 28 | return MatchRuleUtils.matchEntireRequest(); 29 | } 30 | if (MATCH_METHOD_URI._text.equalsIgnoreCase(predefinedMatchRule)) { 31 | return MatchRuleUtils.matchMethodUri(); 32 | } 33 | if (MATCH_METHOD_BODY_URI._text.equalsIgnoreCase(predefinedMatchRule)) { 34 | return MatchRuleUtils.matchMethodBodyUri(); 35 | } 36 | if (MATCH_METHOD_URI_BODY_WITHOUT_BOUNDARY._text.equalsIgnoreCase(predefinedMatchRule)) { 37 | return MatchRuleUtils.matchMethodUriBodyWithAnyBoundary(); 38 | } 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/scene/DummyScene.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.scene; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpExchange; 9 | import java.util.List; 10 | 11 | 12 | /** 13 | * Create a dummy scene for proxy server to start with, then 14 | * the user will need to set their specific scene as need. 15 | */ 16 | public class DummyScene extends Scene { 17 | static final String SCENE_IS_NOT_VALID = "scene is not valid"; 18 | 19 | public DummyScene() { 20 | super(null, null, null, null); 21 | } 22 | 23 | @Override 24 | public List getRecordedHttpExchangeList() { 25 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 26 | } 27 | 28 | @Override 29 | public String getName() { 30 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 31 | } 32 | 33 | @Override 34 | public boolean isReadable() { 35 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 36 | } 37 | 38 | @Override 39 | public boolean isSequential() { 40 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object obj) { 50 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 51 | } 52 | 53 | @Override 54 | public String getSceneRoot() { 55 | throw new IllegalStateException(SCENE_IS_NOT_VALID); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/scene/Scene.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.scene; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpExchange; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import org.apache.commons.lang3.builder.EqualsBuilder; 12 | import org.apache.commons.lang3.builder.HashCodeBuilder; 13 | 14 | 15 | /** 16 | * Scene that contains everything that will be stored in scene file. 17 | * Note: The best practice to create Scene is using SceneFactory 18 | * 19 | * @author shfeng 20 | */ 21 | public class Scene { 22 | private final String _name; 23 | private final List _recordedHttpExchangeList; 24 | private final SceneMode _sceneMode; 25 | private final String _sceneRoot; 26 | 27 | public Scene(String name, SceneMode sceneMode, String sceneRoot, 28 | List recordedHttpExchangeList) { 29 | _name = name; 30 | _sceneMode = sceneMode; 31 | _sceneRoot = sceneRoot; 32 | _recordedHttpExchangeList = recordedHttpExchangeList; 33 | } 34 | 35 | public Scene(SceneConfiguration sceneConfiguration) { 36 | this(sceneConfiguration.getSceneName(), sceneConfiguration.getSceneMode(), sceneConfiguration.getSceneRoot(), 37 | new ArrayList<>()); 38 | } 39 | 40 | public List getRecordedHttpExchangeList() { 41 | return _recordedHttpExchangeList; 42 | } 43 | 44 | public String getName() { 45 | return _name; 46 | } 47 | 48 | public boolean isReadable() { 49 | return _sceneMode == SceneMode.PLAYBACK || _sceneMode == SceneMode.SEQUENTIAL_PLAYBACK; 50 | } 51 | 52 | public boolean isSequential() { 53 | return _sceneMode == SceneMode.SEQUENTIAL_RECORD || _sceneMode == SceneMode.SEQUENTIAL_PLAYBACK; 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return HashCodeBuilder.reflectionHashCode(this); 59 | } 60 | 61 | @Override 62 | public boolean equals(Object obj) { 63 | return EqualsBuilder.reflectionEquals(this, obj); 64 | } 65 | 66 | public String getSceneRoot() { 67 | return _sceneRoot; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/scene/SceneConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.scene; 7 | 8 | /** 9 | * SceneConfiguration that contains all necessary settings for recording and replaying. 10 | * 11 | * @author shfeng 12 | */ 13 | public class SceneConfiguration { 14 | private final String _sceneRoot; 15 | private final SceneMode _sceneMode; 16 | private final String _sceneName; 17 | 18 | public SceneConfiguration(String sceneRoot, SceneMode sceneMode, String sceneName) { 19 | _sceneRoot = sceneRoot; 20 | _sceneMode = sceneMode; 21 | _sceneName = sceneName; 22 | } 23 | 24 | /** 25 | * Get Scene mode: record or replay 26 | * */ 27 | public SceneMode getSceneMode() { 28 | return _sceneMode; 29 | } 30 | 31 | /** 32 | * Get root path of loading/storing scene. 33 | * */ 34 | public String getSceneRoot() { 35 | return _sceneRoot; 36 | } 37 | 38 | /** 39 | * Get name of scene. 40 | * */ 41 | public String getSceneName() { 42 | return _sceneName; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/scene/SceneMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.scene; 7 | 8 | /** 9 | * Scene mode: either record only or playback only 10 | * 11 | * @author shfeng 12 | */ 13 | public enum SceneMode { 14 | RECORD("record"), 15 | PLAYBACK("playback"), 16 | SEQUENTIAL_RECORD("sequential_record"), 17 | SEQUENTIAL_PLAYBACK("sequential_playback"); 18 | 19 | private final String _text; 20 | SceneMode(String text) { 21 | _text = text; 22 | } 23 | 24 | public static SceneMode fromString(String text) { 25 | if (text != null) { 26 | for (SceneMode sceneMode : SceneMode.values()) { 27 | if (text.equalsIgnoreCase(sceneMode._text)) { 28 | return sceneMode; 29 | } 30 | } 31 | } 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedByteHttpBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import org.apache.commons.lang3.builder.EqualsBuilder; 9 | import org.apache.commons.lang3.builder.HashCodeBuilder; 10 | 11 | 12 | /** 13 | * Recorded http body that obtains its content from a byte array 14 | * @author shfeng 15 | */ 16 | public class RecordedByteHttpBody implements RecordedHttpBody { 17 | private final byte[] _content; 18 | 19 | /** 20 | * Construct object with byte array 21 | * */ 22 | public RecordedByteHttpBody(byte[] content) { 23 | _content = content; 24 | } 25 | 26 | @Override 27 | public byte[] getContent(String charSet) { 28 | return getContent(); 29 | } 30 | 31 | public byte[] getContent() { 32 | return _content; 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return HashCodeBuilder.reflectionHashCode(this); 38 | } 39 | 40 | @Override 41 | public boolean equals(Object obj) { 42 | return EqualsBuilder.reflectionEquals(this, obj); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedEncodedHttpBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import com.linkedin.flashback.decorator.compression.AbstractCompressor; 9 | import com.linkedin.flashback.decorator.compression.AbstractDecompressor; 10 | import com.linkedin.flashback.decorator.compression.DeflateCompressor; 11 | import com.linkedin.flashback.decorator.compression.DeflateDecompressor; 12 | import com.linkedin.flashback.decorator.compression.GzipCompressor; 13 | import com.linkedin.flashback.decorator.compression.GzipDecompressor; 14 | import com.linkedin.flashback.http.HttpUtilities; 15 | import java.io.IOException; 16 | 17 | 18 | /** 19 | * This class provides an abstraction for an HTTP body that is encoded using 20 | * gzip or deflate encoding. 21 | * 22 | * Internally, a RecordedEncodedHttpBody needs to store a RecordedHttpBody representing 23 | * the decoded (uncompressed) HTTP body content, as well as the name of the encoding used. 24 | * When asked to provide the byte array representing the content to send over the wire, the 25 | * decoded body is encoded (compressed) and the result is returned. Additionally, this compressed 26 | * value is cached so that the uncompressed body only needs to be compressed once. 27 | * 28 | * @author dvinegra 29 | */ 30 | public class RecordedEncodedHttpBody implements RecordedHttpBody { 31 | 32 | private final RecordedHttpBody _decodedBody; 33 | private final String _encodingName; 34 | 35 | // Cached the encoded content (to send on the wire) so that we don't compress multiple times 36 | private byte[] _encodedContent; 37 | 38 | /** 39 | * Constructor used to create a RecordedHttpBody instance from an already-decoded RecordedHttpBody 40 | * 41 | * @param decodedBody underlying RecordedHttpBody representing the body bytes *after* decoding 42 | * @param encodingName name of the encoding used to encode the content 43 | */ 44 | public RecordedEncodedHttpBody(RecordedHttpBody decodedBody, String encodingName) { 45 | _decodedBody = decodedBody; 46 | _encodingName = encodingName; 47 | } 48 | 49 | /** 50 | * Constructor used to create a RecordedEncodedHttpBody instance from the encoded (wire) bytes 51 | * 52 | * @param encodedContent the already encoded content, representing the bytes transferred over the wire 53 | * @param encodingName name of the encoding used to encode the content 54 | * @param charset charset 55 | * @param contentType http Content-Type for the content (once decoded) 56 | * @throws java.io.IOException 57 | */ 58 | public RecordedEncodedHttpBody(byte[] encodedContent, String encodingName, String charset, String contentType) 59 | throws IOException { 60 | _encodedContent = encodedContent; 61 | _encodingName = encodingName; 62 | 63 | byte[] decodedContent = getDecompressor().decompress(encodedContent); 64 | 65 | if (HttpUtilities.isTextContentType(contentType)) { 66 | _decodedBody = new RecordedStringHttpBody(new String(decodedContent, charset)); 67 | } else { 68 | _decodedBody = new RecordedByteHttpBody(decodedContent); 69 | } 70 | } 71 | 72 | /** 73 | * Returns the bytes meant to be sent over the wire. If necessary, this method 74 | * will encode the underlying http body content and cache the result so that 75 | * we don't have to do this more than once. 76 | * 77 | * @param charSet charset 78 | * @return the (encoded) content for the wire 79 | * @throws java.io.IOException 80 | */ 81 | @Override 82 | public byte[] getContent(String charSet) 83 | throws IOException { 84 | if (_encodedContent == null) { 85 | // Only compress the content once and cache the result 86 | _encodedContent = getCompressor().compress(_decodedBody.getContent(charSet)); 87 | } 88 | return _encodedContent; 89 | } 90 | 91 | public String getEncodingName() { 92 | return _encodingName; 93 | } 94 | 95 | public RecordedHttpBody getDecodedBody() { 96 | return _decodedBody; 97 | } 98 | 99 | private AbstractCompressor getCompressor() { 100 | if (HttpUtilities.GZIP_CONSTANT.equals(_encodingName)) { 101 | return new GzipCompressor(); 102 | } else if (HttpUtilities.DEFLATE_CONSTANT.equals(_encodingName)) { 103 | return new DeflateCompressor(); 104 | } else { 105 | throw new IllegalStateException("Invalid encoding for RecordedEncodedHttpBody"); 106 | } 107 | } 108 | 109 | private AbstractDecompressor getDecompressor() { 110 | if (HttpUtilities.GZIP_CONSTANT.equals(_encodingName)) { 111 | return new GzipDecompressor(); 112 | } else if (HttpUtilities.DEFLATE_CONSTANT.equals(_encodingName)) { 113 | return new DeflateDecompressor(); 114 | } else { 115 | throw new IllegalStateException("Invalid encoding for RecordedEncodedHttpBody"); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedHttpBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import java.io.IOException; 9 | 10 | 11 | /** 12 | *Interface of Recorded http body 13 | * @author shfeng 14 | */ 15 | public interface RecordedHttpBody { 16 | /** 17 | * convert http body to content byte array to be sent over the wire 18 | * @param charSet charset 19 | * */ 20 | byte[] getContent(String charSet) 21 | throws IOException; 22 | } 23 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedHttpExchange.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import java.util.Date; 9 | import org.apache.commons.lang3.builder.EqualsBuilder; 10 | import org.apache.commons.lang3.builder.HashCodeBuilder; 11 | 12 | 13 | /** 14 | * Http exchange which contains the whole http interaction (request, response and time) 15 | * @author shfeng 16 | */ 17 | public class RecordedHttpExchange { 18 | private RecordedHttpRequest _recordedHttpRequest; 19 | private RecordedHttpResponse _recordedHttpResponse; 20 | private Date _updateTime; 21 | 22 | public RecordedHttpExchange(RecordedHttpRequest recordedHttpRequest, RecordedHttpResponse recordedHttpResponse, 23 | Date updateTime) { 24 | _recordedHttpRequest = recordedHttpRequest; 25 | _recordedHttpResponse = recordedHttpResponse; 26 | _updateTime = updateTime; 27 | } 28 | 29 | public RecordedHttpRequest getRecordedHttpRequest() { 30 | return _recordedHttpRequest; 31 | } 32 | 33 | public RecordedHttpResponse getRecordedHttpResponse() { 34 | return _recordedHttpResponse; 35 | } 36 | 37 | public Date getUpdateTime() { 38 | return _updateTime; 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | return HashCodeBuilder.reflectionHashCode(this); 44 | } 45 | 46 | @Override 47 | public boolean equals(Object obj) { 48 | return EqualsBuilder.reflectionEquals(this, obj); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedHttpMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import com.google.common.base.Charsets; 9 | 10 | import com.google.common.collect.LinkedHashMultimap; 11 | import com.google.common.collect.Multimap; 12 | import com.google.common.net.HttpHeaders; 13 | import com.google.common.net.MediaType; 14 | import java.io.IOException; 15 | import java.util.Iterator; 16 | import org.apache.log4j.Logger; 17 | 18 | 19 | /** 20 | * Abstract Recorded http message(Java bean) which contains properties that is used for both 21 | * http request and http response 22 | * @author shfeng 23 | * @author dvinegra 24 | */ 25 | public abstract class RecordedHttpMessage { 26 | private static final String DEFAULT_CHARSET = Charsets.UTF_8.toString(); 27 | private static final Logger logger = Logger.getLogger("RecordedHttpMessage"); 28 | private static final String DEFAULT_CONTENT_TYPE = MediaType.OCTET_STREAM.toString(); 29 | 30 | private Multimap _headers = LinkedHashMultimap.create(); 31 | private RecordedHttpBody _httpBody; 32 | 33 | public RecordedHttpMessage(Multimap headers, RecordedHttpBody httpBody) { 34 | if (headers != null) { 35 | _headers = headers; 36 | } 37 | _httpBody = httpBody; 38 | 39 | // Update the Content-Length header if appropriate 40 | if (_headers.containsKey(HttpHeaders.CONTENT_LENGTH)) { 41 | try { 42 | int contentLength = _httpBody.getContent(getCharset()).length; 43 | _headers = LinkedHashMultimap.create(_headers); 44 | _headers.put(HttpHeaders.CONTENT_LENGTH, Integer.toString(contentLength)); 45 | } catch (IOException e) { 46 | logger.error("Caught exception " + e + " while updating Content-Length header"); 47 | } 48 | } 49 | } 50 | 51 | public RecordedHttpBody getHttpBody() { 52 | return _httpBody; 53 | } 54 | 55 | public boolean hasHttpBody() { 56 | return _httpBody != null; 57 | } 58 | 59 | public Multimap getHeaders() { 60 | return _headers; 61 | } 62 | 63 | public String getCharset() { 64 | // Content_Type cannot have multiple, commas-separated values, so this is safe. 65 | Iterator header = _headers.get(HttpHeaders.CONTENT_TYPE).iterator(); 66 | if (!header.hasNext()) { 67 | return DEFAULT_CHARSET; 68 | } else { 69 | return MediaType.parse(header.next()).charset().or(Charsets.UTF_8).toString(); 70 | } 71 | } 72 | 73 | public String getContentType() { 74 | // Content_Type cannot have multiple, commas-separated values, so this is safe. 75 | Iterator header = _headers.get(HttpHeaders.CONTENT_TYPE).iterator(); 76 | if (!header.hasNext()) { 77 | return DEFAULT_CONTENT_TYPE; 78 | } else { 79 | return MediaType.parse(header.next()).withoutParameters().toString(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedHttpRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import java.net.URI; 9 | import java.util.Map; 10 | 11 | import com.google.common.collect.Multimap; 12 | import org.apache.commons.lang3.builder.EqualsBuilder; 13 | import org.apache.commons.lang3.builder.HashCodeBuilder; 14 | 15 | 16 | /** 17 | * Recorded Http request that contains uri and method. 18 | * @author shfeng 19 | */ 20 | public class RecordedHttpRequest extends RecordedHttpMessage { 21 | private String _httpMethod; 22 | private URI _uri; 23 | 24 | public RecordedHttpRequest(String httpMethod, URI uri, Multimap headers, 25 | RecordedHttpBody recordedHttpBody) { 26 | super(headers, recordedHttpBody); 27 | _httpMethod = httpMethod; 28 | _uri = uri; 29 | } 30 | 31 | public String getMethod() { 32 | return _httpMethod; 33 | } 34 | 35 | public URI getUri() { 36 | return _uri; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return HashCodeBuilder.reflectionHashCode(this); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object obj) { 46 | return EqualsBuilder.reflectionEquals(this, obj); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedHttpResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import java.util.Map; 9 | 10 | import com.google.common.collect.Multimap; 11 | import org.apache.commons.lang3.builder.EqualsBuilder; 12 | import org.apache.commons.lang3.builder.HashCodeBuilder; 13 | 14 | 15 | /** 16 | * Recorded Http response that contains status code. 17 | * @author shfeng 18 | */ 19 | public class RecordedHttpResponse extends RecordedHttpMessage { 20 | private int _status; 21 | 22 | public RecordedHttpResponse(int status, Multimap headers, RecordedHttpBody recordedHttpBody) { 23 | super(headers, recordedHttpBody); 24 | _status = status; 25 | } 26 | 27 | public int getStatus() { 28 | return _status; 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return HashCodeBuilder.reflectionHashCode(this); 34 | } 35 | 36 | @Override 37 | public boolean equals(Object obj) { 38 | return EqualsBuilder.reflectionEquals(this, obj); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serializable/RecordedStringHttpBody.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import java.io.UnsupportedEncodingException; 9 | import org.apache.commons.lang3.builder.EqualsBuilder; 10 | import org.apache.commons.lang3.builder.HashCodeBuilder; 11 | 12 | 13 | /** 14 | * Recorded http body that obtains its content from a String 15 | * @author shfeng 16 | */ 17 | public class RecordedStringHttpBody implements RecordedHttpBody { 18 | private final String _content; 19 | 20 | public RecordedStringHttpBody(String content) { 21 | _content = content; 22 | } 23 | 24 | @Override 25 | public byte[] getContent(String charSet) { 26 | try { 27 | return _content.getBytes(charSet); 28 | } catch (UnsupportedEncodingException e) { 29 | throw new RuntimeException("Failed to get bytes from string"); 30 | } 31 | } 32 | 33 | public String getContent() { 34 | return _content; 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return HashCodeBuilder.reflectionHashCode(this); 40 | } 41 | 42 | @Override 43 | public boolean equals(Object obj) { 44 | return EqualsBuilder.reflectionEquals(this, obj); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serialization/SceneReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serialization; 7 | 8 | import com.google.common.io.Files; 9 | import com.linkedin.flashback.scene.Scene; 10 | import java.io.BufferedReader; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.charset.Charset; 14 | import java.util.ArrayList; 15 | 16 | 17 | /** 18 | * Read de-serialized scene from file. 19 | * 20 | * @author shfeng 21 | */ 22 | public class SceneReader { 23 | /** 24 | * Read scene from file and construct Scene object 25 | * @param name scene name 26 | * @return scene object de-serialized from file 27 | * 28 | * */ 29 | public Scene readScene(String rootPath, String name) 30 | throws IOException { 31 | File file = new File(rootPath, name); 32 | if (file.isFile()) { 33 | if (file.length() == 0) { 34 | return new Scene(name, null, rootPath, new ArrayList<>()); 35 | } 36 | BufferedReader reader = Files.newReader(file, Charset.forName(SceneSerializationConstant.FILE_CHARSET)); 37 | SceneDeserializer sceneDeserializer = new SceneDeserializer(); 38 | return sceneDeserializer.deserialize(reader); 39 | } 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serialization/SceneSerializationConstant.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serialization; 7 | 8 | /** 9 | * CONSTANTS for Scene serialization 10 | * 11 | * @author shfeng 12 | */ 13 | public final class SceneSerializationConstant { 14 | static final String SCENE_TAG_NAME = "NAME"; 15 | static final String SCENE_TAG_HTTPEXCHANGE_LIST = "HTTPEXCHANGELIST"; 16 | static final String SCENE_TAG_HTTPEXCHANGE = "HTTPEXCHANGE"; 17 | static final String SCENE_TAG_UPDATE_TIME = "UPDATETIME"; 18 | static final String SCENE_TAG_HTTPREQUEST = "HTTPREQUEST"; 19 | static final String SCENE_TAG_HTTPRESPONSE = "HTTPRESPONSE"; 20 | static final String SCENE_TAG_HTTPMETHOD = "HTTPMETHOD"; 21 | static final String SCENE_TAG_HTTPURI = "HTTPURI"; 22 | static final String SCENE_TAG_HTTPHEADERS = "HTTPHEADERS"; 23 | static final String SCENE_TAG_STRING_HTTPBODY = "STRINGHTTPBODY"; 24 | static final String SCENE_TAG_BINARY_HTTPBODY = "BINARYHTTPBODY"; 25 | static final String SCENE_TAG_ENCODED_HTTPBODY = "ENCODEDHTTPBODY"; 26 | static final String SCENE_TAG_HTTPBODY_ENCODING = "HTTPBODYENCODING"; 27 | static final String SCENE_TAG_HTTPSTATUS_CODE = "HTTPSTATUSCODE"; 28 | static final String FILE_CHARSET = "UTF-8"; 29 | 30 | private SceneSerializationConstant() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /flashback-core-impl/src/main/java/com/linkedin/flashback/serialization/SceneWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serialization; 7 | 8 | import com.google.common.io.Files; 9 | import com.linkedin.flashback.scene.Scene; 10 | import java.io.BufferedWriter; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.charset.Charset; 14 | 15 | 16 | /** 17 | * Write serialized scene to file. 18 | * 19 | * @author shfeng 20 | */ 21 | public class SceneWriter { 22 | /** 23 | * Store scene in file 24 | * */ 25 | public void writeScene(Scene scene) 26 | throws IOException { 27 | File file = new File(scene.getSceneRoot(), scene.getName()); 28 | File parent = file.getParentFile(); 29 | if (!parent.exists() && !parent.mkdirs()) { 30 | throw new IllegalStateException("Failed to create new directory: " + parent); 31 | } 32 | BufferedWriter bufferedWriter = Files.newWriter(file, Charset.forName(SceneSerializationConstant.FILE_CHARSET)); 33 | SceneSerializer sceneSerializer = new SceneSerializer(); 34 | sceneSerializer.serialize(scene, bufferedWriter); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/decorator/compression/DeflateCompressorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.IOException; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | 13 | /** 14 | * @author shfeng 15 | * @author dvinegra 16 | */ 17 | public class DeflateCompressorTest { 18 | @Test 19 | public void testCompress() 20 | throws IOException { 21 | String str = "Hello world"; 22 | String charset = "ISO-8859-1"; 23 | byte[] content = str.getBytes(charset); 24 | DeflateCompressor deflateCompressor = new DeflateCompressor(); 25 | 26 | byte[] compressedContent = deflateCompressor.compress(content); 27 | Assert.assertNotEquals(deflateCompressor.compress(content).length, content.length); 28 | 29 | DeflateDecompressor deflateDecompressor = new DeflateDecompressor(); 30 | Assert.assertEquals(deflateDecompressor.decompress(compressedContent), content); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/decorator/compression/GzipCompressorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.decorator.compression; 7 | 8 | import java.io.IOException; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | 13 | /** 14 | * @author shfeng 15 | * @author dvinegra 16 | */ 17 | public class GzipCompressorTest { 18 | 19 | @Test 20 | public void testCompress() 21 | throws IOException { 22 | String str = "Hello world"; 23 | String charset = "ISO-8859-1"; 24 | byte[] content = str.getBytes(charset); 25 | GzipCompressor gzipCompressor = new GzipCompressor(); 26 | 27 | byte[] compressedContent = gzipCompressor.compress(content); 28 | Assert.assertNotEquals(compressedContent.length, content.length); 29 | 30 | GzipDecompressor gzipDecompressor = new GzipDecompressor(); 31 | Assert.assertEquals(gzipDecompressor.decompress(compressedContent), content); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/factory/RecordedHttpBodyFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.factory; 7 | 8 | import com.linkedin.flashback.decorator.compression.DeflateCompressor; 9 | import com.linkedin.flashback.decorator.compression.GzipCompressor; 10 | import com.linkedin.flashback.serializable.RecordedByteHttpBody; 11 | import com.linkedin.flashback.serializable.RecordedEncodedHttpBody; 12 | import com.linkedin.flashback.serializable.RecordedHttpBody; 13 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 14 | import java.io.ByteArrayInputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import org.testng.Assert; 18 | import org.testng.annotations.Test; 19 | 20 | 21 | /** 22 | * @author shfeng . 23 | * @author dvinegra 24 | */ 25 | public class RecordedHttpBodyFactoryTest { 26 | @Test 27 | public void testCreateStringHttpBody() 28 | throws IOException { 29 | String str = "Hello world"; 30 | byte[] content = str.getBytes(); 31 | int size = content.length; 32 | InputStream is = new ByteArrayInputStream(content); 33 | RecordedHttpBody recordedHttpBody = RecordedHttpBodyFactory.create("text/html", "identity", is, "UTF-8"); 34 | Assert.assertTrue(recordedHttpBody instanceof RecordedStringHttpBody); 35 | Assert.assertFalse(recordedHttpBody instanceof RecordedByteHttpBody); 36 | Assert.assertEquals(size, recordedHttpBody.getContent("UTF-8").length); 37 | } 38 | 39 | @Test 40 | public void testCreateByteHttpBody() 41 | throws IOException { 42 | String str = "Hello world"; 43 | byte[] content = str.getBytes(); 44 | InputStream is = new ByteArrayInputStream(content); 45 | RecordedHttpBody recordedHttpBody = RecordedHttpBodyFactory.create("image/gif", "identity", is, "UTF-8"); 46 | Assert.assertTrue(recordedHttpBody instanceof RecordedByteHttpBody); 47 | Assert.assertFalse(recordedHttpBody instanceof RecordedStringHttpBody); 48 | Assert.assertEquals(content, recordedHttpBody.getContent("UTF-8")); 49 | } 50 | 51 | @Test 52 | public void testCreateGZipEncodedStringHttpBody() 53 | throws IOException { 54 | String str = "Hello world. This is some extra text to make the string longer so that gzip makes it smaller"; 55 | byte[] content = str.getBytes(); 56 | byte[] compressedContent = new GzipCompressor().compress(content); 57 | InputStream is = new ByteArrayInputStream(compressedContent); 58 | RecordedHttpBody recordedHttpBody = RecordedHttpBodyFactory.create("text/html", "gzip", is, "UTF-8"); 59 | Assert.assertTrue(recordedHttpBody instanceof RecordedEncodedHttpBody); 60 | RecordedEncodedHttpBody encodedBody = (RecordedEncodedHttpBody) recordedHttpBody; 61 | Assert.assertEquals(encodedBody.getEncodingName(), "gzip"); 62 | Assert.assertTrue(encodedBody.getDecodedBody() instanceof RecordedStringHttpBody); 63 | Assert.assertEquals(encodedBody.getDecodedBody().getContent("UTF-8"), content); 64 | Assert.assertEquals(encodedBody.getContent("UTF-8"), compressedContent); 65 | } 66 | 67 | @Test 68 | public void testCreateDeflateEncodedStringHttpBody() 69 | throws IOException { 70 | String str = "Hello world. This is some extra text to make the string longer so that deflate makes it smaller"; 71 | byte[] content = str.getBytes(); 72 | byte[] compressedContent = new DeflateCompressor().compress(content); 73 | InputStream is = new ByteArrayInputStream(compressedContent); 74 | RecordedHttpBody recordedHttpBody = RecordedHttpBodyFactory.create("text/html", "deflate", is, "UTF-8"); 75 | Assert.assertTrue(recordedHttpBody instanceof RecordedEncodedHttpBody); 76 | RecordedEncodedHttpBody encodedBody = (RecordedEncodedHttpBody) recordedHttpBody; 77 | Assert.assertEquals(encodedBody.getEncodingName(), "deflate"); 78 | Assert.assertTrue(encodedBody.getDecodedBody() instanceof RecordedStringHttpBody); 79 | Assert.assertEquals(encodedBody.getDecodedBody().getContent("UTF-8"), content); 80 | Assert.assertEquals(encodedBody.getContent("UTF-8"), compressedContent); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/http/HttpUtilitiesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.http; 7 | 8 | import java.net.URI; 9 | import java.net.URLEncoder; 10 | import java.util.LinkedHashMap; 11 | import java.util.Map; 12 | import org.testng.Assert; 13 | import org.testng.annotations.Test; 14 | 15 | 16 | /** 17 | * @author dvinegra 18 | */ 19 | public class HttpUtilitiesTest { 20 | 21 | @Test 22 | public void testIsTextContentType() { 23 | Assert.assertTrue(HttpUtilities.isTextContentType("application/json")); 24 | Assert.assertTrue(HttpUtilities.isTextContentType("text/javascript")); 25 | Assert.assertTrue(HttpUtilities.isTextContentType("text/x-javascript")); 26 | Assert.assertTrue(HttpUtilities.isTextContentType("text/x-json")); 27 | Assert.assertTrue(HttpUtilities.isTextContentType("text/html")); 28 | Assert.assertTrue(HttpUtilities.isTextContentType("application/xhtml+xml")); 29 | Assert.assertTrue(HttpUtilities.isTextContentType("text/xml")); 30 | Assert.assertTrue(HttpUtilities.isTextContentType("application/xml")); 31 | Assert.assertTrue(HttpUtilities.isTextContentType("application/x-www-form-urlencoded")); 32 | Assert.assertFalse(HttpUtilities.isTextContentType("image/gif")); 33 | Assert.assertFalse(HttpUtilities.isTextContentType(null)); 34 | } 35 | 36 | @Test 37 | public void testIsCompressedContentEncoding() { 38 | Assert.assertTrue(HttpUtilities.isCompressedContentEncoding("gzip")); 39 | Assert.assertTrue(HttpUtilities.isCompressedContentEncoding("deflate")); 40 | Assert.assertFalse(HttpUtilities.isCompressedContentEncoding("identity")); 41 | } 42 | 43 | @Test 44 | public void isFormURLEncodedContentType() { 45 | Assert.assertTrue(HttpUtilities.isFormURLEncodedContentType("application/x-www-form-urlencoded")); 46 | Assert.assertFalse(HttpUtilities.isFormURLEncodedContentType("application/x-javascript")); 47 | } 48 | 49 | @Test 50 | public void testStringUrlParameterConversion() 51 | throws Exception { 52 | String queryString = "foo=bar&a=a&b=b&c=c"; 53 | Map expected = new LinkedHashMap<>(); 54 | expected.put("foo", "bar"); 55 | expected.put("a", "a"); 56 | expected.put("b", "b"); 57 | expected.put("c", "c"); 58 | Assert.assertEquals(HttpUtilities.stringToUrlParams(queryString, "UTF-8"), expected); 59 | Assert.assertEquals(HttpUtilities.urlParamsToString(expected, "UTF-8"), queryString); 60 | Assert.assertEquals(HttpUtilities.stringToUrlParams(queryString, "UTF-8").toString(), expected.toString()); 61 | } 62 | 63 | @Test 64 | public void testStringUrlParameterConversionWithNestedUrl() 65 | throws Exception { 66 | String nestedUriString = "http://www.google.com/?a=b"; 67 | URI uri = new URI("http://www.example.org/?foo=bar&ref=" + URLEncoder.encode(nestedUriString, "UTF-8")); 68 | Map expected = new LinkedHashMap<>(); 69 | expected.put("foo", "bar"); 70 | expected.put("ref", nestedUriString); 71 | Map result = HttpUtilities.stringToUrlParams(uri.getRawQuery(), "UTF-8"); 72 | Assert.assertEquals(result, expected); 73 | Assert.assertEquals(HttpUtilities.urlParamsToString(result, "UTF-8"), uri.getRawQuery()); 74 | } 75 | 76 | @Test 77 | public void testStringUrlParameterConversionWrongOrder() 78 | throws Exception { 79 | String queryString = "foo=bar&a=a&b=b&c=c"; 80 | Map expectedOutOfOrder = new LinkedHashMap<>(); 81 | expectedOutOfOrder.put("a", "a"); 82 | expectedOutOfOrder.put("b", "b"); 83 | expectedOutOfOrder.put("c", "c"); 84 | expectedOutOfOrder.put("foo", "bar"); 85 | Assert.assertNotEquals(HttpUtilities.urlParamsToString(expectedOutOfOrder, "UTF-8"), queryString); 86 | Assert.assertNotEquals(HttpUtilities.stringToUrlParams(queryString, "UTF-8").toString(), 87 | expectedOutOfOrder.toString()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/BaseMatchRuleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | import org.testng.Assert; 11 | import org.testng.annotations.Test; 12 | 13 | 14 | /** 15 | * @author shfeng 16 | */ 17 | public class BaseMatchRuleTest { 18 | @Test 19 | public void testInsertion() { 20 | Set matchRuleSet = new HashSet<>(); 21 | matchRuleSet.add(new MatchBody()); 22 | Assert.assertEquals(matchRuleSet.size(), 1); 23 | 24 | matchRuleSet.add(new MatchBody()); 25 | Assert.assertEquals(matchRuleSet.size(), 1); 26 | 27 | matchRuleSet.add(new MatchHeaders()); 28 | matchRuleSet.add(MatchRuleUtils.matchHeadersWithBlacklist(null)); 29 | matchRuleSet.add(MatchRuleUtils.matchHeadersWithWhitelist(null)); 30 | Assert.assertEquals(matchRuleSet.size(), 4); 31 | } 32 | 33 | @Test 34 | public void testLookup() { 35 | MatchRule matchRule1 = new MatchBody(); 36 | MatchRule matchRule2 = new MatchBody(); 37 | MatchRule matchRule3 = new MatchHeaders(); 38 | Set matchRuleSet = new HashSet<>(); 39 | matchRuleSet.add(matchRule1); 40 | Assert.assertTrue(matchRuleSet.contains(matchRule2)); 41 | Assert.assertFalse(matchRuleSet.contains(matchRule3)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/CompositeMatchRuleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | import java.net.URI; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import org.testng.Assert; 13 | import org.testng.annotations.Test; 14 | 15 | 16 | /** 17 | * @author shfeng 18 | */ 19 | public class CompositeMatchRuleTest { 20 | @Test 21 | public void testIsMatch() 22 | throws Exception { 23 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest("get", new URI("google.com"), null, null); 24 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest("get", new URI("google.com"), null, null); 25 | 26 | Set matchRuleSet = new HashSet(); 27 | matchRuleSet.add(new MatchUri()); 28 | matchRuleSet.add(new MatchMethod()); 29 | CompositeMatchRule compositeMatchRule = new CompositeMatchRule(); 30 | compositeMatchRule.addAll(matchRuleSet); 31 | 32 | Assert.assertTrue(compositeMatchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 33 | } 34 | 35 | @Test 36 | public void testIsNotMatch() 37 | throws Exception { 38 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest("get", new URI("google.com"), null, null); 39 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest("post", new URI("google.com"), null, null); 40 | 41 | Set matchRuleSet = new HashSet(); 42 | matchRuleSet.add(new MatchUri()); 43 | matchRuleSet.add(new MatchMethod()); 44 | CompositeMatchRule compositeMatchRule = new CompositeMatchRule(); 45 | compositeMatchRule.addAll(matchRuleSet); 46 | 47 | Assert.assertFalse(compositeMatchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/MatchBodyPostParametersTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.google.common.collect.LinkedHashMultimap; 9 | import com.google.common.collect.Multimap; 10 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 11 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import org.testng.Assert; 15 | import org.testng.annotations.Test; 16 | 17 | 18 | /** 19 | * 20 | * @author dvinegra 21 | */ 22 | public class MatchBodyPostParametersTest { 23 | 24 | @Test 25 | public void testExactMatch() 26 | throws Exception { 27 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("a=a&b=b&c=c"); 28 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("a=a&b=b&c=c"); 29 | 30 | Multimap headers = LinkedHashMultimap.create(); 31 | headers.put("Content-Type", "application/x-www-form-urlencoded"); 32 | 33 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, headers, stringHttpBody1); 34 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, headers, stringHttpBody2); 35 | MatchRule matchRule = new MatchBodyPostParameters(); 36 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 37 | } 38 | 39 | @Test 40 | public void testNotMatchDifferentOrder() 41 | throws Exception { 42 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("a=a&b=b&c=c"); 43 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("a=a&c=c&b=b"); 44 | 45 | Multimap headers = LinkedHashMultimap.create(); 46 | headers.put("Content-Type", "application/x-www-form-urlencoded"); 47 | 48 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, headers, stringHttpBody1); 49 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, headers, stringHttpBody2); 50 | MatchRule matchRule = new MatchBodyPostParameters(); 51 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 52 | } 53 | 54 | @Test 55 | public void testDifferentParameters() 56 | throws Exception { 57 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("a=a&b=b&c=c"); 58 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("a=a&b=b&c=ccc"); 59 | 60 | Multimap headers = LinkedHashMultimap.create(); 61 | headers.put("Content-Type", "application/x-www-form-urlencoded"); 62 | 63 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, headers, stringHttpBody1); 64 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, headers, stringHttpBody2); 65 | MatchRule matchRule = new MatchBodyPostParameters(); 66 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 67 | } 68 | 69 | @Test 70 | public void testMatchWithSomeNullParameters() 71 | throws Exception { 72 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("a=a&b=&c=c&d="); 73 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("a=a&b=&c=c&d="); 74 | 75 | Multimap headers = LinkedHashMultimap.create(); 76 | headers.put("Content-Type", "application/x-www-form-urlencoded"); 77 | 78 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, headers, stringHttpBody1); 79 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, headers, stringHttpBody2); 80 | MatchRule matchRule = new MatchBodyPostParameters(); 81 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/MatchBodyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.google.common.collect.LinkedHashMultimap; 9 | import com.google.common.collect.Multimap; 10 | import com.google.common.net.HttpHeaders; 11 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 12 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import org.testng.Assert; 16 | import org.testng.annotations.Test; 17 | 18 | 19 | /** 20 | * @author shfeng 21 | */ 22 | public class MatchBodyTest { 23 | @Test 24 | public void testIsStringBodyMatch() 25 | throws Exception { 26 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("abc"); 27 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("abc"); 28 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, null, stringHttpBody1); 29 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, null, stringHttpBody2); 30 | MatchRule matchRule = new MatchBody(); 31 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 32 | } 33 | 34 | @Test 35 | public void testBothNullBodyMatch() { 36 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, null, null); 37 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, null, null); 38 | MatchRule matchRule = new MatchBody(); 39 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 40 | } 41 | 42 | @Test 43 | public void testNullAndEmptyBodyMatch() { 44 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody(""); 45 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, null, null); 46 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, null, stringHttpBody2); 47 | MatchRule matchRule = new MatchBody(); 48 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 49 | } 50 | 51 | @Test 52 | public void testNullAndNonEmptyBodyNotMatch() { 53 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("abc"); 54 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, null, null); 55 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, null, stringHttpBody2); 56 | MatchRule matchRule = new MatchBody(); 57 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 58 | } 59 | 60 | @Test 61 | public void testIsStringBodyNotMatch() 62 | throws Exception { 63 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("abc"); 64 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("abcd"); 65 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, null, stringHttpBody1); 66 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, null, stringHttpBody2); 67 | MatchRule matchRule = new MatchBody(); 68 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 69 | } 70 | 71 | @Test 72 | public void testIsStringBodyNotMatchWithDiffCharset() 73 | throws Exception { 74 | RecordedStringHttpBody stringHttpBody1 = new RecordedStringHttpBody("造字"); 75 | RecordedStringHttpBody stringHttpBody2 = new RecordedStringHttpBody("造字"); 76 | Multimap headers1 = LinkedHashMultimap.create(); 77 | headers1.put(HttpHeaders.CONTENT_TYPE, "text/html; charset=euc-kr"); 78 | Multimap headers2 = LinkedHashMultimap.create(); 79 | headers2.put(HttpHeaders.CONTENT_TYPE, "text/html; charset=big5"); 80 | 81 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, null, headers1, stringHttpBody1); 82 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, null, headers2, stringHttpBody2); 83 | MatchRule matchRule = new MatchBody(); 84 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/MatchCaseInsensitiveMethodTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | 13 | /** 14 | * @author shfeng 15 | */ 16 | public class MatchCaseInsensitiveMethodTest { 17 | @Test 18 | public void testIsMatch() 19 | throws Exception { 20 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest("GET", null, null, null); 21 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest("GET", null, null, null); 22 | MatchRule matchRule = new MatchCaseInsensitiveMethod(); 23 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 24 | } 25 | 26 | @Test 27 | public void testIsMatchWithCaseInsensitive() 28 | throws Exception { 29 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest("GET", null, null, null); 30 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest("get", null, null, null); 31 | MatchRule matchRule = new MatchCaseInsensitiveMethod(); 32 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/MatchMethodTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | 13 | /** 14 | * @author shfeng 15 | */ 16 | public class MatchMethodTest { 17 | @Test 18 | public void testIsMatch() 19 | throws Exception { 20 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest("GET", null, null, null); 21 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest("GET", null, null, null); 22 | MatchRule matchRule = new MatchMethod(); 23 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 24 | } 25 | 26 | @Test 27 | public void testIsNotMatch() 28 | throws Exception { 29 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest("GET", null, null, null); 30 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest("get", null, null, null); 31 | MatchRule matchRule = new MatchMethod(); 32 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/matchrules/MatchUriTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.matchrules; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 9 | import java.net.URI; 10 | import java.net.URISyntaxException; 11 | import org.testng.Assert; 12 | import org.testng.annotations.Test; 13 | 14 | 15 | /** 16 | * @author shfeng 17 | */ 18 | public class MatchUriTest { 19 | @Test 20 | public void testIsMatch() 21 | throws Exception { 22 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, new URI("google.com"), null, null); 23 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, new URI("google.com"), null, null); 24 | MatchRule matchRule = new MatchUri(); 25 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 26 | } 27 | 28 | @Test 29 | public void testMatchDefaultPortHttp() 30 | throws Exception { 31 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, new URI("http://www.example.org/"), null, null); 32 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, new URI("http://www.example.org:80/"), null, null); 33 | MatchRule matchRule = new MatchUri(); 34 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 35 | } 36 | 37 | @Test 38 | public void testMatchDefaultPortHttps() 39 | throws Exception { 40 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, new URI("https://www.example.org/"), null, null); 41 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, new URI("https://www.example.org:443/"), null, null); 42 | MatchRule matchRule = new MatchUri(); 43 | Assert.assertTrue(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 44 | } 45 | 46 | @Test 47 | public void testNotMatchNonDefaultPort() 48 | throws Exception { 49 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, new URI("http://www.example.org/"), null, null); 50 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, new URI("http://www.example.org:8080/"), null, null); 51 | MatchRule matchRule = new MatchUri(); 52 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 53 | } 54 | 55 | @Test 56 | public void testIsNotMatch() 57 | throws URISyntaxException { 58 | RecordedHttpRequest recordedHttpRequest1 = new RecordedHttpRequest(null, new URI("google.com"), null, null); 59 | RecordedHttpRequest recordedHttpRequest2 = new RecordedHttpRequest(null, new URI("yahoo.com"), null, null); 60 | MatchRule matchRule = new MatchUri(); 61 | Assert.assertFalse(matchRule.test(recordedHttpRequest1, recordedHttpRequest2)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/serializable/RecordedEncodedHttpBodyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import com.linkedin.flashback.decorator.compression.GzipCompressor; 9 | import org.testng.Assert; 10 | import org.testng.annotations.Test; 11 | 12 | 13 | /** 14 | * @author dvinegra 15 | */ 16 | public class RecordedEncodedHttpBodyTest { 17 | 18 | @Test 19 | public void testCreateFromDecodedStringBody() 20 | throws Exception { 21 | String str = "Gaap is awesome"; 22 | byte[] content = str.getBytes(); 23 | byte[] compressedContent = new GzipCompressor().compress(content); 24 | RecordedStringHttpBody recordedStringHttpBody = new RecordedStringHttpBody(str); 25 | RecordedEncodedHttpBody recordedEncodedHttpBody = new RecordedEncodedHttpBody(recordedStringHttpBody, "gzip"); 26 | Assert.assertEquals(recordedEncodedHttpBody.getContent("UTF-8"), compressedContent); 27 | } 28 | 29 | @Test 30 | public void testCreateFromEncodedBytes() 31 | throws Exception { 32 | String str = "Gaap is awesome"; 33 | byte[] content = str.getBytes(); 34 | byte[] compressedContent = new GzipCompressor().compress(content); 35 | RecordedEncodedHttpBody recordedEncodedHttpBody = 36 | new RecordedEncodedHttpBody(compressedContent, "gzip", "UTF-8", "text/html"); 37 | Assert.assertEquals(recordedEncodedHttpBody.getContent("UTF-8"), compressedContent); 38 | Assert.assertEquals(recordedEncodedHttpBody.getEncodingName(), "gzip"); 39 | RecordedHttpBody decodedBody = recordedEncodedHttpBody.getDecodedBody(); 40 | Assert.assertTrue(decodedBody instanceof RecordedStringHttpBody); 41 | Assert.assertEquals(((RecordedStringHttpBody) decodedBody).getContent(), str); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/serializable/RecordedHttpMessageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import com.google.common.base.Charsets; 9 | import com.google.common.collect.LinkedHashMultimap; 10 | import com.google.common.collect.Multimap; 11 | import com.google.common.net.HttpHeaders; 12 | import java.net.URI; 13 | import java.net.URISyntaxException; 14 | import java.nio.charset.Charset; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import org.testng.Assert; 18 | import org.testng.annotations.Test; 19 | 20 | 21 | /** 22 | * @author shfeng 23 | */ 24 | public class RecordedHttpMessageTest { 25 | @Test 26 | public void testGetCharset() 27 | throws URISyntaxException { 28 | Multimap headers = LinkedHashMultimap.create(); 29 | headers.put(HttpHeaders.CONTENT_TYPE, "text/html; charset=iso-8859-9"); 30 | RecordedHttpRequest recordedHttpRequest = new RecordedHttpRequest("GET", new URI("google.com"), headers, null); 31 | Assert.assertEquals(recordedHttpRequest.getCharset(), Charset.forName("iso-8859-9").toString()); 32 | } 33 | 34 | @Test 35 | public void testGetCharsetNoContentType() 36 | throws URISyntaxException { 37 | Multimap headers = LinkedHashMultimap.create(); 38 | RecordedHttpRequest recordedHttpRequest = new RecordedHttpRequest("GET", new URI("google.com"), headers, null); 39 | Assert.assertEquals(recordedHttpRequest.getCharset(), Charsets.UTF_8.toString()); 40 | } 41 | 42 | @Test 43 | public void testPassNullHeaders() 44 | throws URISyntaxException { 45 | RecordedHttpRequest recordedHttpRequest = new RecordedHttpRequest("GET", new URI("google.com"), null, null); 46 | Assert.assertEquals(recordedHttpRequest.getHeaders().get("anykey").size(), 0); 47 | } 48 | 49 | @Test 50 | public void testGetContentType() 51 | throws URISyntaxException { 52 | Multimap headers = LinkedHashMultimap.create(); 53 | headers.put(HttpHeaders.CONTENT_TYPE, "text/html"); 54 | RecordedHttpRequest recordedHttpRequest = new RecordedHttpRequest("GET", new URI("google.com"), headers, null); 55 | Assert.assertEquals(recordedHttpRequest.getContentType(), "text/html"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/serializable/RecordedStringHttpBodyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serializable; 7 | 8 | import org.testng.Assert; 9 | import org.testng.annotations.Test; 10 | 11 | 12 | /** 13 | * @author shfeng 14 | */ 15 | public class RecordedStringHttpBodyTest { 16 | @Test 17 | public void testGetByteArray() 18 | throws Exception { 19 | String str = "Hello world"; 20 | byte[] content = str.getBytes(); 21 | RecordedStringHttpBody recordedStringHttpBody = new RecordedStringHttpBody(str); 22 | Assert.assertEquals(content, recordedStringHttpBody.getContent("UTF-8")); 23 | } 24 | 25 | @Test(expectedExceptions = RuntimeException.class) 26 | public void testGetByteArrayUnsupportEncoding() 27 | throws Exception { 28 | String str = "Hello world"; 29 | RecordedStringHttpBody recordedStringHttpBody = new RecordedStringHttpBody(str); 30 | recordedStringHttpBody.getContent("UNKNOWN"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/serialization/SceneDeserializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serialization; 7 | 8 | import com.linkedin.flashback.scene.Scene; 9 | import java.io.StringReader; 10 | import org.testng.Assert; 11 | import org.testng.annotations.Test; 12 | 13 | 14 | /** 15 | * @author shfeng 16 | */ 17 | public class SceneDeserializerTest { 18 | @Test 19 | public void testDeserialize() 20 | throws Exception { 21 | StringReader stringReader = new StringReader(MockDataGenerator.getSerializedScene()); 22 | SceneDeserializer sceneDeserializer = new SceneDeserializer(); 23 | Scene scene = sceneDeserializer.deserialize(stringReader); 24 | Scene expectedScene = MockDataGenerator.getMockScene(); 25 | Assert.assertEquals(scene, expectedScene); 26 | } 27 | 28 | @Test 29 | public void testDeserializeWithoutHeaders() 30 | throws Exception { 31 | StringReader stringReader = new StringReader(MockDataGenerator.getSerializedSceneWithoutHeaders()); 32 | SceneDeserializer sceneDeserializer = new SceneDeserializer(); 33 | Scene scene = sceneDeserializer.deserialize(stringReader); 34 | Scene expectedScene = MockDataGenerator.getMockSceneWithoutHeaders(); 35 | Assert.assertEquals(scene, expectedScene); 36 | } 37 | 38 | @Test 39 | public void testDeserializeWithoutBody() 40 | throws Exception { 41 | StringReader stringReader = new StringReader(MockDataGenerator.getSerializedSceneWithoutBody()); 42 | SceneDeserializer sceneDeserializer = new SceneDeserializer(); 43 | Scene scene = sceneDeserializer.deserialize(stringReader); 44 | Scene expectedScene = MockDataGenerator.getMockSceneWithoutBody(); 45 | Assert.assertEquals(scene, expectedScene); 46 | } 47 | 48 | @Test 49 | public void testDeserializeWithoutBodyAndHeaders() 50 | throws Exception { 51 | StringReader stringReader = new StringReader(MockDataGenerator.getSerializedSceneWithoutBodyAndHeader()); 52 | SceneDeserializer sceneDeserializer = new SceneDeserializer(); 53 | Scene scene = sceneDeserializer.deserialize(stringReader); 54 | Scene expectedScene = MockDataGenerator.getMockSceneWithoutBodyAndHeader(); 55 | Assert.assertEquals(scene, expectedScene); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /flashback-core-impl/src/test/java/com/linkedin/flashback/serialization/SceneSerializerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.serialization; 7 | 8 | import java.io.IOException; 9 | import java.io.StringWriter; 10 | import java.net.URISyntaxException; 11 | import org.testng.Assert; 12 | import org.testng.annotations.Test; 13 | 14 | 15 | /** 16 | * @author shfeng 17 | */ 18 | public class SceneSerializerTest { 19 | @Test 20 | public void testSerialization() 21 | throws IOException, URISyntaxException { 22 | 23 | SceneSerializer sceneSerializer = new SceneSerializer(); 24 | StringWriter stringWriter = new StringWriter(); 25 | 26 | sceneSerializer.serialize(MockDataGenerator.getMockScene(), stringWriter); 27 | Assert.assertEquals(MockDataGenerator.getSerializedScene(), stringWriter.toString()); 28 | } 29 | 30 | @Test 31 | public void testSerializationWithoutHeaders() 32 | throws URISyntaxException, IOException { 33 | 34 | SceneSerializer sceneSerializer = new SceneSerializer(); 35 | StringWriter stringWriter = new StringWriter(); 36 | 37 | sceneSerializer.serialize(MockDataGenerator.getMockSceneWithoutHeaders(), stringWriter); 38 | Assert.assertEquals(MockDataGenerator.getSerializedSceneWithoutHeaders(), stringWriter.toString()); 39 | } 40 | 41 | @Test 42 | public void testSerializationWithoutBody() 43 | throws IOException, URISyntaxException { 44 | 45 | SceneSerializer sceneSerializer = new SceneSerializer(); 46 | StringWriter stringWriter = new StringWriter(); 47 | 48 | sceneSerializer.serialize(MockDataGenerator.getMockSceneWithoutBody(), stringWriter); 49 | Assert.assertEquals(MockDataGenerator.getSerializedSceneWithoutBody(), stringWriter.toString()); 50 | } 51 | 52 | @Test 53 | public void testSerializationWithoutBodyAndHeaders() 54 | throws IOException, URISyntaxException { 55 | 56 | SceneSerializer sceneSerializer = new SceneSerializer(); 57 | StringWriter stringWriter = new StringWriter(); 58 | 59 | sceneSerializer.serialize(MockDataGenerator.getMockSceneWithoutBodyAndHeader(), stringWriter); 60 | Assert.assertEquals(MockDataGenerator.getSerializedSceneWithoutBodyAndHeader(), stringWriter.toString()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /flashback-netty/src/main/java/com/linkedin/flashback/netty/builder/RecordedHttpRequestBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.netty.builder; 7 | 8 | import com.google.common.base.Strings; 9 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 10 | import io.netty.handler.codec.http.HttpHeaders; 11 | import io.netty.handler.codec.http.HttpMessage; 12 | import io.netty.handler.codec.http.HttpRequest; 13 | import java.net.URI; 14 | import java.net.URISyntaxException; 15 | 16 | 17 | /** 18 | * Implementation of builder for {@link com.linkedin.flashback.serializable.RecordedHttpRequest} 19 | * 20 | * @author shfeng. 21 | */ 22 | public class RecordedHttpRequestBuilder extends RecordedHttpMessageBuilder { 23 | private String _httpMethod; 24 | private URI _uri; 25 | private transient String _path = ""; 26 | 27 | /** 28 | * Take necessary available parameters from original netty http requests 29 | * If uri we get is not absolute, we need check if we can get it from host headers. 30 | * if not, we store relative path to _path. 31 | * */ 32 | public RecordedHttpRequestBuilder(HttpRequest nettyHttpRequest) { 33 | super(nettyHttpRequest); 34 | interpretHttpRequest(nettyHttpRequest); 35 | } 36 | 37 | public void interpretHttpRequest(HttpRequest nettyHttpRequest) { 38 | _httpMethod = nettyHttpRequest.getMethod().toString(); 39 | try { 40 | URI uri = new URI(nettyHttpRequest.getUri()); 41 | if (uri.isAbsolute()) { 42 | _uri = uri; 43 | } else { 44 | String hostName = nettyHttpRequest.headers().get(HttpHeaders.Names.HOST); 45 | if (!Strings.isNullOrEmpty(hostName)) { 46 | _uri = new URI(String.format("https://%s%s", hostName, uri)); 47 | } else { 48 | _path = uri.toString(); 49 | } 50 | } 51 | } catch (URISyntaxException e) { 52 | throw new IllegalStateException("Invalid URI in underlying request", e); 53 | } 54 | } 55 | 56 | /** 57 | * Add headers from http message and also check if uri is properly set. 58 | * If not, we need check host header and construct uri using relative path 59 | * and host name. 60 | * 61 | * @param httpMessage netty http message 62 | * */ 63 | @Override 64 | public void addHeaders(HttpMessage httpMessage) { 65 | super.addHeaders(httpMessage); 66 | if (_uri == null) { 67 | String hostName = httpMessage.headers().get(HttpHeaders.Names.HOST); 68 | if (!Strings.isNullOrEmpty(hostName)) { 69 | try { 70 | _uri = new URI(String.format("https://%s%s", hostName, _path)); 71 | } catch (URISyntaxException e) { 72 | throw new IllegalStateException("Invalid URI in underlying request", e); 73 | } 74 | } 75 | } 76 | } 77 | 78 | public RecordedHttpRequest build() { 79 | return new RecordedHttpRequest(_httpMethod, _uri, getHeaders(), getBody()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flashback-netty/src/main/java/com/linkedin/flashback/netty/builder/RecordedHttpResponseBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.netty.builder; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpResponse; 9 | import io.netty.handler.codec.http.HttpResponse; 10 | 11 | 12 | /** 13 | * Implementation of builder for {@link com.linkedin.flashback.serializable.RecordedHttpResponse} 14 | * 15 | * @author shfeng. 16 | */ 17 | public class RecordedHttpResponseBuilder extends RecordedHttpMessageBuilder { 18 | private int _status; 19 | 20 | public RecordedHttpResponseBuilder(HttpResponse nettyHttpResponse) { 21 | super(nettyHttpResponse); 22 | _status = nettyHttpResponse.getStatus().code(); 23 | } 24 | 25 | public RecordedHttpResponse build() { 26 | return new RecordedHttpResponse(_status, getHeaders(), getBody()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /flashback-netty/src/main/java/com/linkedin/flashback/netty/mapper/NettyHttpResponseMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.netty.mapper; 7 | 8 | import com.google.common.base.Splitter; 9 | import com.linkedin.flashback.serializable.RecordedHttpResponse; 10 | import io.netty.buffer.ByteBuf; 11 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 12 | import io.netty.handler.codec.http.FullHttpResponse; 13 | import io.netty.handler.codec.http.HttpResponseStatus; 14 | import java.io.IOException; 15 | import java.util.Base64; 16 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | import java.util.stream.StreamSupport; 19 | 20 | import static io.netty.buffer.Unpooled.*; 21 | import static io.netty.handler.codec.http.HttpVersion.*; 22 | 23 | /** 24 | * Mapper from RecordedHttpResponse to Netty HttpResponse 25 | * 26 | * @author shfeng. 27 | */ 28 | public final class NettyHttpResponseMapper { 29 | 30 | public static final String SET_COOKIE = "Set-Cookie"; 31 | 32 | private NettyHttpResponseMapper() { 33 | } 34 | 35 | public static FullHttpResponse from(RecordedHttpResponse recordedHttpResponse) 36 | throws IOException { 37 | FullHttpResponse fullHttpResponse; 38 | HttpResponseStatus status = HttpResponseStatus.valueOf(recordedHttpResponse.getStatus()); 39 | if (recordedHttpResponse.hasHttpBody()) { 40 | ByteBuf content = wrappedBuffer(createHttpBodyBytes(recordedHttpResponse)); 41 | fullHttpResponse = new DefaultFullHttpResponse(HTTP_1_1, status, content); 42 | } else { 43 | fullHttpResponse = new DefaultFullHttpResponse(HTTP_1_1, status); 44 | } 45 | for (Map.Entry header : recordedHttpResponse.getHeaders().entries()) { 46 | fullHttpResponse.headers().add(header.getKey(), header.getValue()); 47 | } 48 | return fullHttpResponse; 49 | } 50 | 51 | private static byte[] createHttpBodyBytes(RecordedHttpResponse recordedHttpResponse) 52 | throws IOException { 53 | return recordedHttpResponse.getHttpBody().getContent(recordedHttpResponse.getCharset()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /flashback-netty/src/test/java/com/linkedin/flashback/netty/builder/RecordedHttpMessageBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.netty.builder; 7 | 8 | import com.google.common.collect.Multimap; 9 | import io.netty.handler.codec.http.DefaultFullHttpRequest; 10 | import io.netty.handler.codec.http.HttpMethod; 11 | import io.netty.handler.codec.http.HttpRequest; 12 | import io.netty.handler.codec.http.HttpVersion; 13 | import java.net.URISyntaxException; 14 | import org.testng.Assert; 15 | import org.testng.annotations.Test; 16 | 17 | 18 | /** 19 | * @author shfeng 20 | */ 21 | public class RecordedHttpMessageBuilderTest { 22 | @Test 23 | public void testAddHeaders() 24 | throws Exception { 25 | HttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "www.google.com"); 26 | nettyRequest.headers().add("key1", "value1"); 27 | nettyRequest.headers().add("key1", "value2"); 28 | nettyRequest.headers().add("key2", "value1"); 29 | RecordedHttpRequestBuilder recordedHttpRequestBuilder = new RecordedHttpRequestBuilder(nettyRequest); 30 | Multimap headers = recordedHttpRequestBuilder.getHeaders(); 31 | Assert.assertEquals(headers.size(), 3); 32 | } 33 | 34 | @Test 35 | public void testGetHeaders() 36 | throws Exception { 37 | HttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "www.google.com"); 38 | nettyRequest.headers().add("key1", "value1"); 39 | nettyRequest.headers().add("key1", "value2"); 40 | nettyRequest.headers().add("key2", "value1"); 41 | RecordedHttpRequestBuilder recordedHttpRequestBuilder = new RecordedHttpRequestBuilder(nettyRequest); 42 | Multimap headers = recordedHttpRequestBuilder.getHeaders(); 43 | Assert.assertEquals(headers.size(), 3); 44 | Assert.assertTrue(headers.get("key1").contains("value1")); 45 | Assert.assertTrue(headers.get("key1").contains("value2")); 46 | Assert.assertTrue(headers.get("key2").contains("value1")); 47 | } 48 | 49 | @Test 50 | public void testSetCookieHeader() throws URISyntaxException { 51 | HttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "www.abc.com"); 52 | nettyRequest.headers() 53 | .add("Set-Cookie", 54 | "a,b,c"); 55 | nettyRequest.headers() 56 | .add("Set-Cookie", 57 | "d,e,f"); 58 | RecordedHttpRequestBuilder recordedHttpRequestBuilder = new RecordedHttpRequestBuilder(nettyRequest); 59 | Multimap headers = recordedHttpRequestBuilder.getHeaders(); 60 | 61 | Assert.assertEquals(headers.size(), 2); 62 | Assert.assertTrue(headers.get("Set-Cookie").contains("a,b,c")); 63 | Assert.assertTrue(headers.get("Set-Cookie").contains("d,e,f")); 64 | } 65 | 66 | @Test 67 | public void testNonSetCookieHeader() throws URISyntaxException { 68 | HttpRequest nettyRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "www.abc.com"); 69 | nettyRequest.headers() 70 | .add("Not-Set-Cookie", 71 | "a,b,c"); 72 | nettyRequest.headers() 73 | .add("Not-Set-Cookie", 74 | "d,e,f"); 75 | 76 | RecordedHttpRequestBuilder recordedHttpRequestBuilder = new RecordedHttpRequestBuilder(nettyRequest); 77 | Multimap headers = recordedHttpRequestBuilder.getHeaders(); 78 | 79 | Assert.assertEquals(headers.size(), 2); 80 | Assert.assertTrue(headers.get("Not-Set-Cookie").contains("a,b,c")); 81 | Assert.assertTrue(headers.get("Not-Set-Cookie").contains("d,e,f")); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /flashback-netty/src/test/java/com/linkedin/flashback/netty/builder/RecordedHttpResponseBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.netty.builder; 7 | 8 | import com.linkedin.flashback.serializable.RecordedHttpResponse; 9 | import io.netty.buffer.Unpooled; 10 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 11 | import io.netty.handler.codec.http.DefaultHttpContent; 12 | import io.netty.handler.codec.http.DefaultLastHttpContent; 13 | import io.netty.handler.codec.http.HttpContent; 14 | import io.netty.handler.codec.http.HttpResponse; 15 | import io.netty.handler.codec.http.HttpResponseStatus; 16 | import io.netty.handler.codec.http.HttpVersion; 17 | import java.io.IOException; 18 | import org.testng.Assert; 19 | import org.testng.annotations.Test; 20 | 21 | 22 | /** 23 | * @author shfeng 24 | */ 25 | public class RecordedHttpResponseBuilderTest { 26 | 27 | @Test 28 | public void testBuild() 29 | throws IOException { 30 | HttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.GATEWAY_TIMEOUT); 31 | RecordedHttpResponseBuilder recordedHttpResponseBuilder = new RecordedHttpResponseBuilder(httpResponse); 32 | 33 | String charset = "UTF-8"; 34 | String str1 = "Hello world"; 35 | HttpContent httpContent1 = new DefaultHttpContent(Unpooled.copiedBuffer(str1.getBytes(charset))); 36 | recordedHttpResponseBuilder.appendHttpContent(httpContent1); 37 | String str2 = "second content"; 38 | HttpContent httpContent2 = new DefaultHttpContent(Unpooled.copiedBuffer(str2.getBytes(charset))); 39 | recordedHttpResponseBuilder.appendHttpContent(httpContent2); 40 | 41 | String lastStr = "Last chunk"; 42 | HttpContent lastContent = new DefaultLastHttpContent(Unpooled.copiedBuffer(lastStr.getBytes(charset))); 43 | recordedHttpResponseBuilder.appendHttpContent(lastContent); 44 | RecordedHttpResponse recordedHttpResponse = recordedHttpResponseBuilder.build(); 45 | Assert.assertEquals(recordedHttpResponse.getStatus(), HttpResponseStatus.GATEWAY_TIMEOUT.code()); 46 | Assert.assertEquals((str1 + str2 + lastStr).getBytes(charset), 47 | recordedHttpResponse.getHttpBody().getContent(charset)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /flashback-smartproxy/src/main/java/com/linkedin/flashback/smartproxy/proxycontroller/RecordController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.smartproxy.proxycontroller; 7 | 8 | import com.linkedin.flashback.SceneAccessLayer; 9 | import com.linkedin.flashback.netty.builder.RecordedHttpRequestBuilder; 10 | import com.linkedin.flashback.netty.builder.RecordedHttpResponseBuilder; 11 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 12 | import com.linkedin.mitm.proxy.dataflow.ProxyModeController; 13 | import io.netty.handler.codec.http.HttpContent; 14 | import io.netty.handler.codec.http.HttpObject; 15 | import io.netty.handler.codec.http.HttpRequest; 16 | import io.netty.handler.codec.http.HttpResponse; 17 | import io.netty.handler.codec.http.LastHttpContent; 18 | import java.io.IOException; 19 | import org.apache.log4j.Logger; 20 | 21 | 22 | /** 23 | * Record controller which record original http request and store in files. 24 | * New instance gets created for each new connection coming. 25 | * 26 | * @author shfeng 27 | */ 28 | public class RecordController implements ProxyModeController { 29 | private static final Logger LOG = Logger.getLogger(RecordController.class); 30 | 31 | private final RecordedHttpRequestBuilder _clientRequestBuilder; 32 | private RecordedHttpResponseBuilder _serverResponseBuilder; 33 | private final SceneAccessLayer _sceneAccessLayer; 34 | 35 | public RecordController(SceneAccessLayer sceneAccessLayer, HttpRequest httpRequest) { 36 | _clientRequestBuilder = new RecordedHttpRequestBuilder(httpRequest); 37 | _sceneAccessLayer = sceneAccessLayer; 38 | } 39 | 40 | @Override 41 | public void handleReadFromClient(ChannelMediator channelMediator, HttpObject httpObject) { 42 | if (channelMediator == null) { 43 | throw new IllegalStateException("HRFC: ChannelMediator can't be null"); 44 | } 45 | 46 | try { 47 | if (httpObject instanceof HttpRequest) { 48 | HttpRequest httpRequest = (HttpRequest) httpObject; 49 | _clientRequestBuilder.interpretHttpRequest(httpRequest); 50 | _clientRequestBuilder.addHeaders(httpRequest); 51 | } 52 | 53 | if (httpObject instanceof HttpContent) { 54 | _clientRequestBuilder.appendHttpContent((HttpContent) httpObject); 55 | } 56 | } catch (IOException e) { 57 | throw new RuntimeException("HRFC: Failed to record HttpContent", e); 58 | } 59 | 60 | channelMediator.writeToServer(httpObject); 61 | } 62 | 63 | @Override 64 | public void handleReadFromServer(HttpObject httpObject) { 65 | if (httpObject instanceof HttpResponse) { 66 | _serverResponseBuilder = new RecordedHttpResponseBuilder((HttpResponse) httpObject); 67 | } 68 | 69 | try { 70 | if (httpObject instanceof HttpContent) { 71 | _serverResponseBuilder.appendHttpContent((HttpContent) httpObject); 72 | } 73 | 74 | if (httpObject instanceof LastHttpContent) { 75 | _sceneAccessLayer.record(_clientRequestBuilder.build(), _serverResponseBuilder.build()); 76 | } 77 | } catch (IOException e) { 78 | throw new RuntimeException("HRFS: Failed to record HttpContent", e); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flashback-smartproxy/src/main/java/com/linkedin/flashback/smartproxy/proxycontroller/ReplayController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.smartproxy.proxycontroller; 7 | 8 | import com.linkedin.flashback.SceneAccessLayer; 9 | import com.linkedin.flashback.netty.builder.RecordedHttpRequestBuilder; 10 | import com.linkedin.flashback.netty.mapper.NettyHttpResponseMapper; 11 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 12 | import com.linkedin.flashback.serializable.RecordedHttpResponse; 13 | import com.linkedin.flashback.smartproxy.utils.NoMatchResponseGenerator; 14 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 15 | import com.linkedin.mitm.proxy.dataflow.ProxyModeController; 16 | import io.netty.handler.codec.http.FullHttpResponse; 17 | import io.netty.handler.codec.http.HttpContent; 18 | import io.netty.handler.codec.http.HttpObject; 19 | import io.netty.handler.codec.http.HttpRequest; 20 | import io.netty.handler.codec.http.HttpResponse; 21 | import io.netty.handler.codec.http.LastHttpContent; 22 | import java.io.IOException; 23 | import org.apache.log4j.Logger; 24 | 25 | 26 | /** 27 | * Replay controller which playback http response based on matched http request 28 | * New instance gets created for each new connection coming. 29 | * 30 | * @author shfeng 31 | */ 32 | public class ReplayController implements ProxyModeController { 33 | private static final Logger LOG = Logger.getLogger(ReplayController.class); 34 | 35 | private final RecordedHttpRequestBuilder _clientRequestBuilder; 36 | private final SceneAccessLayer _sceneAccessLayer; 37 | 38 | 39 | public ReplayController(SceneAccessLayer sceneAccessLayer, HttpRequest httpRequest) { 40 | _clientRequestBuilder = new RecordedHttpRequestBuilder(httpRequest); 41 | _sceneAccessLayer = sceneAccessLayer; 42 | } 43 | 44 | @Override 45 | public void handleReadFromClient(ChannelMediator channelMediator, HttpObject httpObject) { 46 | if (channelMediator == null) { 47 | throw new IllegalStateException("HRFC: ChannelMediator can't be null"); 48 | } 49 | 50 | try { 51 | if (httpObject instanceof HttpRequest) { 52 | HttpRequest httpRequest = (HttpRequest) httpObject; 53 | _clientRequestBuilder.interpretHttpRequest(httpRequest); 54 | _clientRequestBuilder.addHeaders(httpRequest); 55 | } 56 | 57 | if (httpObject instanceof HttpContent) { 58 | _clientRequestBuilder.appendHttpContent((HttpContent) httpObject); 59 | } 60 | 61 | if (httpObject instanceof LastHttpContent) { 62 | HttpResponse httpResponse = playBack(); 63 | channelMediator.writeToClientAndDisconnect(httpResponse); 64 | } 65 | } catch (IOException e) { 66 | throw new RuntimeException("HRFC: Failed to replay HttpContent", e); 67 | } 68 | } 69 | 70 | @Override 71 | public void handleReadFromServer(HttpObject httpObject) { 72 | throw new IllegalStateException("No read from server in replay mode"); 73 | } 74 | 75 | /** 76 | * If found matched request, then return response accordingly. 77 | * Otherwise, return bad request. 78 | * 79 | * @return bad request if not matched request/response found in the scene. 80 | * */ 81 | private FullHttpResponse playBack() 82 | throws IOException { 83 | RecordedHttpRequest recordedHttpRequest = _clientRequestBuilder.build(); 84 | boolean found = _sceneAccessLayer.hasMatchRequest(recordedHttpRequest); 85 | if (!found) { 86 | if (LOG.isDebugEnabled()) { 87 | LOG.debug(_sceneAccessLayer.getMatchFailureDescription(recordedHttpRequest)); 88 | } 89 | return NoMatchResponseGenerator.generateNoMatchResponse(recordedHttpRequest); 90 | } 91 | RecordedHttpResponse recordedHttpResponse = _sceneAccessLayer.playback(recordedHttpRequest); 92 | return NettyHttpResponseMapper.from(recordedHttpResponse); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /flashback-smartproxy/src/main/java/com/linkedin/flashback/smartproxy/utils/NoMatchResponseGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.flashback.smartproxy.utils; 7 | 8 | import com.linkedin.flashback.serializable.RecordedEncodedHttpBody; 9 | import com.linkedin.flashback.serializable.RecordedHttpBody; 10 | import com.linkedin.flashback.serializable.RecordedHttpRequest; 11 | import com.linkedin.flashback.serializable.RecordedStringHttpBody; 12 | import io.netty.buffer.ByteBuf; 13 | import io.netty.buffer.Unpooled; 14 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 15 | import io.netty.handler.codec.http.FullHttpResponse; 16 | import io.netty.handler.codec.http.HttpResponseStatus; 17 | import io.netty.handler.codec.http.HttpVersion; 18 | import java.nio.charset.Charset; 19 | 20 | 21 | /** 22 | * Util class to generate response to indicate request has no match in the scene 23 | */ 24 | public class NoMatchResponseGenerator { 25 | /* 26 | * Builds the 400/Bad Request response to return when there is no matching request 27 | */ 28 | public static FullHttpResponse generateNoMatchResponse(RecordedHttpRequest recordedHttpRequest) { 29 | StringBuilder bodyTextBuilder = new StringBuilder(); 30 | bodyTextBuilder.append("No Matching Request\n").append("Incoming Request Method: ") 31 | .append(recordedHttpRequest.getMethod()).append("\n").append("Incoming Request URI: ") 32 | .append(recordedHttpRequest.getUri()).append("\n").append("Incoming Request Headers: ") 33 | .append(recordedHttpRequest.getHeaders()).append("\n"); 34 | RecordedHttpBody incomingBody = recordedHttpRequest.getHttpBody(); 35 | if (incomingBody != null) { 36 | if (incomingBody instanceof RecordedEncodedHttpBody) { 37 | incomingBody = ((RecordedEncodedHttpBody) incomingBody).getDecodedBody(); 38 | } 39 | if (incomingBody instanceof RecordedStringHttpBody) { 40 | bodyTextBuilder.append("Incoming Request Body: ").append(((RecordedStringHttpBody) incomingBody).getContent()); 41 | } else { 42 | bodyTextBuilder.append("Incoming Request Body: (binary content)"); 43 | } 44 | } 45 | ByteBuf badRequestBody = Unpooled.wrappedBuffer(bodyTextBuilder.toString().getBytes(Charset.forName("UTF-8"))); 46 | return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST, badRequestBody); 47 | } 48 | 49 | private NoMatchResponseGenerator() { 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /flashback-smartproxy/src/test/resources/flashback/scene/setCookie: -------------------------------------------------------------------------------- 1 | { 2 | "NAME" : "setCookie", 3 | "HTTPEXCHANGELIST" : { 4 | "HTTPEXCHANGE1" : { 5 | "UPDATETIME" : "9 Apr 2017 21:13:27 GMT", 6 | "HTTPREQUEST" : { 7 | "HTTPMETHOD" : "GET", 8 | "HTTPURI" : "http://www.example.org/", 9 | "HTTPHEADERS" : { 10 | "Set-Cookie" : "ABC=\\\"R:0|g:fcaa967e-asdfa-484a-8a5e-asdfa\\\"; Version=1; Max-Age=30; Expires=Thu, 23-Mar-2017 18:01:20 GMT; Path=/", 11 | "Set-Cookie" : "ABC=\\\"R:0|g:fcaa967e-asdfasdf-484a-8a5e-asdf|n:asdfasdfasd-37ca-42cf-a909-95e0dd19e334\\\"; Version=1; Max-Age=30; Expires=Thu, 23-Mar-2017 18:01:20 GMT; Path=/", 12 | "Set-Cookie" : "ABC=\\\"R:0|i:138507\\\"; Version=1; Max-Age=30; Expires=Thu, 23-Mar-2017 18:01:20 GMT; Path=/", 13 | "Set-Cookie" : "ABC=\\\"R:0|i:138507|e:42\\\"; Version=1; Max-Age=30; Expires=Thu, 23-Mar-2017 18:01:20 GMT; Path=/", 14 | "Set-Cookie" : "guestidc=0d28bda6-5d42-4ee9-bd1e-asdasda; Domain=asdafsdfasdfasdfa.com; Path=/", 15 | "Host" : "www.example.org", 16 | "Proxy-Connection" : "Keep-Alive", 17 | "User-Agent" : "Apache-HttpClient/4.3.1 (java 1.5)", 18 | "Accept-Encoding" : "gzip,deflate" 19 | }, 20 | "BINARYHTTPBODY" : "" 21 | }, 22 | "HTTPRESPONSE" : { 23 | "HTTPSTATUSCODE" : 200, 24 | "HTTPHEADERS" : { 25 | "X-Cache" : "HIT", 26 | "Server" : "ECS (ewr/15BD)", 27 | "Last-Modified" : "Fri, 09 Aug 2013 23:54:35 GMT", 28 | "Date" : "Sun, 09 Apr 2017 21:13:26 GMT", 29 | "Accept-Ranges" : "bytes", 30 | "Cache-Control" : "max-age=604800", 31 | "Etag" : "\"359670651+gzip\"", 32 | "Content-Encoding" : "gzip", 33 | "Vary" : "Accept-Encoding", 34 | "Expires" : "Sun, 16 Apr 2017 21:13:26 GMT", 35 | "Content-Length" : "606", 36 | "Content-Type" : "text/html" 37 | }, 38 | "ENCODEDHTTPBODY" : { 39 | "HTTPBODYENCODING" : "gzip", 40 | "STRINGHTTPBODY" : "I am from Flashback scene, not http://example.org\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is established to be used for illustrative examples in documents. You may use this\n domain in examples without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n" 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------- 2 | #For details on recommended settings, see go/gradle.properties 3 | #------------------------------------------------------------- 4 | 5 | #long-running Gradle process speeds up local builds 6 | #to stop the daemon run 'ligradle --stop' 7 | org.gradle.daemon=true 8 | 9 | #configures only relevant projects to speed up the configuration of large projects 10 | #useful when specific project/task is invoked e.g: ligradle :cloud:cloud-api:build 11 | org.gradle.configureondemand=true 12 | 13 | #Gradle will run tasks from subprojects in parallel 14 | #Higher CPU usage, faster builds 15 | org.gradle.parallel=true 16 | 17 | #Allows generation of idea/eclipse metadata for a specific subproject and its upstream project dependencies 18 | ide.recursive=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/Integration-test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/images/Integration-test.jpg -------------------------------------------------------------------------------- /images/Production-environment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/images/Production-environment.jpg -------------------------------------------------------------------------------- /images/Recording.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/images/Recording.jpg -------------------------------------------------------------------------------- /images/Use-case-need-dynamic-changes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/images/Use-case-need-dynamic-changes.jpg -------------------------------------------------------------------------------- /images/non-java-high-level-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/images/non-java-high-level-diagram.png -------------------------------------------------------------------------------- /images/non-java-service-interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin/flashback/4b45993ffad8cfbe5e8c86ddeb291d33e313d82a/images/non-java-service-interaction.png -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/factory/CertificateKeyStoreFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.factory; 7 | 8 | import com.linkedin.mitm.services.CertificateService; 9 | import java.io.IOException; 10 | import java.security.InvalidKeyException; 11 | import java.security.KeyPair; 12 | import java.security.KeyStore; 13 | import java.security.KeyStoreException; 14 | import java.security.NoSuchAlgorithmException; 15 | import java.security.NoSuchProviderException; 16 | import java.security.PrivateKey; 17 | import java.security.PublicKey; 18 | import java.security.SignatureException; 19 | import java.security.UnrecoverableKeyException; 20 | import java.security.cert.CertificateException; 21 | import java.security.cert.X509Certificate; 22 | import java.util.List; 23 | import org.bouncycastle.asn1.ASN1Encodable; 24 | import org.bouncycastle.operator.OperatorCreationException; 25 | 26 | 27 | /** 28 | * This factory would be used to generate {@link KeyStore} either from CA certificate 29 | * or server identity certificates 30 | * It will be only used to generate keyManagers not trustManagers. 31 | * 32 | * @author shfeng 33 | */ 34 | public class CertificateKeyStoreFactory { 35 | private static final String KEY_STORE_TYPE = "PKCS12"; 36 | 37 | private final KeyPairFactory _keyPairFactory; 38 | private final CertificateService _certificateService; 39 | 40 | /** 41 | * @param keyPairFactory factory that would be used to generate public/private key pairs 42 | * @param certificateService decide which type of certificate to create 43 | * 44 | * */ 45 | public CertificateKeyStoreFactory(KeyPairFactory keyPairFactory, CertificateService certificateService) 46 | throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { 47 | _keyPairFactory = keyPairFactory; 48 | _certificateService = certificateService; 49 | } 50 | 51 | /** 52 | * create a {@link java.security.KeyStore} for this certificate 53 | * @param commonName this field is only used for generating new identity certificate 54 | * @param sans a list of alternate subject names, that also will be used for generating identity certificate 55 | * @return keystore for this new certificate 56 | * 57 | * */ 58 | public KeyStore create(String commonName, List sans) 59 | throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, OperatorCreationException, 60 | NoSuchProviderException, InvalidKeyException, SignatureException { 61 | KeyPair keyPair = _keyPairFactory.create(); 62 | PublicKey publicKey = keyPair.getPublic(); 63 | PrivateKey privateKey = keyPair.getPrivate(); 64 | X509Certificate identityCertificate = 65 | _certificateService.createSignedCertificate(publicKey, privateKey, commonName, sans); 66 | KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE); 67 | keyStore.load(null, null); 68 | 69 | _certificateService.updateKeyStore(keyStore, privateKey, identityCertificate); 70 | return keyStore; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/factory/KeyPairFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.factory; 7 | 8 | import java.security.KeyPair; 9 | import java.security.NoSuchAlgorithmException; 10 | 11 | 12 | /** 13 | * Abstract public and private key pair generating factory 14 | * @author shfeng 15 | */ 16 | public interface KeyPairFactory { 17 | KeyPair create() 18 | throws NoSuchAlgorithmException; 19 | } 20 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/factory/RSASha1KeyPairFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.factory; 7 | 8 | import java.security.KeyPair; 9 | import java.security.KeyPairGenerator; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.security.SecureRandom; 12 | 13 | 14 | /** 15 | * Generate public key and private key pair that use RSA and SHA1PRNG 16 | * 17 | * @author shfeng 18 | */ 19 | public class RSASha1KeyPairFactory implements KeyPairFactory { 20 | private static final String KEYGEN_ALGORITHM = "RSA"; 21 | private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; 22 | 23 | private final int _keySize; 24 | 25 | public RSASha1KeyPairFactory(int keySize) { 26 | _keySize = keySize; 27 | } 28 | 29 | @Override 30 | public KeyPair create() 31 | throws NoSuchAlgorithmException { 32 | KeyPairGenerator generator = KeyPairGenerator.getInstance(KEYGEN_ALGORITHM); 33 | SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM); 34 | generator.initialize(_keySize, secureRandom); 35 | return generator.generateKeyPair(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/model/CertificateAuthority.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.model; 7 | 8 | /** 9 | * Hold all parameters that is used to build your own Certificate Authority 10 | * https://docs.oracle.com/cd/E19509-01/820-3503/ggezy/index.html 11 | * 12 | * @author shfeng 13 | */ 14 | public class CertificateAuthority { 15 | 16 | private final String _alias; 17 | private final char[] _passPhrase; 18 | private final String _commonName; 19 | private final String _organizationalUnit; 20 | private final String _organization; 21 | private final String _locality; 22 | private final String _countryCode; 23 | 24 | public CertificateAuthority(String alias, char[] passPhrase, String commonName, String organizationalUnit, 25 | String organization, String locality, String countryCode) { 26 | _alias = alias; 27 | _passPhrase = passPhrase; 28 | _commonName = commonName; 29 | _organizationalUnit = organizationalUnit; 30 | _organization = organization; 31 | _locality = locality; 32 | _countryCode = countryCode; 33 | } 34 | 35 | public String getAlias() { 36 | return _alias; 37 | } 38 | 39 | public char[] getPassPhrase() { 40 | return _passPhrase; 41 | } 42 | 43 | public String getCommonName() { 44 | return _commonName; 45 | } 46 | 47 | public String getOrganizationalUnit() { 48 | return _organizationalUnit; 49 | } 50 | 51 | public String getOrganization() { 52 | return _organization; 53 | } 54 | 55 | public String getLocality() { 56 | return _locality; 57 | } 58 | 59 | public String getCountryCode() { 60 | return _countryCode; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/model/CertificateValidPeriod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.model; 7 | 8 | import java.util.Calendar; 9 | import java.util.Date; 10 | 11 | 12 | /** 13 | * Certificate signed by CA will be valid in this period 14 | * @author shfeng 15 | */ 16 | public class CertificateValidPeriod { 17 | private final Date _start; 18 | private final Date _end; 19 | 20 | public CertificateValidPeriod() { 21 | Date today = new Date(); 22 | Calendar cal = Calendar.getInstance(); 23 | cal.setTime(today); 24 | cal.add(Calendar.YEAR, -1); 25 | _start = cal.getTime(); 26 | cal.setTime(today); 27 | cal.add(Calendar.YEAR, 10); 28 | _end = cal.getTime(); 29 | } 30 | 31 | public CertificateValidPeriod(Date start, Date end) { 32 | _start = start; 33 | _end = end; 34 | } 35 | 36 | public Date getStart() { 37 | return _start; 38 | } 39 | 40 | public Date getEnd() { 41 | return _end; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/model/Protocol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.model; 7 | 8 | /** 9 | * Protocols that MITM proxy support. 10 | * 11 | * @author shfeng 12 | */ 13 | public enum Protocol { 14 | HTTP, HTTPS 15 | } 16 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/ProxyInitializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import com.linkedin.mitm.proxy.channel.ClientChannelHandler; 10 | import io.netty.channel.ChannelInitializer; 11 | import io.netty.channel.ChannelPipeline; 12 | import io.netty.channel.socket.SocketChannel; 13 | import io.netty.handler.codec.http.HttpRequestDecoder; 14 | import io.netty.handler.codec.http.HttpResponseEncoder; 15 | import io.netty.handler.timeout.IdleStateHandler; 16 | 17 | 18 | /** 19 | * Initialize client to proxy channel and channel pipeline. 20 | * 21 | * @author shfeng 22 | */ 23 | public class ProxyInitializer extends ChannelInitializer { 24 | private final ProxyServer _proxyServer; 25 | 26 | public ProxyInitializer(ProxyServer proxyServer) { 27 | _proxyServer = proxyServer; 28 | } 29 | 30 | @Override 31 | public void initChannel(SocketChannel socketChannel) { 32 | ChannelPipeline channelPipeline = socketChannel.pipeline(); 33 | channelPipeline.addLast("decoder", new HttpRequestDecoder()); 34 | channelPipeline.addLast("encoder", new HttpResponseEncoder()); 35 | channelPipeline.addLast("idle", new IdleStateHandler(0, 0, _proxyServer.getClientConnectionIdleTimeout())); 36 | ChannelMediator channelMediator = new ChannelMediator(socketChannel, 37 | _proxyServer.getProxyModeControllerFactory(), 38 | _proxyServer.getDownstreamWorkerGroup(), 39 | _proxyServer.getServerConnectionIdleTimeout(), 40 | _proxyServer.getAllChannels()); 41 | ClientChannelHandler clientChannelHandler = 42 | new ClientChannelHandler(channelMediator, _proxyServer.getConnectionFlowRegistry()); 43 | 44 | channelPipeline.addLast("handler", clientChannelHandler); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/channel/ChannelHandlerDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.channel; 7 | 8 | import io.netty.handler.codec.http.HttpObject; 9 | 10 | 11 | /** 12 | * Interface to create Handler delegate which will 13 | * be used by {@link ClientChannelHandler} 14 | * 15 | * @author shfeng . 16 | */ 17 | public interface ChannelHandlerDelegate { 18 | /** 19 | * Initializes before we can read data. 20 | * */ 21 | void onCreate(); 22 | 23 | /** 24 | * Read HttpObject 25 | * */ 26 | void onRead(HttpObject httpObject); 27 | } 28 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/channel/ClientChannelHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.channel; 7 | 8 | import com.linkedin.mitm.model.Protocol; 9 | import com.linkedin.mitm.proxy.connectionflow.steps.ConnectionFlowStep; 10 | import com.linkedin.mitm.proxy.factory.ErrorResponseFactory; 11 | import com.linkedin.mitm.proxy.factory.HandlerDelegateFactory; 12 | import io.netty.channel.ChannelHandlerContext; 13 | import io.netty.channel.SimpleChannelInboundHandler; 14 | import io.netty.handler.codec.http.HttpObject; 15 | import io.netty.handler.codec.http.HttpRequest; 16 | import java.util.List; 17 | import java.util.Map; 18 | import org.apache.log4j.Logger; 19 | 20 | 21 | /** 22 | * Client channel handler that implemented read logic by 23 | * creating protocol specific ChannelHandlerDelegate to handle read events from client 24 | * It is stateful.New instance gets created every time when proxy receive a new request. 25 | * 26 | * @author shfeng 27 | */ 28 | public class ClientChannelHandler extends SimpleChannelInboundHandler { 29 | private static final Logger LOG = Logger.getLogger(ClientChannelHandler.class); 30 | 31 | private final ChannelMediator _channelMediator; 32 | private final Map> _connectionFlowRegistry; 33 | 34 | private ChannelHandlerDelegate _channelHandlerDelegate; 35 | 36 | public ClientChannelHandler(final ChannelMediator channelMediator, 37 | final Map> connectionFlowRegistry) { 38 | _connectionFlowRegistry = connectionFlowRegistry; 39 | _channelMediator = channelMediator; 40 | } 41 | 42 | @Override 43 | protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) 44 | throws Exception { 45 | // initial request 46 | if (LOG.isDebugEnabled()) { 47 | LOG.debug(String.format("%s: Reading from client %s", System.currentTimeMillis(), httpObject)); 48 | } 49 | if (httpObject instanceof HttpRequest) { 50 | HttpRequest initialRequest = (HttpRequest) httpObject; 51 | if (_channelHandlerDelegate == null) { 52 | _channelHandlerDelegate = 53 | HandlerDelegateFactory.create(initialRequest, _channelMediator, _connectionFlowRegistry); 54 | _channelHandlerDelegate.onCreate(); 55 | } 56 | } 57 | _channelHandlerDelegate.onRead(httpObject); 58 | } 59 | 60 | @Override 61 | public void channelRegistered(ChannelHandlerContext ctx) 62 | throws Exception { 63 | _channelMediator.registerChannel(ctx.channel()); 64 | LOG.debug("client channel registered" + ctx.channel()); 65 | super.channelRegistered(ctx); 66 | } 67 | 68 | @Override 69 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 70 | throws Exception { 71 | LOG.error("Caught exception on client channel", cause); 72 | _channelMediator.writeToClientAndDisconnect(ErrorResponseFactory.createInternalError(cause)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/channel/Flushable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.channel; 7 | 8 | /** 9 | * Callback that give us ability to flush buffered output to server 10 | * 11 | * @author shfeng 12 | */ 13 | public interface Flushable { 14 | /** 15 | * Flush out HttpObjects added in the buffer 16 | * */ 17 | void flush(); 18 | } 19 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/channel/ServerChannelHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.channel; 7 | 8 | import com.linkedin.mitm.proxy.factory.ErrorResponseFactory; 9 | import io.netty.channel.ChannelHandlerContext; 10 | import io.netty.channel.SimpleChannelInboundHandler; 11 | import io.netty.handler.codec.http.DefaultLastHttpContent; 12 | import io.netty.handler.codec.http.HttpObject; 13 | import org.apache.log4j.Logger; 14 | 15 | /** 16 | * Server channel handler that implemented read logic from server side. 17 | * Note: It's stateful. Each {@link com.linkedin.mitm.proxy.channel.ClientChannelHandler} map to one 18 | * ServerChannelHandler. 19 | * 20 | * @author shfeng 21 | */ 22 | public class ServerChannelHandler extends SimpleChannelInboundHandler { 23 | private static final String MODULE = ServerChannelHandler.class.getName(); 24 | private static final Logger LOG = Logger.getLogger(MODULE); 25 | 26 | private final ChannelMediator _channelMediator; 27 | 28 | public ServerChannelHandler(ChannelMediator channelMediator) { 29 | _channelMediator = channelMediator; 30 | } 31 | 32 | @Override 33 | protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) 34 | throws Exception { 35 | _channelMediator.readFromServerChannel(httpObject); 36 | if (httpObject instanceof DefaultLastHttpContent) { 37 | _channelMediator.writeToClientAndDisconnect(httpObject); 38 | } else { 39 | _channelMediator.writeToClient(httpObject); 40 | } 41 | } 42 | 43 | @Override 44 | public void channelRegistered(ChannelHandlerContext ctx) 45 | throws Exception { 46 | _channelMediator.registerChannel(ctx.channel()); 47 | LOG.debug("server channel registered" + ctx.channel()); 48 | super.channelRegistered(ctx); 49 | } 50 | 51 | @Override 52 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 53 | throws Exception { 54 | LOG.error("Caught exception on server channel", cause); 55 | _channelMediator.writeToClientAndDisconnect(ErrorResponseFactory.createInternalError(cause)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/channel/protocol/HttpChannelHandlerDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.channel.protocol; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelHandlerDelegate; 9 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 10 | import com.linkedin.mitm.proxy.channel.Flushable; 11 | import com.linkedin.mitm.proxy.connectionflow.ConnectionFlowProcessor; 12 | import io.netty.handler.codec.http.HttpContent; 13 | import io.netty.handler.codec.http.HttpObject; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | 18 | /** 19 | * Http specific logic to handle reading from Client 20 | * 21 | * @author shfeng 22 | */ 23 | public class HttpChannelHandlerDelegate implements ChannelHandlerDelegate { 24 | private final ConnectionFlowProcessor _connectionFlowProcessor; 25 | private final ChannelMediator _channelMediator; 26 | private final ChannelReadCallback _channelReadCallback = new ChannelReadCallback(); 27 | 28 | public HttpChannelHandlerDelegate(ChannelMediator channelMediator, ConnectionFlowProcessor httpConnectionFlowProcessor) { 29 | _channelMediator = channelMediator; 30 | _connectionFlowProcessor = httpConnectionFlowProcessor; 31 | } 32 | 33 | @Override 34 | public void onCreate() { 35 | _connectionFlowProcessor.startConnectionFlow(_channelReadCallback); 36 | } 37 | 38 | @Override 39 | public void onRead(HttpObject httpObject) { 40 | if (!_connectionFlowProcessor.isComplete()) { 41 | _channelReadCallback.write(httpObject); 42 | // Accroding to http://netty.io/wiki/reference-counted-objects.html 43 | // When an event loop reads data into a ByteBuf and triggers a channelRead() event with it, 44 | // it is the responsibility of the ChannelHandler in the corresponding pipeline to release the buffer. 45 | // Since this is the last ChannelHandler, it release the reference-counted after read. So we need to 46 | // retain to make sure it will not be released until we stored in scene. 47 | if(httpObject instanceof HttpContent){ 48 | ((HttpContent)httpObject).retain(); 49 | } 50 | return; 51 | } 52 | _channelMediator.readFromClientChannel(httpObject); 53 | } 54 | 55 | /** 56 | * Client channel read callback that can hold incoming request and flush them in one shot once connection flow is done. 57 | * */ 58 | private class ChannelReadCallback implements Flushable { 59 | private List _bufferedHttpObjects = new ArrayList<>(); 60 | 61 | private void write(HttpObject httpObject) { 62 | _bufferedHttpObjects.add(httpObject); 63 | } 64 | 65 | @Override 66 | public void flush() { 67 | for (HttpObject httpObject : _bufferedHttpObjects) { 68 | _channelMediator.readFromClientChannel(httpObject); 69 | } 70 | _bufferedHttpObjects.clear(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/channel/protocol/HttpsChannelHandlerDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.channel.protocol; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelHandlerDelegate; 9 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 10 | import com.linkedin.mitm.proxy.channel.Flushable; 11 | import com.linkedin.mitm.proxy.connectionflow.ConnectionFlowProcessor; 12 | import io.netty.handler.codec.http.HttpObject; 13 | import org.apache.log4j.Logger; 14 | 15 | 16 | /** 17 | * Https specific logic to handle reading from Client side 18 | * Note: The https request flow can be explicitly break down to two parts: 19 | * 1. Client send Connect Http Request. When netty framework process request, it fire two read events which 20 | * break to two parts: {@link io.netty.handler.codec.http.HttpRequest} and {@link io.netty.handler.codec.http.LastHttpContent} 21 | * Since mitm proxy just need create a another TCP connection to server, we could drop the initial request. 22 | * 2. After connection is built, we could read real HttpObject and forward to server. 23 | * 24 | * @author shfeng 25 | */ 26 | public class HttpsChannelHandlerDelegate implements ChannelHandlerDelegate { 27 | private static final String MODULE = HttpsChannelHandlerDelegate.class.getName(); 28 | private static final Logger LOG = Logger.getLogger(MODULE); 29 | private final ConnectionFlowProcessor _connectionFlowProcessor; 30 | private final ChannelMediator _channelMediator; 31 | 32 | public HttpsChannelHandlerDelegate(ChannelMediator channelMediator, 33 | ConnectionFlowProcessor httpsConnectionFlowProcessor) { 34 | _channelMediator = channelMediator; 35 | _connectionFlowProcessor = httpsConnectionFlowProcessor; 36 | } 37 | 38 | @Override 39 | public void onCreate() { 40 | _connectionFlowProcessor.startConnectionFlow(new Flushable() { 41 | @Override 42 | public void flush() { 43 | 44 | } 45 | }); 46 | } 47 | 48 | @Override 49 | public void onRead(HttpObject httpObject) { 50 | // Drop initial Connection request. 51 | // Basically,CONNECT request will be sliced to two parts: HttpRequest and LastContent(empty content to indicate end of 52 | // this request). This HttpRequest is only used to indicates that it's time to build tunnel between client and server. 53 | // It invokes Connection flow to build two tunnels: between client to proxy and proxy to server. 54 | // After that, both HttpRequest and HttpLastContent is useless, that's why they are get dropped. 55 | // Only after connection flow is complete, we start reading and processing incoming request from client 56 | if (_connectionFlowProcessor.isComplete()) { 57 | _channelMediator.readFromClientChannel(httpObject); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/ConnectionFlowProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import com.linkedin.mitm.proxy.channel.Flushable; 10 | import com.linkedin.mitm.proxy.connectionflow.steps.ConnectionFlowStep; 11 | import io.netty.handler.codec.http.HttpRequest; 12 | import java.net.InetSocketAddress; 13 | import java.util.List; 14 | import java.util.regex.Pattern; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.apache.log4j.Logger; 17 | 18 | 19 | /** 20 | * Connection flow processor class which implemented common 21 | * connection flow processing logic for both Http and Https. 22 | * It's stateful. New instance get created every time we execute one connection flow. 23 | * It holds immutable connectionFlow which is defined at proxy boostrap 24 | * and maintained the state of connectionFlow execution. 25 | * 26 | * @author shfeng 27 | */ 28 | public class ConnectionFlowProcessor { 29 | private static final String MODULE = ConnectionFlowProcessor.class.getName(); 30 | private static final Logger LOG = Logger.getLogger(MODULE); 31 | private static final Pattern HTTP_PREFIX = Pattern.compile("^https?://.*", Pattern.CASE_INSENSITIVE); 32 | private final ChannelMediator _channelMediator; 33 | private final List _connectionFlow; 34 | private final long _startTime; 35 | private final InetSocketAddress _remoteAddress; 36 | private boolean _complete; 37 | private int _stepIndex = 0; 38 | private Flushable _flushable; 39 | 40 | public ConnectionFlowProcessor(ChannelMediator channelMediator, HttpRequest httpRequest, 41 | List connectionFlow) { 42 | _channelMediator = channelMediator; 43 | _connectionFlow = connectionFlow; 44 | _complete = false; 45 | _startTime = System.currentTimeMillis(); 46 | _remoteAddress = getRemoteAddress(httpRequest); 47 | LOG.debug(String.format("Start connection flow at: %d", _startTime)); 48 | } 49 | 50 | /** 51 | * Check if connection flow is completed or not 52 | * */ 53 | public boolean isComplete() { 54 | return _complete; 55 | } 56 | 57 | /** 58 | * Start connection flow and execute callback one by one 59 | * */ 60 | public void startConnectionFlow(Flushable flushable) { 61 | _flushable = flushable; 62 | nextStep(); 63 | } 64 | 65 | /** 66 | * Process next connection flow step 67 | * */ 68 | public void nextStep() { 69 | if (_connectionFlow.size() == _stepIndex) { 70 | //succeed, should notify all of threads waiting for this connection. 71 | LOG.debug(String.format("Finished connection flow in: %d ms", System.currentTimeMillis() - _startTime)); 72 | _complete = true; 73 | _flushable.flush(); 74 | } else { 75 | ConnectionFlowStep connectionFlowStep = _connectionFlow.get(_stepIndex++); 76 | process(connectionFlowStep); 77 | } 78 | } 79 | 80 | /** 81 | * Process this connection flow step 82 | * */ 83 | private void process(ConnectionFlowStep connectionFlowStep) { 84 | connectionFlowStep.execute(_channelMediator, _remoteAddress).addListener(future -> { 85 | if (future.isSuccess()) { 86 | LOG.debug("Finished processing at" + System.currentTimeMillis()); 87 | nextStep(); 88 | } else { 89 | //TODO: send back 503 before close channel. 90 | _channelMediator.disconnectBothChannels(); 91 | } 92 | }); 93 | } 94 | 95 | /** 96 | * Resolve remote address 97 | * */ 98 | private static InetSocketAddress getRemoteAddress(HttpRequest httpRequest) { 99 | String uri = httpRequest.getUri(); 100 | String uriWithoutProtocol; 101 | if (HTTP_PREFIX.matcher(uri).matches()) { 102 | uriWithoutProtocol = StringUtils.substringAfter(uri, "://"); 103 | } else { 104 | uriWithoutProtocol = uri; 105 | } 106 | String hostAndPort; 107 | if (uriWithoutProtocol.contains("/")) { 108 | hostAndPort = uriWithoutProtocol.substring(0, uriWithoutProtocol.indexOf("/")); 109 | } else { 110 | hostAndPort = uriWithoutProtocol; 111 | } 112 | String hostName; 113 | int port; 114 | if (hostAndPort.contains(":")) { 115 | int index = hostAndPort.indexOf(":"); 116 | hostName = hostAndPort.substring(0, index); 117 | port = Integer.parseInt(hostAndPort.substring(index + 1)); 118 | } else { 119 | hostName = hostAndPort; 120 | port = 80; 121 | } 122 | return new InetSocketAddress(hostName, port); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/AcceptTCPConnectionFromClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 10 | import io.netty.handler.codec.http.HttpResponse; 11 | import io.netty.handler.codec.http.HttpResponseStatus; 12 | import io.netty.handler.codec.http.HttpVersion; 13 | import io.netty.util.concurrent.Future; 14 | import java.net.InetSocketAddress; 15 | import org.apache.log4j.Logger; 16 | 17 | /** 18 | * This class is used to finish establishing TCP connection with client. 19 | * 20 | * @author shfeng 21 | */ 22 | public class AcceptTCPConnectionFromClient implements ConnectionFlowStep { 23 | private static final String MODULE = AcceptTCPConnectionFromClient.class.getName(); 24 | private static final Logger LOG = Logger.getLogger(MODULE); 25 | private static final HttpResponseStatus CONNECTION_ESTABLISHED = 26 | new HttpResponseStatus(200, "HTTP/1.1 200 Connection established"); 27 | 28 | @Override 29 | public Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress) { 30 | LOG.debug("Returning 200 response to client to indicate connection established"); 31 | HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, CONNECTION_ESTABLISHED); 32 | return channelMediator.writeToClient(response); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/ConnectionFlowStep.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.util.concurrent.Future; 10 | import java.net.InetSocketAddress; 11 | 12 | 13 | /** 14 | * Represent callback for each connection flow step 15 | * Note: Customized connection flow steps need to be thread safe 16 | * 17 | * @author shfeng 18 | */ 19 | public interface ConnectionFlowStep { 20 | /** 21 | * Implement this method to actually do the work involved in this step of 22 | * the flow. 23 | * 24 | * @param channelMediator client to proxy channel handler 25 | * @param remoteAddress remote address to upstream service 26 | * 27 | */ 28 | Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress); 29 | } 30 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/EstablishTCPConnectionToServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.util.concurrent.Future; 10 | import java.net.InetSocketAddress; 11 | import org.apache.log4j.Logger; 12 | 13 | /** 14 | * Establish TCP connection with server. 15 | * 16 | * @author shfeng 17 | */ 18 | public class EstablishTCPConnectionToServer implements ConnectionFlowStep { 19 | private static final String MODULE = EstablishTCPConnectionToServer.class.getName(); 20 | private static final Logger LOG = Logger.getLogger(MODULE); 21 | 22 | @Override 23 | public Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress) { 24 | LOG.debug("Connecting to server over TCP"); 25 | return channelMediator.connectToServer(remoteAddress); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/HandshakeWithClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.factory.CertificateKeyStoreFactory; 9 | import com.linkedin.mitm.model.CertificateAuthority; 10 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 11 | import com.linkedin.mitm.services.SSLContextGenerator; 12 | import io.netty.util.concurrent.Future; 13 | import java.io.IOException; 14 | import java.net.InetSocketAddress; 15 | import java.security.InvalidKeyException; 16 | import java.security.KeyManagementException; 17 | import java.security.KeyStore; 18 | import java.security.KeyStoreException; 19 | import java.security.NoSuchAlgorithmException; 20 | import java.security.NoSuchProviderException; 21 | import java.security.SignatureException; 22 | import java.security.UnrecoverableKeyException; 23 | import java.security.cert.CertificateException; 24 | import java.util.ArrayList; 25 | import javax.net.ssl.SSLContext; 26 | import org.apache.log4j.Logger; 27 | import org.bouncycastle.operator.OperatorCreationException; 28 | 29 | 30 | /** 31 | * Accept client handshaking and complete handshaking using dynamically generated certificate. 32 | * 33 | * @author shfeng 34 | */ 35 | public class HandshakeWithClient implements ConnectionFlowStep { 36 | private static final String MODULE = HandshakeWithClient.class.getName(); 37 | private static final Logger LOG = Logger.getLogger(MODULE); 38 | private final CertificateKeyStoreFactory _certificateKeyStoreFactory; 39 | private final CertificateAuthority _certificateAuthority; 40 | 41 | public HandshakeWithClient(CertificateKeyStoreFactory certificateKeyStoreFactory, 42 | CertificateAuthority certificateAuthority) { 43 | _certificateKeyStoreFactory = certificateKeyStoreFactory; 44 | _certificateAuthority = certificateAuthority; 45 | } 46 | 47 | @Override 48 | public Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress) { 49 | 50 | //dynamically create SSLEngine based on CN and SANs 51 | LOG.debug("Starting client to proxy connection handshaking"); 52 | try { 53 | //TODO: if connect request only contains ip address, we need get either CA 54 | //TODO: or SANS from server response 55 | KeyStore keyStore = _certificateKeyStoreFactory.create(remoteAddress.getHostName(), new ArrayList<>()); 56 | SSLContext sslContext = SSLContextGenerator.createClientContext(keyStore, _certificateAuthority.getPassPhrase()); 57 | return channelMediator.handshakeWithClient(sslContext.createSSLEngine()); 58 | } catch (NoSuchAlgorithmException | KeyStoreException | IOException | CertificateException | OperatorCreationException 59 | | NoSuchProviderException | InvalidKeyException | SignatureException | KeyManagementException | UnrecoverableKeyException e) { 60 | throw new RuntimeException("Failed to create server identity certificate", e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/HandshakeWithServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.util.concurrent.Future; 10 | import java.net.InetSocketAddress; 11 | import javax.net.ssl.SSLContext; 12 | import org.apache.log4j.Logger; 13 | 14 | /** 15 | * Start handshaking with server. It behaves as client and handshake with server. 16 | * 17 | * @author shfeng 18 | */ 19 | public class HandshakeWithServer implements ConnectionFlowStep { 20 | private static final String MODULE = HandshakeWithServer.class.getName(); 21 | private static final Logger LOG = Logger.getLogger(MODULE); 22 | private final SSLContext _sslContext; 23 | 24 | public HandshakeWithServer(SSLContext sslContext) { 25 | _sslContext = sslContext; 26 | } 27 | 28 | @Override 29 | public Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress) { 30 | LOG.debug("Starting proxy to server connection handshaking"); 31 | return channelMediator.handshakeWithServer(_sslContext.createSSLEngine(remoteAddress.getHostName(), 443)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/ResumeReadingFromClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.util.concurrent.Future; 10 | import java.net.InetSocketAddress; 11 | import org.apache.log4j.Logger; 12 | 13 | 14 | /** 15 | * Resume reading from client 16 | * 17 | * @author shfeng 18 | */ 19 | public class ResumeReadingFromClient implements ConnectionFlowStep { 20 | private static final String MODULE = ResumeReadingFromClient.class.getName(); 21 | private static final Logger LOG = Logger.getLogger(MODULE); 22 | 23 | @Override 24 | public Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress) { 25 | LOG.debug("Resume reading from client"); 26 | return channelMediator.resumeReadingFromClientChannel(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/connectionflow/steps/StopReadingFromClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.connectionflow.steps; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.util.concurrent.Future; 10 | import java.net.InetSocketAddress; 11 | import org.apache.log4j.Logger; 12 | 13 | 14 | /** 15 | * Stop reading from the client 16 | * 17 | * @author shfeng 18 | */ 19 | public class StopReadingFromClient implements ConnectionFlowStep { 20 | private static final String MODULE = StopReadingFromClient.class.getName(); 21 | private static final Logger LOG = Logger.getLogger(MODULE); 22 | 23 | @Override 24 | public Future execute(ChannelMediator channelMediator, InetSocketAddress remoteAddress) { 25 | LOG.info("Stop reading from client"); 26 | return channelMediator.stopReadingFromClientChannel(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/dataflow/NormalProxyModeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.dataflow; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.handler.codec.http.HttpObject; 10 | 11 | 12 | /** 13 | * Normal ProxyModeController that proxy request from client to server and 14 | * from server to client. 15 | * 16 | * @author shfeng 17 | */ 18 | public class NormalProxyModeController implements ProxyModeController { 19 | private static NormalProxyModeController _normalProxyModeController = new NormalProxyModeController(); 20 | 21 | public static NormalProxyModeController getInstance() { 22 | return _normalProxyModeController; 23 | } 24 | 25 | private NormalProxyModeController() { 26 | } 27 | 28 | @Override 29 | public void handleReadFromClient(final ChannelMediator channelMediator, final HttpObject httpObject) { 30 | if (channelMediator != null) { 31 | channelMediator.writeToServer(httpObject); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/dataflow/NormalProxyModeControllerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.dataflow; 7 | 8 | import io.netty.handler.codec.http.HttpRequest; 9 | 10 | /** 11 | * Default proxy mode controller factory which create singleton 12 | * normal proxy controller. 13 | * 14 | * @author shfeng 15 | */ 16 | public class NormalProxyModeControllerFactory implements ProxyModeControllerFactory { 17 | @Override 18 | public ProxyModeController create(HttpRequest httpRequest) { 19 | return NormalProxyModeController.getInstance(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/dataflow/ProxyModeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.dataflow; 7 | 8 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 9 | import io.netty.handler.codec.http.HttpObject; 10 | 11 | 12 | /** 13 | * This is the interface that controls 1) proxy mode that indicate 14 | * direction of data flow 2) what data to send out or store somewhere 15 | * 16 | * @author shfeng 17 | */ 18 | public interface ProxyModeController { 19 | /** 20 | * Decide if request should be sent out to server using channelMediator 21 | * or sent response back to client using channelMediator 22 | * 23 | * @param channelMediator it knows how to communicate to client/server. 24 | * @param httpObject the initial data from {@link com.linkedin.mitm.proxy.channel.ClientChannelHandler} 25 | * 26 | */ 27 | void handleReadFromClient(final ChannelMediator channelMediator, final HttpObject httpObject); 28 | 29 | /** 30 | * Manipulate outgoing request to server 31 | * 32 | * @param httpObject outgoing request 33 | * @return final httpObject that need to be wrote to server 34 | */ 35 | default HttpObject handleWriteToServer(HttpObject httpObject) { 36 | return httpObject; 37 | } 38 | 39 | /** 40 | * Could be used to store incoming response 41 | * 42 | * @param httpObject incoming response from server 43 | */ 44 | default void handleReadFromServer(final HttpObject httpObject) { 45 | } 46 | 47 | /** 48 | * Manipulate outgoing response to client 49 | * 50 | * @param httpObject outgoing response that is about to send back to client 51 | * @return final httpObject that need to be written to server 52 | */ 53 | default HttpObject handleWriteToClient(HttpObject httpObject) { 54 | return httpObject; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/dataflow/ProxyModeControllerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.dataflow; 7 | 8 | import io.netty.handler.codec.http.HttpRequest; 9 | 10 | /** 11 | * ProxyModeController factory which is used for generate either default 12 | * Normal proxy controller or customized proxy controllers. 13 | * 14 | * @author shfeng 15 | */ 16 | public interface ProxyModeControllerFactory { 17 | ProxyModeController create(HttpRequest httpRequest); 18 | } 19 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/factory/ErrorResponseFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.factory; 7 | 8 | import io.netty.buffer.ByteBuf; 9 | import io.netty.buffer.Unpooled; 10 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 11 | import io.netty.handler.codec.http.FullHttpResponse; 12 | import io.netty.handler.codec.http.HttpResponseStatus; 13 | import io.netty.handler.codec.http.HttpVersion; 14 | import java.nio.charset.Charset; 15 | 16 | 17 | /** 18 | * Create various error response 19 | * 20 | * @author shfeng 21 | */ 22 | public class ErrorResponseFactory { 23 | 24 | public static FullHttpResponse createInternalError(Throwable throwable) { 25 | ByteBuf badRequestBody = 26 | Unpooled.wrappedBuffer(throwable.getMessage().getBytes(Charset.forName("UTF-8"))); 27 | return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, badRequestBody); 28 | } 29 | 30 | private ErrorResponseFactory() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/factory/HandlerDelegateFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.factory; 7 | 8 | import com.linkedin.mitm.model.Protocol; 9 | import com.linkedin.mitm.proxy.channel.ChannelHandlerDelegate; 10 | import com.linkedin.mitm.proxy.channel.ChannelMediator; 11 | import com.linkedin.mitm.proxy.channel.protocol.HttpChannelHandlerDelegate; 12 | import com.linkedin.mitm.proxy.channel.protocol.HttpsChannelHandlerDelegate; 13 | import com.linkedin.mitm.proxy.connectionflow.ConnectionFlowProcessor; 14 | import com.linkedin.mitm.proxy.connectionflow.steps.ConnectionFlowStep; 15 | import io.netty.handler.codec.http.HttpMethod; 16 | import io.netty.handler.codec.http.HttpRequest; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | 21 | /** 22 | * Create Protocol specific handler delegate 23 | * 24 | * @author shfeng 25 | */ 26 | public class HandlerDelegateFactory { 27 | public static ChannelHandlerDelegate create(HttpRequest httpRequest, ChannelMediator channelMediator, 28 | Map> connectionFlowRegistry) { 29 | if (HttpMethod.CONNECT.equals(httpRequest.getMethod())) { 30 | List connectionFlow = connectionFlowRegistry.get(Protocol.HTTPS); 31 | ConnectionFlowProcessor httpsConnectionFlowProcessor = 32 | new ConnectionFlowProcessor(channelMediator, httpRequest, connectionFlow); 33 | channelMediator.initializeProxyModeController(httpRequest); 34 | return new HttpsChannelHandlerDelegate(channelMediator, httpsConnectionFlowProcessor); 35 | } else { 36 | List connectionFlow = connectionFlowRegistry.get(Protocol.HTTP); 37 | ConnectionFlowProcessor httpConnectionFlowProcessor = 38 | new ConnectionFlowProcessor(channelMediator, httpRequest, connectionFlow); 39 | channelMediator.initializeProxyModeController(httpRequest); 40 | return new HttpChannelHandlerDelegate(channelMediator, httpConnectionFlowProcessor); 41 | } 42 | } 43 | 44 | private HandlerDelegateFactory() { 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/proxy/factory/NamedThreadFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.proxy.factory; 7 | 8 | import java.util.concurrent.ThreadFactory; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | /** 12 | * A ThreadFactory that produces threads that include the given name. 13 | * 14 | * @author shfeng 15 | */ 16 | public class NamedThreadFactory implements ThreadFactory { 17 | private final String _namePrefix; 18 | private AtomicInteger _threadCount = new AtomicInteger(0); 19 | 20 | public NamedThreadFactory(String namePrefix) { 21 | _namePrefix = namePrefix; 22 | } 23 | 24 | @Override 25 | public Thread newThread(Runnable runnable) { 26 | return new Thread(runnable, String.format("%s-%d", _namePrefix, _threadCount.getAndIncrement())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/services/CACertificateService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.services; 7 | 8 | import com.linkedin.mitm.model.CertificateAuthority; 9 | import com.linkedin.mitm.model.CertificateValidPeriod; 10 | import java.io.IOException; 11 | import java.math.BigInteger; 12 | import java.security.InvalidKeyException; 13 | import java.security.KeyStore; 14 | import java.security.KeyStoreException; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.security.NoSuchProviderException; 17 | import java.security.PrivateKey; 18 | import java.security.PublicKey; 19 | import java.security.SignatureException; 20 | import java.security.UnrecoverableKeyException; 21 | import java.security.cert.Certificate; 22 | import java.security.cert.CertificateException; 23 | import java.security.cert.X509Certificate; 24 | import java.util.List; 25 | import org.bouncycastle.asn1.ASN1Encodable; 26 | import org.bouncycastle.asn1.ASN1EncodableVector; 27 | import org.bouncycastle.asn1.DERSequence; 28 | import org.bouncycastle.asn1.x500.X500Name; 29 | import org.bouncycastle.asn1.x509.BasicConstraints; 30 | import org.bouncycastle.asn1.x509.Extension; 31 | import org.bouncycastle.asn1.x509.KeyPurposeId; 32 | import org.bouncycastle.asn1.x509.KeyUsage; 33 | import org.bouncycastle.cert.X509v3CertificateBuilder; 34 | import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; 35 | import org.bouncycastle.operator.OperatorCreationException; 36 | 37 | 38 | /** 39 | * CA certificate service that would be used to generate CA certificate, update key store entry etc. 40 | * ref:https://access.redhat.com/documentation/en-US/Red_Hat_Certificate_System/8.0/html/Admin_Guide/Standard_X.509_v3_Certificate_Extensions.html 41 | * @author shfeng 42 | */ 43 | public class CACertificateService extends AbstractX509CertificateService implements CertificateService { 44 | 45 | /** 46 | * The Key Usage extension defines the purpose of the key contained in the certificate 47 | * */ 48 | private static final KeyUsage KEY_USAGE = new KeyUsage( 49 | KeyUsage.keyCertSign | KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment 50 | | KeyUsage.cRLSign); 51 | 52 | //The Extended Key Usage extension indicates the purposes for which the certified public key may be used. 53 | private static final ASN1Encodable EXTERNAL_KEY_USAGE; 54 | 55 | private static final BasicConstraints BASIC_CONSTRAINTS = new BasicConstraints(true); 56 | 57 | static { 58 | ASN1EncodableVector purposes = new ASN1EncodableVector(); 59 | purposes.add(KeyPurposeId.id_kp_serverAuth); 60 | purposes.add(KeyPurposeId.id_kp_clientAuth); 61 | purposes.add(KeyPurposeId.anyExtendedKeyUsage); 62 | EXTERNAL_KEY_USAGE = new DERSequence(purposes); 63 | } 64 | 65 | public CACertificateService(CertificateAuthority certificateAuthority, CertificateValidPeriod certificateValidPeriod) 66 | throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { 67 | super(certificateAuthority, certificateValidPeriod); 68 | } 69 | 70 | @Override 71 | public X509Certificate createSignedCertificate(PublicKey publicKey, PrivateKey privateKey, String commonName, 72 | List sans) 73 | throws CertificateException, IOException, OperatorCreationException, NoSuchProviderException, 74 | NoSuchAlgorithmException, InvalidKeyException, SignatureException { 75 | BigInteger serial = getSerial(); 76 | X500Name subject = getSubject(commonName); 77 | X500Name issuer = subject; 78 | 79 | X509v3CertificateBuilder x509v3CertificateBuilder = 80 | new JcaX509v3CertificateBuilder(issuer, serial, getValidDateFrom(), getValidDateTo(), subject, publicKey); 81 | buildExtensions(x509v3CertificateBuilder, publicKey); 82 | return createCertificate(privateKey, x509v3CertificateBuilder); 83 | } 84 | 85 | protected void buildExtensions(X509v3CertificateBuilder x509v3CertificateBuilder, PublicKey publicKey) 86 | throws IOException { 87 | 88 | x509v3CertificateBuilder.addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(publicKey)); 89 | 90 | // The Key Usage, Extended Key Usage, and Basic Constraints extensions act together to define the purposes for 91 | // which the certificate is intended to be used 92 | x509v3CertificateBuilder.addExtension(Extension.basicConstraints, true, BASIC_CONSTRAINTS); 93 | 94 | x509v3CertificateBuilder.addExtension(Extension.keyUsage, false, KEY_USAGE); 95 | 96 | x509v3CertificateBuilder.addExtension(Extension.extendedKeyUsage, false, EXTERNAL_KEY_USAGE); 97 | } 98 | 99 | @Override 100 | public void updateKeyStore(KeyStore keyStore, PrivateKey privateKey, Certificate identityCertificate) 101 | throws KeyStoreException { 102 | updateKeyStore(keyStore, privateKey, new Certificate[]{identityCertificate}); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/services/CertificateService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.services; 7 | 8 | import java.io.IOException; 9 | import java.security.InvalidKeyException; 10 | import java.security.KeyStore; 11 | import java.security.KeyStoreException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.NoSuchProviderException; 14 | import java.security.PrivateKey; 15 | import java.security.PublicKey; 16 | import java.security.SignatureException; 17 | import java.security.cert.Certificate; 18 | import java.security.cert.CertificateException; 19 | import java.security.cert.X509Certificate; 20 | import java.util.List; 21 | import org.bouncycastle.asn1.ASN1Encodable; 22 | import org.bouncycastle.operator.OperatorCreationException; 23 | 24 | 25 | /** 26 | * Interface of certificate service 27 | * @author shfeng 28 | */ 29 | public interface CertificateService { 30 | 31 | /** 32 | * create signed certificate using various parameters 33 | * 34 | * @param publicKey Certificate public key 35 | * @param privateKey Certificate private key 36 | * @param commonName For CA certificate, it comes from Certificate Authority For server identity common name. it's from server certificate. 37 | * @param sans a list of subject alternate name. It's not needed for generating CA certificate 38 | * @return signed certificate either by itself(CA certificate) or issuer(CA) 39 | * */ 40 | public X509Certificate createSignedCertificate(PublicKey publicKey, PrivateKey privateKey, String commonName, 41 | List sans) 42 | throws CertificateException, IOException, OperatorCreationException, NoSuchProviderException, 43 | NoSuchAlgorithmException, InvalidKeyException, SignatureException; 44 | 45 | /** 46 | * update key store entry 47 | * @param keyStore to be updated 48 | * @param privateKey private key of this identity certificate 49 | * @param identityCertificate 50 | * */ 51 | public void updateKeyStore(KeyStore keyStore, PrivateKey privateKey, Certificate identityCertificate) 52 | throws KeyStoreException; 53 | } 54 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/services/RandomNumberGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.services; 7 | 8 | import java.security.SecureRandom; 9 | 10 | 11 | /** 12 | * Singleton to generate secure random number 13 | * 14 | * @author shfeng 15 | */ 16 | public class RandomNumberGenerator { 17 | private static RandomNumberGenerator _randomNumberGenerator = new RandomNumberGenerator(); 18 | private SecureRandom _secureRandom; 19 | 20 | public static RandomNumberGenerator getInstance() { 21 | return _randomNumberGenerator; 22 | } 23 | 24 | public SecureRandom getSecureRandom() { 25 | return _secureRandom; 26 | } 27 | 28 | private RandomNumberGenerator() { 29 | _secureRandom = new SecureRandom(); 30 | _secureRandom.setSeed(System.currentTimeMillis()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/services/SSLContextGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.services; 7 | 8 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory; 9 | import java.security.KeyManagementException; 10 | import java.security.KeyStore; 11 | import java.security.KeyStoreException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.SecureRandom; 14 | import java.security.UnrecoverableKeyException; 15 | import javax.net.ssl.KeyManager; 16 | import javax.net.ssl.KeyManagerFactory; 17 | import javax.net.ssl.SSLContext; 18 | import javax.net.ssl.TrustManager; 19 | import javax.net.ssl.TrustManagerFactory; 20 | 21 | 22 | /** 23 | * Wrapper class for SSLContext generating code 24 | * @author shfeng 25 | */ 26 | public class SSLContextGenerator { 27 | private static final String SSL_CONTEXT_PROTOCOL = "TLS"; 28 | private static final String KEY_MANAGER_TYPE = "SunX509"; 29 | private static final String TRUST_MANAGER_TYPE = "SunX509"; 30 | 31 | /** 32 | * Create client side SSLContext {@link javax.net.ssl.SSLContext} 33 | * 34 | * */ 35 | public static SSLContext createClientContext(KeyStore keyStore, char[] passphrase) 36 | throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { 37 | String keyManAlg = KeyManagerFactory.getDefaultAlgorithm(); 38 | KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyManAlg); 39 | kmf.init(keyStore, passphrase); 40 | KeyManager[] keyManagers = kmf.getKeyManagers(); 41 | return create(keyManagers, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), 42 | RandomNumberGenerator.getInstance().getSecureRandom()); 43 | } 44 | 45 | /** 46 | * Create default server side SSLContext {@link javax.net.ssl.SSLContext} 47 | * 48 | * */ 49 | public static SSLContext createDefaultServerContext() 50 | throws KeyManagementException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { 51 | return create(null, null, RandomNumberGenerator.getInstance().getSecureRandom()); 52 | } 53 | 54 | private static SSLContext create(KeyManager[] keyManagers, TrustManager[] trustManagers, SecureRandom secureRandom) 55 | throws NoSuchAlgorithmException, KeyManagementException { 56 | SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL); 57 | sslContext.init(keyManagers, trustManagers, secureRandom); 58 | return sslContext; 59 | } 60 | 61 | private SSLContextGenerator() { 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/store/JKSTrustStoreReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.store; 7 | 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.security.KeyStore; 12 | import java.security.KeyStoreException; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.security.cert.CertificateException; 15 | 16 | 17 | /** 18 | * Read trust store from the file. This class will be used mainly for testing purpose. 19 | * 20 | * @author shfeng 21 | */ 22 | public class JKSTrustStoreReader implements KeyStoreReader { 23 | private static final String TRUST_STORE_TYPE = "JKS"; 24 | 25 | @Override 26 | public KeyStore load(InputStream inputStream, String password) 27 | throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { 28 | KeyStore ksTrust = KeyStore.getInstance(TRUST_STORE_TYPE); 29 | try { 30 | ksTrust.load(inputStream, password.toCharArray()); 31 | } finally { 32 | inputStream.close(); 33 | } 34 | return ksTrust; 35 | } 36 | 37 | @Override 38 | public KeyStore load(String path, String password) 39 | throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { 40 | return load(new FileInputStream(path), password); 41 | } 42 | } -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/store/KeyStoreReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.store; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.security.KeyStore; 11 | import java.security.KeyStoreException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.cert.CertificateException; 14 | 15 | 16 | /** 17 | * Interface of Key store reader 18 | * @author shfeng 19 | */ 20 | public interface KeyStoreReader { 21 | KeyStore load(InputStream inputStream, String password) 22 | throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException; 23 | 24 | KeyStore load(String path, String password) 25 | throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException; 26 | } 27 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/store/KeyStoreWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.store; 7 | 8 | import java.io.IOException; 9 | import java.security.KeyStore; 10 | import java.security.KeyStoreException; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.security.cert.CertificateException; 13 | 14 | 15 | /** 16 | * Interface of Key store writer 17 | * @author shfeng 18 | */ 19 | public interface KeyStoreWriter { 20 | void store(String filePath, KeyStore keyStore, String passphrase) 21 | throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException; 22 | } 23 | -------------------------------------------------------------------------------- /mitm/src/main/java/com/linkedin/mitm/store/PKC12KeyStoreReadWriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | package com.linkedin.mitm.store; 7 | 8 | import java.io.FileInputStream; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.security.KeyStore; 14 | import java.security.KeyStoreException; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.security.cert.CertificateException; 17 | 18 | 19 | /** 20 | * @author shfeng 21 | */ 22 | public class PKC12KeyStoreReadWriter implements KeyStoreReader, KeyStoreWriter { 23 | /** 24 | * The P12 format has to be implemented by every vendor. Oracles proprietary 25 | * JKS type is not available in Android. 26 | */ 27 | private static final String KEY_STORE_TYPE = "PKCS12"; 28 | 29 | @Override 30 | public KeyStore load(InputStream inputstream, String password) 31 | throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { 32 | KeyStore ksKeys = KeyStore.getInstance(KEY_STORE_TYPE); 33 | try { 34 | ksKeys.load(inputstream, password.toCharArray()); 35 | } finally { 36 | inputstream.close(); 37 | } 38 | return ksKeys; 39 | } 40 | 41 | @Override 42 | public KeyStore load(String path, String password) 43 | throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException { 44 | return load(new FileInputStream(path), password); 45 | } 46 | 47 | @Override 48 | public void store(String filePath, KeyStore keyStore, String passphrase) 49 | throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { 50 | try (OutputStream fos = new FileOutputStream(filePath)) { 51 | keyStore.store(fos, passphrase.toCharArray()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'flashback-core-impl', 2 | 'flashback-netty', 3 | 'flashback-smartproxy', 4 | 'flashback-test-util', 5 | 'mitm', 6 | 'flashback-admin', 7 | 'flashback-all' 8 | -------------------------------------------------------------------------------- /startAdminServer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./gradlew startAdminServer "-PArgs=$*" 4 | --------------------------------------------------------------------------------