Repository: misakuo/Dream-Catcher Branch: master Commit: 7782266d8b77 Files: 289 Total size: 1.0 MB Directory structure: gitextract_4zj9hsfj/ ├── .gitignore ├── .idea/ │ ├── compiler.xml │ ├── copyright/ │ │ └── profiles_settings.xml │ ├── encodings.xml │ ├── gradle.xml │ ├── misc.xml │ ├── modules.xml │ └── runConfigurations.xml ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── dream_catcher-1.0.1.apk │ ├── dream_catcher-1.1.0.apk │ ├── dream_catcher-1.2.0.apk │ ├── libs/ │ │ └── netty-android.jar │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── me/ │ │ └── moxun/ │ │ └── dreamcatcher/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ ├── me/ │ │ │ │ └── moxun/ │ │ │ │ └── dreamcatcher/ │ │ │ │ ├── CaptureActivity.java │ │ │ │ ├── DCApplication.java │ │ │ │ ├── State.java │ │ │ │ ├── connector/ │ │ │ │ │ ├── Connector.java │ │ │ │ │ ├── InspectorModulesProvider.java │ │ │ │ │ ├── console/ │ │ │ │ │ │ ├── CLog.java │ │ │ │ │ │ ├── ConsolePeerManager.java │ │ │ │ │ │ ├── RuntimeRepl.java │ │ │ │ │ │ ├── RuntimeReplFactory.java │ │ │ │ │ │ └── command/ │ │ │ │ │ │ └── CommandHandler.java │ │ │ │ │ ├── inspector/ │ │ │ │ │ │ ├── ChromeDevtoolsServer.java │ │ │ │ │ │ ├── ChromeDiscoveryHandler.java │ │ │ │ │ │ ├── DevtoolsSocketHandler.java │ │ │ │ │ │ ├── MessageHandlingException.java │ │ │ │ │ │ ├── MethodDispatcher.java │ │ │ │ │ │ ├── MismatchedResponseException.java │ │ │ │ │ │ ├── helper/ │ │ │ │ │ │ │ ├── ChromePeerManager.java │ │ │ │ │ │ │ ├── ObjectIdMapper.java │ │ │ │ │ │ │ ├── PeerRegistrationListener.java │ │ │ │ │ │ │ ├── PeersRegisteredListener.java │ │ │ │ │ │ │ ├── ThreadBoundProxy.java │ │ │ │ │ │ │ └── TracingPeerManager.java │ │ │ │ │ │ ├── protocol/ │ │ │ │ │ │ │ ├── ChromeDevtoolsDomain.java │ │ │ │ │ │ │ ├── ChromeDevtoolsMethod.java │ │ │ │ │ │ │ ├── SimpleBooleanResult.java │ │ │ │ │ │ │ ├── SimpleIntegerResult.java │ │ │ │ │ │ │ ├── SimpleStringResult.java │ │ │ │ │ │ │ └── module/ │ │ │ │ │ │ │ ├── Console.java │ │ │ │ │ │ │ ├── FileSystem.java │ │ │ │ │ │ │ ├── Inspector.java │ │ │ │ │ │ │ ├── Network.java │ │ │ │ │ │ │ ├── Page.java │ │ │ │ │ │ │ ├── Profiler.java │ │ │ │ │ │ │ └── Runtime.java │ │ │ │ │ │ └── runtime/ │ │ │ │ │ │ └── DefaultRuntimeReplFactory.java │ │ │ │ │ ├── json/ │ │ │ │ │ │ ├── ObjectMapper.java │ │ │ │ │ │ └── annotation/ │ │ │ │ │ │ ├── JsonProperty.java │ │ │ │ │ │ └── JsonValue.java │ │ │ │ │ ├── jsonrpc/ │ │ │ │ │ │ ├── DisconnectReceiver.java │ │ │ │ │ │ ├── JsonRpcException.java │ │ │ │ │ │ ├── JsonRpcPeer.java │ │ │ │ │ │ ├── JsonRpcResult.java │ │ │ │ │ │ ├── PendingRequest.java │ │ │ │ │ │ ├── PendingRequestCallback.java │ │ │ │ │ │ └── protocol/ │ │ │ │ │ │ ├── EmptyResult.java │ │ │ │ │ │ ├── JsonRpcError.java │ │ │ │ │ │ ├── JsonRpcEvent.java │ │ │ │ │ │ ├── JsonRpcRequest.java │ │ │ │ │ │ └── JsonRpcResponse.java │ │ │ │ │ ├── log/ │ │ │ │ │ │ ├── AELog.java │ │ │ │ │ │ ├── AELogImpl.java │ │ │ │ │ │ └── IAELog.java │ │ │ │ │ ├── manager/ │ │ │ │ │ │ ├── Lifecycle.java │ │ │ │ │ │ └── SimpleConnectorLifecycleManager.java │ │ │ │ │ ├── reporter/ │ │ │ │ │ │ ├── AsyncPrettyPrinter.java │ │ │ │ │ │ ├── AsyncPrettyPrinterExecutorHolder.java │ │ │ │ │ │ ├── AsyncPrettyPrinterFactory.java │ │ │ │ │ │ ├── AsyncPrettyPrinterInitializer.java │ │ │ │ │ │ ├── AsyncPrettyPrinterRegistry.java │ │ │ │ │ │ ├── CountingOutputStream.java │ │ │ │ │ │ ├── DecompressionHelper.java │ │ │ │ │ │ ├── DefaultResponseHandler.java │ │ │ │ │ │ ├── DownloadingAsyncPrettyPrinterFactory.java │ │ │ │ │ │ ├── GunzippingOutputStream.java │ │ │ │ │ │ ├── MimeMatcher.java │ │ │ │ │ │ ├── NetworkEventReporter.java │ │ │ │ │ │ ├── NetworkEventReporterImpl.java │ │ │ │ │ │ ├── NetworkPeerManager.java │ │ │ │ │ │ ├── PrettyPrinterDisplayType.java │ │ │ │ │ │ ├── RequestBodyHelper.java │ │ │ │ │ │ ├── ResourceTypeHelper.java │ │ │ │ │ │ ├── ResponseBodyData.java │ │ │ │ │ │ ├── ResponseBodyFileManager.java │ │ │ │ │ │ ├── ResponseHandler.java │ │ │ │ │ │ └── ResponseHandlingInputStream.java │ │ │ │ │ ├── server/ │ │ │ │ │ │ ├── AddressNameHelper.java │ │ │ │ │ │ ├── CompositeInputStream.java │ │ │ │ │ │ ├── LazySocketHandler.java │ │ │ │ │ │ ├── LeakyBufferedInputStream.java │ │ │ │ │ │ ├── LocalSocketServer.java │ │ │ │ │ │ ├── PeerAuthorizationException.java │ │ │ │ │ │ ├── ProtocolDetectingSocketHandler.java │ │ │ │ │ │ ├── SecureSocketHandler.java │ │ │ │ │ │ ├── ServerManager.java │ │ │ │ │ │ ├── SocketHandler.java │ │ │ │ │ │ ├── SocketHandlerFactory.java │ │ │ │ │ │ ├── SocketLike.java │ │ │ │ │ │ ├── SocketLikeHandler.java │ │ │ │ │ │ └── http/ │ │ │ │ │ │ ├── ExactPathMatcher.java │ │ │ │ │ │ ├── HandlerRegistry.java │ │ │ │ │ │ ├── HttpHandler.java │ │ │ │ │ │ ├── HttpHeaders.java │ │ │ │ │ │ ├── HttpStatus.java │ │ │ │ │ │ ├── LightHttpBody.java │ │ │ │ │ │ ├── LightHttpMessage.java │ │ │ │ │ │ ├── LightHttpRequest.java │ │ │ │ │ │ ├── LightHttpResponse.java │ │ │ │ │ │ ├── LightHttpServer.java │ │ │ │ │ │ ├── PathMatcher.java │ │ │ │ │ │ └── RegexpPathMatcher.java │ │ │ │ │ ├── util/ │ │ │ │ │ │ ├── Accumulator.java │ │ │ │ │ │ ├── ArrayListAccumulator.java │ │ │ │ │ │ ├── ColorStringUtil.java │ │ │ │ │ │ ├── DreamCatcherCrashHandler.java │ │ │ │ │ │ ├── ExceptionUtil.java │ │ │ │ │ │ ├── IServerManager.java │ │ │ │ │ │ ├── KLog.java │ │ │ │ │ │ ├── KLogImpl.java │ │ │ │ │ │ ├── Keys.java │ │ │ │ │ │ ├── ListUtil.java │ │ │ │ │ │ ├── LogFilter.java │ │ │ │ │ │ ├── LogInterface.java │ │ │ │ │ │ ├── LogUtil.java │ │ │ │ │ │ ├── Predicate.java │ │ │ │ │ │ ├── ProcessUtil.java │ │ │ │ │ │ ├── ReflectionUtil.java │ │ │ │ │ │ ├── SocketServerManager.java │ │ │ │ │ │ ├── StringUtil.java │ │ │ │ │ │ ├── ThreadBound.java │ │ │ │ │ │ ├── UncheckedCallable.java │ │ │ │ │ │ ├── Utf8Charset.java │ │ │ │ │ │ └── Util.java │ │ │ │ │ └── websocket/ │ │ │ │ │ ├── CloseCodes.java │ │ │ │ │ ├── Frame.java │ │ │ │ │ ├── FrameHelper.java │ │ │ │ │ ├── MaskingHelper.java │ │ │ │ │ ├── ReadCallback.java │ │ │ │ │ ├── ReadHandler.java │ │ │ │ │ ├── SimpleEndpoint.java │ │ │ │ │ ├── SimpleSession.java │ │ │ │ │ ├── WebSocketHandler.java │ │ │ │ │ ├── WebSocketSession.java │ │ │ │ │ ├── WriteCallback.java │ │ │ │ │ └── WriteHandler.java │ │ │ │ ├── event/ │ │ │ │ │ ├── CaptureEvent.java │ │ │ │ │ └── OperateEvent.java │ │ │ │ ├── misc/ │ │ │ │ │ └── X509ExtendedTrustManager.java │ │ │ │ ├── service/ │ │ │ │ │ ├── ConnectorService.java │ │ │ │ │ └── ProxyService.java │ │ │ │ └── wrapper/ │ │ │ │ ├── DCHeader.java │ │ │ │ ├── DCRequest.java │ │ │ │ ├── DCResponse.java │ │ │ │ └── ProxyManager.java │ │ │ └── net/ │ │ │ └── lightbody/ │ │ │ └── bmp/ │ │ │ ├── BrowserMobProxy.java │ │ │ ├── BrowserMobProxyServer.java │ │ │ ├── client/ │ │ │ │ └── ClientUtil.java │ │ │ ├── core/ │ │ │ │ ├── har/ │ │ │ │ │ ├── Har.java │ │ │ │ │ ├── HarCache.java │ │ │ │ │ ├── HarCacheStatus.java │ │ │ │ │ ├── HarContent.java │ │ │ │ │ ├── HarCookie.java │ │ │ │ │ ├── HarEntry.java │ │ │ │ │ ├── HarLog.java │ │ │ │ │ ├── HarNameValuePair.java │ │ │ │ │ ├── HarNameVersion.java │ │ │ │ │ ├── HarPage.java │ │ │ │ │ ├── HarPageTimings.java │ │ │ │ │ ├── HarPostData.java │ │ │ │ │ ├── HarPostDataParam.java │ │ │ │ │ ├── HarRequest.java │ │ │ │ │ ├── HarResponse.java │ │ │ │ │ └── HarTimings.java │ │ │ │ └── json/ │ │ │ │ ├── ISO8601DateFormatter.java │ │ │ │ └── ISO8601WithTDZDateFormatter.java │ │ │ ├── exception/ │ │ │ │ ├── DecompressionException.java │ │ │ │ └── UnsupportedCharsetException.java │ │ │ ├── filters/ │ │ │ │ ├── AddHeadersFilter.java │ │ │ │ ├── AutoBasicAuthFilter.java │ │ │ │ ├── BlacklistFilter.java │ │ │ │ ├── BrowserMobHttpFilterChain.java │ │ │ │ ├── ClientRequestCaptureFilter.java │ │ │ │ ├── HarCaptureFilter.java │ │ │ │ ├── HttpConnectHarCaptureFilter.java │ │ │ │ ├── HttpsAwareFiltersAdapter.java │ │ │ │ ├── HttpsHostCaptureFilter.java │ │ │ │ ├── HttpsOriginalHostCaptureFilter.java │ │ │ │ ├── LatencyFilter.java │ │ │ │ ├── ModifiedRequestAwareFilter.java │ │ │ │ ├── RegisterRequestFilter.java │ │ │ │ ├── RequestFilter.java │ │ │ │ ├── RequestFilterAdapter.java │ │ │ │ ├── ResolvedHostnameCacheFilter.java │ │ │ │ ├── ResponseFilter.java │ │ │ │ ├── ResponseFilterAdapter.java │ │ │ │ ├── RewriteUrlFilter.java │ │ │ │ ├── ServerResponseCaptureFilter.java │ │ │ │ ├── UnregisterRequestFilter.java │ │ │ │ ├── WhitelistFilter.java │ │ │ │ ├── support/ │ │ │ │ │ └── HttpConnectTiming.java │ │ │ │ └── util/ │ │ │ │ └── HarCaptureUtil.java │ │ │ ├── mitm/ │ │ │ │ ├── CertificateAndKey.java │ │ │ │ ├── CertificateAndKeySource.java │ │ │ │ ├── CertificateInfo.java │ │ │ │ ├── CertificateInfoGenerator.java │ │ │ │ ├── ExistingCertificateSource.java │ │ │ │ ├── HostnameCertificateInfoGenerator.java │ │ │ │ ├── KeyStoreCertificateSource.java │ │ │ │ ├── KeyStoreFileCertificateSource.java │ │ │ │ ├── PemFileCertificateSource.java │ │ │ │ ├── RootCertificateGenerator.java │ │ │ │ ├── TrustSource.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── CertificateCreationException.java │ │ │ │ │ ├── CertificateSourceException.java │ │ │ │ │ ├── ExportException.java │ │ │ │ │ ├── ImportException.java │ │ │ │ │ ├── KeyGeneratorException.java │ │ │ │ │ ├── KeyStoreAccessException.java │ │ │ │ │ ├── MitmException.java │ │ │ │ │ ├── SslContextInitializationException.java │ │ │ │ │ ├── TrustSourceException.java │ │ │ │ │ └── UncheckedIOException.java │ │ │ │ ├── keys/ │ │ │ │ │ ├── ECKeyGenerator.java │ │ │ │ │ ├── KeyGenerator.java │ │ │ │ │ └── RSAKeyGenerator.java │ │ │ │ ├── manager/ │ │ │ │ │ └── ImpersonatingMitmManager.java │ │ │ │ ├── stats/ │ │ │ │ │ └── CertificateGenerationStatistics.java │ │ │ │ ├── tools/ │ │ │ │ │ ├── BouncyCastleSecurityProviderTool.java │ │ │ │ │ ├── DefaultSecurityProviderTool.java │ │ │ │ │ └── SecurityProviderTool.java │ │ │ │ ├── trustmanager/ │ │ │ │ │ ├── InsecureExtendedTrustManager.java │ │ │ │ │ └── InsecureTrustManagerFactory.java │ │ │ │ └── util/ │ │ │ │ ├── EncryptionUtil.java │ │ │ │ ├── KeyStoreUtil.java │ │ │ │ ├── MitmConstants.java │ │ │ │ ├── SslUtil.java │ │ │ │ └── TrustUtil.java │ │ │ ├── proxy/ │ │ │ │ ├── ActivityMonitor.java │ │ │ │ ├── BlacklistEntry.java │ │ │ │ ├── CaptureType.java │ │ │ │ ├── RewriteRule.java │ │ │ │ ├── Whitelist.java │ │ │ │ ├── auth/ │ │ │ │ │ └── AuthType.java │ │ │ │ └── dns/ │ │ │ │ ├── AbstractHostNameRemapper.java │ │ │ │ ├── AdvancedHostResolver.java │ │ │ │ ├── BasicHostResolver.java │ │ │ │ ├── ChainedHostResolver.java │ │ │ │ ├── DelegatingHostResolver.java │ │ │ │ ├── DnsJavaResolver.java │ │ │ │ ├── HostResolver.java │ │ │ │ ├── NativeCacheManipulatingResolver.java │ │ │ │ └── NativeResolver.java │ │ │ └── util/ │ │ │ ├── BrowserMobHttpUtil.java │ │ │ ├── BrowserMobProxyUtil.java │ │ │ ├── ClasspathResourceUtil.java │ │ │ ├── HttpMessageContents.java │ │ │ ├── HttpMessageInfo.java │ │ │ ├── HttpObjectUtil.java │ │ │ └── HttpUtil.java │ │ ├── res/ │ │ │ ├── layout/ │ │ │ │ └── activity_capture.xml │ │ │ ├── menu/ │ │ │ │ └── menu_main.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── values-w820dp/ │ │ │ └── dimens.xml │ │ └── resources/ │ │ ├── cacerts.pem │ │ ├── default-ciphers.txt │ │ ├── net/ │ │ │ └── lightbody/ │ │ │ └── bmp/ │ │ │ └── version │ │ └── sslSupport/ │ │ ├── ca-certificate-ec.cer │ │ ├── ca-certificate-rsa.cer │ │ ├── ca-keystore-ec.p12 │ │ └── ca-keystore-rsa.p12 │ └── test/ │ └── java/ │ └── me/ │ └── moxun/ │ └── dreamcatcher/ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures .externalNativeBuild ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/copyright/profiles_settings.xml ================================================ ================================================ FILE: .idea/encodings.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ 1.8 ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 moxun 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: README.md ================================================ # Dream-Catcher [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Dream%20Catcher-green.svg?style=flat)](http://android-arsenal.com/details/1/4834) Inspection the Android HTTP(S) traffic in Chrome Developer Tools ## Introduction Dream Catcher is a HTTP proxy based traffic analysis tools, provides the ability to view http(s) traffic in chrome by adapting the [Chrome Remote Debug Protocol](https://chromedevtools.github.io/debugger-protocol-viewer/). ## Usage - Install and launch the [Dream Catcher APP](https://github.com/misakuo/Dream-Catcher/releases) - Connect your device to PC, make sure the USB debugging is active and the `adb` is usable - Click to enable the HTTP proxy (It's maybe need waiting a bit time) - Click **Install Certificate** to install CA for enable [MITM](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) (Dream Catcher will not do evil, it just be using to decryption the HTTPS traffic. If do not need HTTPS inspection, you can skip this step) - Click **Trusted Credentials** to examine the MITM CA or remove it - Click **Setting Proxy** to setup proxy on active connection (General steps: 1. long click the active connection; 2. select *Modify Network* on dialog; 3. select *Manual* on the *Proxy* options; 4. input `127.0.0.1` to the *Proxy hostname* textbox; 5. input proxy port (default is `9999`) to the *Proxy port* textbox; 6. click the *SAVE* button; 7. In some cases you may need to turn off and then turn on WiFi) - Open Chrome and navigate to **`chrome://inspect`** - Click **inspect** when Dream Catcher is ready - Select the **Network** tab to start inspection ## Example ### The homepage ### Enable proxy ### Let's trying to visit Google Play **Got it!** ![QQ20161213-0.png](https://ooo.0o0.ooo/2016/12/13/584f7a42daf42.png) **The headers** ![QQ20161213-1.png](https://ooo.0o0.ooo/2016/12/13/584f7a42ac344.png) ### Preview response **Image** ![QQ20161213-2.png](https://ooo.0o0.ooo/2016/12/13/584f7a422daf8.png) **JSON** ![QQ20161213-3.png](https://ooo.0o0.ooo/2016/12/13/584f7a4230aba.png) ## Download | Version | Github source | CDN source| | :-----: |:------------:|:-----:| | 1.2.0![new2_e0.gif](https://ooo.0o0.ooo/2016/12/16/58535628362c0.gif) | [Github](https://github.com/misakuo/Dream-Catcher/releases/download/release-1.2.0/dream_catcher-1.2.0.apk) | | | 1.1.0 | [Github](https://github.com/misakuo/Dream-Catcher/releases/download/release-1.0.1/dream_catcher-1.1.0.apk) | [Qiniu CDN](http://sodaless.qiniudn.com/dream_catcher-1.1.0.apk)| | ~~1.0.1~~ (Deprecated) | [Github](https://github.com/misakuo/Dream-Catcher/releases/download/release-1.0.1/dream_catcher-1.0.1.apk) | [Qiniu CDN](http://sodaless.qiniudn.com/dream_catcher-1.0.1.apk)|     ## Acknowledgments [Stetho](https://github.com/facebook/stetho) [browsermob-proxy](https://github.com/lightbody/browsermob-proxy) [android-morphing-button](https://github.com/dmytrodanylyk/android-morphing-button) ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 24 buildToolsVersion "24.0.3" defaultConfig { applicationId "me.moxun.dreamcatcher" minSdkVersion 14 targetSdkVersion 22 versionCode 4 versionName "1.2.0" multiDexEnabled true testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } configurations.all { resolutionStrategy.force 'com.google.code.findbugs:jsr305:2.0.1' } packagingOptions { exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/NOTICE' exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE.txt' exclude 'META-INF/BCKEY.DSA' exclude 'META-INF/BCKEY.SF' exclude 'META-INF/LICENSE.uas.txt' } useLibrary 'org.apache.http.legacy' repositories { mavenLocal() jcenter() maven { url "https://jitpack.io" } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:24.2.1' compile 'com.android.support:design:24.2.1' compile 'com.android.support:support-annotations:24.2.1' compile 'com.android.support:multidex:1.0.1' compile 'org.greenrobot:eventbus:3.0.0' compile 'com.github.dmytrodanylyk:android-morphing-button:98a4986e56' //WELCOME TO HELL compile('net.lightbody.bmp:littleproxy:1.1.0-beta-bmp-13') { exclude group: 'io.netty' } compile 'net.sf.qualitycheck:quality-check:1.3' compile 'javax.annotation:jsr250-api:1.0' compile 'com.fasterxml.jackson.core:jackson-core:2.7.6' compile 'com.fasterxml.jackson.core:jackson-databind:2.7.6' compile 'com.fasterxml.jackson.core:jackson-annotations:2.7.6' compile 'org.bouncycastle:bcprov-jdk15on:1.54' compile 'org.bouncycastle:bcpkix-jdk15on:1.54' compile 'dnsjava:dnsjava:2.1.7' compile 'com.google.guava:guava:19.0' compile 'com.google.code.findbugs:jsr305:3.0.1' compile 'com.google.jimfs:jimfs:1.1' compile 'com.jcraft:jzlib:1.1.3' compile 'org.slf4j:slf4j-api:1.7.21' compile 'com.noveogroup.android:android-logger:1.3.5' testCompile 'junit:junit:4.12' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/moxun/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/androidTest/java/me/moxun/dreamcatcher/ExampleInstrumentedTest.java ================================================ package me.moxun.dreamcatcher; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("me.moxun.dreamcatcher", appContext.getPackageName()); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/CaptureActivity.java ================================================ package me.moxun.dreamcatcher; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.provider.Settings; import android.security.KeyChain; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.view.Window; import android.widget.TextView; import com.dd.morphingbutton.MorphingButton; import com.dd.morphingbutton.impl.IndeterminateProgressButton; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.io.InputStream; import me.moxun.dreamcatcher.event.OperateEvent; import me.moxun.dreamcatcher.service.ProxyService; public class CaptureActivity extends AppCompatActivity { final String CA_RESOURCE = "/sslSupport/ca-certificate-rsa.cer"; final int INSTALL_CA_REQUEST_CODE = 0x99; private int size = 112; private int defaultDuration = 500; private int defaultDelay = 3000; private IndeterminateProgressButton controlButton; private TextView status; private int state = State.IDLE; private long timestamp = 0L; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_capture); EventBus.getDefault().register(this); controlButton = (IndeterminateProgressButton) findViewById(R.id.controller); status = (TextView) findViewById(R.id.status); morphToIdle(controlButton, 0); controlButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { switch (state) { case State.IDLE: case State.FAILURE: timestamp = System.currentTimeMillis(); performProgress(controlButton); Intent intent = new Intent(CaptureActivity.this, ProxyService.class); startService(intent); break; case State.RUNNING: timestamp = System.currentTimeMillis(); performProgress(controlButton); stopService(new Intent(CaptureActivity.this, ProxyService.class)); break; default: break; } } }); findViewById(R.id.install_ca).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { byte[] keychainBytes; InputStream bis = CaptureActivity.class.getResourceAsStream(CA_RESOURCE); keychainBytes = new byte[bis.available()]; bis.read(keychainBytes); Intent intent = KeyChain.createInstallIntent(); intent.putExtra(KeyChain.EXTRA_CERTIFICATE, keychainBytes); intent.putExtra(KeyChain.EXTRA_NAME, "DreamCatcher CA Certificate"); startActivityForResult(intent, INSTALL_CA_REQUEST_CODE); } catch (Exception e) { e.printStackTrace(); } } }); findViewById(R.id.trusted_ca).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent("com.android.settings.TRUSTED_CREDENTIALS_USER"); intent.setFlags(0x14000000); startActivity(intent); } }); findViewById(R.id.setting_proxy).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); } }); } private void performProgress(@NonNull final IndeterminateProgressButton button) { status.setText("This maybe a bit slow, please keep patience ……"); int progressColor1 = Color.parseColor("#ff00ddff"); int progressColor2 = Color.parseColor("#ff99cc00"); int progressColor3 = Color.parseColor("#ffffbb33"); int progressColor4 = Color.parseColor("#ffff4444"); int color = Color.parseColor("#ffdedede"); int progressCornerRadius = $px(4); int width = $px(200); int height = $px(8); button.blockTouch(); button.morphToProgress(color, progressCornerRadius, width, height, defaultDuration, progressColor1, progressColor2, progressColor3, progressColor4); } private int $px(float dpValue) { final float scale = getResources().getDisplayMetrics().density; return (int) (dpValue * scale + 0.5f); } private void morphToSuccess(final IndeterminateProgressButton btnMorph, int duration) { status.setText("Proxy on 127.0.0.1:" + ((DCApplication) getApplication()).getPort()); btnMorph.unblockTouch(); MorphingButton.Params circle = MorphingButton.Params.create() .duration(duration) .cornerRadius($px(size)) .width($px(size)) .height($px(size)) .color(Color.parseColor("#ff99cc00")) .colorPressed(Color.parseColor("#ff6d9b00")) .icon(R.mipmap.ic_check); btnMorph.morph(circle); } private void morphToFailure(final IndeterminateProgressButton btnMorph, int duration) { btnMorph.unblockTouch(); MorphingButton.Params circle = MorphingButton.Params.create() .duration(duration) .cornerRadius($px(size)) .width($px(size)) .height($px(size)) .color(Color.parseColor("#ffff4444")) .colorPressed(Color.parseColor("#ffcd3a3a")) .icon(R.mipmap.ic_closed); btnMorph.morph(circle); } private void morphToIdle(final IndeterminateProgressButton btnMorph, int duration) { btnMorph.unblockTouch(); MorphingButton.Params circle = MorphingButton.Params.create() .duration(duration) .cornerRadius($px(size)) .width($px(size)) .height($px(size)) .color(Color.parseColor("#ff0099cc")) .colorPressed(Color.parseColor("#ff00719b")) .icon(R.mipmap.ic_start); btnMorph.morph(circle); } @Subscribe(threadMode = ThreadMode.MAIN) public void onEvent(final OperateEvent event) { Log.e("Event", event.toString()); if (event.error) { if ((System.currentTimeMillis() - timestamp) < defaultDelay) { getWindow().getDecorView().postDelayed(new Runnable() { @Override public void run() { morphToFailure(controlButton, defaultDuration); state = State.FAILURE; status.setText(event.msg); } }, defaultDelay - (System.currentTimeMillis() - timestamp)); } else { morphToFailure(controlButton, defaultDuration); state = State.FAILURE; status.setText(event.msg); } return; } if (event.target == OperateEvent.TARGET_CONNECTOR) { } else if (event.target == OperateEvent.TARGET_PROXY) { if (event.active) { if ((System.currentTimeMillis() - timestamp) < defaultDelay) { getWindow().getDecorView().postDelayed(new Runnable() { @Override public void run() { morphToSuccess(controlButton, defaultDuration); state = State.RUNNING; } }, defaultDelay - (System.currentTimeMillis() - timestamp)); } else { morphToSuccess(controlButton, defaultDuration); state = State.RUNNING; } } else { if ((System.currentTimeMillis() - timestamp) < defaultDelay) { getWindow().getDecorView().postDelayed(new Runnable() { @Override public void run() { morphToIdle(controlButton, defaultDuration); state = State.IDLE; status.setText("Click to start capture"); } }, defaultDelay - (System.currentTimeMillis() - timestamp)); } else { morphToIdle(controlButton, defaultDuration); state = State.IDLE; status.setText("Click to start capture"); } } } } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/DCApplication.java ================================================ package me.moxun.dreamcatcher; import android.content.Intent; import android.support.multidex.MultiDexApplication; import me.moxun.dreamcatcher.service.ConnectorService; /** * Created by moxun on 6/12/7. */ public class DCApplication extends MultiDexApplication { //forgive me... private int port = 0; @Override public void onCreate() { super.onCreate(); startService(new Intent(this, ConnectorService.class)); } @Override public void onTerminate() { super.onTerminate(); stopService(new Intent(this, ConnectorService.class)); } public int getPort() { return port; } public void setPort(int port) { this.port = port; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/State.java ================================================ package me.moxun.dreamcatcher; /** * Created by moxun on 16/12/12. */ public interface State { int IDLE = 0; int RUNNING = 1; int FAILURE = 2; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/Connector.java ================================================ package me.moxun.dreamcatcher.connector; import android.app.Application; import android.content.Context; import android.content.pm.ApplicationInfo; import android.support.annotation.Nullable; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import me.moxun.dreamcatcher.connector.inspector.DevtoolsSocketHandler; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.module.FileSystem; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Network; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Profiler; import me.moxun.dreamcatcher.connector.log.AELog; import me.moxun.dreamcatcher.connector.manager.Lifecycle; import me.moxun.dreamcatcher.connector.manager.SimpleConnectorLifecycleManager; import me.moxun.dreamcatcher.connector.server.AddressNameHelper; import me.moxun.dreamcatcher.connector.server.LazySocketHandler; import me.moxun.dreamcatcher.connector.server.LocalSocketServer; import me.moxun.dreamcatcher.connector.server.ProtocolDetectingSocketHandler; import me.moxun.dreamcatcher.connector.server.ServerManager; import me.moxun.dreamcatcher.connector.server.SocketHandler; import me.moxun.dreamcatcher.connector.server.SocketHandlerFactory; import me.moxun.dreamcatcher.connector.util.DreamCatcherCrashHandler; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.SocketServerManager; /** * Created by moxun on 16/12/8. */ public class Connector { private static final String DEV_TOOLS_MAGIC_TAG = "_devtools_remote"; private Connector() { } private static void initialize(final Initializer initializer) { loadInnerModule(initializer.mContext); initializer.start(); } private static void loadInnerModule(Context context) { AELog.setLoggable(isDebuggable(context)); setInternalLogEnabled(isDebuggable(context)); if (!isDebuggable(context)) { AELog.setLoggable(false); setInternalLogEnabled(false); } } public static void open(final Context context) { initialize(new Initializer(context) { @Override protected Iterable getInspectorModules() { return new DefaultInspectorModulesBuilder(context).finish(); } }); } public static void close() { SocketServerManager.stopServer(); SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.SHUTDOWN); } private static class PluginBuilder { private final Set mProvidedNames = new HashSet<>(); private final Set mRemovedNames = new HashSet<>(); private final ArrayList mPlugins = new ArrayList<>(); private boolean mFinished; public void provide(String name, T plugin) { throwIfFinished(); mPlugins.add(plugin); mProvidedNames.add(name); } public void provideIfDesired(String name, T plugin) { throwIfFinished(); if (!mRemovedNames.contains(name)) { if (mProvidedNames.add(name)) { mPlugins.add(plugin); } } } public void remove(String pluginName) { throwIfFinished(); mRemovedNames.remove(pluginName); } private void throwIfFinished() { if (mFinished) { throw new IllegalStateException("Must not continue to build after finish()"); } } public Iterable finish() { mFinished = true; return mPlugins; } } private static final class DefaultInspectorModulesBuilder { private final Application mContext; private final PluginBuilder mDelegate = new PluginBuilder<>(); public DefaultInspectorModulesBuilder(Context context) { mContext = (Application) context.getApplicationContext(); } private DefaultInspectorModulesBuilder provideIfDesired(ChromeDevtoolsDomain module) { mDelegate.provideIfDesired(module.getClass().getName(), module); return this; } public Iterable finish() { provideIfDesired(new Network(mContext)); provideIfDesired(new FileSystem(mContext)); provideIfDesired(new Profiler()); return mDelegate.finish(); } } private static abstract class Initializer { private final Context mContext; protected Initializer(Context context) { mContext = context.getApplicationContext(); } @Nullable protected abstract Iterable getInspectorModules(); final void start() { DreamCatcherCrashHandler.getInstance().attach(); //create server to handle request. initServerManager(); SocketServerManager.startServer(SocketServerManager.Type.LOCAL); } private void initServerManager() { LocalSocketServer localSocketServer = new LocalSocketServer( "main", AddressNameHelper.createCustomAddress(DEV_TOOLS_MAGIC_TAG), new LazySocketHandler(new RealSocketHandlerFactory())); ServerManager serverManager = new ServerManager(localSocketServer); SocketServerManager.register(SocketServerManager.KEY_LOCAL_SERVER_MANAGER, serverManager); } private class RealSocketHandlerFactory implements SocketHandlerFactory { @Override public SocketHandler create() { ProtocolDetectingSocketHandler socketHandler = new ProtocolDetectingSocketHandler(mContext); //Create Http server to enable inspector Iterable inspectorModules = getInspectorModules(); if (inspectorModules != null) { socketHandler.addHandler( new ProtocolDetectingSocketHandler.AlwaysMatchMatcher(), new DevtoolsSocketHandler(mContext, inspectorModules)); } return socketHandler; } } } private static boolean isDebuggable(Context context) { ApplicationInfo info = context.getApplicationInfo(); return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; } private static class InitializerBuilder { final Context mContext; @javax.annotation.Nullable InspectorModulesProvider mInspectorModules; private InitializerBuilder(Context context) { mContext = context.getApplicationContext(); } public InitializerBuilder enableInspector(InspectorModulesProvider modules) { mInspectorModules = modules; return this; } public Initializer build() { return new BuilderBasedInitializer(this); } } private static class BuilderBasedInitializer extends Initializer { @javax.annotation.Nullable private final InspectorModulesProvider mInspectorModules; private BuilderBasedInitializer(InitializerBuilder b) { super(b.mContext); mInspectorModules = b.mInspectorModules; } @javax.annotation.Nullable @Override protected Iterable getInspectorModules() { return mInspectorModules != null ? mInspectorModules.get() : null; } } //EXTRA SETTINGS public static void setInternalLogEnabled(boolean enabled) { LogUtil.setLoggable(enabled); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/InspectorModulesProvider.java ================================================ package me.moxun.dreamcatcher.connector; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; public interface InspectorModulesProvider { Iterable get(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/console/CLog.java ================================================ package me.moxun.dreamcatcher.connector.console; import me.moxun.dreamcatcher.connector.inspector.helper.ChromePeerManager; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Console; /** * Utility for reporting an event to the console * WARN:请勿在此类或其子类中以任何方式输出LogCat */ public class CLog { private static final String TAG = "CLog"; public static void writeToConsole( ChromePeerManager chromePeerManager, Console.MessageLevel logLevel, Console.MessageSource messageSource, String messageText) { Console.ConsoleMessage message = new Console.ConsoleMessage(); message.source = messageSource; message.level = logLevel; message.text = messageText; message.type = Console.MessageType.LOG; Console.MessageAddedRequest messageAddedRequest = new Console.MessageAddedRequest(); messageAddedRequest.message = message; chromePeerManager.sendNotificationToPeers("Console.messageAdded", messageAddedRequest); } public static void writeToConsole( Console.MessageLevel logLevel, Console.MessageSource messageSource, String messageText ) { ConsolePeerManager peerManager = ConsolePeerManager.getInstanceOrNull(); if (peerManager == null) { return; } writeToConsole(peerManager, logLevel, messageSource, messageText); } public static void log(String note) { writeToConsole(Console.MessageLevel.LOG, Console.MessageSource.CONSOLE_API, note); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/console/ConsolePeerManager.java ================================================ package me.moxun.dreamcatcher.connector.console; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.inspector.helper.ChromePeerManager; public class ConsolePeerManager extends ChromePeerManager { private static ConsolePeerManager sInstance; private ConsolePeerManager() { super(); } @Nullable public static synchronized ConsolePeerManager getInstanceOrNull() { return sInstance; } public static synchronized ConsolePeerManager getOrCreateInstance() { if (sInstance == null) { sInstance = new ConsolePeerManager(); } return sInstance; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/console/RuntimeRepl.java ================================================ package me.moxun.dreamcatcher.connector.console; public interface RuntimeRepl { Object evaluate(String expression) throws Throwable; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/console/RuntimeReplFactory.java ================================================ package me.moxun.dreamcatcher.connector.console; /** * Allows callers to specify their own Console tab REPL for the DevTools UI. This is part of * early support for a possible optionally included default implementation for Android. *

* A new {@link RuntimeRepl} instances is created for each unique peer such that memory * can be garbage collected when the peer disconnects. *

* This is provided as part of an experimental API. Depend on it at your own risk... */ public interface RuntimeReplFactory { RuntimeRepl newInstance(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/console/command/CommandHandler.java ================================================ package me.moxun.dreamcatcher.connector.console.command; import android.support.annotation.Nullable; /** * Created by moxun on 16/4/12. */ public interface CommandHandler { String desire(); String help(); @Nullable Object onCommand(@Nullable String param); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/ChromeDevtoolsServer.java ================================================ package me.moxun.dreamcatcher.connector.inspector; import android.util.Log; import org.greenrobot.eventbus.EventBus; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.json.ObjectMapper; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcException; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.PendingRequest; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcError; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcRequest; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcResponse; import me.moxun.dreamcatcher.connector.manager.Lifecycle; import me.moxun.dreamcatcher.connector.manager.SimpleConnectorLifecycleManager; import me.moxun.dreamcatcher.connector.util.LogFilter; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.Util; import me.moxun.dreamcatcher.connector.websocket.CloseCodes; import me.moxun.dreamcatcher.connector.websocket.SimpleEndpoint; import me.moxun.dreamcatcher.connector.websocket.SimpleSession; import me.moxun.dreamcatcher.event.CaptureEvent; import me.moxun.dreamcatcher.event.OperateEvent; /** * Implements a limited version of the Chrome Debugger WebSocket protocol (using JSON-RPC 2.0). * The most up-to-date documentation can be found in the Blink source code: * protocol.json */ public class ChromeDevtoolsServer implements SimpleEndpoint { private static final String TAG = "ChromeDevtoolsServer"; public static final String PATH = "/inspector"; private final ObjectMapper mObjectMapper; private final MethodDispatcher mMethodDispatcher; private final Map mPeers = Collections.synchronizedMap( new HashMap()); public ChromeDevtoolsServer(Iterable domainModules) { mObjectMapper = new ObjectMapper(); mMethodDispatcher = new MethodDispatcher(mObjectMapper, domainModules); } @Override public void onOpen(SimpleSession session) { LogUtil.e(TAG, "Open session: " + session.toString()); mPeers.put(session, new JsonRpcPeer(mObjectMapper, session)); SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.WAITING_FOR_DISCOVERY); CaptureEvent.send("Waiting for chrome discovery"); } @Override public void onClose(SimpleSession session, int code, String reasonPhrase) { LogUtil.e(TAG, "Close session: " + session.toString() + ", cause: " + reasonPhrase + "(" + code + ")"); JsonRpcPeer peer = mPeers.remove(session); if (peer != null) { peer.invokeDisconnectReceivers(); } } @Override public void onMessage(SimpleSession session, byte[] message, int messageLen) { LogUtil.d(TAG, "Ignoring binary message of length " + messageLen); } @Override public void onMessage(SimpleSession session, String message) { if (LogUtil.isLoggable(TAG, Log.VERBOSE)) { LogUtil.v(TAG, "onMessage: message=" + message); } try { JsonRpcPeer peer = mPeers.get(session); Util.throwIfNull(peer); handleRemoteMessage(peer, message); } catch (IOException e) { if (LogUtil.isLoggable(TAG, Log.VERBOSE)) { LogUtil.v(TAG, "Unexpected I/O exception processing message: " + e); } closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } catch (MessageHandlingException e) { LogUtil.i(TAG, "Message could not be processed by implementation: " + e); closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } catch (JSONException e) { LogUtil.v(TAG, "Unexpected JSON exception processing message", e); closeSafely(session, CloseCodes.UNEXPECTED_CONDITION, e.getClass().getSimpleName()); } } private void closeSafely(SimpleSession session, int code, String reasonPhrase) { session.close(code, reasonPhrase); } private void handleRemoteMessage(JsonRpcPeer peer, String message) throws IOException, MessageHandlingException, JSONException { // Parse as a generic JSONObject first since we don't know if this is a request or response. JSONObject messageNode = new JSONObject(message); if (messageNode.has("method")) { handleRemoteRequest(peer, messageNode); } else if (messageNode.has("result")) { handleRemoteResponse(peer, messageNode); } else { throw new MessageHandlingException("Improper JSON-RPC message: " + message); } } private void handleRemoteRequest(JsonRpcPeer peer, JSONObject requestNode) throws MessageHandlingException { JsonRpcRequest request; request = mObjectMapper.convertValue( requestNode, JsonRpcRequest.class); if (!request.method.equals("Runtime.evaluate")) { //LogUtil.w(TAG, "JsonRpcRequest received: " + request.toString()); } JSONObject result = null; JSONObject error = null; try { result = mMethodDispatcher.dispatch(peer, request.method, request.params); } catch (JsonRpcException e) { logDispatchException(e); error = mObjectMapper.convertValue(e.getErrorMessage(), JSONObject.class); } if (request.id != null) { JsonRpcResponse response = new JsonRpcResponse(); response.id = request.id; response.result = result; response.error = error; JSONObject jsonObject = mObjectMapper.convertValue(response, JSONObject.class); String responseString; try { responseString = jsonObject.toString(); } catch (OutOfMemoryError e) { // JSONStringer can cause an OOM when the Json to handle is too big. response.result = null; response.error = mObjectMapper.convertValue(e.getMessage(), JSONObject.class); jsonObject = mObjectMapper.convertValue(response, JSONObject.class); responseString = jsonObject.toString(); } JSONObject log = LogFilter.filter(requestNode, jsonObject); if (log != null) { LogUtil.w(log.toString()); } peer.getWebSocket().sendText(responseString); } } private static void logDispatchException(JsonRpcException e) { JsonRpcError errorMessage = e.getErrorMessage(); switch (errorMessage.code) { case METHOD_NOT_FOUND: LogUtil.e(TAG, "Method not implemented: " + errorMessage.message); break; default: LogUtil.w(TAG, "Error processing remote message", e); } } private void handleRemoteResponse(JsonRpcPeer peer, JSONObject responseNode) throws MismatchedResponseException { LogUtil.w(responseNode.toString()); JsonRpcResponse response = mObjectMapper.convertValue( responseNode, JsonRpcResponse.class); PendingRequest pendingRequest = peer.getAndRemovePendingRequest(response.id); if (pendingRequest == null) { throw new MismatchedResponseException(response.id); } if (pendingRequest.callback != null) { pendingRequest.callback.onResponse(peer, response); } } @Override public void onError(SimpleSession session, Throwable ex) { LogUtil.w(TAG, "Session error: " + ex.toString()); SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.WEBSOCKET_SESSION_CLOSED); CaptureEvent.send("Websocket session closes unexpectedly"); EventBus.getDefault().post(new OperateEvent(OperateEvent.TARGET_CONNECTOR, false, true, "Websocket session closed")); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/ChromeDiscoveryHandler.java ================================================ package me.moxun.dreamcatcher.connector.inspector; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.manager.Lifecycle; import me.moxun.dreamcatcher.connector.manager.SimpleConnectorLifecycleManager; import me.moxun.dreamcatcher.connector.server.SocketLike; import me.moxun.dreamcatcher.connector.server.http.ExactPathMatcher; import me.moxun.dreamcatcher.connector.server.http.HandlerRegistry; import me.moxun.dreamcatcher.connector.server.http.HttpHandler; import me.moxun.dreamcatcher.connector.server.http.HttpStatus; import me.moxun.dreamcatcher.connector.server.http.LightHttpBody; import me.moxun.dreamcatcher.connector.server.http.LightHttpRequest; import me.moxun.dreamcatcher.connector.server.http.LightHttpResponse; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.ProcessUtil; import me.moxun.dreamcatcher.event.CaptureEvent; /** * Provides sufficient responses to convince Chrome's {@code chrome://inspect/devices} that we're * "one of them". Note that we are being discovered automatically by the name of our socket * as defined in {@link android.net.LocalServerSocket}. After discovery, we're required to provide * some context on how exactly to display and inspect what we have. */ public class ChromeDiscoveryHandler implements HttpHandler { private static final String PAGE_ID = "1"; private static final String PATH_PAGE_LIST = "/json"; private static final String PATH_VERSION = "/json/version"; private static final String PATH_ACTIVATE = "/json/activate/" + PAGE_ID; private static final String PATH_SERVER_GET = "/json/remote"; private static boolean INVALID = false; /** * Latest version of the WebKit Inspector UI that we've tested again (ideally). */ private static final String WEBKIT_REV = "@188492"; private static final String WEBKIT_VERSION = "537.36 (" + WEBKIT_REV + ")"; private static final String USER_AGENT = "DreamCatcher"; /** * Structured version of the WebKit Inspector protocol that we understand. */ private static final String PROTOCOL_VERSION = "1.1"; private final Context mContext; private final String mInspectorPath; @Nullable private LightHttpBody mVersionResponse; @Nullable private LightHttpBody mPageListResponse; public static void setInvalid(boolean invalid) { ChromeDiscoveryHandler.INVALID = invalid; } public ChromeDiscoveryHandler(Context context, String inspectorPath) { mContext = context; mInspectorPath = inspectorPath; SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.CHROME_DISCOVERY_CONNECTED); CaptureEvent.send("Chrome discovery success"); } public void register(HandlerRegistry registry) { registry.register(new ExactPathMatcher(PATH_PAGE_LIST), this); registry.register(new ExactPathMatcher(PATH_VERSION), this); registry.register(new ExactPathMatcher(PATH_ACTIVATE), this); registry.register(new ExactPathMatcher(PATH_SERVER_GET), this); } @Override public boolean handleRequest(SocketLike socket, LightHttpRequest request, LightHttpResponse response) { String path = request.uri.getPath(); LogUtil.d(request.toString()); try { if (PATH_VERSION.equals(path)) { handleVersion(response); } else if (PATH_PAGE_LIST.equals(path)) { handlePageList(response); } else if (PATH_ACTIVATE.equals(path)) { handleActivate(response); } else { response.code = HttpStatus.HTTP_NOT_IMPLEMENTED; response.reasonPhrase = "Not implemented"; response.body = LightHttpBody.create("No support for " + path + "\n", "text/plain"); } } catch (JSONException e) { response.code = HttpStatus.HTTP_INTERNAL_SERVER_ERROR; response.reasonPhrase = "Internal server error"; response.body = LightHttpBody.create(e.toString() + "\n", "text/plain"); } return true; } private void handleVersion(LightHttpResponse response) throws JSONException { if (mVersionResponse == null) { JSONObject reply = new JSONObject(); reply.put("WebKit-Version", WEBKIT_VERSION); reply.put("User-Agent", USER_AGENT); reply.put("Protocol-Version", PROTOCOL_VERSION); reply.put("Browser", getAppLabelAndVersion()); reply.put("Android-Package", mContext.getPackageName()); mVersionResponse = LightHttpBody.create(reply.toString(), "application/json"); LogUtil.d("Version: " + reply.toString()); } setSuccessfulResponse(response, mVersionResponse); } private void handlePageList(LightHttpResponse response) throws JSONException { SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.WAITING_FOR_WEBSOCKET); CaptureEvent.send("Waiting for websocket"); if (mPageListResponse == null || INVALID) { JSONArray reply = new JSONArray(); JSONObject page = new JSONObject(); page.put("type", "app"); page.put("title", makeTitle()); page.put("id", PAGE_ID); page.put("description", ""); page.put("webSocketDebuggerUrl", makeWSAddress()); Uri chromeFrontendUrl = new Uri.Builder() .scheme("http") .authority("chrome-devtools-frontend.appspot.com") .appendEncodedPath("serve_rev") .appendEncodedPath(WEBKIT_REV) .appendEncodedPath("devtools.html") .appendQueryParameter("ws", mInspectorPath) .build(); page.put("devtoolsFrontendUrl", chromeFrontendUrl.toString()); reply.put(page); mPageListResponse = LightHttpBody.create(reply.toString(), "application/json"); LogUtil.w("PageList: " + reply.toString()); INVALID = false; } setSuccessfulResponse(response, mPageListResponse); } private String makeWSAddress() { return "ws://" + mInspectorPath; } private String makeTitle() { StringBuilder b = new StringBuilder(); b.append(getAppLabel()); if (SimpleConnectorLifecycleManager.isProxyEnabled()) { b.append(" (Ready)"); } else { b.append(" (Preparing ……)"); } String processName = ProcessUtil.getProcessName(); int colonIndex = processName.indexOf(':'); if (colonIndex >= 0) { String nonDefaultProcessName = processName.substring(colonIndex); b.append(nonDefaultProcessName); } return b.toString(); } private void handleActivate(LightHttpResponse response) { // Arbitrary response seem acceptable :) setSuccessfulResponse( response, LightHttpBody.create("Target activation ignored\n", "text/plain")); } private static void setSuccessfulResponse( LightHttpResponse response, LightHttpBody body) { response.code = HttpStatus.HTTP_OK; response.reasonPhrase = "OK"; response.body = body; LogUtil.w(LogUtil.filter("ChromeDiscovery Response", response)); } private String getAppLabelAndVersion() { StringBuilder b = new StringBuilder(); PackageManager pm = mContext.getPackageManager(); b.append(getAppLabel()); b.append('/'); try { PackageInfo info = pm.getPackageInfo(mContext.getPackageName(), 0 /* flags */); b.append(info.versionName); } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } return b.toString(); } private CharSequence getAppLabel() { PackageManager pm = mContext.getPackageManager(); return pm.getApplicationLabel(mContext.getApplicationInfo()); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/DevtoolsSocketHandler.java ================================================ package me.moxun.dreamcatcher.connector.inspector; import android.content.Context; import java.io.IOException; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.server.SocketLike; import me.moxun.dreamcatcher.connector.server.SocketLikeHandler; import me.moxun.dreamcatcher.connector.server.http.ExactPathMatcher; import me.moxun.dreamcatcher.connector.server.http.HandlerRegistry; import me.moxun.dreamcatcher.connector.server.http.LightHttpServer; import me.moxun.dreamcatcher.connector.websocket.WebSocketHandler; public class DevtoolsSocketHandler implements SocketLikeHandler { private final Context mContext; private final Iterable mModules; private final LightHttpServer mServer; public DevtoolsSocketHandler(Context context, Iterable modules) { mContext = context; mModules = modules; mServer = createServer(); } private LightHttpServer createServer() { HandlerRegistry registry = new HandlerRegistry(); ChromeDiscoveryHandler discoveryHandler = new ChromeDiscoveryHandler( mContext, ChromeDevtoolsServer.PATH); discoveryHandler.register(registry); registry.register( new ExactPathMatcher(ChromeDevtoolsServer.PATH), new WebSocketHandler(new ChromeDevtoolsServer(mModules))); return new LightHttpServer(registry); } @Override public void onAccepted(SocketLike socket) throws IOException { mServer.serve(socket); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/MessageHandlingException.java ================================================ package me.moxun.dreamcatcher.connector.inspector; public class MessageHandlingException extends Exception { public MessageHandlingException(Throwable cause) { super(cause); } public MessageHandlingException(String message) { super(message); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/MethodDispatcher.java ================================================ package me.moxun.dreamcatcher.connector.inspector; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.json.ObjectMapper; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcException; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.EmptyResult; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcError; import me.moxun.dreamcatcher.connector.util.ExceptionUtil; import me.moxun.dreamcatcher.connector.util.Util; @ThreadSafe public class MethodDispatcher { @GuardedBy("this") private Map mMethods; private final ObjectMapper mObjectMapper; private final Iterable mDomainHandlers; public MethodDispatcher( ObjectMapper objectMapper, Iterable domainHandlers) { mObjectMapper = objectMapper; mDomainHandlers = domainHandlers; } private synchronized MethodDispatchHelper findMethodDispatcher(String methodName) { if (mMethods == null) { mMethods = buildDispatchTable(mObjectMapper, mDomainHandlers); } return mMethods.get(methodName); } public JSONObject dispatch(JsonRpcPeer peer, String methodName, @Nullable JSONObject params) throws JsonRpcException { MethodDispatchHelper dispatchHelper = findMethodDispatcher(methodName); if (dispatchHelper == null) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.METHOD_NOT_FOUND, "Not implemented: " + methodName, null /* data */)); } if (!dispatchHelper.mMethod.getName().equals("evaluate")) { //LogUtil.w("Call method: " + dispatchHelper.mMethod.getName()); } try { return dispatchHelper.invoke(peer, params); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); ExceptionUtil.propagateIfInstanceOf(cause, JsonRpcException.class); throw ExceptionUtil.propagate(cause); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (JSONException e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } } private static class MethodDispatchHelper { private final ObjectMapper mObjectMapper; private final ChromeDevtoolsDomain mInstance; private final Method mMethod; public MethodDispatchHelper(ObjectMapper objectMapper, ChromeDevtoolsDomain instance, Method method) { mObjectMapper = objectMapper; mInstance = instance; mMethod = method; } public JSONObject invoke(JsonRpcPeer peer, @Nullable JSONObject params) throws InvocationTargetException, IllegalAccessException, JSONException, JsonRpcException { Object internalResult = mMethod.invoke(mInstance, peer, params); if (internalResult == null || internalResult instanceof EmptyResult) { return new JSONObject(); } else { JsonRpcResult convertableResult = (JsonRpcResult) internalResult; return mObjectMapper.convertValue(convertableResult, JSONObject.class); } } } private static Map buildDispatchTable( ObjectMapper objectMapper, Iterable domainHandlers) { Util.throwIfNull(objectMapper); HashMap methods = new HashMap(); for (ChromeDevtoolsDomain domainHandler : Util.throwIfNull(domainHandlers)) { Class handlerClass = domainHandler.getClass(); String domainName = handlerClass.getSimpleName(); for (Method method : handlerClass.getDeclaredMethods()) { if (isDevtoolsMethod(method)) { MethodDispatchHelper dispatchHelper = new MethodDispatchHelper( objectMapper, domainHandler, method); //LogUtil.e("DispatchTable", domainName + "." + method.getName() + " ==> " + dispatchHelper.mMethod.getName()); methods.put(domainName + "." + method.getName(), dispatchHelper); } } } return Collections.unmodifiableMap(methods); } /** * Determines if the method is a {@link ChromeDevtoolsMethod}, and validates accordingly * if it is. * * @throws IllegalArgumentException Thrown if it is a {@link ChromeDevtoolsMethod} but * it otherwise fails to satisfy requirements. */ private static boolean isDevtoolsMethod(Method method) throws IllegalArgumentException { if (!method.isAnnotationPresent(ChromeDevtoolsMethod.class)) { return false; } else { Class args[] = method.getParameterTypes(); String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName(); Util.throwIfNot(args.length == 2, "%s: expected 2 args, got %s", methodName, args.length); Util.throwIfNot(args[0].equals(JsonRpcPeer.class), "%s: expected 1st arg of JsonRpcPeer, got %s", methodName, args[0].getName()); Util.throwIfNot(args[1].equals(JSONObject.class), "%s: expected 2nd arg of JSONObject, got %s", methodName, args[1].getName()); Class returnType = method.getReturnType(); if (!returnType.equals(void.class)) { Util.throwIfNot(JsonRpcResult.class.isAssignableFrom(returnType), "%s: expected JsonRpcResult return type, got %s", methodName, returnType.getName()); } return true; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/MismatchedResponseException.java ================================================ package me.moxun.dreamcatcher.connector.inspector; public class MismatchedResponseException extends MessageHandlingException { public long mRequestId; public MismatchedResponseException(long requestId) { super("Response for request id " + requestId + ", but no such request is pending"); mRequestId = requestId; } public long getRequestId() { return mRequestId; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/helper/ChromePeerManager.java ================================================ package me.moxun.dreamcatcher.connector.inspector.helper; import java.nio.channels.NotYetConnectedException; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import me.moxun.dreamcatcher.connector.jsonrpc.DisconnectReceiver; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.PendingRequestCallback; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.Util; /** * Interface glue that allows a particular domain to manage the enabled peers. The way the * WebKit inspector protocol works is that each functionality domain has an enable/disable JSON-RPC * method call which alerts the server (that's us) that we can now begin sending local events * to the peer to have them appear in the inspector UI. This class simplifies managing those * enabled peers for each functionality domain. */ public class ChromePeerManager { private static final String TAG = "ChromePeerManager"; /** * Set of registered peers, mapped to the disconnect receiver for automatic unregistration * purposes. */ @GuardedBy("this") private final Map mReceivingPeers = new HashMap<>(); /** * This should be set to null anytime mReceivingPeers is changed. It should always be * retrieved by calling getReceivingPeersSnapshot(). */ @GuardedBy("this") private JsonRpcPeer[] mReceivingPeersSnapshot; @GuardedBy("this") private PeerRegistrationListener mListener; public ChromePeerManager() { } /** * Set a listener which can receive notifications of unique registration event (see * {@link #addPeer} and {@link #removePeer}). * * @param listener */ public synchronized void setListener(PeerRegistrationListener listener) { mListener = listener; } /** * Register a new peer, adding them to an internal list of receivers. * * @param peer * @return True if this is a newly registered peer; false if it was already registered. */ public synchronized boolean addPeer(JsonRpcPeer peer) { if (mReceivingPeers.containsKey(peer)) { return false; } DisconnectReceiver disconnectReceiver = new UnregisterOnDisconnect(peer); peer.registerDisconnectReceiver(disconnectReceiver); mReceivingPeers.put(peer, disconnectReceiver); mReceivingPeersSnapshot = null; if (mListener != null) { mListener.onPeerRegistered(peer); } return true; } /** * Unregister an existing peer. * * @param peer */ public synchronized void removePeer(JsonRpcPeer peer) { if (mReceivingPeers.remove(peer) != null) { mReceivingPeersSnapshot = null; if (mListener != null) { mListener.onPeerUnregistered(peer); } } } public synchronized boolean hasRegisteredPeers() { return !mReceivingPeers.isEmpty(); } private synchronized JsonRpcPeer[] getReceivingPeersSnapshot() { if (mReceivingPeersSnapshot == null) { mReceivingPeersSnapshot = mReceivingPeers.keySet().toArray( new JsonRpcPeer[mReceivingPeers.size()]); } return mReceivingPeersSnapshot; } public void sendNotificationToPeers(String method, Object params) { sendMessageToPeers(method, params, null /* callback */); } public void invokeMethodOnPeers(String method, Object params, PendingRequestCallback callback) { Util.throwIfNull(callback); sendMessageToPeers(method, params, callback); } private void sendMessageToPeers(String method, Object params, @Nullable PendingRequestCallback callback) { JsonRpcPeer[] peers = getReceivingPeersSnapshot(); for (JsonRpcPeer peer : peers) { try { peer.invokeMethod(method, params, callback); } catch (NotYetConnectedException e) { LogUtil.e(TAG, "Error delivering data to Chrome", e); } } } private class UnregisterOnDisconnect implements DisconnectReceiver { private final JsonRpcPeer mPeer; public UnregisterOnDisconnect(JsonRpcPeer peer) { mPeer = peer; } @Override public void onDisconnect() { removePeer(mPeer); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/helper/ObjectIdMapper.java ================================================ package me.moxun.dreamcatcher.connector.inspector.helper; import android.util.SparseArray; import java.util.IdentityHashMap; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; public class ObjectIdMapper { protected final Object mSync = new Object(); @GuardedBy("mSync") private int mNextId = 1; @GuardedBy("mSync") private final Map mObjectToIdMap = new IdentityHashMap(); @GuardedBy("mSync") private SparseArray mIdToObjectMap = new SparseArray(); public void clear() { SparseArray idToObjectMap; synchronized (mSync) { idToObjectMap = mIdToObjectMap; mObjectToIdMap.clear(); mIdToObjectMap = new SparseArray(); } int size = idToObjectMap.size(); for (int i = 0; i < size; ++i) { int id = idToObjectMap.keyAt(i); Object object = idToObjectMap.valueAt(i); onUnmapped(object, id); } } public boolean containsId(int id) { synchronized (mSync) { return mIdToObjectMap.get(id) != null; } } public boolean containsObject(Object object) { synchronized (mSync) { return mObjectToIdMap.containsKey(object); } } @Nullable public Object getObjectForId(int id) { synchronized (mSync) { return mIdToObjectMap.get(id); } } @Nullable public Integer getIdForObject(Object object) { synchronized (mSync) { return mObjectToIdMap.get(object); } } public int putObject(Object object) { Integer id; synchronized (mSync) { id = mObjectToIdMap.get(object); if (id != null) { return id; } id = mNextId++; mObjectToIdMap.put(object, id); mIdToObjectMap.put(id, object); } onMapped(object, id); return id; } @Nullable public Object removeObjectById(int id) { Object object; synchronized (mSync) { object = mIdToObjectMap.get(id); if (object == null) { return null; } mIdToObjectMap.remove(id); mObjectToIdMap.remove(object); } onUnmapped(object, id); return object; } @Nullable public Integer removeObject(Object object) { Integer id; synchronized (mSync) { id = mObjectToIdMap.remove(object); if (id == null) { return null; } mIdToObjectMap.remove(id); } onUnmapped(object, id); return id; } public int size() { synchronized (mSync) { return mObjectToIdMap.size(); } } public synchronized int getFirstId() { return mIdToObjectMap.keyAt(0); } protected void onMapped(Object object, int id) { } protected void onUnmapped(Object object, int id) { } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/helper/PeerRegistrationListener.java ================================================ package me.moxun.dreamcatcher.connector.inspector.helper; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; public interface PeerRegistrationListener { void onPeerRegistered(JsonRpcPeer peer); void onPeerUnregistered(JsonRpcPeer peer); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/helper/PeersRegisteredListener.java ================================================ package me.moxun.dreamcatcher.connector.inspector.helper; import java.util.concurrent.atomic.AtomicInteger; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; public abstract class PeersRegisteredListener implements PeerRegistrationListener { private AtomicInteger mPeers = new AtomicInteger(0); @Override public final void onPeerRegistered(JsonRpcPeer peer) { if (mPeers.incrementAndGet() == 1) { onFirstPeerRegistered(); } onPeerAdded(peer); } @Override public final void onPeerUnregistered(JsonRpcPeer peer) { if (mPeers.decrementAndGet() == 0) { onLastPeerUnregistered(); } onPeerRemoved(peer); } protected void onPeerAdded(JsonRpcPeer peer) {} protected void onPeerRemoved(JsonRpcPeer peer) {} protected abstract void onFirstPeerRegistered(); protected abstract void onLastPeerUnregistered(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/helper/ThreadBoundProxy.java ================================================ package me.moxun.dreamcatcher.connector.inspector.helper; import me.moxun.dreamcatcher.connector.util.ThreadBound; import me.moxun.dreamcatcher.connector.util.UncheckedCallable; import me.moxun.dreamcatcher.connector.util.Util; /** * This class is for those cases when a class' threading * policy is determined by one of its member variables. */ public abstract class ThreadBoundProxy implements ThreadBound { private final ThreadBound mEnforcer; public ThreadBoundProxy(ThreadBound enforcer) { mEnforcer = Util.throwIfNull(enforcer); } @Override public final boolean checkThreadAccess() { return mEnforcer.checkThreadAccess(); } @Override public final void verifyThreadAccess() { mEnforcer.verifyThreadAccess(); } @Override public final V postAndWait(UncheckedCallable c) { return mEnforcer.postAndWait(c); } @Override public final void postAndWait(Runnable r) { mEnforcer.postAndWait(r); } @Override public final void postDelayed(Runnable r, long delayMillis) { mEnforcer.postDelayed(r, delayMillis); } @Override public final void removeCallbacks(Runnable r) { mEnforcer.removeCallbacks(r); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/helper/TracingPeerManager.java ================================================ package me.moxun.dreamcatcher.connector.inspector.helper; import javax.annotation.Nullable; /** * Created by moxun on 16/4/26. */ public class TracingPeerManager extends ChromePeerManager { private static TracingPeerManager sInstance; @Nullable public static synchronized TracingPeerManager getInstanceOrNull() { return sInstance; } public static synchronized TracingPeerManager getOrCreateInstance() { if (sInstance == null) { sInstance = new TracingPeerManager(); } return sInstance; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/ChromeDevtoolsDomain.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol; /** * Created by moxun on 16/3/18. */ public interface ChromeDevtoolsDomain { } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/ChromeDevtoolsMethod.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Created by moxun on 16/3/18. */ @Retention(RetentionPolicy.RUNTIME) public @interface ChromeDevtoolsMethod { } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/SimpleBooleanResult.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; public class SimpleBooleanResult implements JsonRpcResult { @JsonProperty(required = true) public boolean result; public SimpleBooleanResult() { } public SimpleBooleanResult(boolean result) { this.result = result; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/SimpleIntegerResult.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; /** * Created by moxun on 16/11/17. */ public class SimpleIntegerResult implements JsonRpcResult { @JsonProperty(required = true) public int result; public SimpleIntegerResult() { } public SimpleIntegerResult(int result) { this.result = result; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/SimpleStringResult.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; /** * Created by moxun on 16/12/1. */ public class SimpleStringResult implements JsonRpcResult { @JsonProperty(required = true) public String data; public SimpleStringResult() { } public SimpleStringResult(String data) { this.data = data; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/Console.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import android.annotation.SuppressLint; import org.json.JSONObject; import java.util.List; import me.moxun.dreamcatcher.connector.console.ConsolePeerManager; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.json.annotation.JsonValue; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; public class Console implements ChromeDevtoolsDomain { public Console() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { ConsolePeerManager.getOrCreateInstance().addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { ConsolePeerManager.getOrCreateInstance().removePeer(peer); } @SuppressLint({"UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse"}) public static class MessageAddedRequest { @JsonProperty(required = true) public ConsoleMessage message; } @SuppressLint({"UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse"}) public static class ConsoleMessage { @JsonProperty(required = true) public MessageSource source; @JsonProperty(required = true) public MessageLevel level; @JsonProperty(required = true) public String text; @JsonProperty(required = false) public MessageType type; @JsonProperty(required = false) public List parameters; } public enum MessageSource { XML("xml"), JAVASCRIPT("javascript"), NETWORK("network"), CONSOLE_API("console-api"), STORAGE("storage"), APPCACHE("appcache"), RENDERING("rendering"), CSS("css"), SECURITY("security"), OTHER("other"); private final String mProtocolValue; private MessageSource(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public enum MessageLevel { LOG("log"), INFO("info"), WARNING("warning"), ERROR("error"), DEBUG("debug"); private final String mProtocolValue; private MessageLevel(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public enum MessageType { LOG("log"), TABLE("table"), GROUP_START("startGroup"), GROUP_END("endGroup"), DIR("dir"); private final String mProtocolValue; private MessageType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } @SuppressLint({"UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse"}) public static class CallFrame { @JsonProperty(required = true) public String functionName; @JsonProperty(required = true) public String url; @JsonProperty(required = true) public int lineNumber; @JsonProperty(required = true) public int columnNumber; public CallFrame() { } public CallFrame(String functionName, String url, int lineNumber, int columnNumber) { this.functionName = functionName; this.url = url; this.lineNumber = lineNumber; this.columnNumber = columnNumber; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/FileSystem.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import android.content.Context; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.ArrayList; import java.util.List; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.json.ObjectMapper; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; /** * EXPERIMENTS * Created by moxun on 16/4/26. */ public class FileSystem implements ChromeDevtoolsDomain { private ObjectMapper mObjectMapper; private Context context; public static final String KEY_FS_ROOT = "FileSystem"; private static boolean isFSEnabled = false; private List textFile = new ArrayList<>(); public static boolean isFSEnabled() { return isFSEnabled; } public FileSystem(Context context) { isFSEnabled = true; mObjectMapper = new ObjectMapper(); this.context = context; textFile.add(".java"); textFile.add(".txt"); textFile.add(".html"); textFile.add(".xml"); textFile.add(".js"); textFile.add(".json"); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public JsonRpcResult requestFileSystemRoot(JsonRpcPeer peer, JSONObject params) { final RequestFileSystemRootRequest param = mObjectMapper.convertValue(params, RequestFileSystemRootRequest.class); if (param.origin.equals(KEY_FS_ROOT) && param.type.equals("persistent")) { return buildFileRoot(); } else { return null; } } private RequestFileSystemRootResponse buildFileRoot() { RequestFileSystemRootResponse root = new RequestFileSystemRootResponse(); root.errorCode = 0; Entry entry = new Entry(); entry.name = "Data"; entry.isDirectory = true; entry.url = context.getApplicationInfo().dataDir; root.root = entry; return root; } @ChromeDevtoolsMethod public JsonRpcResult requestDirectoryContent(JsonRpcPeer peer, JSONObject params) { final RequestDirectoryContentRequest param = mObjectMapper.convertValue(params, RequestDirectoryContentRequest.class); File file = new File(param.url); if (file.exists() && file.isDirectory()) { final RequestDirectoryContentResponse response = new RequestDirectoryContentResponse(); response.errorCode = 0; List entries = new ArrayList<>(); for (File f : file.listFiles()) { Entry entry = new Entry(); entry.isDirectory = f.isDirectory(); entry.url = f.getPath(); entry.name = f.getName(); entry.isTextFile = false; entry.resourceType = Page.ResourceType.OTHER; if (isImage(f)) { entry.resourceType = Page.ResourceType.IMAGE; } if (isTextFile(f)) { entry.resourceType = Page.ResourceType.DOCUMENT; entry.isTextFile = isTextFile(f); } if (!entry.isDirectory) { String[] strings = f.getName().split("\\."); if (strings.length > 1) { entry.mimeType = strings[strings.length - 1].toUpperCase(); } else { entry.mimeType = "FILE"; } } entries.add(entry); } response.entries = entries; return response; } return null; } private boolean isTextFile(File file) { for (String s : textFile) { if (file.getName().toLowerCase().endsWith(s)) { return true; } } return false; } private boolean isImage(File file) { String[] images = {".jpg", ".jpeg", ".png", ".gif", ".webp"}; for (String s : images) { if (file.getName().toLowerCase().endsWith(s)) { return true; } } return false; } @ChromeDevtoolsMethod public JsonRpcResult requestMetadata(JsonRpcPeer peer, JSONObject params) { final RequestMetadataRequest param = mObjectMapper.convertValue( params, RequestMetadataRequest.class); File file = new File(param.url); if (file.exists()) { final RequestMetadataResponse response = new RequestMetadataResponse(); response.errorCode = 0; Metadata meta = new Metadata(); if (!file.isDirectory()) { meta.size = file.length(); } meta.modificationTime = file.lastModified(); response.metadata = meta; return response; } else { return null; } } @ChromeDevtoolsMethod public JsonRpcResult requestFileContent(JsonRpcPeer peer, JSONObject params) { final RequestFileContentRequest param = mObjectMapper.convertValue( params, RequestFileContentRequest.class); final RequestFileContentResponse response = new RequestFileContentResponse(); if (param.readAsText) { File file = new File(param.url); response.content = readFileAsText(file); } return response; } private String readFileAsText(File file) { String result = ""; try { BufferedReader br = new BufferedReader(new FileReader(file)); String s = null; while ((s = br.readLine()) != null) { result = result + "\n" + s; } br.close(); } catch (Exception e) { e.printStackTrace(); } return result; } @ChromeDevtoolsMethod public JsonRpcResult deleteEntry(JsonRpcPeer peer, JSONObject params) { final DeleteEntryRequest param = mObjectMapper.convertValue( params, DeleteEntryRequest.class); final DeleteEntryResponse response = new DeleteEntryResponse(); File file = new File(param.url); boolean success = file.delete(); response.errorCode = success ? 0 : -1; return response; } public static class DeleteEntryRequest { @JsonProperty(required = true) public String url; } public static class DeleteEntryResponse implements JsonRpcResult { @JsonProperty(required = true) public int errorCode; } public static class RequestFileContentRequest { @JsonProperty(required = true) public String url; @JsonProperty(required = true) public boolean readAsText; @JsonProperty public Integer start; @JsonProperty public Integer end; @JsonProperty public String charset; } public static class RequestFileContentResponse implements JsonRpcResult { @JsonProperty(required = true) public int errorCode; @JsonProperty public String content; @JsonProperty public String charset; } public static class RequestMetadataRequest { @JsonProperty(required = true) public String url; } public static class RequestMetadataResponse implements JsonRpcResult { @JsonProperty(required = true) public int errorCode; @JsonProperty public Metadata metadata; } public static class Metadata { @JsonProperty(required = true) public double modificationTime; @JsonProperty(required = true) public double size; } public static class RequestDirectoryContentRequest { @JsonProperty(required = true) public String url; } public static class RequestDirectoryContentResponse implements JsonRpcResult { @JsonProperty(required = true) public int errorCode; @JsonProperty public List entries; } public static class Entry { @JsonProperty(required = true) public String url; @JsonProperty(required = true) public String name; @JsonProperty(required = true) public boolean isDirectory; @JsonProperty public String mimeType; @JsonProperty public Page.ResourceType resourceType; @JsonProperty public Boolean isTextFile; } public static class RequestFileSystemRootRequest { @JsonProperty(required = true) public String origin; @JsonProperty(required = true) public String type; } public static class RequestFileSystemRootResponse implements JsonRpcResult { @JsonProperty(required = true) public int errorCode; @JsonProperty public Entry root; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/Inspector.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import org.json.JSONObject; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; public class Inspector implements ChromeDevtoolsDomain { public Inspector() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/Network.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import android.content.Context; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.util.List; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.json.annotation.JsonValue; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcException; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcError; import me.moxun.dreamcatcher.connector.reporter.AsyncPrettyPrinterInitializer; import me.moxun.dreamcatcher.connector.reporter.NetworkPeerManager; import me.moxun.dreamcatcher.connector.reporter.ResponseBodyData; import me.moxun.dreamcatcher.connector.reporter.ResponseBodyFileManager; import me.moxun.dreamcatcher.connector.util.Util; public class Network implements ChromeDevtoolsDomain { private final NetworkPeerManager mNetworkPeerManager; private final ResponseBodyFileManager mResponseBodyFileManager; public Network(Context context) { mNetworkPeerManager = NetworkPeerManager.getOrCreateInstance(context); mResponseBodyFileManager = mNetworkPeerManager.getResponseBodyFileManager(); } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { mNetworkPeerManager.addPeer(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { mNetworkPeerManager.removePeer(peer); } @ChromeDevtoolsMethod public void setUserAgentOverride(JsonRpcPeer peer, JSONObject params) { // Not implemented... } @ChromeDevtoolsMethod public JsonRpcResult getResponseBody(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { try { String requestId = params.getString("requestId"); return readResponseBody(requestId); } catch (IOException e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } catch (JSONException e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } } private GetResponseBodyResponse readResponseBody(String requestId) throws IOException, JsonRpcException { GetResponseBodyResponse response = new GetResponseBodyResponse(); ResponseBodyData bodyData; try { bodyData = mResponseBodyFileManager.readFile(requestId); } catch (OutOfMemoryError e) { throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR, e.toString(), null /* data */)); } response.body = bodyData.data; response.base64Encoded = bodyData.base64Encoded; return response; } /** * Method that allows callers to provide an {@link AsyncPrettyPrinterInitializer} that is * responsible for registering all * Note that AsyncPrettyPrinterInitializer cannot be null and can only be set once. * @param initializer */ public void setPrettyPrinterInitializer(AsyncPrettyPrinterInitializer initializer) { Util.throwIfNull(initializer); mNetworkPeerManager.setPrettyPrinterInitializer(initializer); } private static class GetResponseBodyResponse implements JsonRpcResult { @JsonProperty(required = true) public String body; @JsonProperty(required = true) public boolean base64Encoded; } public static class RequestWillBeSentParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public String frameId; @JsonProperty(required = true) public String loaderId; @JsonProperty(required = true) public String documentURL; @JsonProperty(required = true) public Request request; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public Initiator initiator; @JsonProperty public Response redirectResponse; @JsonProperty public Page.ResourceType type; } public static class ResponseReceivedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public String frameId; @JsonProperty(required = true) public String loaderId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public Page.ResourceType type; @JsonProperty(required = true) public Response response; } public static class LoadingFinishedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; } public static class LoadingFailedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public String errorText; // Chrome introduced this undocumented new addition that, if not sent, will cause the row // to be removed from the UI and raise a JavaScript exception in the console. This is // clearly an upstream bug that needs to be fixed, though we can work around it by // providing this new undocumented field. @JsonProperty public Page.ResourceType type; } public static class DataReceivedParams { @JsonProperty(required = true) public String requestId; @JsonProperty(required = true) public double timestamp; @JsonProperty(required = true) public int dataLength; @JsonProperty(required = true) public int encodedDataLength; } public static class Request { @JsonProperty(required = true) public String url; @JsonProperty(required = true) public String method; @JsonProperty(required = true) public JSONObject headers; @JsonProperty public String postData; } public static class Initiator { @JsonProperty(required = true) public InitiatorType type; @JsonProperty public List stackTrace; } public enum InitiatorType { PARSER("parser"), SCRIPT("script"), OTHER("other"); private final String mProtocolValue; private InitiatorType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public static class Response { @JsonProperty(required = true) public String url; @JsonProperty(required = true) public int status; @JsonProperty(required = true) public String statusText; @JsonProperty(required = true) public JSONObject headers; @JsonProperty public String headersText; @JsonProperty(required = true) public String mimeType; @JsonProperty public JSONObject requestHeaders; @JsonProperty public String requestHeadersTest; @JsonProperty(required = true) public boolean connectionReused; @JsonProperty(required = true) public int connectionId; @JsonProperty(required = true) public Boolean fromDiskCache; @JsonProperty public ResourceTiming timing; } public static class ResourceTiming { @JsonProperty(required = true) public double requestTime; @JsonProperty(required = true) public double proxyStart; @JsonProperty(required = true) public double proxyEnd; @JsonProperty(required = true) public double dnsStart; @JsonProperty(required = true) public double dnsEnd; @JsonProperty(required = true) public double connectStart; @JsonProperty(required = true) public double connectEnd; @JsonProperty(required = true) public double sslStart; @JsonProperty(required = true) public double sslEnd; @JsonProperty(required = true) public double sendStart; @JsonProperty(required = true) public double sendEnd; @JsonProperty(required = true) public double receiveHeadersEnd; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/Page.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import android.content.Context; import org.json.JSONException; import org.json.JSONObject; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.inspector.protocol.SimpleBooleanResult; import me.moxun.dreamcatcher.connector.json.ObjectMapper; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.json.annotation.JsonValue; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.ProcessUtil; public class Page implements ChromeDevtoolsDomain { private final Context mContext; private final ObjectMapper mObjectMapper = new ObjectMapper(); private int lastFrame = 0; public Page(Context context) { mContext = context; } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { notifyExecutionContexts(peer); sendWelcomeMessage(peer); } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } private void notifyExecutionContexts(JsonRpcPeer peer) { ExecutionContextDescription context = new ExecutionContextDescription(); context.frameId = "1"; context.id = 1; ExecutionContextCreatedParams params = new ExecutionContextCreatedParams(); params.context = context; peer.invokeMethod("Runtime.executionContextCreated", params, null /* callback */); } private void sendWelcomeMessage(JsonRpcPeer peer) { Console.ConsoleMessage message = new Console.ConsoleMessage(); message.source = Console.MessageSource.JAVASCRIPT; message.level = Console.MessageLevel.LOG; String art = " Welcome to DreamCatcher\n Attached to process " + ProcessUtil.getProcessName() + "\n\n"; message.text = art; // Note: not using Android resources so we can maintain .jar distribution for now. Console.MessageAddedRequest messageAddedRequest = new Console.MessageAddedRequest(); messageAddedRequest.message = message; peer.invokeMethod("Console.messageAdded", messageAddedRequest, null /* callback */); } private static FrameResourceTree createSimpleFrameResourceTree( String id, String parentId, String name, String securityOrigin) { Frame frame = new Frame(); frame.id = id; frame.parentId = parentId; frame.loaderId = "1"; frame.name = name; frame.url = ""; frame.securityOrigin = securityOrigin; frame.mimeType = "text/plain"; FrameResourceTree tree = new FrameResourceTree(); tree.frame = frame; tree.resources = Collections.emptyList(); tree.childFrames = null; return tree; } @ChromeDevtoolsMethod public JsonRpcResult canScreencast(JsonRpcPeer peer, JSONObject params) { return new SimpleBooleanResult(false); } @ChromeDevtoolsMethod public JsonRpcResult hasTouchInputs(JsonRpcPeer peer, JSONObject params) { return new SimpleBooleanResult(false); } @ChromeDevtoolsMethod public void setDeviceMetricsOverride(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void clearDeviceOrientationOverride(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void startScreencast(final JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void stopScreencast(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void screencastFrameAck(JsonRpcPeer peer, JSONObject params) { // Nothing to do here, just need to make sure Chrome doesn't get an error that this method // isn't implemented int ackFrame = 0; try { ackFrame = params.getInt("sessionId"); } catch (JSONException e) { e.printStackTrace(); } if ((ackFrame - lastFrame) != 1 ) { LogUtil.w("Screencast", "Lost " + (ackFrame - lastFrame) + " frame(s)! current frame is " + ackFrame); } lastFrame = ackFrame; } @ChromeDevtoolsMethod public void clearGeolocationOverride(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setTouchEmulationEnabled(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setEmulatedMedia(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setShowViewportSizeOnResize(JsonRpcPeer peer, JSONObject params) { } private static class GetResourceTreeParams implements JsonRpcResult { @JsonProperty(required = true) public FrameResourceTree frameTree; } private static class FrameResourceTree { @JsonProperty(required = true) public Frame frame; @JsonProperty public List childFrames; @JsonProperty(required = true) public List resources; } private static class Frame { @JsonProperty(required = true) public String id; @JsonProperty public String parentId; @JsonProperty(required = true) public String loaderId; @JsonProperty public String name; @JsonProperty(required = true) public String url; @JsonProperty(required = true) public String securityOrigin; @JsonProperty(required = true) public String mimeType; } private static class Resource { // Incomplete... } public enum ResourceType { DOCUMENT("Document"), STYLESHEET("Stylesheet"), IMAGE("Image"), FONT("Font"), SCRIPT("Script"), XHR("XHR"), WEBSOCKET("WebSocket"), OTHER("Other"); private final String mProtocolValue; private ResourceType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } private static class ExecutionContextCreatedParams { @JsonProperty(required = true) public ExecutionContextDescription context; } private static class ExecutionContextDescription { @JsonProperty(required = true) public String frameId; @JsonProperty(required = true) public int id; } public static class ScreencastFrameEvent { @JsonProperty(required = true) public String data; @JsonProperty(required = true) public ScreencastFrameEventMetadata metadata; @JsonProperty(required = true) public int sessionId; private static AtomicInteger generator = new AtomicInteger(); public void increment() { sessionId = generator.incrementAndGet(); } } public static class ScreencastFrameEventMetadata { @JsonProperty(required = true) public float pageScaleFactor; @JsonProperty(required = true) public int offsetTop; @JsonProperty(required = true) public int deviceWidth; @JsonProperty(required = true) public int deviceHeight; @JsonProperty(required = true) public int scrollOffsetX; @JsonProperty(required = true) public int scrollOffsetY; } public static class StartScreencastRequest { @JsonProperty public String format; @JsonProperty public int quality; @JsonProperty public int maxWidth; @JsonProperty public int maxHeight; } public static class NavigationHistory implements JsonRpcResult { @JsonProperty(required = true) public int currentIndex; @JsonProperty(required = true) public List entries; } public static class NavigationEntry { @JsonProperty(required = true) public int id; @JsonProperty(required = true) public String url; @JsonProperty(required = true) public String title; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/Profiler.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import org.json.JSONObject; import java.util.Collections; import java.util.List; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; public class Profiler implements ChromeDevtoolsDomain { public Profiler() { } @ChromeDevtoolsMethod public void enable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void disable(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void stop(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public void setSamplingInterval(JsonRpcPeer peer, JSONObject params) { } @ChromeDevtoolsMethod public JsonRpcResult getProfileHeaders(JsonRpcPeer peer, JSONObject params) { ProfileHeaderResponse response = new ProfileHeaderResponse(); response.headers = Collections.emptyList(); return response; } private static class ProfileHeaderResponse implements JsonRpcResult { @JsonProperty(required = true) public List headers; } private static class ProfileHeader { @JsonProperty(required = true) String typeId; @JsonProperty(required = true) String title; @JsonProperty(required = true) int uid; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/protocol/module/Runtime.java ================================================ package me.moxun.dreamcatcher.connector.inspector.protocol.module; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.console.RuntimeRepl; import me.moxun.dreamcatcher.connector.console.RuntimeReplFactory; import me.moxun.dreamcatcher.connector.inspector.helper.ObjectIdMapper; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsDomain; import me.moxun.dreamcatcher.connector.inspector.protocol.ChromeDevtoolsMethod; import me.moxun.dreamcatcher.connector.json.ObjectMapper; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.json.annotation.JsonValue; import me.moxun.dreamcatcher.connector.jsonrpc.DisconnectReceiver; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcException; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcPeer; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcError; import me.moxun.dreamcatcher.connector.util.LogUtil; /** * Created by moxun on 16/3/31. */ public class Runtime implements ChromeDevtoolsDomain { private final ObjectMapper mObjectMapper = new ObjectMapper(); private static final Map sSessions = Collections.synchronizedMap(new HashMap()); private final RuntimeReplFactory mReplFactory; /** * @see #Runtime(RuntimeReplFactory) * @deprecated Provided for ABI compatibility */ @Deprecated public Runtime() { this(new RuntimeReplFactory() { @Override public RuntimeRepl newInstance() { return new RuntimeRepl() { @Override public Object evaluate(String expression) throws Throwable { return "Not supported with legacy Runtime module"; } }; } }); } public Runtime(RuntimeReplFactory replFactory) { mReplFactory = replFactory; } public static int mapObject(JsonRpcPeer peer, Object object) { return getSession(peer).getObjects().putObject(object); } @Nonnull private static synchronized Session getSession(final JsonRpcPeer peer) { Session session = sSessions.get(peer); if (session == null) { session = new Session(); sSessions.put(peer, session); peer.registerDisconnectReceiver(new DisconnectReceiver() { @Override public void onDisconnect() { sSessions.remove(peer); } }); } return session; } @ChromeDevtoolsMethod public void releaseObject(JsonRpcPeer peer, JSONObject params) throws JSONException { String objectId = params.getString("objectId"); getSession(peer).getObjects().removeObjectById(Integer.parseInt(objectId)); } @ChromeDevtoolsMethod public void releaseObjectGroup(JsonRpcPeer peer, JSONObject params) { LogUtil.w("Ignoring request to releaseObjectGroup: " + params); } @ChromeDevtoolsMethod public CallFunctionOnResponse callFunctionOn(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { CallFunctionOnRequest args = mObjectMapper.convertValue(params, CallFunctionOnRequest.class); Session session = getSession(peer); Object object = session.getObjectOrThrow(args.objectId); // The DevTools UI thinks it can run arbitrary JavaScript against us in order to figure out // the class structure of an object. That obviously won't fly, and there's no way to // translate without building a crude JavaScript parser so let's just go ahead and guess // what this function does by name. if (!args.functionDeclaration.startsWith("function protoList(")) { throw new JsonRpcException( new JsonRpcError( JsonRpcError.ErrorCode.INTERNAL_ERROR, "Expected protoList, got: " + args.functionDeclaration, null /* data */)); } // Since this is really a function call we have to create this fake object to hold the // "result" of the function. ObjectProtoContainer objectContainer = new ObjectProtoContainer(object); RemoteObject result = new RemoteObject(); result.type = ObjectType.OBJECT; result.subtype = ObjectSubType.NODE; result.className = object.getClass().getName(); result.description = getPropertyClassName(object); result.objectId = String.valueOf(session.getObjects().putObject(objectContainer)); CallFunctionOnResponse response = new CallFunctionOnResponse(); response.result = result; response.wasThrown = false; return response; } @ChromeDevtoolsMethod public JsonRpcResult evaluate(JsonRpcPeer peer, JSONObject params) { return getSession(peer).evaluate(mReplFactory, params); } @ChromeDevtoolsMethod public JsonRpcResult getProperties(JsonRpcPeer peer, JSONObject params) throws JsonRpcException { return getSession(peer).getProperties(params); } private static String getPropertyClassName(Object o) { String name = o.getClass().getSimpleName(); if (name == null || name.length() == 0) { // Looks better for anonymous classes. name = o.getClass().getName(); } return name; } private static class ObjectProtoContainer { public final Object object; public ObjectProtoContainer(Object object) { this.object = object; } } /** * Object representing a session with a single client. *

*

Clients inherently leak object references because they can expand any object in the UI * at any time. Grouping references by client allows us to drop them when the client * disconnects. */ private static class Session { private final ObjectIdMapper mObjects = new ObjectIdMapper(); private final ObjectMapper mObjectMapper = new ObjectMapper(); @Nullable private RuntimeRepl mRepl; public ObjectIdMapper getObjects() { return mObjects; } public Object getObjectOrThrow(String objectId) throws JsonRpcException { Object object = getObjects().getObjectForId(Integer.parseInt(objectId)); if (object == null) { throw new JsonRpcException(new JsonRpcError( JsonRpcError.ErrorCode.INVALID_REQUEST, "No object found for " + objectId, null /* data */)); } return object; } public RemoteObject objectForRemote(Object value) { RemoteObject result = new RemoteObject(); if (value == null) { result.type = ObjectType.OBJECT; result.subtype = ObjectSubType.NULL; result.value = JSONObject.NULL; } else if (value instanceof Boolean) { result.type = ObjectType.BOOLEAN; result.value = value; } else if (value instanceof Number) { result.type = ObjectType.NUMBER; result.value = value; } else if (value instanceof Character) { // Unclear whether we should expose these as strings, numbers, or something else. result.type = ObjectType.NUMBER; result.value = Integer.valueOf(((Character) value).charValue()); } else if (value instanceof String) { result.type = ObjectType.STRING; result.value = String.valueOf(value); } else { result.type = ObjectType.OBJECT; result.className = "What??"; // I have no idea where this is used. result.objectId = String.valueOf(mObjects.putObject(value)); if (value.getClass().isArray()) { result.description = "array"; } else if (value instanceof List) { result.description = "List"; } else if (value instanceof Set) { result.description = "Set"; } else if (value instanceof Map) { result.description = "Map"; } else { result.description = getPropertyClassName(value); } } return result; } public EvaluateResponse evaluate(RuntimeReplFactory replFactory, JSONObject params) { EvaluateRequest request = mObjectMapper.convertValue(params, EvaluateRequest.class); try { if (!request.objectGroup.equals("console")) { return buildExceptionResponse("Not supported by FAB"); } RuntimeRepl repl = getRepl(replFactory); Object result = repl.evaluate(request.expression); if (result == null) { return null; } return buildNormalResponse(result); } catch (Throwable t) { return buildExceptionResponse(t); } } @Nonnull private synchronized RuntimeRepl getRepl(RuntimeReplFactory replFactory) { if (mRepl == null) { mRepl = replFactory.newInstance(); } return mRepl; } private EvaluateResponse buildNormalResponse(Object retval) { EvaluateResponse response = new EvaluateResponse(); response.wasThrown = false; response.result = objectForRemote(retval); return response; } private EvaluateResponse buildExceptionResponse(Object retval) { EvaluateResponse response = new EvaluateResponse(); response.wasThrown = true; response.result = objectForRemote(retval); response.exceptionDetails = new ExceptionDetails(); response.exceptionDetails.text = retval.toString(); return response; } public GetPropertiesResponse getProperties(JSONObject params) throws JsonRpcException { GetPropertiesRequest request = mObjectMapper.convertValue(params, GetPropertiesRequest.class); if (!request.ownProperties) { GetPropertiesResponse response = new GetPropertiesResponse(); response.result = new ArrayList<>(); return response; } Object object = getObjectOrThrow(request.objectId); if (object.getClass().isArray()) { object = arrayToList(object); } if (object instanceof ObjectProtoContainer) { return getPropertiesForProtoContainer((ObjectProtoContainer) object); } else if (object instanceof List) { return getPropertiesForIterable((List) object, /* enumerate */ true); } else if (object instanceof Set) { return getPropertiesForIterable((Set) object, /* enumerate */ false); } else if (object instanceof Map) { return getPropertiesForMap(object); } else { return getPropertiesForObject(object); } } private List arrayToList(Object object) { Class type = object.getClass(); if (!type.isArray()) { throw new IllegalArgumentException("Argument must be an array. Was " + type); } Class component = type.getComponentType(); if (!component.isPrimitive()) { return Arrays.asList((Object[]) object); } // Loop manually for primitives. int length = Array.getLength(object); List ret = new ArrayList<>(length); for (int i = 0; i < length; i++) { ret.add(Array.get(object, i)); } return ret; } // Normally JavaScript will return the full class hierarchy as a list. That seems less // useful for Java since it's more natural (IMO) to see all available member variables in one // big list. private GetPropertiesResponse getPropertiesForProtoContainer(ObjectProtoContainer proto) { Object target = proto.object; RemoteObject protoRemote = new RemoteObject(); protoRemote.type = ObjectType.OBJECT; protoRemote.subtype = ObjectSubType.NODE; protoRemote.className = target.getClass().getName(); protoRemote.description = getPropertyClassName(target); protoRemote.objectId = String.valueOf(mObjects.putObject(target)); PropertyDescriptor descriptor = new PropertyDescriptor(); descriptor.name = "1"; descriptor.value = protoRemote; GetPropertiesResponse response = new GetPropertiesResponse(); response.result = new ArrayList<>(1); response.result.add(descriptor); return response; } private GetPropertiesResponse getPropertiesForIterable(Iterable object, boolean enumerate) { GetPropertiesResponse response = new GetPropertiesResponse(); List properties = new ArrayList<>(); int index = 0; for (Object value : object) { PropertyDescriptor property = new PropertyDescriptor(); property.name = enumerate ? String.valueOf(index++) : null; property.value = objectForRemote(value); properties.add(property); } response.result = properties; return response; } private GetPropertiesResponse getPropertiesForMap(Object object) { GetPropertiesResponse response = new GetPropertiesResponse(); List properties = new ArrayList<>(); for (Map.Entry entry : ((Map) object).entrySet()) { PropertyDescriptor property = new PropertyDescriptor(); property.name = String.valueOf(entry.getKey()); property.value = objectForRemote(entry.getValue()); properties.add(property); } response.result = properties; return response; } private GetPropertiesResponse getPropertiesForObject(Object object) { GetPropertiesResponse response = new GetPropertiesResponse(); List properties = new ArrayList<>(); for ( Class declaringClass = object.getClass(); declaringClass != null; declaringClass = declaringClass.getSuperclass() ) { // Reverse the list of fields while going up the superclass chain. // When we're done, we'll reverse the full list so that the superclasses // appear at the top, but within each class they properties are in declared order. List fields = new ArrayList(Arrays.asList(declaringClass.getDeclaredFields())); Collections.reverse(fields); String prefix = declaringClass == object.getClass() ? "" : declaringClass.getSimpleName() + "."; for (Field field : fields) { if (Modifier.isStatic(field.getModifiers())) { continue; } field.setAccessible(true); try { Object fieldValue = field.get(object); PropertyDescriptor property = new PropertyDescriptor(); property.name = prefix + field.getName(); property.value = objectForRemote(fieldValue); properties.add(property); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } Collections.reverse(properties); response.result = properties; return response; } } private static class CallFunctionOnRequest { @JsonProperty public String objectId; @JsonProperty public String functionDeclaration; @JsonProperty public List arguments; @JsonProperty(required = false) public Boolean doNotPauseOnExceptionsAndMuteConsole; @JsonProperty(required = false) public Boolean returnByValue; @JsonProperty(required = false) public Boolean generatePreview; } private static class CallFunctionOnResponse implements JsonRpcResult { @JsonProperty public RemoteObject result; @JsonProperty(required = false) public Boolean wasThrown; } private static class CallArgument { @JsonProperty(required = false) public Object value; @JsonProperty(required = false) public String objectId; @JsonProperty(required = false) public ObjectType type; } private static class GetPropertiesRequest implements JsonRpcResult { @JsonProperty(required = true) public boolean ownProperties; @JsonProperty(required = true) public String objectId; } private static class GetPropertiesResponse implements JsonRpcResult { @JsonProperty(required = true) public List result; } private static class EvaluateRequest implements JsonRpcResult { @JsonProperty(required = true) public String objectGroup; @JsonProperty(required = true) public String expression; } private static class EvaluateResponse implements JsonRpcResult { @JsonProperty(required = true) public RemoteObject result; @JsonProperty(required = true) public boolean wasThrown; @JsonProperty public ExceptionDetails exceptionDetails; } private static class ExceptionDetails { @JsonProperty(required = true) public String text; } public static class RemoteObject { @JsonProperty(required = true) public ObjectType type; @JsonProperty public ObjectSubType subtype; @JsonProperty public Object value; @JsonProperty public String className; @JsonProperty public String description; @JsonProperty public String objectId; } private static class PropertyDescriptor { @JsonProperty(required = true) public String name; @JsonProperty(required = true) public RemoteObject value; @JsonProperty(required = true) public final boolean isOwn = true; @JsonProperty(required = true) public final boolean configurable = false; @JsonProperty(required = true) public final boolean enumerable = true; @JsonProperty(required = true) public final boolean writable = false; } public static enum ObjectType { OBJECT("object"), FUNCTION("function"), UNDEFINED("undefined"), STRING("string"), NUMBER("number"), BOOLEAN("boolean"), SYMBOL("symbol"); private final String mProtocolValue; private ObjectType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } public static enum ObjectSubType { ARRAY("array"), NULL("null"), NODE("node"), REGEXP("regexp"), DATE("date"), MAP("map"), SET("set"), ITERATOR("iterator"), GENERATOR("generator"), ERROR("error"); private final String mProtocolValue; private ObjectSubType(String protocolValue) { mProtocolValue = protocolValue; } @JsonValue public String getProtocolValue() { return mProtocolValue; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/inspector/runtime/DefaultRuntimeReplFactory.java ================================================ package me.moxun.dreamcatcher.connector.inspector.runtime; import java.util.HashMap; import java.util.Map; import me.moxun.dreamcatcher.connector.console.RuntimeRepl; import me.moxun.dreamcatcher.connector.console.RuntimeReplFactory; import me.moxun.dreamcatcher.connector.console.command.CommandHandler; /** * Created by moxun on 16/3/31. */ public class DefaultRuntimeReplFactory implements RuntimeReplFactory { private static Map handlerMap; @Override public RuntimeRepl newInstance() { if (handlerMap == null) { handlerMap = new HashMap<>(); handlerMap.clear(); } return new RuntimeRepl() { @Override public Object evaluate(String expression) throws Throwable { Object ret = null; try { ret = processCommand(expression); } catch (Exception ex) { //don't throw out ret = ex.getMessage(); ex.printStackTrace(); } return ret; } }; } public static void register(String command, CommandHandler handler) { if (handlerMap == null) { handlerMap = new HashMap<>(); handlerMap.clear(); } handlerMap.put(command, handler); } private Command getCommand(String command) { Command c = new Command(); if (command.contains(" ")) { int i = command.indexOf(" "); c.command = command.substring(0, i); c.param = command.substring(i + 1); } else { c.command = command; c.param = null; } return c; } private Object processCommand(String input) { Command command = getCommand(input); CommandHandler handler = handlerMap.get(command.command); if (handler != null) { return handler.onCommand(command.param); } else { return "command '" + input + "' not support now."; } } private class Command { String command; String param; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/json/ObjectMapper.java ================================================ package me.moxun.dreamcatcher.connector.json; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.json.annotation.JsonValue; import me.moxun.dreamcatcher.connector.util.ExceptionUtil; /** * This class is a lightweight version of Jackson's ObjectMapper. It is designed to have a minimal * subset of the functionality required for dreamcatcher. *

* It would be awesome if there were a lightweight library that supported converting between * arbitrary {@link Object} and {@link JSONObject} representations. *

* Admittedly the other approach would be to use an Annotation Processor to create static conversion * functions that discover something like a {@link JsonProperty} and create a function at compile * time however since this is just being used for a simple debug utility and Kit-Kat caches the * results of reflection this class is sufficient for dreamcatcher needs. */ public class ObjectMapper { @GuardedBy("mJsonValueMethodCache") private final Map, Method> mJsonValueMethodCache = new IdentityHashMap<>(); /** * Support mapping between arbitrary classes and {@link JSONObject}. * * It is possible for a {@link Throwable} to be propagated out of this class if there is an * {@link InvocationTargetException}. * * @param fromValue * @param toValueType * @param * @return * @throws IllegalArgumentException when there is an error converting. One of either * {@code fromValue.getClass()} or {@code toValueType} must be {@link JSONObject}. */ public T convertValue(Object fromValue, Class toValueType) throws IllegalArgumentException { if (fromValue == null) { return null; } if (toValueType != Object.class && toValueType.isAssignableFrom(fromValue.getClass())) { return (T) fromValue; } try { if (fromValue instanceof JSONObject) { return _convertFromJSONObject((JSONObject) fromValue, toValueType); } else if (toValueType == JSONObject.class) { return (T) _convertToJSONObject(fromValue); } else { throw new IllegalArgumentException( "Expecting either fromValue or toValueType to be a JSONObject"); } } catch (NoSuchMethodException e) { throw new IllegalArgumentException(e); } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } catch (InstantiationException e) { throw new IllegalArgumentException(e); } catch (JSONException e) { throw new IllegalArgumentException(e); } catch (InvocationTargetException e) { throw ExceptionUtil.propagate(e.getCause()); } } private T _convertFromJSONObject(JSONObject jsonObject, Class type) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, JSONException { Constructor constructor = type.getDeclaredConstructor((Class[]) null); constructor.setAccessible(true); T instance = constructor.newInstance(); Field[] fields = type.getFields(); for (int i = 0; i < fields.length; ++i) { Field field = fields[i]; Object value = jsonObject.opt(field.getName()); Object setValue = getValueForField(field, value); try { field.set(instance, setValue); } catch (IllegalArgumentException e) { throw new IllegalArgumentException( "Class: " + type.getSimpleName() + " " + "Field: " + field.getName() + " type " + setValue.getClass().getName(), e); } } return instance; } private Object getValueForField(Field field, Object value) throws JSONException { try { if (value != null) { if (value == JSONObject.NULL) { return null; } if (value.getClass() == field.getType()) { return value; } if (value instanceof JSONObject) { return convertValue(value, field.getType()); } else { if (field.getType().isEnum()) { return getEnumValue((String) value, field.getType().asSubclass(Enum.class)); } else if (value instanceof JSONArray) { return convertArrayToList(field, (JSONArray) value); } else if (value instanceof Number) { // Need to convert value to Number This happens because json treats 1 as an Integer even // if the field is supposed to be a Long Number numberValue = (Number) value; Class clazz = field.getType(); if (clazz == Integer.class || clazz == int.class) { return numberValue.intValue(); } else if (clazz == Long.class || clazz == long.class) { return numberValue.longValue(); } else if (clazz == Double.class || clazz == double.class) { return numberValue.doubleValue(); } else if (clazz == Float.class || clazz == float.class) { return numberValue.floatValue(); } else if (clazz == Byte.class || clazz == byte.class) { return numberValue.byteValue(); } else if (clazz == Short.class || clazz == short.class) { return numberValue.shortValue(); } else { throw new IllegalArgumentException("Not setup to handle class " + clazz.getName()); } } } } } catch (IllegalAccessException e) { throw new IllegalArgumentException("Unable to set value for field " + field.getName(), e); } return value; } private Enum getEnumValue(String value, Class clazz) { Method method = getJsonValueMethod(clazz); if (method != null) { return getEnumByMethod(value, clazz, method); } else { return Enum.valueOf(clazz, value); } } /** * In this case we know that there is an {@link Enum} decorated with {@link JsonValue}. This means * that we need to iterate through all of the values of the {@link Enum} returned by the given * {@link Method} to check the given value. * @param value * @param clazz * @param method * @return */ private Enum getEnumByMethod(String value, Class clazz, Method method) { Enum[] enumValues = clazz.getEnumConstants(); // Start at the front to ensure first always wins for (int i = 0; i < enumValues.length; ++i) { Enum enumValue = enumValues[i]; try { Object o = method.invoke(enumValue); if (o != null) { if (o.toString().equals(value)) { return enumValue; } } } catch (Exception ex) { throw new IllegalArgumentException(ex); } } throw new IllegalArgumentException("No enum constant " + clazz.getName() + "." + value); } private List convertArrayToList(Field field, JSONArray array) throws IllegalAccessException, JSONException { if (List.class.isAssignableFrom(field.getType())) { ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); Type[] types = parameterizedType.getActualTypeArguments(); if (types.length != 1) { throw new IllegalArgumentException("Only able to handle a single type in a list " + field.getName()); } Class arrayClass = (Class)types[0]; List objectList = new ArrayList(); for (int i = 0; i < array.length(); ++i) { if (arrayClass.isEnum()) { objectList.add(getEnumValue(array.getString(i), arrayClass)); } else if (canDirectlySerializeClass(arrayClass)) { objectList.add(array.get(i)); } else { JSONObject jsonObject = array.getJSONObject(i); if (jsonObject == null) { objectList.add(null); } else { objectList.add(convertValue(jsonObject, arrayClass)); } } } return objectList; } else { throw new IllegalArgumentException("only know how to deserialize List on field " + field.getName()); } } private JSONObject _convertToJSONObject(Object fromValue) throws JSONException, InvocationTargetException, IllegalAccessException { JSONObject jsonObject = new JSONObject(); Field[] fields = fromValue.getClass().getFields(); for (int i = 0; i < fields.length; ++i) { JsonProperty property = fields[i].getAnnotation(JsonProperty.class); if (property != null) { // AutoBox here ... Object value = fields[i].get(fromValue); Class clazz = fields[i].getType(); if (value != null) { clazz = value.getClass(); } String name = fields[i].getName(); if (property.required() && value == null) { value = JSONObject.NULL; } else if (value == JSONObject.NULL) { // Leave it as null in this case. } else { value = getJsonValue(value, clazz, fields[i]); } jsonObject.put(name, value); } } return jsonObject; } private Object getJsonValue(Object value, Class clazz, Field field) throws InvocationTargetException, IllegalAccessException { if (value == null) { // Now technically we /could/ return JsonNode.NULL here but Chrome's webkit inspector croaks // if you pass a null "id" return null; } if (List.class.isAssignableFrom(clazz)) { return convertListToJsonArray(value); } // Finally check to see if there is a JsonValue present Method m = getJsonValueMethod(clazz); if (m != null) { return m.invoke(value); } if (!canDirectlySerializeClass(clazz)) { return convertValue(value, JSONObject.class); } // JSON has no support for NaN, Infinity or -Infinity, so we serialize // then as strings. Google Chrome's inspector will accept them just fine. if (clazz.equals(Double.class) || clazz.equals(Float.class)) { double doubleValue = ((Number) value).doubleValue(); if (Double.isNaN(doubleValue)) { return "NaN"; } else if (doubleValue == Double.POSITIVE_INFINITY) { return "Infinity"; } else if (doubleValue == Double.NEGATIVE_INFINITY) { return "-Infinity"; } } // hmm we should be able to directly serialize here... return value; } private JSONArray convertListToJsonArray(Object value) throws InvocationTargetException, IllegalAccessException { JSONArray array = new JSONArray(); List list = (List) value; for(Object obj : list) { // Send null, if this is an array of arrays we are screwed array.put(obj != null ? getJsonValue(obj, obj.getClass(), null /* field */) : null); } return array; } /** * * @param clazz * @return the first method annotated with {@link JsonValue} or null if one does not exist. */ @Nullable private Method getJsonValueMethod(Class clazz) { synchronized (mJsonValueMethodCache) { Method method = mJsonValueMethodCache.get(clazz); if (method == null && !mJsonValueMethodCache.containsKey(clazz)) { method = getJsonValueMethodImpl(clazz); mJsonValueMethodCache.put(clazz, method); } return method; } } @Nullable private static Method getJsonValueMethodImpl(Class clazz) { Method[] methods = clazz.getMethods(); for(int i = 0; i < methods.length; ++i) { Annotation jsonValue = methods[i].getAnnotation(JsonValue.class); if (jsonValue != null) { return methods[i]; } } return null; } private static boolean canDirectlySerializeClass(Class clazz) { return isWrapperOrPrimitiveType(clazz) || clazz.equals(String.class); } private static boolean isWrapperOrPrimitiveType(Class clazz) { return clazz.isPrimitive() || clazz.equals(Boolean.class) || clazz.equals(Integer.class) || clazz.equals(Character.class) || clazz.equals(Byte.class) || clazz.equals(Short.class) || clazz.equals(Double.class) || clazz.equals(Long.class) || clazz.equals(Float.class); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/json/annotation/JsonProperty.java ================================================ package me.moxun.dreamcatcher.connector.json.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface JsonProperty { boolean required() default false; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/json/annotation/JsonValue.java ================================================ package me.moxun.dreamcatcher.connector.json.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface JsonValue { } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/DisconnectReceiver.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc; /** * @see JsonRpcPeer#registerDisconnectReceiver(DisconnectReceiver) */ public interface DisconnectReceiver { /** * Invoked when a WebSocket peer disconnects. */ void onDisconnect(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/JsonRpcException.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcError; import me.moxun.dreamcatcher.connector.util.Util; public class JsonRpcException extends Exception { private final JsonRpcError mErrorMessage; public JsonRpcException(JsonRpcError errorMessage) { super(errorMessage.code + ": " + errorMessage.message); mErrorMessage = Util.throwIfNull(errorMessage); } public JsonRpcError getErrorMessage() { return mErrorMessage; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/JsonRpcPeer.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc; import android.database.Observable; import org.json.JSONObject; import java.nio.channels.NotYetConnectedException; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import me.moxun.dreamcatcher.connector.json.ObjectMapper; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcRequest; import me.moxun.dreamcatcher.connector.util.Util; import me.moxun.dreamcatcher.connector.websocket.SimpleSession; @ThreadSafe public class JsonRpcPeer { private final SimpleSession mPeer; private final ObjectMapper mObjectMapper; @GuardedBy("this") private long mNextRequestId; @GuardedBy("this") private final Map mPendingRequests = new HashMap<>(); private final DisconnectObservable mDisconnectObservable = new DisconnectObservable(); public JsonRpcPeer(ObjectMapper objectMapper, SimpleSession peer) { mObjectMapper = objectMapper; mPeer = Util.throwIfNull(peer); } public SimpleSession getWebSocket() { return mPeer; } public void invokeMethod(String method, Object paramsObject, @Nullable PendingRequestCallback callback) throws NotYetConnectedException { Util.throwIfNull(method); Long requestId = (callback != null) ? preparePendingRequest(callback) : null; // magic, can basically convert anything for some amount of runtime overhead... JSONObject params = mObjectMapper.convertValue(paramsObject, JSONObject.class); JsonRpcRequest message = new JsonRpcRequest(requestId, method, params); String requestString; JSONObject jsonObject = mObjectMapper.convertValue(message, JSONObject.class); requestString = jsonObject.toString(); mPeer.sendText(requestString); } public void registerDisconnectReceiver(DisconnectReceiver callback) { mDisconnectObservable.registerObserver(callback); } public void unregisterDisconnectReceiver(DisconnectReceiver callback) { mDisconnectObservable.unregisterObserver(callback); } public void invokeDisconnectReceivers() { mDisconnectObservable.onDisconnect(); } private synchronized long preparePendingRequest(PendingRequestCallback callback) { long requestId = mNextRequestId++; mPendingRequests.put(requestId, new PendingRequest(requestId, callback)); return requestId; } public synchronized PendingRequest getAndRemovePendingRequest(long requestId) { return mPendingRequests.remove(requestId); } private static class DisconnectObservable extends Observable { public void onDisconnect() { for (int i = 0, N = mObservers.size(); i < N; ++i) { final DisconnectReceiver observer = mObservers.get(i); observer.onDisconnect(); } } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/JsonRpcResult.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcResponse; /** * Marker interface used to denote a JSON-RPC result. After conversion from Jackson, * this will be placed into {@link JsonRpcResponse#result}. */ public interface JsonRpcResult { } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/PendingRequest.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc; import javax.annotation.Nullable; /** * Represents an outstanding request to the peer (issued by us). This callback will be * fired when the server responds. Note that with JSON-RPC, there is a special kind of * request called a notification which does not require a callback (and thus won't use * this class). */ public class PendingRequest { public final long requestId; public final @Nullable PendingRequestCallback callback; public PendingRequest(long requestId, @Nullable PendingRequestCallback callback) { this.requestId = requestId; this.callback = callback; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/PendingRequestCallback.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc; import me.moxun.dreamcatcher.connector.jsonrpc.protocol.JsonRpcResponse; public interface PendingRequestCallback { void onResponse(JsonRpcPeer peer, JsonRpcResponse response); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/protocol/EmptyResult.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc.protocol; import me.moxun.dreamcatcher.connector.jsonrpc.JsonRpcResult; public class EmptyResult implements JsonRpcResult { } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/protocol/JsonRpcError.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc.protocol; import android.annotation.SuppressLint; import org.json.JSONObject; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; import me.moxun.dreamcatcher.connector.json.annotation.JsonValue; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcError { @JsonProperty(required = true) public ErrorCode code; @JsonProperty(required = true) public String message; @JsonProperty public JSONObject data; public JsonRpcError() { } public JsonRpcError(ErrorCode code, String message, @Nullable JSONObject data) { this.code = code; this.message = message; this.data = data; } public enum ErrorCode { PARSER_ERROR(-32700), INVALID_REQUEST(-32600), METHOD_NOT_FOUND(-32601), INVALID_PARAMS(-32602), INTERNAL_ERROR(-32603); private final int mProtocolValue; private ErrorCode(int protocolValue) { mProtocolValue = protocolValue; } @JsonValue public int getProtocolValue() { return mProtocolValue; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/protocol/JsonRpcEvent.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc.protocol; import android.annotation.SuppressLint; import org.json.JSONObject; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcEvent { @JsonProperty(required = true) public String method; @JsonProperty public JSONObject params; public JsonRpcEvent() { } public JsonRpcEvent(String method, @Nullable JSONObject params) { this.method = method; this.params = params; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/protocol/JsonRpcRequest.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc.protocol; import android.annotation.SuppressLint; import org.json.JSONObject; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; @SuppressLint({"UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse"}) public class JsonRpcRequest { /** * This field is not required so that we can support JSON-RPC "notification" requests. */ @JsonProperty public Long id; @JsonProperty(required = true) public String method; @JsonProperty public JSONObject params; public JsonRpcRequest() { } @Override public String toString() { return "Id:" + id + ",method:" + method + ",params:" + params; } public JsonRpcRequest(Long id, String method, JSONObject params) { this.id = id; this.method = method; this.params = params; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/jsonrpc/protocol/JsonRpcResponse.java ================================================ package me.moxun.dreamcatcher.connector.jsonrpc.protocol; import android.annotation.SuppressLint; import org.json.JSONObject; import me.moxun.dreamcatcher.connector.json.annotation.JsonProperty; @SuppressLint({ "UsingDefaultJsonDeserializer", "EmptyJsonPropertyUse" }) public class JsonRpcResponse { @JsonProperty(required = true) public long id; @JsonProperty public JSONObject result; @JsonProperty public JSONObject error; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/log/AELog.java ================================================ package me.moxun.dreamcatcher.connector.log; /** * 输出日志到Chrome控制台 * Created by moxun on 16/4/25. */ public class AELog { private static final IAELog log; private static boolean isLoggable = true; static { log = new AELogImpl(); } public static void setLoggable(boolean isLoggable) { AELog.isLoggable = isLoggable; } public static void l(String msg) { if (isLoggable) { log.l(msg); } } public static void i(String msg) { if (isLoggable) { log.i(msg); } } public static void d(String msg) { if (isLoggable) { log.d(msg); } } public static void w(String msg) { if (isLoggable) { log.w(msg); } } public static void e(String msg) { if (isLoggable) { log.e(msg); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/log/AELogImpl.java ================================================ package me.moxun.dreamcatcher.connector.log; import me.moxun.dreamcatcher.connector.console.CLog; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Console; /** * Created by moxun on 16/4/25. */ final class AELogImpl implements IAELog { @Override public boolean isLoggable() { return true; } @Override public void l(String msg) { if (isLoggable()) { log(Console.MessageLevel.LOG, msg); } } @Override public void i(String msg) { if (isLoggable()) { log(Console.MessageLevel.INFO, msg); } } @Override public void d(String msg) { if (isLoggable()) { log(Console.MessageLevel.DEBUG, msg); } } @Override public void w(String msg) { if (isLoggable()) { log(Console.MessageLevel.WARNING, msg); } } @Override public void e(String msg) { if (isLoggable()) { log(Console.MessageLevel.ERROR, msg); } } private static void log(Console.MessageLevel level, String note) { CLog.writeToConsole(level, Console.MessageSource.CONSOLE_API, note); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/log/IAELog.java ================================================ package me.moxun.dreamcatcher.connector.log; /** * Created by moxun on 16/4/25. */ public interface IAELog { boolean isLoggable(); void l(String msg); void i(String msg); void d(String msg); void w(String msg); void e(String msg); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/manager/Lifecycle.java ================================================ package me.moxun.dreamcatcher.connector.manager; /** * Created by moxun on 16/12/9. */ public interface Lifecycle { int SHUTDOWN = 1; int LOCAL_SERVER_SOCKET_OPENING = 2; int WAITING_FOR_DISCOVERY = 3; int CHROME_DISCOVERY_CONNECTED = 4; int WAITING_FOR_WEBSOCKET = 5; int WEBSOCKET_SESSION_OPENING = 6; int WEBSOCKET_SESSION_CLOSED = 7; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/manager/SimpleConnectorLifecycleManager.java ================================================ package me.moxun.dreamcatcher.connector.manager; import me.moxun.dreamcatcher.connector.inspector.ChromeDiscoveryHandler; /** * Created by moxun on 16/12/9. */ public class SimpleConnectorLifecycleManager { private static int CURRENT_STATE = Lifecycle.SHUTDOWN; private static boolean PROXY_ENABLED = false; public static void setCurrentState(int state) { CURRENT_STATE = state; } public static int getCurrentState() { return CURRENT_STATE; } public static boolean isSessionActive() { return CURRENT_STATE == Lifecycle.WEBSOCKET_SESSION_OPENING; } public static boolean isProxyEnabled() { return PROXY_ENABLED; } public static void setProxyEnabled(boolean proxyEnabled) { PROXY_ENABLED = proxyEnabled; ChromeDiscoveryHandler.setInvalid(true); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/AsyncPrettyPrinter.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; /** * Interface that callers need to implement in order to pretty print binary payload received by DreamCatcher */ public interface AsyncPrettyPrinter { /** * Prints the prettified version of payload to output. This method can block * for a certain period of time. Note that DreamCatcher may impose arbitrary * time out on this method. * * @param output Writes the prettified version of payload * @param payload Response stream that has the raw data to be prettified * @throws IOException */ public void printTo(PrintWriter output, InputStream payload) throws IOException; /** * Specifies the type of pretty printed content. Note that this method is called * before the content is actually pretty printed. DreamCatcher uses this * method to make a hopeful guess of the type of prettified content * * @return an enum defined by PrettyPrinterDisplayType class */ public PrettyPrinterDisplayType getPrettifiedType(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/AsyncPrettyPrinterExecutorHolder.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.Nullable; /** * A holder class for the executor service used for pretty printing related tasks */ final class AsyncPrettyPrinterExecutorHolder { private static ExecutorService sExecutorService; private AsyncPrettyPrinterExecutorHolder() { } public static void ensureInitialized() { if (sExecutorService == null) { sExecutorService = Executors.newCachedThreadPool(); } } @Nullable public static ExecutorService getExecutorService() { return sExecutorService; } public static void shutdown() { sExecutorService.shutdown(); sExecutorService = null; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/AsyncPrettyPrinterFactory.java ================================================ package me.moxun.dreamcatcher.connector.reporter; /** * Interface for creating a factory for asynchronous pretty printers */ public interface AsyncPrettyPrinterFactory { /** * Creates an asynchronous pretty printer. This method must not be blocking. * * @param headerName header name of a response which is used to associate * with an asynchronous pretty printer * @param headerValue header value of a response which contains the URI for * the schema data needed to pretty print the response body * @return an asynchronous pretty printer to prettify the response body */ public AsyncPrettyPrinter getInstance(String headerName, String headerValue); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/AsyncPrettyPrinterInitializer.java ================================================ package me.moxun.dreamcatcher.connector.reporter; /** * Interface that is called if AsyncPrettyPrinterRegistry is unpopulated when * the first peer connects to DreamCatcher. It is responsible for registering header * names and their corresponding pretty printers */ public interface AsyncPrettyPrinterInitializer { /** * Populates AsyncPrettyPrinterRegistry with header names and their corresponding pretty * printers. This is responsible for registering all {@link AsyncPrettyPrinter} to the * provided registry. * @param registry */ void populatePrettyPrinters(AsyncPrettyPrinterRegistry registry); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/AsyncPrettyPrinterRegistry.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe public class AsyncPrettyPrinterRegistry { private final Map mRegistry = new HashMap<>(); public synchronized void register(String headerName, AsyncPrettyPrinterFactory factory) { mRegistry.put(headerName, factory); } @Nullable public synchronized AsyncPrettyPrinterFactory lookup(String headerName) { return mRegistry.get(headerName); } public synchronized boolean unregister(String headerName) { return mRegistry.remove(headerName) != null; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/CountingOutputStream.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; class CountingOutputStream extends FilterOutputStream { private long mCount; public CountingOutputStream(OutputStream out) { super(out); } public long getCount() { return mCount; } @Override public void write(int oneByte) throws IOException { out.write(oneByte); mCount++; } @Override public void write(byte[] buffer) throws IOException { write(buffer, 0, buffer.length); } @Override public void write(byte[] buffer, int offset, int length) throws IOException { out.write(buffer, offset, length); mCount += length; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/DecompressionHelper.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.InflaterOutputStream; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.console.CLog; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Console; // @VisibleForTest public class DecompressionHelper { static final String GZIP_ENCODING = "gzip"; static final String DEFLATE_ENCODING = "deflate"; public static InputStream teeInputWithDecompression( NetworkPeerManager peerManager, String requestId, InputStream availableInputStream, OutputStream decompressedOutput, @Nullable String contentEncoding, ResponseHandler responseHandler) throws IOException { OutputStream output = decompressedOutput; CountingOutputStream decompressedCounter = null; if (contentEncoding != null) { boolean gzipEncoding = GZIP_ENCODING.equals(contentEncoding); boolean deflateEncoding = DEFLATE_ENCODING.equals(contentEncoding); if (gzipEncoding || deflateEncoding) { decompressedCounter = new CountingOutputStream(decompressedOutput); if (gzipEncoding) { output = GunzippingOutputStream.create(decompressedCounter); } else if (deflateEncoding) { output = new InflaterOutputStream(decompressedCounter); } } else { CLog.writeToConsole( peerManager, Console.MessageLevel.WARNING, Console.MessageSource.NETWORK, "Unsupported Content-Encoding in response for request #" + requestId + ": " + contentEncoding); } } return new ResponseHandlingInputStream( availableInputStream, requestId, output, decompressedCounter, peerManager, responseHandler); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/DefaultResponseHandler.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.IOException; /** * Simple interceptor that delegates response read events to {@link NetworkEventReporter}. */ public class DefaultResponseHandler implements ResponseHandler { private final NetworkEventReporter mEventReporter; private final String mRequestId; private int mBytesRead = 0; private int mDecodedBytesRead = -1; public DefaultResponseHandler(NetworkEventReporter eventReporter, String requestId) { mEventReporter = eventReporter; mRequestId = requestId; } @Override public void onRead(int numBytes) { mBytesRead += numBytes; } @Override public void onReadDecoded(int numBytes) { if (mDecodedBytesRead == -1) { mDecodedBytesRead = 0; } mDecodedBytesRead += numBytes; } public void onEOF() { reportDataReceived(); mEventReporter.responseReadFinished(mRequestId); } public void onError(IOException e) { reportDataReceived(); mEventReporter.responseReadFailed(mRequestId, e.toString()); } private void reportDataReceived() { mEventReporter.dataReceived( mRequestId, mBytesRead, mDecodedBytesRead >= 0 ? mDecodedBytesRead : mBytesRead); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/DownloadingAsyncPrettyPrinterFactory.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.util.ExceptionUtil; import me.moxun.dreamcatcher.connector.util.Util; /** * Abstract class for pretty printer factory that asynchronously downloads schema needed for * pretty printing the payload */ public abstract class DownloadingAsyncPrettyPrinterFactory implements AsyncPrettyPrinterFactory { @Override public AsyncPrettyPrinter getInstance(final String headerName, final String headerValue) { final MatchResult result = matchAndParseHeader(headerName, headerValue); if (result == null) { return null; } String uri = result.getSchemaUri(); URL schemaURL = parseURL(uri); if (schemaURL == null) { return getErrorAsyncPrettyPrinter(headerName, headerValue); } else { ExecutorService executorService = AsyncPrettyPrinterExecutorHolder.getExecutorService(); if (executorService == null) { //last peer is unregistered... return null; } final Future response = executorService.submit(new Request(schemaURL)); return new AsyncPrettyPrinter() { public void printTo(PrintWriter output, InputStream payload) throws IOException { try { String schema; try { schema = response.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (IOException.class.isInstance(cause)) { doErrorPrint( output, payload, "Cannot successfully download schema: " + e.getMessage()); return; } else { throw e; } } doPrint(output, payload, schema); } catch (InterruptedException e) { doErrorPrint( output, payload, "Encountered spurious interrupt while downloading schema for pretty printing: " + e.getMessage()); } catch (ExecutionException e) { Throwable cause = e.getCause(); throw ExceptionUtil.propagate(cause); } } public PrettyPrinterDisplayType getPrettifiedType() { return result.getDisplayType(); } }; } } /** * Match the correct header that contains information about the schema uri * @param headerName header name of a response that needs to be pretty printed * @param headerValue header value which contains the URI for * the schema data needed to pretty print the response body * @return MatchResult that has the schema uri and the type of prettified result. Null * if there is no correct header match. */ @Nullable protected abstract MatchResult matchAndParseHeader(String headerName, String headerValue); /** * Note that the IOException thrown by this method will be propagated all the way up and * yield an error to the chrome devtools */ protected abstract void doPrint(PrintWriter output, InputStream payload, String schema) throws IOException; @Nullable private static URL parseURL(String uri) { try { return new URL(uri); } catch (MalformedURLException e) { return null; } } private static void doErrorPrint(PrintWriter output, InputStream payload, String errorMessage) throws IOException { output.print(errorMessage + "\n" + Util.readAsUTF8(payload)); } private static AsyncPrettyPrinter getErrorAsyncPrettyPrinter( final String headerName, final String headerValue) { return new AsyncPrettyPrinter() { @Override public void printTo(PrintWriter output, InputStream payload) throws IOException { String errorMessage = "[Failed to parse header: " + headerName + " : " + headerValue + " ]"; doErrorPrint(output, payload, errorMessage); } @Override public PrettyPrinterDisplayType getPrettifiedType() { return PrettyPrinterDisplayType.TEXT; } }; } protected class MatchResult { private final String mSchemaUri; private final PrettyPrinterDisplayType mDisplayType; public MatchResult(String schemaUri, PrettyPrinterDisplayType displayType) { mSchemaUri = schemaUri; mDisplayType = displayType; } public String getSchemaUri() { return mSchemaUri; } public PrettyPrinterDisplayType getDisplayType() { return mDisplayType; } } private static class Request implements Callable { private URL url; public Request(URL url) { this.url = url; } @Override public String call() throws IOException { HttpURLConnection connection = (HttpURLConnection)url.openConnection(); int statusCode = connection.getResponseCode(); if (statusCode != 200) { throw new IOException("Got status code: " + statusCode + " while downloading " + "schema with url: " + url.toString()); } InputStream urlStream = connection.getInputStream(); try { return Util.readAsUTF8(urlStream); } finally { urlStream.close(); } } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/GunzippingOutputStream.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.zip.GZIPInputStream; import me.moxun.dreamcatcher.connector.util.ExceptionUtil; import me.moxun.dreamcatcher.connector.util.Util; /** * An {@link OutputStream} filter which decompresses gzip data before it is written to the * specified destination output stream. This is functionally equivalent to * {@link java.util.zip.InflaterOutputStream} but provides gzip header awareness. The * implementation however is very different to avoid actually interpreting the gzip header. */ class GunzippingOutputStream extends FilterOutputStream { private final Future mCopyFuture; private static final ExecutorService sExecutor = Executors.newCachedThreadPool(); public static GunzippingOutputStream create(OutputStream finalOut) throws IOException { PipedInputStream pipeIn = new PipedInputStream(); PipedOutputStream pipeOut = new PipedOutputStream(pipeIn); Future copyFuture = sExecutor.submit( new GunzippingCallable(pipeIn, finalOut)); return new GunzippingOutputStream(pipeOut, copyFuture); } private GunzippingOutputStream(OutputStream out, Future copyFuture) throws IOException { super(out); mCopyFuture = copyFuture; } @Override public void close() throws IOException { boolean success = false; try { super.close(); success = true; } finally { try { getAndRethrow(mCopyFuture); } catch (IOException e) { if (success) { throw e; } } } } private static T getAndRethrow(Future future) throws IOException { while (true) { try { return future.get(); } catch (InterruptedException e) { // Continue... } catch (ExecutionException e) { Throwable cause = e.getCause(); ExceptionUtil.propagateIfInstanceOf(cause, IOException.class); ExceptionUtil.propagate(cause); } } } private static class GunzippingCallable implements Callable { private final InputStream mIn; private final OutputStream mOut; public GunzippingCallable(InputStream in, OutputStream out) { mIn = in; mOut = out; } @Override public Void call() throws IOException { GZIPInputStream in = new GZIPInputStream(mIn); try { Util.copy(in, mOut, new byte[1024]); } finally { in.close(); mOut.close(); } return null; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/MimeMatcher.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import android.annotation.SuppressLint; import java.util.ArrayList; import javax.annotation.Nullable; public class MimeMatcher { private final ArrayList mRuleMap = new ArrayList(); /** * Add a matching rule in the canonical MIME T form such as "image/*" or a MIME T * literal such as "text/html". * * @param ruleExpression Expression to match, in the order it was added. * @param resultIfMatched Result if this expression matches. */ public void addRule(String ruleExpression, T resultIfMatched) { mRuleMap.add(new MimeMatcherRule(ruleExpression, resultIfMatched)); } public void clear() { mRuleMap.clear(); } @Nullable public T match(String mimeT) { int ruleMapN = mRuleMap.size(); for (int i = 0; i < ruleMapN; i++) { MimeMatcherRule rule = mRuleMap.get(i); if (rule.match(mimeT)) { return rule.getResultIfMatched(); } } return null; } @SuppressLint("BadMethodUse-java.lang.String.length") private class MimeMatcherRule { private final boolean mHasWildcard; private final String mMatchPrefix; private final T mResultIfMatched; public MimeMatcherRule(String ruleExpression, T resultIfMatched) { if (ruleExpression.endsWith("*")) { mHasWildcard = true; mMatchPrefix = ruleExpression.substring(0, ruleExpression.length() - 1); } else { mHasWildcard = false; mMatchPrefix = ruleExpression; } if (mMatchPrefix.contains("*")) { throw new IllegalArgumentException("Multiple wildcards present in rule expression " + ruleExpression); } mResultIfMatched = resultIfMatched; } public boolean match(String mimeType) { // Make sure the string literal matches. if (!mimeType.startsWith(mMatchPrefix)) { return false; } // If we have a wildcard and the prefix matches, then we've matched; otherwise if the // string literal and the mime T are the same length then we must have a match. return (mHasWildcard || mimeType.length() == mMatchPrefix.length()); } public T getResultIfMatched() { return mResultIfMatched; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/NetworkEventReporter.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Network; /** * Interface that callers must invoke in order to supply data to the Network tab in * the WebKit Inspector. * *
 * requestWillBeSent +---> responseHeadersReceived +---> interpretResponseStream
 *                   |           |                 |
 *                   |           `---> dataSent    |
 *                   |                             |
 *                   `-----------------------------`--------> httpExchangeFailed
 * 
* * Note that {@link #interpretResponseStream} combined with {@link DefaultResponseHandler} * will automatically invoke {@link #dataReceived}, {@link #responseReadFailed} and * {@link #responseReadFinished}. If you use your own custom {@link ResponseHandler} you * must be sure to invoke these methods manually. */ public interface NetworkEventReporter { /** * Returns true if there is at least one peer listening for network events; false otherwise. * This value is provided as an optimization to avoid expensive work when the WebKit Inspector is * not being used. It is otherwise safe to invoke methods defined in this interface when * the value is false. */ boolean isEnabled(); /** * Indicates that a request is about to be sent, but has not yet been delivered over the wire. * * @param request Request descriptor. */ void requestWillBeSent(InspectorRequest request); /** * Indicates that a response message was just received from the network, but the body * has not yet been read. * * @param response Response descriptor. */ void responseHeadersReceived(InspectorResponse response); /** * Indicates that communication with the server has failed. You are expected to call this for any * exception before you call {@link #interpretResponseStream}. After * {@link #interpretResponseStream} is called we will reporting any * {@link IOException} during reading from the {@link InputStream}. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param errorText Text to report for the error; using {@link IOException#toString()} is * recommended. */ void httpExchangeFailed(String requestId, String errorText); /** * Intercept the stream as given by the underlying HTTP library that contains the body of the * response. In order to have the response show up in inspector (and to have the request be * completed successfully) you need to call this AND read until exhaustion/EOF of the returned * stream. * *

* We will internally signal a failure if there is an {@link IOException} received while reading * from the stream. * *

* Do not invoke {@link #httpExchangeFailed(String, String)} after calling this method. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param contentType The {@code Content-Type} header value that was specified in * {@link InspectorResponse}. This header is used to determine the appropriate * storage format for the body. For instance, {@code image/*} is necessary to cause * images to appear in the Inspector UI. * @param contentEncoding The {@code Content-Encoding} header value that was specified in * {@link InspectorResponse}. This header is used to determine what type of decompression * is to be applied when delivering the raw response stream to the debugging interface. * If null, no decompression will be used. * @param inputStream Response stream if applicable ("HEAD" for instance does not have a body). * {@code null} otherwise. * @param responseHandler Callback to forward stream events back to the relevant event reporter * methods. Recommend using {@link DefaultResponseHandler} for most callers. * * @return {@link InputStream} that has been intercepted if WebkitInspector is active and enabled * otherwise it will return {@code inputStream} */ @Nullable InputStream interpretResponseStream( String requestId, @Nullable String contentType, @Nullable String contentEncoding, @Nullable InputStream inputStream, ResponseHandler responseHandler); /** * Indicates that there was a failure while reading from response stream. If you use * {@link #interpretResponseStream} with {@link DefaultResponseHandler} (as is recommended), * this method will be invoked automatically for you. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param errorText Text to report for the error; using {@link IOException#toString()} is * recommended. */ void responseReadFailed(String requestId, String errorText); /** * Indicates that the response stream has been fully exhausted and the request is now * complete. If you use {@link #interpretResponseStream} with {@link DefaultResponseHandler} * (as is recommended), this method will be invoked automatically for you. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} */ void responseReadFinished(String requestId); /** * Indicates that raw data was sent over the network. It is permissible to invoke this * method just once after the full size of the request is known. *

* Invoking this method is optional and merely provides additional timing metrics and actual * payload sizes to the Inspector UI. * * @param requestId Unique identifier for the request as per {@link InspectorRequest#id()} * @param dataLength Uncompressed data segment length * @param encodedDataLength Compressed data segment length */ void dataSent(String requestId, int dataLength, int encodedDataLength); /** * Indicates that raw data was received from the network. * * @see #dataSent */ void dataReceived(String requestId, int dataLength, int encodedDataLength); /** * Represents the request that will be sent over HTTP. Note that for many implementations * of HTTP the request constructed may differ from the request actually sent over the wire. * For instance, additional headers like {@code Host}, {@code User-Agent}, {@code Content-Type}, * etc may not be part of this request but should be injected if necessary. Some stacks offer * inspection of the raw request about to be sent to the server which is preferable. */ interface InspectorRequest extends InspectorHeaders { /** * Unique identifier for this request. This identifier must be used in all other network * events corresponding to this request. Identifiers may be re-used after * {@link NetworkEventReporter#httpExchangeFailed} or {@link NetworkEventReporter#loadingFinished} * are invoked. */ String id(); /** * Arbitrary debug-friendly name of the request. */ String friendlyName(); /** * Provide an extra integer to decorate the {@link #friendlyName()}. This shows up next to * it in the WebKit Inspector UI and can be used to indicate things like request priority. */ @Nullable Integer friendlyNameExtra(); String url(); /** * HTTP method ("GET", "POST", "DELETE", etc). */ String method(); /** * Provide the body if part of an entity-enclosing request (like "POST" or "PUT"). May * return null otherwise. */ @Nullable byte[] body() throws IOException; } interface InspectorResponse extends InspectorHeaders { /** @see InspectorRequest#id() */ String requestId(); String url(); int statusCode(); String reasonPhrase(); /** * True if the response was furnished on a re-used socket; false otherwise or if unknown. */ boolean connectionReused(); /** * Unique connection identifier representing the socket that was used to furnish the response. */ int connectionId(); /** * True if the response was furnished by disk cache; false otherwise or if unknown. */ boolean fromDiskCache(); @Nullable Network.ResourceTiming getTiming(); } interface InspectorHeaders { int headerCount(); String headerName(int index); String headerValue(int index); @Nullable String firstHeaderValue(String name); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/NetworkEventReporterImpl.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import android.os.SystemClock; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.console.CLog; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Console; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Network; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Page; import me.moxun.dreamcatcher.connector.util.Utf8Charset; /** * Implementation of {@link NetworkEventReporter} which allows callers to inform the DreamCatcher * system of network traffic. Callers can safely eagerly access this class and store a * reference if they wish. When WebKit Inspector clients are connected, the internal * implementation will be automatically wired up to them. */ public class NetworkEventReporterImpl implements NetworkEventReporter { @Nullable private ResourceTypeHelper mResourceTypeHelper; private static NetworkEventReporter sInstance; private NetworkEventReporterImpl() { } /** * Static accessor allowing callers to easily hook into the WebKit Inspector system without * creating dependencies on the main DreamCatcher initialization code path. */ public static synchronized NetworkEventReporter get() { if (sInstance == null) { sInstance = new NetworkEventReporterImpl(); } return sInstance; } @Override public boolean isEnabled() { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); return peerManager != null; } @Nullable private NetworkPeerManager getPeerManagerIfEnabled() { NetworkPeerManager peerManager = NetworkPeerManager.getInstanceOrNull(); if (peerManager != null && peerManager.hasRegisteredPeers()) { return peerManager; } return null; } @Override public void requestWillBeSent(InspectorRequest request) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.Request requestJSON = new Network.Request(); requestJSON.url = request.url(); requestJSON.method = request.method(); requestJSON.headers = formatHeadersAsJSON(request); requestJSON.postData = readBodyAsString(peerManager, request); // Hack to use the initiator of SCRIPT to generate a fake call stack that includes // the request's "friendly" name. String requestFriendlyName = request.friendlyName(); Integer requestPriority = request.friendlyNameExtra(); Network.Initiator initiatorJSON = new Network.Initiator(); initiatorJSON.type = Network.InitiatorType.SCRIPT; initiatorJSON.stackTrace = new ArrayList(); initiatorJSON.stackTrace.add(new Console.CallFrame(requestFriendlyName, requestFriendlyName, requestPriority != null ? requestPriority : 0 /* lineNumber */, 0 /* columnNumber */)); Network.RequestWillBeSentParams params = new Network.RequestWillBeSentParams(); params.requestId = request.id(); params.frameId = "1"; params.loaderId = "1"; params.documentURL = request.url(); params.request = requestJSON; params.timestamp = DreamCatcherNow() / 1000.0; params.initiator = initiatorJSON; params.redirectResponse = null; // Type is now required as of at least WebKit Inspector rev @188492. If you don't send // it, Chrome will refuse to draw the row in the Network tab until the response is // received (providing the type). This delay is very noticable on slow networks. params.type = Page.ResourceType.OTHER; peerManager.sendNotificationToPeers("Network.requestWillBeSent", params); } } @Nullable private static String readBodyAsString( NetworkPeerManager peerManager, InspectorRequest request) { try { byte[] body = request.body(); if (body != null) { return new String(body, Utf8Charset.INSTANCE); } } catch (IOException | OutOfMemoryError e) { CLog.writeToConsole( peerManager, Console.MessageLevel.WARNING, Console.MessageSource.NETWORK, "Could not reproduce POST body: " + e); } return null; } @Override public void responseHeadersReceived(InspectorResponse response) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.Response responseJSON = new Network.Response(); responseJSON.url = response.url(); responseJSON.status = response.statusCode(); responseJSON.statusText = response.reasonPhrase(); responseJSON.headers = formatHeadersAsJSON(response); String contentType = getContentType(response); responseJSON.mimeType = contentType != null ? getResourceTypeHelper().stripContentExtras(contentType) : "application/octet-stream"; responseJSON.connectionReused = response.connectionReused(); responseJSON.connectionId = response.connectionId(); responseJSON.fromDiskCache = response.fromDiskCache(); //holding responseJSON.timing = response.getTiming(); Network.ResponseReceivedParams receivedParams = new Network.ResponseReceivedParams(); receivedParams.requestId = response.requestId(); receivedParams.frameId = "1"; receivedParams.loaderId = "1"; receivedParams.timestamp = DreamCatcherNow() / 1000.0; receivedParams.response = responseJSON; AsyncPrettyPrinter asyncPrettyPrinter = initAsyncPrettyPrinterForResponse(response, peerManager); receivedParams.type = determineResourceType(asyncPrettyPrinter, contentType, getResourceTypeHelper()); peerManager.sendNotificationToPeers("Network.responseReceived", receivedParams); } } @Nullable private static AsyncPrettyPrinter initAsyncPrettyPrinterForResponse( InspectorResponse response, NetworkPeerManager peerManager) { AsyncPrettyPrinterRegistry registry = peerManager.getAsyncPrettyPrinterRegistry(); AsyncPrettyPrinter asyncPrettyPrinter = createPrettyPrinterForResponse(response, registry); if (asyncPrettyPrinter != null) { peerManager.getResponseBodyFileManager().associateAsyncPrettyPrinterWithId( response.requestId(), asyncPrettyPrinter); } return asyncPrettyPrinter; } private static Page.ResourceType determineResourceType( AsyncPrettyPrinter asyncPrettyPrinter, String contentType, ResourceTypeHelper resourceTypeHelper) { if (asyncPrettyPrinter != null) { return asyncPrettyPrinter.getPrettifiedType().getResourceType(); } else { return contentType != null ? resourceTypeHelper.determineResourceType(contentType) : Page.ResourceType.OTHER; } } //@VisibleForTesting @Nullable static AsyncPrettyPrinter createPrettyPrinterForResponse( InspectorResponse response, @Nullable AsyncPrettyPrinterRegistry registry) { if (registry != null) { for (int i = 0, count = response.headerCount(); i < count; i++) { AsyncPrettyPrinterFactory factory = registry.lookup(response.headerName(i)); if (factory != null) { AsyncPrettyPrinter asyncPrettyPrinter = factory.getInstance( response.headerName(i), response.headerValue(i)); return asyncPrettyPrinter; } } } return null; } @Override public InputStream interpretResponseStream( String requestId, @Nullable String contentType, @Nullable String contentEncoding, @Nullable InputStream availableInputStream, ResponseHandler responseHandler) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { if (availableInputStream == null) { responseHandler.onEOF(); return null; } Page.ResourceType resourceType = contentType != null ? getResourceTypeHelper().determineResourceType(contentType) : null; // There's this weird logic at play that only knows how to base64 decode certain kinds of // resources. boolean base64Encode = false; if (resourceType != null && resourceType == Page.ResourceType.IMAGE) { base64Encode = true; } try { OutputStream fileOutputStream = peerManager.getResponseBodyFileManager().openResponseBodyFile( requestId, base64Encode); return DecompressionHelper.teeInputWithDecompression( peerManager, requestId, availableInputStream, fileOutputStream, contentEncoding, responseHandler); } catch (IOException e) { CLog.writeToConsole( peerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "Error writing response body data for request #" + requestId); } } return availableInputStream; } @Override public void httpExchangeFailed(String requestId, String errorText) { loadingFailed(requestId, errorText); } @Override public void responseReadFinished(String requestId) { loadingFinished(requestId); } private void loadingFinished(String requestId) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.LoadingFinishedParams finishedParams = new Network.LoadingFinishedParams(); finishedParams.requestId = requestId; finishedParams.timestamp = DreamCatcherNow() / 1000.0; peerManager.sendNotificationToPeers("Network.loadingFinished", finishedParams); } } @Override public void responseReadFailed(String requestId, String errorText) { loadingFailed(requestId, errorText); } private void loadingFailed(String requestId, String errorText) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.LoadingFailedParams failedParams = new Network.LoadingFailedParams(); failedParams.requestId = requestId; failedParams.timestamp = DreamCatcherNow() / 1000.0; failedParams.errorText = errorText; failedParams.type = Page.ResourceType.OTHER; peerManager.sendNotificationToPeers("Network.loadingFailed", failedParams); } } @Override public void dataSent( String requestId, int dataLength, int encodedDataLength) { // The inspector protocol only gives us the dataReceived event, but we can happily combine // upstream and downstream data into this to visualize the real size of the request, not // strictly the size of the "content" as reported in the UI. dataReceived(requestId, dataLength, encodedDataLength); } @Override public void dataReceived( String requestId, int dataLength, int encodedDataLength) { NetworkPeerManager peerManager = getPeerManagerIfEnabled(); if (peerManager != null) { Network.DataReceivedParams dataReceivedParams = new Network.DataReceivedParams(); dataReceivedParams.requestId = requestId; dataReceivedParams.timestamp = now(); dataReceivedParams.dataLength = dataLength; dataReceivedParams.encodedDataLength = encodedDataLength; peerManager.sendNotificationToPeers("Network.dataReceived", dataReceivedParams); } } @Nullable private String getContentType(InspectorHeaders headers) { // This may need to change in the future depending on how cumbersome header simulation // is for the various hooks we expose. return headers.firstHeaderValue("Content-Type"); } private static JSONObject formatHeadersAsJSON(InspectorHeaders headers) { JSONObject json = new JSONObject(); for (int i = 0; i < headers.headerCount(); i++) { String name = headers.headerName(i); String value = headers.headerValue(i); try { if (json.has(name)) { // Multiple headers are separated with a new line. json.put(name, json.getString(name) + "\n" + value); } else { json.put(name, value); } } catch (JSONException e) { throw new RuntimeException(e); } } return json; } @Nonnull private ResourceTypeHelper getResourceTypeHelper() { if (mResourceTypeHelper == null) { mResourceTypeHelper = new ResourceTypeHelper(); } return mResourceTypeHelper; } private static long DreamCatcherNow() { return SystemClock.elapsedRealtime(); } public static double now() { return DreamCatcherNow() / 1000.0; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/NetworkPeerManager.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import android.content.Context; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.inspector.helper.ChromePeerManager; import me.moxun.dreamcatcher.connector.inspector.helper.PeersRegisteredListener; import me.moxun.dreamcatcher.connector.util.Util; public class NetworkPeerManager extends ChromePeerManager { private static NetworkPeerManager sInstance; private final ResponseBodyFileManager mResponseBodyFileManager; private AsyncPrettyPrinterInitializer mPrettyPrinterInitializer; private AsyncPrettyPrinterRegistry mAsyncPrettyPrinterRegistry; @Nullable public static synchronized NetworkPeerManager getInstanceOrNull() { return sInstance; } public static synchronized NetworkPeerManager getOrCreateInstance(Context context) { if (sInstance == null) { sInstance = new NetworkPeerManager( new ResponseBodyFileManager( context.getApplicationContext())); } return sInstance; } public NetworkPeerManager( ResponseBodyFileManager responseBodyFileManager) { mResponseBodyFileManager = responseBodyFileManager; setListener(mTempFileCleanup); } public ResponseBodyFileManager getResponseBodyFileManager() { return mResponseBodyFileManager; } @Nullable public AsyncPrettyPrinterRegistry getAsyncPrettyPrinterRegistry() { return mAsyncPrettyPrinterRegistry; } public void setPrettyPrinterInitializer(AsyncPrettyPrinterInitializer initializer) { Util.throwIfNotNull(mPrettyPrinterInitializer); mPrettyPrinterInitializer = Util.throwIfNull(initializer); } private final PeersRegisteredListener mTempFileCleanup = new PeersRegisteredListener() { @Override protected void onFirstPeerRegistered() { AsyncPrettyPrinterExecutorHolder.ensureInitialized(); if (mAsyncPrettyPrinterRegistry == null && mPrettyPrinterInitializer != null) { mAsyncPrettyPrinterRegistry = new AsyncPrettyPrinterRegistry(); mPrettyPrinterInitializer.populatePrettyPrinters(mAsyncPrettyPrinterRegistry); } mResponseBodyFileManager.cleanupFiles(); } @Override protected void onLastPeerUnregistered() { mResponseBodyFileManager.cleanupFiles(); AsyncPrettyPrinterExecutorHolder.shutdown(); } }; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/PrettyPrinterDisplayType.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Page; public enum PrettyPrinterDisplayType { JSON(Page.ResourceType.XHR), HTML(Page.ResourceType.DOCUMENT), TEXT(Page.ResourceType.DOCUMENT), ; private final Page.ResourceType mResourceType; private PrettyPrinterDisplayType(Page.ResourceType resourceType) { mResourceType = resourceType; } /** * Converts PrettyPrinterDisplayType values to the appropriate * {@link Page.ResourceType} values that DreamCatcher understands */ public Page.ResourceType getResourceType() { return mResourceType; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/RequestBodyHelper.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.InflaterOutputStream; import javax.annotation.Nullable; /** * Helper which manages provides computed request sizes as well as transparent decompression. * Note that request compression is not officially part of the HTTP standard however it is * commonly in use and can be conveniently supported here. *

* To use, invoke {@link #createBodySink} to prepare an output stream where the raw body can be * written. Then invoke {@link #getDisplayBody()} to retrieve the possibly decoded body. * Finally, {@link #reportDataSent()} can be called to report to DreamCatcher the raw and decompressed * payload sizes. */ public class RequestBodyHelper { private final NetworkEventReporter mEventReporter; private final String mRequestId; private ByteArrayOutputStream mDeflatedOutput; private CountingOutputStream mDeflatingOutput; public RequestBodyHelper(NetworkEventReporter eventReporter, String requestId) { mEventReporter = eventReporter; mRequestId = requestId; } public OutputStream createBodySink(@Nullable String contentEncoding) throws IOException { OutputStream deflatingOutput; ByteArrayOutputStream deflatedOutput = new ByteArrayOutputStream(); if (DecompressionHelper.GZIP_ENCODING.equals(contentEncoding)) { deflatingOutput = GunzippingOutputStream.create(deflatedOutput); } else if (DecompressionHelper.DEFLATE_ENCODING.equals(contentEncoding)) { deflatingOutput = new InflaterOutputStream(deflatedOutput); } else { deflatingOutput = deflatedOutput; } mDeflatingOutput = new CountingOutputStream(deflatingOutput); mDeflatedOutput = deflatedOutput; return mDeflatingOutput; } public byte[] getDisplayBody() { throwIfNoBody(); return mDeflatedOutput.toByteArray(); } public boolean hasBody() { return mDeflatedOutput != null; } public void reportDataSent() { throwIfNoBody(); mEventReporter.dataSent( mRequestId, mDeflatedOutput.size(), (int)mDeflatingOutput.getCount()); } private void throwIfNoBody() { if (!hasBody()) { throw new IllegalStateException("No body found; has createBodySink been called?"); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/ResourceTypeHelper.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Page; public class ResourceTypeHelper { private final MimeMatcher mMimeMatcher; public ResourceTypeHelper() { mMimeMatcher = new MimeMatcher(); mMimeMatcher.addRule("text/css", Page.ResourceType.STYLESHEET); mMimeMatcher.addRule("image/*", Page.ResourceType.IMAGE); mMimeMatcher.addRule("application/x-javascript", Page.ResourceType.SCRIPT); // This is apparently important to allow the WebKit inspector to do JSON preview. I don't // know exactly why, but whatever. mMimeMatcher.addRule("text/javascript", Page.ResourceType.XHR); mMimeMatcher.addRule("application/json", Page.ResourceType.XHR); // Everything else gets a lame, unformatted blob. mMimeMatcher.addRule("text/*", Page.ResourceType.DOCUMENT); // I think this disables preview. Perhaps that's not what we want as the default but we'll // need some time to soak in real data to see for sure. mMimeMatcher.addRule("*", Page.ResourceType.OTHER); } public Page.ResourceType determineResourceType(String contentType) { String mimeType = stripContentExtras(contentType); return mMimeMatcher.match(mimeType); } /** * Strip out any extra data following the semicolon (e.g. \"text/javascript; charset=UTF-8"). * * @return MIME type with content extras stripped out (if there were any). */ public String stripContentExtras(String contentType) { int index = contentType.indexOf(';'); return (index >= 0) ? contentType.substring(0, index) : contentType; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/ResponseBodyData.java ================================================ package me.moxun.dreamcatcher.connector.reporter; /** * Special file data necessary to comply with the Chrome DevTools instance which doesn't let * us just naively base64 encode everything. */ public class ResponseBodyData { public String data; public boolean base64Encoded; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/ResponseBodyFileManager.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import android.content.Context; import android.util.Base64; import android.util.Base64OutputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import me.moxun.dreamcatcher.connector.util.ExceptionUtil; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.Util; /** * Manages temporary files created by {@link ChromeHttpFlowObserver} to serve request bodies. */ public class ResponseBodyFileManager { private static final String TAG = "ResponseBodyFileManager"; private static final String FILENAME_PREFIX = "network-response-body-"; private static final int PRETTY_PRINT_TIMEOUT_SEC = 10; private final Context mContext; private final Map mRequestIdMap = Collections.synchronizedMap( new HashMap()); public ResponseBodyFileManager(Context context) { mContext = context; } public void cleanupFiles() { for (File file : mContext.getFilesDir().listFiles()) { if (file.getName().startsWith(FILENAME_PREFIX)) { if (!file.delete()) { LogUtil.w(TAG, "Failed to delete " + file.getAbsolutePath()); } } } LogUtil.i(TAG, "Cleaned up temporary network files."); } public ResponseBodyData readFile(String requestId) throws IOException { InputStream in = mContext.openFileInput(getFilename(requestId)); try { int firstByte = in.read(); if (firstByte == -1) { throw new EOFException("Failed to read base64Encode byte"); } ResponseBodyData bodyData = new ResponseBodyData(); bodyData.base64Encoded = firstByte != 0; AsyncPrettyPrinter asyncPrettyPrinter = mRequestIdMap.get(requestId); if (asyncPrettyPrinter != null) { // TODO: this line blocks for up to 10 seconds and create problems as described // in issue #243 allow asynchronous dispatch for MethodDispatcher bodyData.data = prettyPrintContentWithTimeOut(asyncPrettyPrinter, in); } else { bodyData.data = Util.readAsUTF8(in); } return bodyData; } finally { in.close(); } } private String prettyPrintContentWithTimeOut( AsyncPrettyPrinter asyncPrettyPrinter, InputStream in) throws IOException { AsyncPrettyPrintingCallable prettyPrintingCallable = new AsyncPrettyPrintingCallable( in, asyncPrettyPrinter); ExecutorService executorService = AsyncPrettyPrinterExecutorHolder.getExecutorService(); if (executorService == null) { //last peer is unregistered... return null; } Future future = executorService.submit(prettyPrintingCallable); try { return Util.getUninterruptibly(future, PRETTY_PRINT_TIMEOUT_SEC, TimeUnit.SECONDS); } catch (TimeoutException e) { future.cancel(true); return "Time out after " + PRETTY_PRINT_TIMEOUT_SEC + " seconds of attempting to pretty print\n" + Util.readAsUTF8(in); } catch (ExecutionException e) { Throwable cause = e.getCause(); ExceptionUtil.propagateIfInstanceOf(cause, IOException.class); throw ExceptionUtil.propagate(cause); } } public OutputStream openResponseBodyFile(String requestId, boolean base64Encode) throws IOException { OutputStream out = mContext.openFileOutput(getFilename(requestId), Context.MODE_PRIVATE); out.write(base64Encode ? 1 : 0); if (base64Encode) { return new Base64OutputStream(out, Base64.DEFAULT); } else { return out; } } private static String getFilename(String requestId) { return FILENAME_PREFIX + requestId; } /** * Associates an asynchronous pretty printer with a response request id * The pretty printer will be used to pretty print the response body that has * the particular request id * * @param requestId Unique identifier for the response * as per {@link NetworkEventReporter.InspectorResponse#requestId()} * @param asyncPrettyPrinter Asynchronous Pretty Printer to pretty print the response body */ public void associateAsyncPrettyPrinterWithId( String requestId, AsyncPrettyPrinter asyncPrettyPrinter) { if (mRequestIdMap.put(requestId, asyncPrettyPrinter) != null) { throw new IllegalArgumentException("cannot associate different " + "pretty printers with the same request id: "+requestId); } } private class AsyncPrettyPrintingCallable implements Callable { private final InputStream mInputStream; private final AsyncPrettyPrinter mAsyncPrettyPrinter; public AsyncPrettyPrintingCallable( InputStream in, AsyncPrettyPrinter asyncPrettyPrinter) { mInputStream = in; mAsyncPrettyPrinter = asyncPrettyPrinter; } @Override public String call() throws IOException { return prettyPrintContent(mInputStream, mAsyncPrettyPrinter); } private String prettyPrintContent(InputStream in, AsyncPrettyPrinter asyncPrettyPrinter) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(out); asyncPrettyPrinter.printTo(writer, in); writer.flush(); return out.toString("UTF-8"); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/ResponseHandler.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.IOException; /** * Custom hook to intercept read events delivered by * {@link NetworkEventReporter#interpretResponseStream}. */ public interface ResponseHandler { /** * Signal that data has been read from the response stream. * * @param numBytes Bytes read from the network stack's stream as established by * {@link NetworkEventReporter#interpretResponseStream}. */ void onRead(int numBytes); /** * Signal that data has been decoded (reversing the response's {@code Content-Encoding}) while * reading a raw stream. This method is only called when the stream is known to have * a supported encoding. Note that for HTTP, content encoding almost always is used for * some form of response compression. * * @param numBytes Bytes yielded after decoding bytes received from the network stack's * stream. */ void onReadDecoded(int numBytes); /** * Signals that EOF has been reached reading the response stream from the network * stack. */ void onEOF(); /** * Signals that an error occurred while reading the response stream. */ void onError(IOException e); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/reporter/ResponseHandlingInputStream.java ================================================ package me.moxun.dreamcatcher.connector.reporter; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import me.moxun.dreamcatcher.connector.console.CLog; import me.moxun.dreamcatcher.connector.inspector.helper.ChromePeerManager; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Console; /** * {@link InputStream} that caches the data as the data is read, and writes them to the given * {@link OutputStream}. This also guarantees that we will attempt to reach EOF on the * {@link InputStream} passing all data to the {@link OutputStream}. * This is done to allow us to guarantee all responses are represented in the webkit inspector. */ // @VisibleForTest public final class ResponseHandlingInputStream extends FilterInputStream { public static final String TAG = "ResponseHandlingInputStream"; private static final int BUFFER_SIZE = 1024; private final String mRequestId; private final OutputStream mOutputStream; @Nullable private final CountingOutputStream mDecompressedCounter; private final ChromePeerManager mNetworkPeerManager; private final ResponseHandler mResponseHandler; /** * This stream will no longer be usable if {@link #close()} has been called on this stream. */ @GuardedBy("this") private boolean mClosed; @GuardedBy("this") private boolean mEofSeen; @Nullable @GuardedBy("this") private byte[] mSkipBuffer; private long mLastDecompressedCount = 0; /** * @param inputStream * @param requestId the requestId to use when we call the {@link NetworkEventReporter} * @param outputStream stream to write to. * @param decompressedCounter Optional decompressing counting output stream which * can be queried after each write to determine the number of decompressed bytes * yielded. Used to implement {@link ResponseHandler#onReadDecoded(int)}. * @param networkPeerManager A peer manager which is used to log internal errors to the * Inspector console. * @param responseHandler Special interface to intercept read events before they are sent * to peers via {@link NetworkEventReporter} methods. */ public ResponseHandlingInputStream( InputStream inputStream, String requestId, OutputStream outputStream, @Nullable CountingOutputStream decompressedCounter, ChromePeerManager networkPeerManager, ResponseHandler responseHandler) { super(inputStream); mRequestId = requestId; mOutputStream = outputStream; mDecompressedCounter = decompressedCounter; mNetworkPeerManager = networkPeerManager; mResponseHandler = responseHandler; mClosed = false; } private synchronized int checkEOF(int n) { if (n == -1) { closeOutputStreamQuietly(); mResponseHandler.onEOF(); mEofSeen = true; } return n; } @Override public int read() throws IOException { try { int result = checkEOF(in.read()); if (result != -1) { mResponseHandler.onRead(1); writeToOutputStream(result); } return result; } catch (IOException ex) { throw handleIOException(ex); } } @Override public int read(byte[] b) throws IOException { return this.read(b, 0, b.length); } @Override public int read(byte[] b, int off, int len) throws IOException { try { int result = checkEOF(in.read(b, off, len)); if (result != -1) { mResponseHandler.onRead(result); writeToOutputStream(b, off, result); } return result; } catch (IOException ex) { throw handleIOException(ex); } } @Override public synchronized long skip(long n) throws IOException { byte[] buffer = getSkipBufferLocked(); long total = 0; while (total < n) { long bytesDiff = n - total; int bytesToRead = (int) Math.min((long) buffer.length, bytesDiff); int result = this.read(buffer, 0, bytesToRead); if (result == -1) { break; } total += result; } return total; } @Nonnull private byte[] getSkipBufferLocked() { if (mSkipBuffer == null) { mSkipBuffer = new byte[BUFFER_SIZE]; } return mSkipBuffer; } @Override public boolean markSupported() { // this can be implemented, but isn't needed for TeedInputStream's behavior return false; } @Override public void mark(int readlimit) { // noop -- mark is not supported } @Override public void reset() throws IOException { throw new UnsupportedOperationException("Mark not supported"); } @Override public void close() throws IOException { try { long bytesRead = 0; if (!mEofSeen) { byte[] buffer = new byte[BUFFER_SIZE]; int count; while ((count = this.read(buffer)) != -1) { bytesRead += count; } } if (bytesRead > 0) { CLog.writeToConsole( mNetworkPeerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "There were " + String.valueOf(bytesRead) + " bytes that were not consumed while " + "processing request " + mRequestId); } } finally { super.close(); closeOutputStreamQuietly(); } } /** * Attempts to close all the output stream, and swallows any exceptions. */ private synchronized void closeOutputStreamQuietly() { if (!mClosed) { try { mOutputStream.close(); reportDecodedSizeIfApplicable(); } catch (IOException e) { CLog.writeToConsole( mNetworkPeerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "Could not close the output stream" + e); } finally { mClosed = true; } } } /** * Handles reporting an {@link IOException}. We do this so we can centralize the logic while still * maintaining the ability of the catch clause to throw. * @param ex * @return */ private IOException handleIOException(IOException ex) { mResponseHandler.onError(ex); return ex; } private void reportDecodedSizeIfApplicable() { if (mDecompressedCounter != null) { long currentCount = mDecompressedCounter.getCount(); int delta = (int)(currentCount - mLastDecompressedCount); mResponseHandler.onReadDecoded(delta); mLastDecompressedCount = currentCount; } } /** * Writes the byte to all the output streams. If we get an exception when writing to any * of the streams, we close all the streams, and then propagate the first exception that * occurred when writing. */ private synchronized void writeToOutputStream(int oneByte) { if (mClosed) { return; } try { mOutputStream.write(oneByte); reportDecodedSizeIfApplicable(); } catch (IOException e) { handleIOExceptionWritingToStream(e); } } /** * Same as {@link #writeToOutputStream(int)}, but we write a buffer instead. */ private synchronized void writeToOutputStream(byte[] b, int offset, int count) { if (mClosed) { return; } try { mOutputStream.write(b, offset, count); reportDecodedSizeIfApplicable(); } catch (IOException e) { handleIOExceptionWritingToStream(e); } } private void handleIOExceptionWritingToStream(IOException e) { CLog.writeToConsole( mNetworkPeerManager, Console.MessageLevel.ERROR, Console.MessageSource.NETWORK, "Could not write response body to the stream " + e); closeOutputStreamQuietly(); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/AddressNameHelper.java ================================================ package me.moxun.dreamcatcher.connector.server; import me.moxun.dreamcatcher.connector.util.ProcessUtil; public class AddressNameHelper { private static final String PREFIX = "dreamcatcher"; public static String createCustomAddress(String suffix) { return PREFIX + ProcessUtil.getProcessName() + suffix; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/CompositeInputStream.java ================================================ package me.moxun.dreamcatcher.connector.server; import java.io.IOException; import java.io.InputStream; import javax.annotation.concurrent.NotThreadSafe; import me.moxun.dreamcatcher.connector.util.LogUtil; @NotThreadSafe public class CompositeInputStream extends InputStream { private final InputStream[] mStreams; private int mCurrentIndex; public CompositeInputStream(InputStream[] streams) { if (streams == null || streams.length < 2) { throw new IllegalArgumentException("Streams must be non-null and have more than 1 entry"); } mStreams = streams; mCurrentIndex = 0; } @Override public int available() throws IOException { return mStreams[mCurrentIndex].available(); } @Override public void close() throws IOException { closeAll(mCurrentIndex); } private void closeAll(int mostImportantIndex) throws IOException { IOException exceptionToThrow = null; for (int i = 0; i < mStreams.length; i++) { try { mStreams[i].close(); } catch (IOException e) { IOException previousException = exceptionToThrow; if (i == mostImportantIndex || exceptionToThrow == null) { exceptionToThrow = e; } if (previousException != null && previousException != exceptionToThrow) { LogUtil.w(previousException, "Suppressing exception"); } } } } @Override public void mark(int readlimit) { throw new UnsupportedOperationException(); } @Override public boolean markSupported() { return false; } @Override public void reset() throws IOException { throw new UnsupportedOperationException(); } @Override public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { int n; while ((n = mStreams[mCurrentIndex].read(buffer, byteOffset, byteCount)) == -1) { if (!tryMoveToNextStream()) { break; } } return n; } @Override public int read() throws IOException { int b; while ((b = mStreams[mCurrentIndex].read()) == -1) { if (!tryMoveToNextStream()) { break; } } return b; } private boolean tryMoveToNextStream() { if (mCurrentIndex + 1 < mStreams.length) { mCurrentIndex++; return true; } return false; } @Override public long skip(long byteCount) throws IOException { byte[] buf = new byte[(int)byteCount]; int n = read(buf); return n >= 0 ? n : -1; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/LazySocketHandler.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.net.LocalSocket; import java.io.IOException; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Optimization designed to allow us to lazily construct/configure the true DreamCatcher server * only after the first caller connects. This gives us much more wiggle room to have performance * impact in the set up path that only applies when DreamCatcher is _used_, not simply enabled. */ public class LazySocketHandler implements SocketHandler { private final SocketHandlerFactory mSocketHandlerFactory; @Nullable private SocketHandler mSocketHandler; public LazySocketHandler(SocketHandlerFactory socketHandlerFactory) { mSocketHandlerFactory = socketHandlerFactory; } @Override public void onAccepted(LocalSocket socket) throws IOException { getSocketHandler().onAccepted(socket); } @Nonnull private synchronized SocketHandler getSocketHandler() { if (mSocketHandler == null) { mSocketHandler = mSocketHandlerFactory.create(); } return mSocketHandler; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/LeakyBufferedInputStream.java ================================================ package me.moxun.dreamcatcher.connector.server; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe public class LeakyBufferedInputStream extends BufferedInputStream { private boolean mLeaked; private boolean mMarked; public LeakyBufferedInputStream(InputStream in, int bufSize) { super(in, bufSize); } @Override public synchronized void mark(int readlimit) { throwIfLeaked(); mMarked = true; super.mark(readlimit); } @Override public synchronized void reset() throws IOException { throwIfLeaked(); mMarked = false; super.reset(); } @Override public boolean markSupported() { return true; } public synchronized InputStream leakBufferAndStream() { throwIfLeaked(); throwIfMarked(); mLeaked = true; return new CompositeInputStream( new InputStream[] { new ByteArrayInputStream(clearBufferLocked()), in }); } private byte[] clearBufferLocked() { byte[] leaked = new byte[count - pos]; System.arraycopy(buf, pos, leaked, 0, leaked.length); pos = 0; count = 0; return leaked; } private void throwIfLeaked() { if (mLeaked) { throw new IllegalStateException(); } } private void throwIfMarked() { if (mMarked) { throw new IllegalStateException(); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/LocalSocketServer.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.net.LocalServerSocket; import android.net.LocalSocket; import java.io.IOException; import java.io.InterruptedIOException; import java.net.BindException; import java.net.SocketException; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nonnull; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.Util; public class LocalSocketServer { private static final String WORKER_THREAD_NAME_PREFIX = "DreamCatcherWorker"; private static final int MAX_BIND_RETRIES = 2; private static final int TIME_BETWEEN_BIND_RETRIES_MS = 1000; private final String mFriendlyName; private final String mAddress; private final SocketHandler mSocketHandler; private final AtomicInteger mThreadId = new AtomicInteger(); private Thread mListenerThread; private boolean mStopped; private LocalServerSocket mServerSocket; /** * @param friendlyName identifier to help debug this server, used for naming threads and such. * @param address the local socket address to listen on. * @param socketHandler functional handler once a socket is accepted. */ public LocalSocketServer( String friendlyName, String address, SocketHandler socketHandler) { mFriendlyName = Util.throwIfNull(friendlyName); mAddress = Util.throwIfNull(address); mSocketHandler = socketHandler; } public String getName() { return mFriendlyName; } /** * Called by ServerManager#run on Thread. * Binds to the address and listens for connections. *

* If successful, this thread blocks forever or until {@link #stop} is called, whichever * happens first. * * @throws IOException Thrown on failure to bind the socket. */ public void run() throws IOException { synchronized (this) { if (mStopped) { return; } mListenerThread = Thread.currentThread(); } listenOnAddress(mAddress); } public void reset() { mStopped = false; mListenerThread = null; } private void listenOnAddress(String address) throws IOException { //The real server mServerSocket = bindToSocket(address); LogUtil.e("DreamCatcher Listening on @" + address); while (!Thread.interrupted()) { try { // Use previously accepted socket the first time around, otherwise wait to // accept another. //Block here LocalSocket socket = mServerSocket.accept(); // Start worker thread Thread t = new WorkerThread(socket, mSocketHandler); String name = WORKER_THREAD_NAME_PREFIX + "-" + mFriendlyName + "-" + mThreadId.incrementAndGet(); LogUtil.d("WorkerThread: " + socket.getFileDescriptor().toString() + "/" + name); t.setName(name); t.setDaemon(true); t.start(); } catch (SocketException se) { // ignore exception if interrupting the thread if (Thread.interrupted()) { break; } LogUtil.w(se, "I/O error"); } catch (InterruptedIOException ex) { break; } catch (IOException e) { LogUtil.w(e, "I/O error initialising connection thread"); break; } } LogUtil.e("DreamCatcher server shutdown on @" + address); } /** * Stops the listener thread and unbinds the address. */ public void stop() { synchronized (this) { mStopped = true; if (mListenerThread == null) { return; } } mListenerThread.interrupt(); try { if (mServerSocket != null) { LogUtil.e("Trying to unbind local socket"); mServerSocket.close(); LogUtil.e("Local socket closed"); } } catch (IOException e) { LogUtil.e(e.toString()); // Don't care... } } @Nonnull private static LocalServerSocket bindToSocket(String address) throws IOException { int retries = MAX_BIND_RETRIES; IOException firstException = null; do { try { LogUtil.e("Trying to bind to @" + address); return new LocalServerSocket(address); } catch (BindException be) { LogUtil.e(be, "Binding error, sleep " + TIME_BETWEEN_BIND_RETRIES_MS + " ms..."); if (firstException == null) { firstException = be; } Util.sleepUninterruptibly(TIME_BETWEEN_BIND_RETRIES_MS); } } while (retries-- > 0); throw firstException; } private static class WorkerThread extends Thread { private final LocalSocket mSocket; private final SocketHandler mSocketHandler; public WorkerThread(LocalSocket socket, SocketHandler socketHandler) { mSocket = socket; mSocketHandler = socketHandler; } @Override public void run() { try { mSocketHandler.onAccepted(mSocket); } catch (IOException ex) { LogUtil.w("I/O error: %s", ex.toString()); } finally { try { mSocket.close(); } catch (IOException ignore) { } } } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/PeerAuthorizationException.java ================================================ package me.moxun.dreamcatcher.connector.server; public class PeerAuthorizationException extends Exception { public PeerAuthorizationException(String message) { super(message); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/ProtocolDetectingSocketHandler.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.content.Context; import android.net.LocalSocket; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import me.moxun.dreamcatcher.connector.util.LogUtil; /** * Socket handler which is designed to detect a difference in protocol signatures very early on * in the connection to figure out which real handler to route to. This is used for performance * and backwards compatibility reasons to maintain DreamCatcher having just one actual socket * connection despite dumpapp and DevTools now diverging in protocol. *

* Note this trick is only possible if the protocol requires that the client initiate the * conversation. Otherwise, the server would be expected to say something before we know what * protocol the client is speaking. */ public class ProtocolDetectingSocketHandler extends SecureSocketHandler { private static final int SENSING_BUFFER_SIZE = 256; private final ArrayList mHandlers = new ArrayList<>(2); public ProtocolDetectingSocketHandler(Context context) { super(context); } public void addHandler(MagicMatcher magicMatcher, SocketLikeHandler handler) { mHandlers.add(new HandlerInfo(magicMatcher, handler)); LogUtil.d("Add handler to ProtocolDetectingSocketHandler ==>" + handler.getClass().getSimpleName()); } //called on onAccepted @Override protected void onSecured(LocalSocket socket) throws IOException { LeakyBufferedInputStream leakyIn = new LeakyBufferedInputStream( socket.getInputStream(), SENSING_BUFFER_SIZE); if (mHandlers.isEmpty()) { throw new IllegalStateException("No handlers added"); } for (int i = 0, N = mHandlers.size(); i < N; i++) { HandlerInfo handlerInfo = mHandlers.get(i); leakyIn.mark(SENSING_BUFFER_SIZE); boolean matches = handlerInfo.magicMatcher.matches(leakyIn); leakyIn.reset(); if (matches) { LogUtil.d("Matches!" + handlerInfo.handler.getClass().getSimpleName()); SocketLike socketLike = new SocketLike(socket, leakyIn); handlerInfo.handler.onAccepted(socketLike); return; } } throw new IOException("No matching handler, firstByte=" + leakyIn.read()); } public interface MagicMatcher { boolean matches(InputStream in) throws IOException; } public static class ExactMagicMatcher implements MagicMatcher { private final byte[] mMagic; public ExactMagicMatcher(byte[] magic) { mMagic = magic; } @Override public boolean matches(InputStream in) throws IOException { byte[] buf = new byte[mMagic.length]; int n = in.read(buf); return n == buf.length && Arrays.equals(buf, mMagic); } } public static class AlwaysMatchMatcher implements MagicMatcher { @Override public boolean matches(InputStream in) throws IOException { return true; } } private static class HandlerInfo { public final MagicMatcher magicMatcher; public final SocketLikeHandler handler; private HandlerInfo(MagicMatcher magicMatcher, SocketLikeHandler handler) { this.magicMatcher = magicMatcher; this.handler = handler; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/SecureSocketHandler.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.net.Credentials; import android.net.LocalSocket; import android.util.Log; import java.io.IOException; import me.moxun.dreamcatcher.connector.util.LogUtil; public abstract class SecureSocketHandler implements SocketHandler { private final Context mContext; public SecureSocketHandler(Context context) { mContext = context; } @Override public final void onAccepted(LocalSocket socket) throws IOException { try { enforcePermission(mContext, socket); onSecured(socket); } catch (PeerAuthorizationException e) { LogUtil.e("Unauthorized request: " + e.getMessage()); } } protected abstract void onSecured(LocalSocket socket) throws IOException; private static void enforcePermission(Context context, LocalSocket peer) throws IOException, PeerAuthorizationException { Credentials credentials = peer.getPeerCredentials(); int uid = credentials.getUid(); int pid = credentials.getPid(); if (LogUtil.isLoggable(Log.VERBOSE)) { LogUtil.v("Got request from uid=%d, pid=%d", uid, pid); } String requiredPermission = Manifest.permission.DUMP; int checkResult = context.checkPermission(requiredPermission, pid, uid); if (checkResult != PackageManager.PERMISSION_GRANTED) { throw new PeerAuthorizationException( "Peer pid=" + pid + ", uid=" + uid + " does not have " + requiredPermission); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/ServerManager.java ================================================ package me.moxun.dreamcatcher.connector.server; import org.greenrobot.eventbus.EventBus; import java.io.IOException; import me.moxun.dreamcatcher.connector.manager.Lifecycle; import me.moxun.dreamcatcher.connector.manager.SimpleConnectorLifecycleManager; import me.moxun.dreamcatcher.connector.util.IServerManager; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.event.CaptureEvent; import me.moxun.dreamcatcher.event.OperateEvent; public class ServerManager implements IServerManager { private static final String THREAD_PREFIX = "DreamCatcherListener"; private final LocalSocketServer mServer; private volatile boolean mStarted; private Thread listenerThread; public ServerManager(LocalSocketServer server) { mServer = server; } @Override public void start() { if (mStarted) { throw new IllegalStateException("Already started"); } mStarted = true; mServer.reset(); startServer(mServer); } @Override public void stop() { if (mStarted) { mServer.stop(); mStarted = false; } EventBus.getDefault().post(new OperateEvent(OperateEvent.TARGET_CONNECTOR, false)); } @Override public void restart() { stop(); start(); } private void startServer(final LocalSocketServer server) { Thread listener = new Thread(THREAD_PREFIX + "-" + server.getName()) { @Override public void run() { try { SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.LOCAL_SERVER_SOCKET_OPENING); CaptureEvent.send("Local socket is open"); EventBus.getDefault().post(new OperateEvent(OperateEvent.TARGET_CONNECTOR, true)); server.run(); } catch (IOException e) { LogUtil.e("Could not start DreamCatcher server: " + server.getName() + ", cause: " + e.toString()); SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.SHUTDOWN); CaptureEvent.send("Exception on bind local socket"); EventBus.getDefault().post(new OperateEvent(OperateEvent.TARGET_CONNECTOR, false, true, e.getMessage())); } } }; listener.start(); listenerThread = listener; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/SocketHandler.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.net.LocalSocket; import java.io.IOException; /** * @see SecureSocketHandler */ public interface SocketHandler { /** * Server socket has been accepted and a dedicated thread has been allocated to process this * callback. Returning from this method or throwing an exception will attempt an orderly * shutdown of the socket, however it will not be treated as an error if returning normally. */ void onAccepted(LocalSocket socket) throws IOException; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/SocketHandlerFactory.java ================================================ package me.moxun.dreamcatcher.connector.server; /** @see LazySocketHandler */ public interface SocketHandlerFactory { SocketHandler create(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/SocketLike.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.net.LocalSocket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Utility to allow reading buffered data from a socket and then "unreading" the data * and combining it with the original unbuffered stream. This is useful when * handing off from one logical protocol layer to the next, such as when upgrading an HTTP * connection to the websocket protocol. */ public class SocketLike { private final LeakyBufferedInputStream mLeakyInput; private final OutputStream outputStream; public SocketLike(SocketLike socketLike, LeakyBufferedInputStream leakyInput) { this(socketLike.outputStream, leakyInput); } public SocketLike(OutputStream output, LeakyBufferedInputStream leakyIn) { outputStream = output; mLeakyInput = leakyIn; } public SocketLike(LocalSocket socket, LeakyBufferedInputStream leakyInput) { OutputStream temp = null; try { temp = socket.getOutputStream(); } catch (IOException e) { e.printStackTrace(); } outputStream = temp; mLeakyInput = leakyInput; } public InputStream getInput() throws IOException { return mLeakyInput.leakBufferAndStream(); } public OutputStream getOutput() throws IOException { return outputStream; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/SocketLikeHandler.java ================================================ package me.moxun.dreamcatcher.connector.server; import android.net.LocalSocket; import java.io.IOException; /** * Similar to {@link SocketHandler} but designed to operate on {@link SocketLike} instances * which allow for buffered "peeks" of data to decide which protocol handler to use. * * @see SocketHandler * @see SocketLike */ public interface SocketLikeHandler { /** @see SocketHandler#onAccepted(LocalSocket) */ void onAccepted(SocketLike socket) throws IOException; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/ExactPathMatcher.java ================================================ package me.moxun.dreamcatcher.connector.server.http; public class ExactPathMatcher implements PathMatcher { private final String mPath; public ExactPathMatcher(String path) { mPath = path; } @Override public String toString() { return "ExactPathMatcher{" + "mPath='" + mPath + '\'' + '}'; } @Override public boolean match(String path) { return mPath.equals(path); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/HandlerRegistry.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import android.support.annotation.Nullable; import java.util.ArrayList; public class HandlerRegistry { private final ArrayList mPathMatchers = new ArrayList<>(); private final ArrayList mHttpHandlers = new ArrayList<>(); public synchronized void register(PathMatcher path, HttpHandler handler) { mPathMatchers.add(path); mHttpHandlers.add(handler); } public synchronized boolean unregister(PathMatcher path, HttpHandler handler) { int index = mPathMatchers.indexOf(path); if (index >= 0) { if (handler == mHttpHandlers.get(index)) { mPathMatchers.remove(index); mHttpHandlers.remove(index); return true; } } return false; } @Nullable public synchronized HttpHandler lookup(String path) { for (int i = 0, N = mPathMatchers.size(); i < N; i++) { if (mPathMatchers.get(i).match(path)) { return mHttpHandlers.get(i); } } return null; } @Override public String toString() { return "HandlerRegistry{" + "mPathMatchers=" + mPathMatchers + ", mHttpHandlers=" + mHttpHandlers + '}'; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/HttpHandler.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import java.io.IOException; import me.moxun.dreamcatcher.connector.server.SocketLike; public interface HttpHandler { boolean handleRequest( SocketLike socket, LightHttpRequest request, LightHttpResponse response) throws IOException; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/HttpHeaders.java ================================================ package me.moxun.dreamcatcher.connector.server.http; public interface HttpHeaders { String CONTENT_TYPE = "Content-Type"; String CONTENT_LENGTH = "Content-Length"; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/HttpStatus.java ================================================ package me.moxun.dreamcatcher.connector.server.http; public interface HttpStatus { int HTTP_SWITCHING_PROTOCOLS = 101; int HTTP_OK = 200; int HTTP_NOT_FOUND = 404; int HTTP_INTERNAL_SERVER_ERROR = 500; int HTTP_NOT_IMPLEMENTED = 501; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/LightHttpBody.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; public abstract class LightHttpBody { private static String mBody; public static LightHttpBody create(String body, String contentType) { mBody = body; try { return create(body.getBytes("UTF-8"), contentType); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static LightHttpBody create(final byte[] body, final String contentType) { return new LightHttpBody() { @Override public String contentType() { return contentType; } @Override public int contentLength() { return body.length; } @Override public void writeTo(OutputStream output) throws IOException { output.write(body); } }; } @Override public String toString() { return mBody; } public abstract String contentType(); public abstract int contentLength(); public abstract void writeTo(OutputStream output) throws IOException; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/LightHttpMessage.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import android.support.annotation.Nullable; import java.util.ArrayList; public class LightHttpMessage { public final ArrayList headerNames = new ArrayList<>(); public final ArrayList headerValues = new ArrayList<>(); public void addHeader(String name, String value) { headerNames.add(name); headerValues.add(value); } @Nullable public String getFirstHeaderValue(String name) { for (int i = 0, N = headerNames.size(); i < N; i++) { if (name.equals(headerNames.get(i))) { return headerValues.get(i); } } return null; } public void reset() { headerNames.clear(); headerValues.clear(); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/LightHttpRequest.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import android.net.Uri; public class LightHttpRequest extends LightHttpMessage { public String method; public Uri uri; public String protocol; @Override public String toString() { return "LightHttpRequest{" + "method='" + method + '\'' + ", uri=" + uri + ", protocol='" + protocol + '\'' + '}'; } @Override public void reset() { super.reset(); this.method = null; this.uri = null; this.protocol = null; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/LightHttpResponse.java ================================================ package me.moxun.dreamcatcher.connector.server.http; public class LightHttpResponse extends LightHttpMessage { public int code; public String reasonPhrase; public LightHttpBody body; @Override public String toString() { return "LightHttpResponse{" + "code=" + code + ", reasonPhrase='" + reasonPhrase + '\'' + ", body=" + body.toString() + '}'; } public void prepare() { if (body != null) { addHeader(HttpHeaders.CONTENT_TYPE, body.contentType()); addHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(body.contentLength())); } } @Override public void reset() { super.reset(); this.code = -1; this.reasonPhrase = null; this.body = null; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/LightHttpServer.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import android.net.Uri; import android.support.annotation.Nullable; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import me.moxun.dreamcatcher.connector.server.LeakyBufferedInputStream; import me.moxun.dreamcatcher.connector.server.SocketLike; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.websocket.WebSocketHandler; /** * Somewhat crude but very fast HTTP server designed exclusively to handle the * Chrome DevTools protocol, though sufficiently general to do other very basic things. * Performance is imperative here as Chrome aggressively polls DreamCatcher asking for * meta data when the discovery window is open in Chrome. */ public class LightHttpServer { private static final String TAG = "LightHttpServer"; private final HandlerRegistry mHandlerRegistry; public LightHttpServer(HandlerRegistry handlerRegistry) { mHandlerRegistry = handlerRegistry; } public void serve(SocketLike socket) throws IOException { LeakyBufferedInputStream input = new LeakyBufferedInputStream(socket.getInput(), 1024); OutputStream output = socket.getOutput(); HttpMessageReader reader = new HttpMessageReader(input); HttpMessageWriter writer = new HttpMessageWriter(new BufferedOutputStream(output)); SocketLike anotherSocketLike = new SocketLike(socket, input); LightHttpRequest scratchRequest = new LightHttpRequest(); LightHttpResponse scratchResponse = new LightHttpResponse(); LightHttpRequest request; // This loops assumes we are always using keep-alive connections. If we're wrong, we // expect the client to just close the connection. while ((request = readRequestMessage(scratchRequest, reader)) != null) { final LightHttpResponse response = scratchResponse; response.reset(); // Note, if we're upgrading to websockets, this will block for the lifetime of the // websocket session... boolean keepGoing = dispatchToHandler(anotherSocketLike, request, response); if (!keepGoing) { // Orderly shutdown, ignore response and break the loop. break; } LogUtil.w(LogUtil.filter(request, response)); writeFullResponse(response, writer, output); } } private boolean dispatchToHandler( SocketLike socketLike, LightHttpRequest request, LightHttpResponse response) throws IOException { HttpHandler handler = mHandlerRegistry.lookup(request.uri.getPath()); if (handler == null) { response.code = HttpStatus.HTTP_NOT_FOUND; response.reasonPhrase = "Not found"; response.body = LightHttpBody.create(build404Body(request.uri.getPath()), "text/html"); return true; } else { if (handler instanceof WebSocketHandler) { LogUtil.e("dispatch to WS handler"); } try { return handler.handleRequest(socketLike, request, response); } catch (RuntimeException e) { response.code = HttpStatus.HTTP_INTERNAL_SERVER_ERROR; response.reasonPhrase = "Internal Server Error"; StringWriter stack = new StringWriter(); PrintWriter stackWriter = new PrintWriter(stack); try { e.printStackTrace(stackWriter); } finally { stackWriter.close(); } response.body = LightHttpBody.create(stack.toString(), "text/plain"); return true; } } } private String build404Body(String request) { String template = "\n" + "Handler Not Found\n" + "\n" + "\n" + "

DreamCatcher: handler not found

\n" + "

Error code 404.\n" + "

Message: Handler not found.\n" + "

Error code explanation: 404 = No handler matches the given URI '%s'.\n" + ""; return String.format(template, request); } @Nullable private static LightHttpRequest readRequestMessage( LightHttpRequest request, HttpMessageReader reader) throws IOException { request.reset(); String requestLine = reader.readLine(); if (requestLine == null) { return null; } // Zero tolerance on URI encoding, that URI better not have a space in it... String[] requestParts = requestLine.split(" ", 3); if (requestParts.length != 3) { throw new IOException("Invalid request line: " + requestLine); } request.method = requestParts[0]; request.uri = Uri.parse(requestParts[1]); request.protocol = requestParts[2]; readHeaders(request, reader); return request; } private static void readHeaders( LightHttpMessage message, HttpMessageReader reader) throws IOException { String headerLine; while (true) { headerLine = reader.readLine(); if (headerLine == null) { throw new EOFException(); } else if ("".equals(headerLine)) { break; } else { String[] headerParts = headerLine.split(": ", 2); if (headerParts.length != 2) { throw new IOException("Malformed header: " + headerLine); } String name = headerParts[0]; String value = headerParts[1]; message.headerNames.add(name); message.headerValues.add(value); } } } private static void writeFullResponse( LightHttpResponse response, HttpMessageWriter writer, OutputStream output) throws IOException { response.prepare(); writeResponseMessage(response, writer); if (response.body != null) { response.body.writeTo(output); } } public static void writeResponseMessage(LightHttpResponse response, HttpMessageWriter writer) throws IOException { writer.writeLine("HTTP/1.1 " + response.code + " " + response.reasonPhrase); for (int i = 0, N = response.headerNames.size(); i < N; i++) { String name = response.headerNames.get(i); String value = response.headerValues.get(i); writer.writeLine(name + ": " + value); } writer.writeLine(); writer.flush(); } /** * Efficient, unbuffered variation of {@link InputStreamReader} which assumes the input is * always ASCII. This is especially useful when you are certain that the client and server * are both mechanized and will not contain non-ASCII characters in the control messages upon * which this reader is applied. */ private static class HttpMessageReader { private final BufferedInputStream mIn; private final StringBuilder mBuffer = new StringBuilder(); private final NewLineDetector mNewLineDetector = new NewLineDetector(); public HttpMessageReader(BufferedInputStream in) { mIn = in; } @Nullable public String readLine() throws IOException { while (true) { int b = mIn.read(); if (b < 0) { return null; } char c = (char) b; mNewLineDetector.accept(c); switch (mNewLineDetector.state()) { case NewLineDetector.STATE_ON_CRLF: String result = mBuffer.toString(); mBuffer.setLength(0); return result; case NewLineDetector.STATE_ON_CR: break; case NewLineDetector.STATE_ON_OTHER: mBuffer.append(c); break; } } } private static class NewLineDetector { private static final int STATE_ON_OTHER = 1; private static final int STATE_ON_CR = 2; private static final int STATE_ON_CRLF = 3; private int state = STATE_ON_OTHER; public void accept(char c) { switch (state) { case STATE_ON_OTHER: if (c == '\r') { state = STATE_ON_CR; } break; case STATE_ON_CR: if (c == '\n') { state = STATE_ON_CRLF; } else { state = STATE_ON_OTHER; } break; case STATE_ON_CRLF: if (c == '\r') { state = STATE_ON_CR; } else { state = STATE_ON_OTHER; } break; default: throw new IllegalArgumentException("Unknown state: " + state); } } public int state() { return state; } } } /** * Similar in spirit to {@link HttpMessageReader} which assumes ASCII for all messages as * a performance optimization. Caller is responsible for flushing the writer. *

* Exposed publicly as a hack to support WebSocket upgrade. */ public static class HttpMessageWriter { private final BufferedOutputStream mOut; private static final byte[] CRLF = "\r\n".getBytes(); public HttpMessageWriter(BufferedOutputStream out) { mOut = out; } public void writeLine(String line) throws IOException { for (int i = 0, N = line.length(); i < N; i++) { char c = line.charAt(i); mOut.write((int) c); } mOut.write(CRLF); } public void writeLine() throws IOException { mOut.write(CRLF); } public void flush() throws IOException { mOut.flush(); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/PathMatcher.java ================================================ package me.moxun.dreamcatcher.connector.server.http; public interface PathMatcher { boolean match(String path); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/server/http/RegexpPathMatcher.java ================================================ package me.moxun.dreamcatcher.connector.server.http; import java.util.regex.Pattern; public class RegexpPathMatcher implements PathMatcher { private final Pattern mPattern; public RegexpPathMatcher(Pattern pattern) { mPattern = pattern; } @Override public boolean match(String path) { return mPattern.matcher(path).matches(); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/Accumulator.java ================================================ package me.moxun.dreamcatcher.connector.util; public interface Accumulator { void store(E object); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ArrayListAccumulator.java ================================================ package me.moxun.dreamcatcher.connector.util; import java.util.ArrayList; public final class ArrayListAccumulator extends ArrayList implements Accumulator { @Override public void store(E object) { add(object); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ColorStringUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; /** * Created by moxun on 16/4/8. */ public class ColorStringUtil { public static String toHexString(int c) { return "#" + Integer.toHexString(c); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/DreamCatcherCrashHandler.java ================================================ package me.moxun.dreamcatcher.connector.util; import me.moxun.dreamcatcher.connector.log.AELog; /** * Created by moxun on 16/7/12. */ public class DreamCatcherCrashHandler implements Thread.UncaughtExceptionHandler { private final static DreamCatcherCrashHandler INSTANCE = new DreamCatcherCrashHandler(); private Thread.UncaughtExceptionHandler oldHandler; private DreamCatcherCrashHandler() { } public static DreamCatcherCrashHandler getInstance() { return INSTANCE; } public void attach() { oldHandler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler(this); } @Override public void uncaughtException(Thread thread, Throwable ex) { AELog.e("发生致命错误,DreamCatcher即将断开连接。"); AELog.e("Fatal exception on thread " + thread.toString() + ", caused by " + ex.toString() + "\r\n" + dumpException(ex)); if (oldHandler != null) { oldHandler.uncaughtException(thread, ex); } } private String dumpException(Throwable ex) { StackTraceElement[] elements = ex.getStackTrace(); StringBuilder sb = new StringBuilder(); for (StackTraceElement element : elements) { sb.append("\tat ").append(element.toString()).append("\r\n"); } return sb.toString(); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ExceptionUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; public class ExceptionUtil { @SuppressWarnings("unchecked") public static void propagateIfInstanceOf(Throwable t, Class type) throws T { if (type.isInstance(t)) { throw (T)t; } } public static RuntimeException propagate(Throwable t) { propagateIfInstanceOf(t, Error.class); propagateIfInstanceOf(t, RuntimeException.class); throw new RuntimeException(t); } @SuppressWarnings("unchecked") public static void sneakyThrow(Throwable t) throws T { throw (T)t; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/IServerManager.java ================================================ package me.moxun.dreamcatcher.connector.util; /** * Created by moxun on 16/7/1. */ public interface IServerManager { void start(); void stop(); void restart(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/KLog.java ================================================ /* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package me.moxun.dreamcatcher.connector.util; import android.text.TextUtils; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * This is a Log tool,with this you can the following *

    *
  1. use KLog.d(),you could print whether the method execute,and the default tag is current class's name
  2. *
  3. use KLog.d(msg),you could print log as before,and you could location the method with a click in Android Studio Logcat
  4. *
  5. use KLog.json(),you could print json string with well format automatic
  6. *
* * @author zhaokaiqiang * github https://github.com/ZhaoKaiQiang/KLog */ public class KLog { private static boolean IS_SHOW_LOG = true; private static final String DEFAULT_MESSAGE = "execute"; private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final int JSON_INDENT = 4; private static final int V = 0x1; private static final int D = 0x2; private static final int I = 0x3; private static final int W = 0x4; private static final int E = 0x5; private static final int A = 0x6; private static final int JSON = 0x7; public static void setLoggable(boolean loggable) { IS_SHOW_LOG = loggable; } public static void v() { printLog(V, null, DEFAULT_MESSAGE); } public static void v(String msg) { printLog(V, null, msg); } public static void v(String tag, String msg) { printLog(V, tag, msg); } public static void d() { printLog(D, null, DEFAULT_MESSAGE); } public static void d(String msg) { printLog(D, null, msg); } public static void d(String tag, String msg) { printLog(D, tag, msg); } public static void i() { printLog(I, null, DEFAULT_MESSAGE); } public static void i(String msg) { printLog(I, null, msg); } public static void i(String tag, String msg) { printLog(I, tag, msg); } public static void w() { printLog(W, null, DEFAULT_MESSAGE); } public static void w(String msg) { printLog(W, null, msg); } public static void w(String tag, String msg) { printLog(W, tag, msg); } public static void e() { printLog(E, null, DEFAULT_MESSAGE); } public static void e(String msg) { printLog(E, null, msg); } public static void e(String tag, String msg) { printLog(E, tag, msg); } public static void a() { printLog(A, null, DEFAULT_MESSAGE); } public static void a(String msg) { printLog(A, null, msg); } public static void a(String tag, String msg) { printLog(A, tag, msg); } public static void json(String jsonFormat) { printLog(JSON, null, jsonFormat); } public static void json(String tag, String jsonFormat) { printLog(JSON, tag, jsonFormat); } private static void printLog(int type, String tagStr, String msg) { if (!IS_SHOW_LOG) { return; } String tag; if (tagStr == null) { tag = "DreamCatcher"; } else { tag = "DreamCatcher-" + tagStr; } if (msg == null) { return; } StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); int index = 6; String className = stackTrace[index].getFileName(); String methodName = stackTrace[index].getMethodName(); int lineNumber = stackTrace[index].getLineNumber(); methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("(").append(className).append(":").append(lineNumber).append("").append(") "); if (msg != null && type != JSON) { stringBuilder.append(msg); } String logStr = stringBuilder.toString(); switch (type) { case V: Log.v(tag, logStr); break; case D: Log.d(tag, logStr); break; case I: Log.i(tag, logStr); break; case W: Log.w(tag, logStr); break; case E: Log.e(tag, logStr); break; case A: Log.wtf(tag, logStr); break; case JSON: { if (TextUtils.isEmpty(msg)) { Log.d(tag, "Empty or Null json content"); return; } String message = null; try { if (msg.startsWith("{")) { JSONObject jsonObject = new JSONObject(msg); message = jsonObject.toString(JSON_INDENT); } else if (msg.startsWith("[")) { JSONArray jsonArray = new JSONArray(msg); message = jsonArray.toString(JSON_INDENT); } } catch (JSONException e) { e(tag, e.getCause().getMessage() + "\n" + msg); return; } printLine(tag, true); message = logStr + LINE_SEPARATOR + message; String[] lines = message.split(LINE_SEPARATOR); StringBuilder jsonContent = new StringBuilder(); for (String line : lines) { jsonContent.append("║ ").append(line).append(LINE_SEPARATOR); } Log.d(tag, jsonContent.toString()); printLine(tag, false); } break; } } private static void printLine(String tag, boolean isTop) { if (isTop) { Log.d(tag, "╔═══════════════════════════════════════════════════════════════════════════════════════"); } else { Log.d(tag, "╚═══════════════════════════════════════════════════════════════════════════════════════"); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/KLogImpl.java ================================================ package me.moxun.dreamcatcher.connector.util; public class KLogImpl implements LogInterface { @Override public void setLoggable(boolean loggable) { KLog.setLoggable(loggable); } @Override public void v(String msg) { KLog.v(msg); } @Override public void v(String tag, String msg) { KLog.v(tag, msg); } @Override public void v(String tag, Throwable tr) { KLog.v(tag, android.util.Log.getStackTraceString(tr)); } @Override public void v(String tag, String msg, Throwable tr) { KLog.v(tag, msg + "\n" + android.util.Log.getStackTraceString(tr)); } @Override public void d(String msg) { KLog.d(msg); } @Override public void d(String tag, String msg) { KLog.d(tag, msg); } @Override public void d(String tag, Throwable tr) { KLog.d(tag, android.util.Log.getStackTraceString(tr)); } @Override public void d(String tag, String msg, Throwable tr) { KLog.d(tag, msg + "\n" + android.util.Log.getStackTraceString(tr)); } @Override public void i(String msg) { KLog.i(msg); } @Override public void i(String tag, String msg) { KLog.i(tag, msg); } @Override public void i(String tag, Throwable tr) { KLog.i(tag, android.util.Log.getStackTraceString(tr)); } @Override public void i(String tag, String msg, Throwable tr) { KLog.i(tag, msg + "\n" + android.util.Log.getStackTraceString(tr)); } @Override public void w(String msg) { KLog.w(msg); } @Override public void w(String tag, String msg) { KLog.w(tag, msg); } @Override public void w(String tag, Throwable tr) { KLog.w(tag, android.util.Log.getStackTraceString(tr)); } @Override public void w(String tag, String msg, Throwable tr) { KLog.w(tag, msg + "\n" + android.util.Log.getStackTraceString(tr)); } @Override public void e(String msg) { KLog.e(msg); } @Override public void e(String tag, String msg) { KLog.e(tag, msg); } @Override public void e(String tag, Throwable tr) { KLog.e(tag, android.util.Log.getStackTraceString(tr)); } @Override public void e(String tag, String msg, Throwable tr) { KLog.e(tag, msg + "\n" + android.util.Log.getStackTraceString(tr)); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/Keys.java ================================================ package me.moxun.dreamcatcher.connector.util; /** * Created by moxun on 16/6/20. */ public interface Keys { String KEY_QUEUE_SIZE = "queue_size"; String KEY_LOG_LEVEL = "log_level"; String KEY_ALLOW_REMOTE_LOG = "allow_remote_log"; String KEY_SHOW_ALL_ATTRS = "show_all_attrs"; String KEY_ALLOW_EXEC = "allow_exec"; String KEY_LOG_LEVEL_NUM = "log_level_num"; String KEY_RAW_SOCKET = "raw_socket"; String KEY_SHOW_FPS = "show_fps"; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ListUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; import java.util.AbstractList; import java.util.Collections; import java.util.List; import java.util.RandomAccess; public final class ListUtil { private ListUtil() { } /** * Compares the contents of two {@link List}s by using object identity. */ public static boolean identityEquals(List list1, List list2) { if (list1 == list2) { return true; } int size = list1.size(); if (size != list2.size()) { return false; } for (int i = 0; i < size; ++i) { if (list1.get(i) != list2.get(i)) { return false; } } return true; } /** * Copies the given {@link List} and returns the copy as an immutable {@link List}. */ public static List copyToImmutableList(List list) { if (list instanceof ImmutableList) { return list; } int size = list.size(); switch (size) { case 0: return Collections.emptyList(); case 1: return new OneItemImmutableList<>(list.get(0)); case 2: return new TwoItemImmutableList<>(list.get(0), list.get(1)); case 3: return new ThreeItemImmutableList<>(list.get(0), list.get(1), list.get(2)); case 4: return new FourItemImmutableList<>(list.get(0), list.get(1), list.get(2), list.get(3)); case 5: return new FiveItemImmutableList<>( list.get(0), list.get(1), list.get(2), list.get(3), list.get(4)); default: Object[] array = list.toArray(); return new ImmutableArrayList<>(array); } } public static List newImmutableList(T item) { return new OneItemImmutableList<>(item); } private static interface ImmutableList extends List, RandomAccess { } private static final class ImmutableArrayList extends AbstractList implements ImmutableList { private final Object[] mArray; public ImmutableArrayList(Object[] array) { mArray = array; } @Override @SuppressWarnings("unchecked") public E get(int location) { return (E) mArray[location]; } @Override public int size() { return mArray.length; } } private static final class OneItemImmutableList extends AbstractList implements ImmutableList { private final E mItem; public OneItemImmutableList(E item) { mItem = item; } @Override public E get(int location) { if (location == 0) { return mItem; } else { throw new IndexOutOfBoundsException(); } } @Override public int size() { return 1; } } private static final class TwoItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; public TwoItemImmutableList(E item0, E item1) { mItem0 = item0; mItem1 = item1; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 2; } } private static final class ThreeItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; private final E mItem2; public ThreeItemImmutableList(E item0, E item1, E item2) { mItem0 = item0; mItem1 = item1; mItem2 = item2; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; case 2: return mItem2; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 3; } } private static final class FourItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; private final E mItem2; private final E mItem3; public FourItemImmutableList(E item0, E item1, E item2, E item3) { mItem0 = item0; mItem1 = item1; mItem2 = item2; mItem3 = item3; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; case 2: return mItem2; case 3: return mItem3; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 4; } } private static final class FiveItemImmutableList extends AbstractList implements ImmutableList { private final E mItem0; private final E mItem1; private final E mItem2; private final E mItem3; private final E mItem4; public FiveItemImmutableList(E item0, E item1, E item2, E item3, E item4) { mItem0 = item0; mItem1 = item1; mItem2 = item2; mItem3 = item3; mItem4 = item4; } @Override public E get(int location) { switch (location) { case 0: return mItem0; case 1: return mItem1; case 2: return mItem2; case 3: return mItem3; case 4: return mItem4; default: throw new IndexOutOfBoundsException(); } } @Override public int size() { return 5; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/LogFilter.java ================================================ package me.moxun.dreamcatcher.connector.util; import org.json.JSONException; import org.json.JSONObject; /** * Created by moxun on 16/3/17. */ public class LogFilter { public static JSONObject filter(JSONObject request, JSONObject response) { JSONObject result = null; try { if (response.has("result")) { if (!response.getJSONObject("result").has("exceptionDetails")) { result = new JSONObject(); result.put("request", request); result.put("response", response); } } } catch (JSONException e) { e.printStackTrace(); } return result; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/LogInterface.java ================================================ /* * Copyright (c) 2014-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package me.moxun.dreamcatcher.connector.util; public interface LogInterface { void setLoggable(boolean loggable); public void v(String msg); public void v(String tag, String msg); public void v(String tag, Throwable tr); public void v(String tag, String msg, Throwable tr); public void d(String msg); public void d(String tag, String msg); public void d(String tag, Throwable tr); public void d(String tag, String msg, Throwable tr); public void i(String msg); public void i(String tag, String msg); public void i(String tag, Throwable tr); public void i(String tag, String msg, Throwable tr); public void w(String msg); public void w(String tag, String msg); public void w(String tag, Throwable tr); public void w(String tag, String msg, Throwable tr); public void e(String msg); public void e(String tag, String msg); public void e(String tag, Throwable tr); public void e(String tag, String msg, Throwable tr); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/LogUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; import android.support.annotation.NonNull; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Locale; import me.moxun.dreamcatcher.connector.server.http.LightHttpRequest; import me.moxun.dreamcatcher.connector.server.http.LightHttpResponse; /** * Logging helper specifically for use by DreamCatcher internals. */ public class LogUtil { private static List requestBlackList = new ArrayList<>(); private static List responseBlackList = new ArrayList<>(); private static boolean isLoggable = true; private static LogInterface logImpl; static { logImpl = new KLogImpl(); logImpl.setLoggable(isLoggable); requestBlackList.add("Runtime.evaluate"); responseBlackList.add("Target activation ignored\n"); } public static void setLoggable(boolean loggable) { isLoggable = loggable; logImpl.setLoggable(isLoggable); } public static String filter(@NonNull LightHttpRequest request, @NonNull LightHttpResponse response) { if (requestBlackList.contains(request.method)) { return null; } if (responseBlackList.contains(response.body.toString())) { return null; } return "Request:" + request.toString() + ", Response:" + response.toString(); } public static String filter(String title, @NonNull LightHttpResponse response) { if (responseBlackList.contains(response.body.toString())) { return null; } return title + ":" + response.toString(); } public static String filter(String title, @NonNull LightHttpRequest request) { if (requestBlackList.contains(request.method)) { return null; } return title + ":" + request.toString(); } public static void e(String format, Object... args) { logImpl.e(format(format, args)); } public static void e(Throwable t, String format, Object... args) { logImpl.e(format(format, args), t); } public static void e(Throwable t, String message) { if (isLoggable(Log.ERROR)) { logImpl.e(message, t); } } public static void w(String format, Object... args) { logImpl.w(format(format, args)); } public static void w(Throwable t, String format, Object... args) { logImpl.w(format(format, args), t); } public static void w(Throwable t, String message) { if (isLoggable(Log.WARN)) { logImpl.w(message, t); } } public static void i(String format, Object... args) { logImpl.i(format(format, args)); } public static void i(Throwable t, String format, Object... args) { logImpl.i(format(format, args), t); } public static void i(Throwable t, String message) { if (isLoggable(Log.INFO)) { logImpl.i(message, t); } } public static void d(String format, Object... args) { logImpl.d(format(format, args)); } public static void d(Throwable t, String format, Object... args) { logImpl.d(format(format, args), t); } public static void d(Throwable t, String message) { if (isLoggable(Log.DEBUG)) { logImpl.d(message, t); } } public static void v(String format, Object... args) { logImpl.v(format(format, args)); } public static void v(Throwable t, String format, Object... args) { logImpl.v(format(format, args), t); } public static void v(Throwable t, String message) { if (isLoggable(Log.VERBOSE)) { logImpl.v(message, t); } } private static String format(String format, Object... args) { return String.format(Locale.US, format, args); } public static boolean isLoggable(int priority) { switch (priority) { case Log.ERROR: case Log.WARN: return true; default: return true; } } public static boolean isLoggable(String tag, int priority) { return isLoggable(priority); } public static void v(String msg) { logImpl.v(msg); } public static void v(String tag, String msg) { logImpl.v(tag, msg); } public static void v(String tag, Throwable tr) { logImpl.v(tag, tr); } public static void v(String tag, String msg, Throwable tr) { logImpl.v(tag, msg, tr); } public static void d(String msg) { logImpl.d(msg); } public static void d(String tag, String msg) { logImpl.d(tag, msg); } public static void d(String tag, Throwable tr) { logImpl.d(tag, tr); } public static void d(String tag, String msg, Throwable tr) { logImpl.d(tag, msg, tr); } public static void i(String msg) { logImpl.i(msg); } public static void i(String tag, String msg) { logImpl.i(tag, msg); } public static void i(String tag, Throwable tr) { logImpl.i(tag, tr); } public static void i(String tag, String msg, Throwable tr) { logImpl.i(tag, msg, tr); } public static void w(String msg) { logImpl.w(msg); } public static void w(String tag, String msg) { logImpl.w(tag, msg); } public static void w(String tag, Throwable tr) { logImpl.w(tag, tr); } public static void w(String tag, String msg, Throwable tr) { logImpl.w(tag, msg, tr); } public static void e(String msg) { logImpl.e(msg); } public static void e(String tag, String msg) { logImpl.e(tag, msg); } public static void e(String tag, Throwable tr) { logImpl.e(tag, tr); } public static void e(String tag, String msg, Throwable tr) { logImpl.e(tag, msg, tr); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/Predicate.java ================================================ package me.moxun.dreamcatcher.connector.util; public interface Predicate { boolean apply(T t); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ProcessUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; import java.io.FileInputStream; import java.io.IOException; import javax.annotation.Nullable; public class ProcessUtil { /** * Maximum length allowed in {@code /proc/self/cmdline}. Imposed to avoid a large buffer * allocation during the init path. */ private static final int CMDLINE_BUFFER_SIZE = 64; private static String sProcessName; private static boolean sProcessNameRead; /** * Get process name by reading {@code /proc/self/cmdline}. * * @return Process name or null if there was an error reading from {@code /proc/self/cmdline}. * It is unknown how this error can occur in practice and should be considered extremely * rare. */ @Nullable public static synchronized String getProcessName() { if (!sProcessNameRead) { sProcessNameRead = true; try { sProcessName = readProcessName(); } catch (IOException e) { } } return sProcessName; } private static String readProcessName() throws IOException { byte[] cmdlineBuffer = new byte[CMDLINE_BUFFER_SIZE]; // Avoid using a Reader to not pick up a forced 16K buffer. Silly java.io... FileInputStream stream = new FileInputStream("/proc/self/cmdline"); boolean success = false; try { int n = stream.read(cmdlineBuffer); success = true; int endIndex = indexOf(cmdlineBuffer, 0, n, (byte)0 /* needle */); return new String(cmdlineBuffer, 0, endIndex > 0 ? endIndex : n); } finally { Util.close(stream, !success); } } private static int indexOf(byte[] haystack, int offset, int length, byte needle) { for (int i = 0; i < haystack.length; i++) { if (haystack[i] == needle) { return i; } } return -1; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ReflectionUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; import android.view.View; import android.widget.TextView; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.HashSet; import java.util.Set; import javax.annotation.Nullable; public final class ReflectionUtil { private ReflectionUtil() { } public static Object invokeMethod(Object receiver, String name) { try { Method method = receiver.getClass().getDeclaredMethod(name); method.setAccessible(true); return method.invoke(receiver); } catch (NoSuchMethodException e) { LogUtil.w(e.toString()); } catch (InvocationTargetException e) { LogUtil.w(e.toString()); } catch (IllegalAccessException e) { LogUtil.w(e.toString()); } return null; } @Nullable public static Class tryGetClassForName(String className) { try { return Class.forName(className); } catch (ClassNotFoundException e) { return null; } } @Nullable public static Field tryGetDeclaredField(Class theClass, String fieldName) { try { return theClass.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { LogUtil.d( e, "Could not retrieve %s field from %s", fieldName, theClass); return null; } } @Nullable public static Object getFieldValue(Field field, Object target) { try { return field.get(target); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } public static Set getAllPairMethodsByView(View target) { return getAllPairMethods(target.getClass()); } public static Set getAllPairMethods(Class clazz) { Method[] methods = clazz.getMethods(); Set getter = new HashSet<>(); Set setter = new HashSet<>(); for (Method method : methods) { if (Modifier.isPublic(method.getModifiers())) { if (method.getParameterTypes().length == 1) { Class param = method.getParameterTypes()[0]; if (method.getName().startsWith("set")) { if (param.isPrimitive() || CharSequence.class.isAssignableFrom(param.getClass()) && !param.getSimpleName().equals("Object")) { //LogUtil.e("Reflect", "Setter: " + method.getName() + "," + param.getSimpleName()); setter.add(method.getName().substring(3)); } } } else if (method.getParameterTypes().length == 0) { if (method.getName().startsWith("get")) { if (method.getReturnType().isPrimitive() || method.getReturnType().isAssignableFrom(CharSequence.class) && !method.getReturnType().getSimpleName().equals("Object")) { //LogUtil.e("Reflect", "Getter: " + method.getName() + "," + method.getReturnType().getSimpleName()); getter.add(method.getName().substring(3)); } } } } } getter.retainAll(setter); return getter; } public static Set getAllPairDeclaredMethods(Class clazz) { Method[] methods = clazz.getDeclaredMethods(); Set getter = new HashSet<>(); Set setter = new HashSet<>(); for (Method method : methods) { if (Modifier.isPublic(method.getModifiers())) { if (method.getParameterTypes().length == 1) { Class param = method.getParameterTypes()[0]; if (method.getName().startsWith("set")) { if (param.isPrimitive() || CharSequence.class.isAssignableFrom(param.getClass()) && !param.getSimpleName().equals("Object")) { setter.add(method.getName().substring(3)); } } } else if (method.getParameterTypes().length == 0) { if (method.getName().startsWith("get")) { if (method.getReturnType().isPrimitive() || method.getReturnType().isAssignableFrom(CharSequence.class) && !method.getReturnType().getSimpleName().equals("Object")) { getter.add(method.getName().substring(3)); } } } } } getter.retainAll(setter); return getter; } public static void main(String args[]) { for (String s : getAllPairDeclaredMethods(TextView.class)) { System.out.println(s); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/SocketServerManager.java ================================================ package me.moxun.dreamcatcher.connector.util; import java.util.HashMap; import java.util.Map; /** * Created by moxun on 16/7/1. */ public class SocketServerManager { public static final String KEY_LOCAL_SERVER_MANAGER = "LOCAL_SERVER_MANAGER"; public static final String KEY_REMOTE_SERVER_MANAGER = "REMOTE_SERVER_MANAGER"; private static Map managers = new HashMap<>(); private static IServerManager currentManager; public enum Type { REMOTE(KEY_REMOTE_SERVER_MANAGER), LOCAL(KEY_LOCAL_SERVER_MANAGER), INVALID("INVALID"); private final String key; Type(String key) { this.key = key; } public String getKey() { return key; } } public static void register(String key, IServerManager manager) { SocketServerManager.managers.put(key, manager); } public static void startServer(Type type) { if (type == Type.REMOTE) { IServerManager manager = getManager(KEY_REMOTE_SERVER_MANAGER); doStart(manager); } else if (type == Type.LOCAL) { IServerManager manager = getManager(KEY_LOCAL_SERVER_MANAGER); doStart(manager); } } public static void stopServer() { if (currentManager != null) { currentManager.stop(); } } private static void doStart(IServerManager manager) { if (currentManager != manager) { if (currentManager != null) { currentManager.stop(); } if (manager != null) { manager.start(); currentManager = manager; } } } public static IServerManager getManager(String key) { return managers.get(key); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/StringUtil.java ================================================ package me.moxun.dreamcatcher.connector.util; public final class StringUtil { private StringUtil() { } @SuppressWarnings("StringEquality") public static String removePrefix(String string, String prefix, String previousAttempt) { if (string != previousAttempt) { return previousAttempt; } else { return removePrefix(string, prefix); } } public static String removePrefix(String string, String prefix) { if (string.startsWith(prefix)) { return string.substring(prefix.length()); } else { return string; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/ThreadBound.java ================================================ package me.moxun.dreamcatcher.connector.util; /** * Implemented by an object whose methods must be called on a specific thread. If a method is * called from a disallowed thread then {@link IllegalStateException} will be thrown. * To marshal a call to the correct thread, you can use {@link #postAndWait(UncheckedCallable)} or * {@link #postAndWait(Runnable)}, both of which complete synchronously. */ public interface ThreadBound { /** * Checks whether the current thread has access to this object. * @return true if this thread has access to this object; otherwise false */ boolean checkThreadAccess(); /** * Enforces that the current thread has access to this object. * @throws IllegalStateException if the current thread does not have access to this object */ void verifyThreadAccess(); /** * Synchronously executes an {@link UncheckedCallable} on the thread that this object is bound to, * and returns its result. * @param c the {@link UncheckedCallable} to execute * @param the return type of the {@link UncheckedCallable} * @return the return value from {@link UncheckedCallable#call()} * @throws RuntimeException if the {@link UncheckedCallable} could not be executed (the cause * will be null), or if {@link UncheckedCallable#call()} threw an exception (the cause will be the * exception that it threw). */ V postAndWait(UncheckedCallable c); /** * Synchronously executes a {@link Runnable} on the thread that this object is bound to. * @param r the {@link Runnable} to execute * @throws RuntimeException if the {@link Runnable} could not be executed (the cause will be * null), or if {@link Runnable#run()} threw an exception (the cause will be the exception that * it threw). */ void postAndWait(Runnable r); /** * Asynchronously executes a {@link Runnable} on the thread that this object is bound to * after the specified delay. * @param r the {@link Runnable} to execute * @param delayMillis The delay (in milliseconds) until the {@link Runnable} will be executed. * @throws RuntimeException if the {@link Runnable} could not be enqueued. */ void postDelayed(Runnable r, long delayMillis); /** * Removes any pending posts of the given {@link Runnable} that are in the queue. * @param r the {@link Runnable} to remove from the queue */ void removeCallbacks(Runnable r); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/UncheckedCallable.java ================================================ package me.moxun.dreamcatcher.connector.util; /** * A task that returns a result. Implementers define a single method with no arguments called * {@code call}. * *

This interface is identical to {@link java.util.concurrent.Callable} but without the checked * exception. * * @param the result type of method {@code call} */ public interface UncheckedCallable { V call(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/Utf8Charset.java ================================================ package me.moxun.dreamcatcher.connector.util; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; public class Utf8Charset { public static final String NAME = "UTF-8"; public static final Charset INSTANCE = Charset.forName(NAME); public static byte[] encodeUTF8(String str) { try { return str.getBytes(NAME); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public static String decodeUTF8(byte[] bytes) { return new String(bytes, INSTANCE); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/util/Util.java ================================================ package me.moxun.dreamcatcher.connector.util; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class Util { public static T throwIfNull(T item) { if (item == null) { throw new NullPointerException(); } return item; } public static void throwIfNull(T1 item1, T2 item2) { throwIfNull(item1); throwIfNull(item2); } public static void throwIfNull(T1 item1, T2 item2, T3 item3) { throwIfNull(item1); throwIfNull(item2); throwIfNull(item3); } public static void throwIfNotNull(Object item) { if (item != null) { throw new IllegalStateException(); } } public static void throwIf(boolean condition) { if (condition) { throw new IllegalStateException(); } } public static void throwIfNot(boolean condition) { if (!condition) { throw new IllegalStateException(); } } public static void throwIfNot(boolean condition, String format, Object...args) { if (!condition) { String message = String.format(format, args); throw new IllegalStateException(message); } } public static void copy(InputStream input, OutputStream output, byte[] buffer) throws IOException { int n; while ((n = input.read(buffer)) != -1) { output.write(buffer, 0, n); } } public static void close(Closeable closeable, boolean hideException) throws IOException { if (closeable != null) { if (hideException) { try { closeable.close(); } catch (IOException e) { LogUtil.e(e, "Hiding IOException because another is pending"); } } else { closeable.close(); } } } public static void sleepUninterruptibly(long millis) { long remaining = millis; long startTime = System.currentTimeMillis(); do { try { Thread.sleep(remaining); return; } catch (InterruptedException e) { long sleptFor = System.currentTimeMillis() - startTime; remaining -= sleptFor; } } while (remaining > 0); } public static void joinUninterruptibly(Thread t) { while (true) { try { t.join(); return; } catch (InterruptedException e) { // Keep going... } } } public static void awaitUninterruptibly(CountDownLatch latch) { while (true) { try { latch.await(); return; } catch (InterruptedException e) { // Keep going... } } } public static T getUninterruptibly( Future future, long timeout, TimeUnit unit) throws TimeoutException, ExecutionException { long remaining = unit.toMillis(timeout); long startTime = System.currentTimeMillis(); while (true) { try { return future.get(remaining, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { long gotFor = System.currentTimeMillis() - startTime; remaining -= gotFor; } } } public static T getUninterruptibly(Future future) throws ExecutionException { while (true) { try { return future.get(); } catch (InterruptedException e) { //Keep going... } } } public static String readAsUTF8(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); copy(in, out, new byte[1024]); return out.toString("UTF-8"); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/CloseCodes.java ================================================ package me.moxun.dreamcatcher.connector.websocket; /** * Close codes as defined by RFC6455. */ public interface CloseCodes { int NORMAL_CLOSURE = 1000; int PROTOCOL_ERROR = 1002; int CLOSED_ABNORMALLY = 1006; int UNEXPECTED_CONDITION = 1011; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/Frame.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; /** * WebSocket frame as per RFC6455. */ class Frame { public static final byte OPCODE_TEXT_FRAME = 0x1; public static final byte OPCODE_BINARY_FRAME = 0x2; public static final byte OPCODE_CONNECTION_CLOSE = 0x8; public static final byte OPCODE_CONNECTION_PING = 0x9; public static final byte OPCODE_CONNECTION_PONG = 0xA; public boolean fin; public boolean rsv1; public boolean rsv2; public boolean rsv3; public byte opcode; public boolean hasMask; public long payloadLen; public byte[] maskingKey; public byte[] payloadData; public void readFrom(BufferedInputStream input) throws IOException { decodeFirstByte(readByteOrThrow(input)); byte maskAndFirstLengthBits = readByteOrThrow(input); hasMask = (maskAndFirstLengthBits & 0x80) != 0; payloadLen = decodeLength((byte)(maskAndFirstLengthBits & ~0x80), input); maskingKey = hasMask ? decodeMaskingKey(input) : null; payloadData = new byte[(int)payloadLen]; readBytesOrThrow(input, payloadData, 0, (int)payloadLen); MaskingHelper.unmask(maskingKey, payloadData, 0, (int)payloadLen); } public void writeTo(BufferedOutputStream output) throws IOException { output.write(encodeFirstByte()); byte[] lengthAndMaskBit = encodeLength(payloadLen); if (hasMask) { lengthAndMaskBit[0] |= 0x80; } output.write(lengthAndMaskBit, 0, lengthAndMaskBit.length); if (hasMask) { throw new UnsupportedOperationException("Writing masked data not implemented"); } output.write(payloadData, 0, (int) payloadLen); } private void decodeFirstByte(byte b) { fin = (b & 0x80) != 0; rsv1 = (b & 0x40) != 0; rsv2 = (b & 0x20) != 0; rsv3 = (b & 0x10) != 0; opcode = (byte)(b & 0xf); } private byte encodeFirstByte() { byte b = 0; if (fin) { b |= 0x80; } if (rsv1) { b |= 0x40; } if (rsv2) { b |= 0x20; } if (rsv3) { b |= 0x10; } b |= (opcode & 0xf); return b; } private long decodeLength(byte firstLenByte, InputStream in) throws IOException { if (firstLenByte <= 125) { return firstLenByte; } else if (firstLenByte == 126) { return (readByteOrThrow(in) & 0xff) << 8 | (readByteOrThrow(in) & 0xff); } else if (firstLenByte == 127) { long len = 0; for (int i = 0; i < 8; i++) { len |= (readByteOrThrow(in) & 0xff); len <<= 8; } return len; } else { throw new IOException("Unexpected length byte: " + firstLenByte); } } private static byte[] encodeLength(long len) { if (len <= 125) { return new byte[] { (byte)len }; } else if (len <= 0xffff) { return new byte[] { 126, (byte)((len >> 8) & 0xff), (byte)((len) & 0xff) }; } else { return new byte[] { 127, (byte)((len >> 56) & 0xff), (byte)((len >> 48) & 0xff), (byte)((len >> 40) & 0xff), (byte)((len >> 32) & 0xff), (byte)((len >> 24) & 0xff), (byte)((len >> 16) & 0xff), (byte)((len >> 8) & 0xff), (byte)((len) & 0xff) }; } } private static byte[] decodeMaskingKey(InputStream in) throws IOException { byte[] key = new byte[4]; readBytesOrThrow(in, key, 0, key.length); return key; } private static void readBytesOrThrow(InputStream in, byte[] buf, int offset, int count) throws IOException { while (count > 0) { int n = in.read(buf, offset, count); if (n == -1) { throw new EOFException(); } count -= n; offset += n; } } private static byte readByteOrThrow(InputStream in) throws IOException { int b = in.read(); if (b == -1) { throw new EOFException(); } return (byte)b; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/FrameHelper.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import me.moxun.dreamcatcher.connector.util.Utf8Charset; class FrameHelper { public static Frame createTextFrame(String payload) { return createSimpleFrame(Frame.OPCODE_TEXT_FRAME, Utf8Charset.encodeUTF8(payload)); } public static Frame createBinaryFrame(byte[] payload) { return createSimpleFrame(Frame.OPCODE_BINARY_FRAME, payload); } public static Frame createCloseFrame(int closeCode, String reasonPhrase) { byte[] reasonPhraseEncoded = null; int payloadLen = 2; if (reasonPhrase != null) { reasonPhraseEncoded = Utf8Charset.encodeUTF8(reasonPhrase); payloadLen += reasonPhraseEncoded.length; } byte[] payload = new byte[payloadLen]; payload[0] = (byte)((closeCode >> 8) & 0xff); payload[1] = (byte)((closeCode) & 0xff); if (reasonPhraseEncoded != null) { System.arraycopy(reasonPhraseEncoded, 0, payload, 2, reasonPhraseEncoded.length); } return createSimpleFrame(Frame.OPCODE_CONNECTION_CLOSE, payload); } public static Frame createPingFrame(byte[] payload, int payloadLen) { return createSimpleFrame(Frame.OPCODE_CONNECTION_PING, payload, payloadLen); } public static Frame createPongFrame(byte[] payload, int payloadLen) { return createSimpleFrame(Frame.OPCODE_CONNECTION_PONG, payload, payloadLen); } private static Frame createSimpleFrame(byte opcode, byte[] payload) { return createSimpleFrame(opcode, payload, payload.length); } private static Frame createSimpleFrame(byte opCode, byte[] payload, int payloadLen) { Frame frame = new Frame(); frame.fin = true; frame.hasMask = false; frame.opcode = opCode; frame.payloadLen = payloadLen; frame.payloadData = payload; return frame; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/MaskingHelper.java ================================================ package me.moxun.dreamcatcher.connector.websocket; class MaskingHelper { public static void unmask(byte[] key, byte[] data, int offset, int count) { int index = 0; while (count-- > 0) { data[offset++] ^= key[index++ % key.length]; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/ReadCallback.java ================================================ package me.moxun.dreamcatcher.connector.websocket; interface ReadCallback { void onCompleteFrame(byte opcode, byte[] payload, int payloadLen); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/ReadHandler.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; class ReadHandler { private final BufferedInputStream mBufferedInput; private final SimpleEndpoint mEndpoint; /** * Used to build a larger payload over multiple frames. */ private final ByteArrayOutputStream mCurrentPayload = new ByteArrayOutputStream(); public ReadHandler(InputStream bufferedInput, SimpleEndpoint endpoint) { mBufferedInput = new BufferedInputStream(bufferedInput, 1024); mEndpoint = endpoint; } /** * Enter a loop processing incoming frames until orderly shutdown or a socket exception is * thrown. This method returns normally on orderly shutdown, throws otherwise. * * @throws IOException Socket exception during the read loop. */ public void readLoop(ReadCallback readCallback) throws IOException { Frame frame = new Frame(); do { frame.readFrom(mBufferedInput); mCurrentPayload.write(frame.payloadData, 0, (int)frame.payloadLen); if (frame.fin) { byte[] completePayload = mCurrentPayload.toByteArray(); readCallback.onCompleteFrame(frame.opcode, completePayload, completePayload.length); mCurrentPayload.reset(); } } while (frame.opcode != Frame.OPCODE_CONNECTION_CLOSE); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/SimpleEndpoint.java ================================================ package me.moxun.dreamcatcher.connector.websocket; /** * Alternative to JSR-356's Endpoint class but with a less insane J2EE-style API. */ public interface SimpleEndpoint { /** * Invoked when a new WebSocket session is established. * * @param session Unique handle for this session. */ void onOpen(SimpleSession session); /** * Invoked when a text-based message is received from the peer. May have spanned multiple * WebSocket packets. * * @param session Unique handle for this session. * @param message Complete payload data. */ void onMessage(SimpleSession session, String message); /** * Invoked when a binary message is received from the peer. May have spanned multiple * WebSocket packets. * * @param session Unique handle for this session. * @param message Complete payload data. * @param messageLen Maximum number of bytes of {@code message} to read. */ void onMessage(SimpleSession session, byte[] message, int messageLen); /** * Invoked when a remote peer closed the WebSocket session or if {@link SimpleSession#close} * is invoked on our side. * * @param session Unique handle for this session. * @param closeReasonCode Close reason code (see RFC6455) * @param closeReasonPhrase Possibly arbitrary text phrase associated with the reason code. */ void onClose(SimpleSession session, int closeReasonCode, String closeReasonPhrase); /** * Invoked when errors occur out of the normal band of the WebSocket protocol. This is * intended for debug purposes and is generally not actionable. The {@link #onClose} method * will still be invoked in all cases, making it reasonable to simply log in this method. * * @param session Unique handle for this session. * @param t Exception that occurred. */ void onError(SimpleSession session, Throwable t); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/SimpleSession.java ================================================ package me.moxun.dreamcatcher.connector.websocket; /** * Alternative to JSR-356's Session class but with a less insane J2EE-style API. */ public interface SimpleSession { void sendText(String payload); void sendBinary(byte[] payload); /** * Request that the session be closed. * * @param closeReason Close reason, as per RFC6455 * @param reasonPhrase Possibly arbitrary close reason phrase. */ void close(int closeReason, String reasonPhrase); boolean isOpen(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/WebSocketHandler.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import android.util.Base64; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.manager.Lifecycle; import me.moxun.dreamcatcher.connector.manager.SimpleConnectorLifecycleManager; import me.moxun.dreamcatcher.connector.server.SocketLike; import me.moxun.dreamcatcher.connector.server.http.HttpHandler; import me.moxun.dreamcatcher.connector.server.http.HttpStatus; import me.moxun.dreamcatcher.connector.server.http.LightHttpBody; import me.moxun.dreamcatcher.connector.server.http.LightHttpMessage; import me.moxun.dreamcatcher.connector.server.http.LightHttpRequest; import me.moxun.dreamcatcher.connector.server.http.LightHttpResponse; import me.moxun.dreamcatcher.connector.server.http.LightHttpServer; import me.moxun.dreamcatcher.connector.util.LogUtil; import me.moxun.dreamcatcher.connector.util.Utf8Charset; import me.moxun.dreamcatcher.event.CaptureEvent; /** * Crazy kludge to support upgrading to the WebSocket protocol while still using the * {@link HttpHandler} harness. *

* The way this works is that we pump the request directly into our WebSocket implementation and * force write the response out to the connection without returning. Then, we extract the * remaining buffered input stream bytes from the socket and stitch them together with the * raw sockets input stream and pass everything onto the WebSocket engine which blocks * until WebSocket orderly shutdown. */ public class WebSocketHandler implements HttpHandler { private static final String HEADER_UPGRADE = "Upgrade"; private static final String HEADER_CONNECTION = "Connection"; private static final String HEADER_SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; private static final String HEADER_SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; private static final String HEADER_SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; private static final String HEADER_SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; private static final String HEADER_UPGRADE_WEBSOCKET = "websocket"; private static final String HEADER_CONNECTION_UPGRADE = "Upgrade"; private static final String HEADER_SEC_WEBSOCKET_VERSION_13 = "13"; // Are you kidding me? The WebSocket spec requires that we append this weird hardcoded String // to the key we receive from the client, SHA-1 that, and base64 encode it back to the client. // I'm guessing this is to prevent replay attacks of some kind but given that there's no actual // security context here, I can only imagine that this is just security through obscurity in // some fashion. private static final String SERVER_KEY_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private final SimpleEndpoint mEndpoint; public WebSocketHandler(SimpleEndpoint endpoint) { mEndpoint = endpoint; } @Override public boolean handleRequest( SocketLike socket, LightHttpRequest request, LightHttpResponse response) throws IOException { LogUtil.e("Try to establish WebSocket connection"); if (!isSupportableUpgradeRequest(request)) { LogUtil.e("WebSocket connection failed: 501"); response.code = HttpStatus.HTTP_NOT_IMPLEMENTED; response.reasonPhrase = "Not Implemented"; response.body = LightHttpBody.create( "Not a supported WebSocket upgrade request\n", "text/plain"); return true; } // This will not return on successful WebSocket upgrade, but rather block until the session is // shut down or a socket error occurs. doUpgrade(socket, request, response); return false; } private static boolean isSupportableUpgradeRequest(LightHttpRequest request) { return HEADER_UPGRADE_WEBSOCKET.equalsIgnoreCase(getFirstHeaderValue(request, HEADER_UPGRADE)) && HEADER_CONNECTION_UPGRADE.equals(getFirstHeaderValue(request, HEADER_CONNECTION)) && HEADER_SEC_WEBSOCKET_VERSION_13.equals( getFirstHeaderValue(request, HEADER_SEC_WEBSOCKET_VERSION)); } private void doUpgrade( SocketLike socketLike, LightHttpRequest request, LightHttpResponse response) throws IOException { response.code = HttpStatus.HTTP_SWITCHING_PROTOCOLS; response.reasonPhrase = "Switching Protocols"; response.addHeader(HEADER_UPGRADE, HEADER_UPGRADE_WEBSOCKET); response.addHeader(HEADER_CONNECTION, HEADER_CONNECTION_UPGRADE); response.body = null; String clientKey = getFirstHeaderValue(request, HEADER_SEC_WEBSOCKET_KEY); if (clientKey != null) { response.addHeader(HEADER_SEC_WEBSOCKET_ACCEPT, generateServerKey(clientKey)); } InputStream in = socketLike.getInput(); OutputStream out = socketLike.getOutput(); LightHttpServer.writeResponseMessage( response, new LightHttpServer.HttpMessageWriter(new BufferedOutputStream(out))); WebSocketSession session = new WebSocketSession(in, out, mEndpoint); LogUtil.e("Successfully upgraded to WebSocket!"); SimpleConnectorLifecycleManager.setCurrentState(Lifecycle.WEBSOCKET_SESSION_OPENING); CaptureEvent.send("Websocket session is open"); session.handle(); } private static String generateServerKey(String clientKey) { try { String serverKey = clientKey + SERVER_KEY_GUID; MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); sha1.update(Utf8Charset.encodeUTF8(serverKey)); return Base64.encodeToString(sha1.digest(), Base64.NO_WRAP); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Nullable private static String getFirstHeaderValue(LightHttpMessage message, String headerName) { return message.getFirstHeaderValue(headerName); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/WebSocketSession.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.atomic.AtomicBoolean; /** * Binding driver between raw socket I/O and a high-level WebSocket interface. This implementation * is generally very weak and doesn't offer sensible optimizations such as re-used buffers, * efficient UTF-8 encoding/decoding, or the full spectrum of features defined in the RFC. */ class WebSocketSession implements SimpleSession { private final ReadHandler mReadHandler; private final WriteHandler mWriteHandler; private final SimpleEndpoint mEndpoint; private AtomicBoolean mIsOpen = new AtomicBoolean(false); private volatile boolean mSentClose; public WebSocketSession( InputStream rawSocketInput, OutputStream rawSocketOutput, SimpleEndpoint endpoint) { mReadHandler = new ReadHandler(rawSocketInput, endpoint); mWriteHandler = new WriteHandler(rawSocketOutput); mEndpoint = endpoint; } public void handle() throws IOException { markAndSignalOpen(); // Loop until orderly shutdown or socket exception. try { mReadHandler.readLoop(mReadCallback); } catch (EOFException e) { // No need to rethrow, this can be considered a graceful shutdown of the socket (though // not the WebSocket). markAndSignalClosed(CloseCodes.UNEXPECTED_CONDITION, "EOF while reading"); } catch (IOException e) { markAndSignalClosed(CloseCodes.CLOSED_ABNORMALLY, null /* reasonPhrase */); throw e; } } @Override public void sendText(String payload) { doWrite(FrameHelper.createTextFrame(payload)); } @Override public void sendBinary(byte[] payload) { doWrite(FrameHelper.createBinaryFrame(payload)); } @Override public void close(int closeReason, String reasonPhrase) { sendClose(closeReason, reasonPhrase); markAndSignalClosed(closeReason, reasonPhrase); } private void sendClose(int closeReason, String reasonPhrase) { doWrite(FrameHelper.createCloseFrame(closeReason, reasonPhrase)); markSentClose(); } void markSentClose() { mSentClose = true; } void markAndSignalOpen() { if (!mIsOpen.getAndSet(true)) { mEndpoint.onOpen(this /* session */); } } void markAndSignalClosed(int closeReason, String reasonPhrase) { if (mIsOpen.getAndSet(false)) { mEndpoint.onClose(this /* session */, closeReason, reasonPhrase); } } @Override public boolean isOpen() { return mIsOpen.get(); } private void doWrite(Frame frame) { if (signalErrorIfNotOpen()) { return; } mWriteHandler.write(frame, mErrorForwardingWriteCallback); } /** * Signals an error to the {@link SimpleEndpoint} if the session is closed. * * @return True if an error was signaled (the session is closed); false otherwise. */ private boolean signalErrorIfNotOpen() { if (!isOpen()) { signalError(new IOException("Session is closed")); return true; } return false; } private void signalError(IOException e) { mEndpoint.onError(this /* session */, e); } private final ReadCallback mReadCallback = new ReadCallback() { @Override public void onCompleteFrame(byte opcode, byte[] payload, int payloadLen) { switch (opcode) { case Frame.OPCODE_CONNECTION_CLOSE: handleClose(payload, payloadLen); break; case Frame.OPCODE_CONNECTION_PING: handlePing(payload, payloadLen); break; case Frame.OPCODE_CONNECTION_PONG: handlePong(payload, payloadLen); break; case Frame.OPCODE_TEXT_FRAME: handleTextFrame(payload, payloadLen); break; case Frame.OPCODE_BINARY_FRAME: handleBinaryFrame(payload, payloadLen); break; default: signalError(new IOException("Unsupported frame opcode=" + opcode)); break; } } private void handleClose(byte[] payload, int payloadLen) { int closeCode; String closeReasonPhrase; if (payloadLen >= 2) { closeCode = ((payload[0] & 0xff) << 8) | (payload[1] & 0xff); closeReasonPhrase = (payloadLen > 2) ? new String(payload, 2, payloadLen - 2) : null; } else { closeCode = CloseCodes.CLOSED_ABNORMALLY; closeReasonPhrase = "Unparseable close frame"; } // We must acknowledge the peer's close frame. if (!mSentClose) { sendClose(CloseCodes.NORMAL_CLOSURE, "Received close frame"); } markAndSignalClosed(closeCode, closeReasonPhrase); } private void handlePing(byte[] payload, int payloadLen) { doWrite(FrameHelper.createPongFrame(payload, payloadLen)); } private void handlePong(byte[] payload, int payloadLen) { // Great, whatever... } private void handleTextFrame(byte[] payload, int payloadLen) { mEndpoint.onMessage(WebSocketSession.this, new String(payload, 0, payloadLen)); } private void handleBinaryFrame(byte[] payload, int payloadLen) { mEndpoint.onMessage(WebSocketSession.this, payload, payloadLen); } }; private final WriteCallback mErrorForwardingWriteCallback = new WriteCallback() { @Override public void onFailure(IOException e) { signalError(e); } @Override public void onSuccess() { // Boring... } }; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/WriteCallback.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import java.io.IOException; interface WriteCallback { void onFailure(IOException e); void onSuccess(); } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/connector/websocket/WriteHandler.java ================================================ package me.moxun.dreamcatcher.connector.websocket; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import javax.annotation.concurrent.ThreadSafe; @ThreadSafe class WriteHandler { private final BufferedOutputStream mBufferedOutput; public WriteHandler(OutputStream rawSocketOutput) { mBufferedOutput = new BufferedOutputStream(rawSocketOutput, 1024); } public synchronized void write(Frame frame, WriteCallback callback) { try { frame.writeTo(mBufferedOutput); mBufferedOutput.flush(); callback.onSuccess(); } catch (IOException e) { callback.onFailure(e); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/event/CaptureEvent.java ================================================ package me.moxun.dreamcatcher.event; import org.greenrobot.eventbus.EventBus; /** * Created by moxun on 16/12/9. */ public class CaptureEvent { private static boolean allowReport = false; public String cause; public CaptureEvent(String cause) { this.cause = cause; } public static void send(String msg) { if (allowReport) { EventBus.getDefault().post(new CaptureEvent(msg)); } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/event/OperateEvent.java ================================================ package me.moxun.dreamcatcher.event; /** * Created by moxun on 16/12/12. */ public class OperateEvent { public static final int TARGET_CONNECTOR = 0; public static final int TARGET_PROXY = 1; public int target = 0; public boolean active = false; public boolean error = false; public String msg = ""; public OperateEvent(int target, boolean active, boolean error) { this(target, active, error, ""); } public OperateEvent(int target, boolean active) { this(target, active, false); } public OperateEvent(int target, boolean active, boolean error, String msg) { this.target = target; this.active = active; this.error = error; this.msg = msg; } @Override public String toString() { final StringBuffer sb = new StringBuffer("OperateEvent{"); sb.append("active=").append(active); sb.append(", target=").append(target); sb.append(", error=").append(error); sb.append('}'); return sb.toString(); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/misc/X509ExtendedTrustManager.java ================================================ package me.moxun.dreamcatcher.misc; /* * Copyright 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.Socket; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; import javax.net.ssl.X509TrustManager; /** * Allows the connection constraints such as hostname verification and algorithm * constraints to be checked along with the checks done in * {@link X509TrustManager}. * * @hide * @see SSLParameters#setEndpointIdentificationAlgorithm(String) * @since 1.7 */ public abstract class X509ExtendedTrustManager implements X509TrustManager { /** * Checks whether the specified certificate chain (partial or complete) can * be validated and is trusted for client authentication for the specified * authentication type. *

* If the {@code socket} is supplied, its {@link SSLParameters} will be * checked for endpoint identification. * * @param chain the certificate chain to validate. * @param authType the authentication type used. * @param socket the socket from which to check the {@link SSLParameters} * @throws CertificateException if the certificate chain can't be validated * or isn't trusted. * @throws IllegalArgumentException if the specified certificate chain is * empty or {@code null}, or if the specified authentication * type is {@code null} or an empty string. */ public abstract void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException; /** * Checks whether the specified certificate chain (partial or complete) can * be validated and is trusted for server authentication for the specified * key exchange algorithm. *

* If the {@code socket} is supplied, its {@link SSLParameters} will be * checked for endpoint identification. * * @param chain the certificate chain to validate. * @param authType the authentication type used. * @param socket the socket from which to check the {@link SSLParameters} * @throws CertificateException if the certificate chain can't be validated * or isn't trusted. * @throws IllegalArgumentException if the specified certificate chain is * empty or {@code null}, or if the specified authentication * type is {@code null} or an empty string. */ public abstract void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException; /** * Checks whether the specified certificate chain (partial or complete) can * be validated and is trusted for client authentication for the specified * authentication type. *

* If the {@code engine} is supplied, its {@link SSLParameters} will be * checked for endpoint identification. * * @param chain the certificate chain to validate. * @param authType the authentication type used. * @param engine the engine from which to check the {@link SSLParameters} * @throws CertificateException if the certificate chain can't be validated * or isn't trusted. * @throws IllegalArgumentException if the specified certificate chain is * empty or {@code null}, or if the specified authentication * type is {@code null} or an empty string. */ public abstract void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException; /** * Checks whether the specified certificate chain (partial or complete) can * be validated and is trusted for server authentication for the specified * key exchange algorithm. *

* If the {@code engine} is supplied, its {@link SSLParameters} will be * checked for endpoint identification. * * @param chain the certificate chain to validate. * @param authType the authentication type used. * @param engine the engine from which to check the {@link SSLParameters} * @throws CertificateException if the certificate chain can't be validated * or isn't trusted. * @throws IllegalArgumentException if the specified certificate chain is * empty or {@code null}, or if the specified authentication * type is {@code null} or an empty string. */ public abstract void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException; } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/service/ConnectorService.java ================================================ package me.moxun.dreamcatcher.service; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.support.annotation.Nullable; import me.moxun.dreamcatcher.connector.Connector; /** * Created by moxun on 16/12/12. */ public class ConnectorService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); new Thread(new Runnable() { @Override public void run() { Connector.open(getApplication()); } }).start(); } @Override public void onDestroy() { super.onDestroy(); Connector.close(); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/service/ProxyService.java ================================================ package me.moxun.dreamcatcher.service; import android.app.Service; import android.content.Intent; import android.os.AsyncTask; import android.os.IBinder; import android.support.annotation.Nullable; import android.util.Log; import net.lightbody.bmp.BrowserMobProxy; import net.lightbody.bmp.BrowserMobProxyServer; import net.lightbody.bmp.proxy.CaptureType; import org.greenrobot.eventbus.EventBus; import me.moxun.dreamcatcher.DCApplication; import me.moxun.dreamcatcher.connector.manager.SimpleConnectorLifecycleManager; import me.moxun.dreamcatcher.event.CaptureEvent; import me.moxun.dreamcatcher.event.OperateEvent; /** * Created by moxun on 16/12/9. */ public class ProxyService extends Service { private BrowserMobProxy proxy = new BrowserMobProxyServer(); @Override public void onDestroy() { super.onDestroy(); proxy.stop(); postEvent("Stop monitoring"); SimpleConnectorLifecycleManager.setProxyEnabled(false); EventBus.getDefault().post(new OperateEvent(OperateEvent.TARGET_PROXY, false)); } @Override public void onCreate() { super.onCreate(); new AsyncTask() { @Override protected Boolean doInBackground(Void... params) { postEvent("Prepare to turn on proxy……"); proxy.setTrustAllServers(true); try { proxy.start(9999); } catch (Exception e) { proxy.start(); } ((DCApplication) getApplication()).setPort(proxy.getPort()); postEvent("Proxy is bound to port " + proxy.getPort()); proxy.enableHarCaptureTypes(CaptureType.REQUEST_HEADERS, CaptureType.REQUEST_COOKIES, CaptureType.REQUEST_CONTENT, CaptureType.RESPONSE_HEADERS, CaptureType.RESPONSE_COOKIES, CaptureType.RESPONSE_CONTENT, CaptureType.RESPONSE_BINARY_CONTENT, CaptureType.REQUEST_BINARY_CONTENT); Log.e("ProxyService", "Serve on port: " + proxy.getPort()); postEvent("Start monitoring"); proxy.newHar(); EventBus.getDefault().post(new OperateEvent(OperateEvent.TARGET_PROXY, true)); SimpleConnectorLifecycleManager.setProxyEnabled(true); Log.e("ProxyService", "Start monitoring"); return Boolean.TRUE; } }.execute(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void postEvent(String s) { CaptureEvent.send(s); } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/wrapper/DCHeader.java ================================================ package me.moxun.dreamcatcher.wrapper; import android.util.Pair; import java.util.ArrayList; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.reporter.NetworkEventReporter; /** * Created by moxun on 16/12/8. */ public class DCHeader implements NetworkEventReporter.InspectorHeaders { private final ArrayList> headers = new ArrayList<>(); private String contentType = "text/plain"; public void addHeader(String key, String value) { Pair header = new Pair<>(key, value); headers.add(header); if (key.toLowerCase().equals("content-type")) { contentType = value; } } public String getContentType() { return contentType; } @Override public int headerCount() { return headers.size(); } @Override public String headerName(int index) { return headers.get(index).first; } @Override public String headerValue(int index) { return headers.get(index).second; } @Nullable @Override public String firstHeaderValue(String name) { for (Pair pair : headers) { if (pair.first.equals(name)) { return pair.second; } } return null; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/wrapper/DCRequest.java ================================================ package me.moxun.dreamcatcher.wrapper; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarRequest; import java.io.IOException; import java.io.OutputStream; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.reporter.NetworkEventReporter; import me.moxun.dreamcatcher.connector.reporter.RequestBodyHelper; /** * Created by moxun on 16/12/8. */ public class DCRequest extends DCHeader implements NetworkEventReporter.InspectorRequest { private HarEntry harEntry; private RequestBodyHelper helper; public void attachBodyHelper(RequestBodyHelper helper) { this.helper = helper; } public DCRequest(HarEntry entry) { this.harEntry = entry; } public HarRequest getRequest() { return harEntry.getRequest(); } @Override public String id() { return harEntry.getId(); } @Override public String friendlyName() { return null; } @Nullable @Override public Integer friendlyNameExtra() { return null; } @Override public String url() { return harEntry.getRequest().getUrl(); } @Override public String method() { return harEntry.getRequest().getMethod(); } @Nullable @Override public byte[] body() throws IOException { byte[] body = harEntry.getRequest().getContent().getBinaryContent(); if (body != null) { OutputStream out = helper.createBodySink(firstHeaderValue("Content-Encoding")); try { out.write(body); } finally { out.close(); } return helper.getDisplayBody(); } else { return null; } } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/wrapper/DCResponse.java ================================================ package me.moxun.dreamcatcher.wrapper; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarResponse; import net.lightbody.bmp.core.har.HarTimings; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.inspector.protocol.module.Network; import me.moxun.dreamcatcher.connector.reporter.NetworkEventReporter; /** * Created by moxun on 16/12/8. */ public class DCResponse extends DCHeader implements NetworkEventReporter.InspectorResponse { void notifyResponse() { harEntry.responseFinish(); } private HarEntry harEntry; public DCResponse(HarEntry entry) { this.harEntry = entry; } public HarResponse getResponse() { return harEntry.getResponse(); } @Override public String requestId() { return harEntry.getId(); } @Override public String url() { return harEntry.getRequest().getUrl(); } @Override public int statusCode() { return harEntry.getResponse().getStatus(); } @Override public String reasonPhrase() { return harEntry.getResponse().getStatusText(); } @Override public boolean connectionReused() { return false; } @Override public int connectionId() { return 0; } @Override public boolean fromDiskCache() { return false; } @Nullable @Override public Network.ResourceTiming getTiming() { HarTimings timings = harEntry.getTimings(); Network.ResourceTiming resourceTiming = new Network.ResourceTiming(); resourceTiming.requestTime = harEntry.getRequestTime(); resourceTiming.proxyStart = timings.getWait(); resourceTiming.proxyEnd = resourceTiming.proxyStart; resourceTiming.dnsStart = resourceTiming.proxyEnd; resourceTiming.dnsEnd = resourceTiming.dnsStart + timings.getDns(); resourceTiming.connectStart = resourceTiming.dnsEnd; resourceTiming.connectEnd = resourceTiming.connectStart + timings.getConnect(); resourceTiming.sslStart = resourceTiming.connectEnd; resourceTiming.sslEnd = resourceTiming.sslStart + timings.getSsl(); resourceTiming.sendStart = resourceTiming.sslEnd; resourceTiming.sendEnd = resourceTiming.sendStart + timings.getSend(); resourceTiming.receiveHeadersEnd = harEntry.getTotalTime(); return resourceTiming; } } ================================================ FILE: app/src/main/java/me/moxun/dreamcatcher/wrapper/ProxyManager.java ================================================ package me.moxun.dreamcatcher.wrapper; import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import me.moxun.dreamcatcher.connector.reporter.DefaultResponseHandler; import me.moxun.dreamcatcher.connector.reporter.NetworkEventReporter; import me.moxun.dreamcatcher.connector.reporter.NetworkEventReporterImpl; import me.moxun.dreamcatcher.connector.reporter.RequestBodyHelper; import me.moxun.dreamcatcher.event.CaptureEvent; /** * Created by moxun on 16/12/8. */ public class ProxyManager { private static final AtomicInteger sSequenceNumberGenerator = new AtomicInteger(0); private final NetworkEventReporter DCHook = NetworkEventReporterImpl.get(); private final int mRequestId; @Nullable private String mRequestIdString; @Nullable private RequestBodyHelper mRequestBodyHelper; private ProxyManager() { mRequestId = sSequenceNumberGenerator.getAndIncrement(); mRequestBodyHelper = new RequestBodyHelper(DCHook, getDCRequestId()); CaptureEvent.send("Capture request with id " + mRequestId); } public RequestBodyHelper getRequestBodyHelper() { return mRequestBodyHelper; } public static ProxyManager newInstance() { return new ProxyManager(); } public void requestWillBeSent(DCRequest request) { Log.e("Manager", "requestWillBeSent " + request.id()); DCHook.requestWillBeSent(request); } public void responseHeadersReceived(DCResponse response) { response.notifyResponse(); DCHook.responseHeadersReceived(response); } public void httpExchangeFailed(String errorText) { DCHook.httpExchangeFailed(getDCRequestId(), errorText); } public void interpretResponseStream(DCResponse response) { InputStream inputStream = new ByteArrayInputStream(response.getResponse().getContent().getBinaryContent()); inputStream = DCHook.interpretResponseStream(getDCRequestId(), response.getContentType(), null, inputStream, new DefaultResponseHandler(DCHook,getDCRequestId())); try { read(inputStream); } catch (IOException e) { e.printStackTrace(); } responseReadFinished(); } private byte[] read(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024 * 4]; int n = 0; while ((n = in.read(buffer)) != -1) { out.write(buffer, 0, n); } byte[] result = out.toByteArray(); out.flush(); out.close(); return result; } public void responseReadFailed(String errorText) { httpExchangeFailed(errorText); } public void responseReadFinished() { DCHook.responseReadFinished(getDCRequestId()); } public void dataSent(DCRequest request) { DCHook.dataSent(getDCRequestId(), (int) request.getRequest().getBodySize(), 0); mRequestBodyHelper.reportDataSent(); } public void dataReceived(int bodySize) { //DCHook.dataReceived(getDCRequestId(), bodySize, 0); } public String getDCRequestId() { if (mRequestIdString == null) { mRequestIdString = String.valueOf(mRequestId); } return mRequestIdString; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/BrowserMobProxy.java ================================================ package net.lightbody.bmp; import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.filters.RequestFilter; import net.lightbody.bmp.filters.ResponseFilter; import net.lightbody.bmp.mitm.TrustSource; import net.lightbody.bmp.proxy.BlacklistEntry; import net.lightbody.bmp.proxy.CaptureType; import net.lightbody.bmp.proxy.auth.AuthType; import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; import org.littleshoot.proxy.HttpFiltersSource; import org.littleshoot.proxy.MitmManager; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Collection; import java.util.EnumSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; public interface BrowserMobProxy { /** * Starts the proxy on port 0 (a JVM-selected open port). The proxy will bind the listener to the wildcard address (0:0:0:0 - all network interfaces). * * @throws java.lang.IllegalStateException if the proxy has already been started */ void start(); /** * Starts the proxy on the specified port. The proxy will bind the listener to the wildcard address (0:0:0:0 - all network interfaces). * * @param port port to listen on * @throws java.lang.IllegalStateException if the proxy has already been started */ void start(int port); /** * Starts the proxy on the specified port. The proxy will listen for connections on the network interface specified by the bindAddress, and will * also initiate connections to upstream servers on the same network interface. * * @param port port to listen on * @param bindAddress address of the network interface on which the proxy will listen for connections and also attempt to connect to upstream servers. * @throws java.lang.IllegalStateException if the proxy has already been started */ void start(int port, InetAddress bindAddress); /** * Starts the proxy on the specified port. The proxy will listen for connections on the network interface specified by the clientBindAddress, and will * initiate connections to upstream servers from the network interface specified by the serverBindAddress. * * @param port port to listen on * @param clientBindAddress address of the network interface on which the proxy will listen for connections * @param serverBindAddress address of the network interface on which the proxy will connect to upstream servers * @throws java.lang.IllegalStateException if the proxy has already been started */ void start(int port, InetAddress clientBindAddress, InetAddress serverBindAddress); /** * Returns true if the proxy is started and listening for connections, otherwise false. */ boolean isStarted(); /** * Stops accepting new client connections and initiates a graceful shutdown of the proxy server, waiting up to 5 seconds for network * traffic to stop. If the proxy was previously stopped or aborted, this method has no effect. * * @throws java.lang.IllegalStateException if the proxy has not been started. */ void stop(); /** * Like {@link #stop()}, shuts down the proxy server and no longer accepts incoming connections, but does not wait for any existing * network traffic to cease. Any existing connections to clients or to servers may be force-killed immediately. * If the proxy was previously stopped or aborted, this method has no effect. * * @throws java.lang.IllegalStateException if the proxy has not been started */ void abort(); /** * Returns the address of the network interface on which the proxy is listening for client connections. * * @return the client bind address, or null if the proxy has not been started */ InetAddress getClientBindAddress(); /** * Returns the actual port on which the proxy is listening for client connections. * * @throws java.lang.IllegalStateException if the proxy has not been started */ int getPort(); /** * Returns the address address of the network interface the proxy will use to initiate upstream connections. If no server bind address * has been set, this method returns null, even if the proxy has been started. * * @return server bind address if one has been set, otherwise null */ InetAddress getServerBindAddress(); /** * Retrieves the current HAR. * * @return current HAR, or null if HAR capture is not enabled */ Har getHar(); /** * Starts a new HAR file with the default page name (see {@link #newPage()}. Enables HAR capture if it was not previously enabled. * * @return existing HAR file, or null if none exists or HAR capture was disabled */ Har newHar(); /** * Starts a new HAR file with the specified initialPageRef as the page name and page title. Enables HAR capture if it was not previously enabled. * * @param initialPageRef initial page name of the new HAR file * @return existing HAR file, or null if none exists or HAR capture was disabled */ Har newHar(String initialPageRef); /** * Starts a new HAR file with the specified page name and page title. Enables HAR capture if it was not previously enabled. * * @param initialPageRef initial page name of the new HAR file * @param initialPageTitle initial page title of the new HAR file * @return existing HAR file, or null if none exists or HAR capture was disabled */ Har newHar(String initialPageRef, String initialPageTitle); /** * Sets the data types that will be captured in the HAR file for future requests. Replaces any existing capture types with the specified * capture types. A null or empty set will not disable HAR capture, but will disable collection of * additional {@link net.lightbody.bmp.proxy.CaptureType} data types. {@link net.lightbody.bmp.proxy.CaptureType} provides several * convenience methods to retrieve commonly-used capture settings. *

* Note: HAR capture must still be explicitly enabled via {@link #newHar()} or {@link #newHar(String)} to begin capturing * any request and response contents. * * @param captureTypes HAR data types to capture */ void setHarCaptureTypes(Set captureTypes); /** * Sets the data types that will be captured in the HAR file for future requests. Replaces any existing capture types with the specified * capture types. A null or empty set will not disable HAR capture, but will disable collection of * additional {@link net.lightbody.bmp.proxy.CaptureType} data types. {@link net.lightbody.bmp.proxy.CaptureType} provides several * convenience methods to retrieve commonly-used capture settings. *

* Note: HAR capture must still be explicitly enabled via {@link #newHar()} or {@link #newHar(String)} to begin capturing * any request and response contents. * * @param captureTypes HAR data types to capture */ void setHarCaptureTypes(CaptureType... captureTypes); /** * @return A copy of HAR capture types currently in effect. The EnumSet cannot be used to modify the HAR capture types currently in effect. */ EnumSet getHarCaptureTypes(); /** * Enables the specified HAR capture types. Does not replace or disable any other capture types that may already be enabled. * * @param captureTypes capture types to enable */ void enableHarCaptureTypes(Set captureTypes); /** * Enables the specified HAR capture types. Does not replace or disable any other capture types that may already be enabled. * * @param captureTypes capture types to enable */ void enableHarCaptureTypes(CaptureType... captureTypes); /** * Disables the specified HAR capture types. Does not replace or disable any other capture types that may already be enabled. * * @param captureTypes capture types to disable */ void disableHarCaptureTypes(Set captureTypes); /** * Disables the specified HAR capture types. Does not replace or disable any other capture types that may already be enabled. * * @param captureTypes capture types to disable */ void disableHarCaptureTypes(CaptureType... captureTypes); /** * Starts a new HAR page using the default page naming convention. The default page naming convention is "Page #", where "#" resets to 1 * every time {@link #newHar()} or {@link #newHar(String)} is called, and increments on every subsequent call to {@link #newPage()} or * {@link #newHar(String)}. Populates the {@link net.lightbody.bmp.core.har.HarPageTimings#onLoad} value based on the amount of time * the current page has been captured. * * @return the HAR as it existed immediately after ending the current page * @throws java.lang.IllegalStateException if HAR capture has not been enabled via {@link #newHar()} or {@link #newHar(String)} */ Har newPage(); /** * Starts a new HAR page using the specified pageRef as the page name and the page title. Populates the * {@link net.lightbody.bmp.core.har.HarPageTimings#onLoad} value based on the amount of time the current page has been captured. * * @param pageRef name of the new page * @return the HAR as it existed immediately after ending the current page * @throws java.lang.IllegalStateException if HAR capture has not been enabled via {@link #newHar()} or {@link #newHar(String)} */ Har newPage(String pageRef); /** * Starts a new HAR page using the specified pageRef as the page name and the pageTitle as the page title. Populates the * {@link net.lightbody.bmp.core.har.HarPageTimings#onLoad} value based on the amount of time the current page has been captured. * * @param pageRef name of the new page * @param pageTitle title of the new page * @return the HAR as it existed immediately after ending the current page * @throws java.lang.IllegalStateException if HAR capture has not been enabled via {@link #newHar()} or {@link #newHar(String)} */ Har newPage(String pageRef, String pageTitle); /** * Stops capturing traffic in the HAR. Populates the {@link net.lightbody.bmp.core.har.HarPageTimings#onLoad} value for the current page * based on the amount of time it has been captured. * * @return the existing HAR */ Har endHar(); /** * Sets the maximum bandwidth to consume when reading server responses. * * @param bytesPerSecond maximum bandwidth, in bytes per second */ void setReadBandwidthLimit(long bytesPerSecond); /** * Returns the current bandwidth limit for reading, in bytes per second. */ long getReadBandwidthLimit(); /** * Sets the maximum bandwidth to consume when sending requests to servers. * * @param bytesPerSecond maximum bandwidth, in bytes per second */ void setWriteBandwidthLimit(long bytesPerSecond); /** * Returns the current bandwidth limit for writing, in bytes per second. */ long getWriteBandwidthLimit(); /** * The minimum amount of time that will elapse between the time the proxy begins receiving a response from the server and the time the * proxy begins sending the response to the client. * * @param latency minimum latency, or 0 for no minimum * @param timeUnit TimeUnit for the latency */ void setLatency(long latency, TimeUnit timeUnit); /** * Maximum amount of time to wait to establish a connection to a remote server. If the connection has not been established within the * specified time, the proxy will respond with an HTTP 502 Bad Gateway. The default value is 60 seconds. * * @param connectionTimeout maximum time to wait to establish a connection to a server, or 0 to wait indefinitely * @param timeUnit TimeUnit for the connectionTimeout */ void setConnectTimeout(int connectionTimeout, TimeUnit timeUnit); /** * Maximum amount of time to allow a connection to remain idle. A connection becomes idle when it has not received data from a server * within the the specified timeout. If the proxy has not yet begun to forward the response to the client, the proxy * will respond with an HTTP 504 Gateway Timeout. If the proxy has already started forwarding the response to the client, the * connection to the client may be closed abruptly. The default value is 60 seconds. * * @param idleConnectionTimeout maximum time to allow a connection to remain idle, or 0 to wait indefinitely. * @param timeUnit TimeUnit for the idleConnectionTimeout */ void setIdleConnectionTimeout(int idleConnectionTimeout, TimeUnit timeUnit); /** * Maximum amount of time to wait for an HTTP response from the remote server after the request has been sent in its entirety. The HTTP * request must complete within the specified time. If the proxy has not yet begun to forward the response to the client, the proxy * will respond with an HTTP 504 Gateway Timeout. If the proxy has already started forwarding the response to the client, the * connection to the client may be closed abruptly. The default value is 0 (wait indefinitely). * * @param requestTimeout maximum time to wait for an HTTP response, or 0 to wait indefinitely * @param timeUnit TimeUnit for the requestTimeout */ void setRequestTimeout(int requestTimeout, TimeUnit timeUnit); /** * Enables automatic authorization for the specified domain and auth type. Every request sent to the specified domain will contain the * specified authorization information. * * @param domain domain automatically send authorization information to * @param username authorization username * @param password authorization password * @param authType authorization type */ void autoAuthorization(String domain, String username, String password, AuthType authType); /** * Stops automatic authorization for the specified domain. * * @param domain domain to stop automatically sending authorization information to */ void stopAutoAuthorization(String domain); /** * Enables chained proxy authorization using the Proxy-Authorization header described in RFC 7235, section 4.4 (https://tools.ietf.org/html/rfc7235#section-4.4). * Currently, only {@link AuthType#BASIC} authentication is supported. * * @param username the username to use to authenticate with the chained proxy * @param password the password to use to authenticate with the chained proxy * @param authType the auth type to use (currently, must be BASIC) */ void chainedProxyAuthorization(String username, String password, AuthType authType); /** * Adds a rewrite rule for the specified URL-matching regular expression. If there are any existing rewrite rules, the new rewrite * rule will be applied last, after all other rewrite rules are applied. The specified urlPattern will be replaced with the specified * replacement expression. The urlPattern is treated as a Java regular expression and must be properly escaped (see {@link java.util.regex.Pattern}). * The replacementExpression may consist of capture groups specified in the urlPattern, denoted * by a $ (see {@link java.util.regex.Matcher#appendReplacement(StringBuffer, String)}. *

* For HTTP requests (not HTTPS), if the hostname and/or port is changed as a result of a rewrite rule, the Host header of the request will be modified * to reflect the updated hostname/port. For HTTPS requests, the host and port cannot be changed by rewrite rules * (use {@link #getHostNameResolver()} and {@link AdvancedHostResolver#remapHost(String, String)} to direct HTTPS requests * to a different host). *

* Note: The rewriting applies to the entire URL, including scheme (http:// or https://), hostname/address, port, and query string. Note that this means * a urlPattern of {@code "http://www\.website\.com/page"} will NOT match {@code http://www.website.com:80/page}. *

* For example, the following rewrite rule: * *

   {@code proxy.rewriteUrl("http://www\\.(yahoo|bing)\\.com/\\?(\\w+)=(\\w+)", "http://www.google.com/?originalDomain=$1&$2=$3");}
* * will match an HTTP request (but not HTTPS!) to www.yahoo.com or www.bing.com with exactly 1 query parameter, * and replace it with a call to www.google.com with an 'originalDomain' query parameter, as well as the original query parameter. *

* When applied to the URL: *

   {@code http://www.yahoo.com?theFirstParam=someValue}
* will result in the proxy making a request to: *
   {@code http://www.google.com?originalDomain=yahoo&theFirstParam=someValue}
* When applied to the URL: *
   {@code http://www.bing.com?anotherParam=anotherValue}
* will result in the proxy making a request to: *
   {@code http://www.google.com?originalDomain=bing&anotherParam=anotherValue}
* * @param urlPattern URL-matching regular expression * @param replacementExpression an expression, which may optionally contain capture groups, which will replace any URL which matches urlPattern */ void rewriteUrl(String urlPattern, String replacementExpression); /** * Replaces existing rewrite rules with the specified patterns and replacement expressions. The rules will be applied in the order * specified by the Map's iterator. *

* See {@link #rewriteUrl(String, String)} for details on the format of the rewrite rules. * * @param rewriteRules {@code Map} */ void rewriteUrls(Map rewriteRules); /** * Returns all rewrite rules currently in effect. Iterating over the returned Map is guaranteed to return rewrite rules * in the order in which the rules are actually applied. * * @return {@code Map} */ Map getRewriteRules(); /** * Removes an existing rewrite rule whose urlPattern matches the specified pattern. * * @param urlPattern rewrite rule pattern to remove */ void removeRewriteRule(String urlPattern); /** * Clears all existing rewrite rules. */ void clearRewriteRules(); /** * Adds a URL-matching regular expression to the blacklist. Requests that match a blacklisted URL will return the specified HTTP * statusCode for all HTTP methods. If there are existing patterns on the blacklist, the urlPattern will be evaluated last, * after the URL is checked against all other blacklist entries. *

* The urlPattern matches the full URL of the request, including scheme, host, and port, path, and query parameters * for both HTTP and HTTPS requests. For example, to blacklist both HTTP and HTTPS requests to www.google.com, * use a urlPattern of "https?://www\\.google\\.com/.*". * * @param urlPattern URL-matching regular expression to blacklist * @param statusCode HTTP status code to return */ void blacklistRequests(String urlPattern, int statusCode); /** * Adds a URL-matching regular expression to the blacklist. Requests that match a blacklisted URL will return the specified HTTP * statusCode only when the request's HTTP method (GET, POST, PUT, etc.) matches the specified httpMethodPattern regular expression. * If there are existing patterns on the blacklist, the urlPattern will be evaluated last, after the URL is checked against all * other blacklist entries. *

* See {@link #blacklistRequests(String, int)} for details on the URL the urlPattern will match. * * @param urlPattern URL-matching regular expression to blacklist * @param statusCode HTTP status code to return * @param httpMethodPattern regular expression matching a request's HTTP method */ void blacklistRequests(String urlPattern, int statusCode, String httpMethodPattern); /** * Replaces any existing blacklist with the specified blacklist. URLs will be evaluated against the blacklist in the order * specified by the Collection's iterator. * * @param blacklist new blacklist entries */ void setBlacklist(Collection blacklist); /** * Returns all blacklist entries currently in effect. Iterating over the returned Collection is guaranteed to return * blacklist entries in the order in which URLs are actually evaluated against the blacklist. * * @return blacklist entries, or an empty collection if none exist */ Collection getBlacklist(); /** * Clears any existing blacklist. */ void clearBlacklist(); /** * Whitelists URLs matching the specified regular expression patterns. Replaces any existing whitelist. * The urlPattern matches the full URL of the request, including scheme, host, and port, path, and query parameters * for both HTTP and HTTPS requests. For example, to whitelist both HTTP and HTTPS requests to www.google.com, use a urlPattern * of "https?://www\\.google\\.com/.*". *

* Note: All HTTP CONNECT requests are automatically whitelisted and cannot be short-circuited using the * whitelist response code. * * @param urlPatterns URL-matching regular expressions to whitelist; null or an empty collection will enable an empty whitelist * @param statusCode HTTP status code to return to clients when a URL matches a pattern */ void whitelistRequests(Collection urlPatterns, int statusCode); /** * Adds a URL-matching regular expression to an existing whitelist. * * @param urlPattern URL-matching regular expressions to whitelist * @throws java.lang.IllegalStateException if the whitelist is not enabled */ void addWhitelistPattern(String urlPattern); /** * Enables the whitelist, but with no matching URLs. All requests will generated the specified HTTP statusCode. * * @param statusCode HTTP status code to return to clients on all requests */ void enableEmptyWhitelist(int statusCode); /** * Clears any existing whitelist and disables whitelisting. */ void disableWhitelist(); /** * Returns the URL-matching regular expressions currently in effect. If the whitelist is disabled, this method always returns an empty collection. * If the whitelist is enabled but empty, this method return an empty collection. * * @return whitelist currently in effect, or an empty collection if the whitelist is disabled or empty */ Collection getWhitelistUrls(); /** * Returns the status code returned for all URLs that do not match the whitelist. If the whitelist is not currently enabled, returns -1. * * @return HTTP status code returned for non-whitelisted URLs, or -1 if the whitelist is disabled. */ int getWhitelistStatusCode(); /** * Returns true if the whitelist is enabled, otherwise false. */ boolean isWhitelistEnabled(); /** * Adds the specified HTTP headers to every request. Replaces any existing additional headers with the specified headers. * * @param headers {@code Map

} to append to every request. */ void addHeaders(Map headers); /** * Adds a new HTTP header to every request. If the header already exists on the request, it will be replaced with the specified header. * * @param name name of the header to add * @param value new header's value */ void addHeader(String name, String value); /** * Removes a header previously added with {@link #addHeader(String name, String value)}. * * @param name previously-added header's name */ void removeHeader(String name); /** * Removes all headers previously added with {@link #addHeader(String name, String value)}. */ void removeAllHeaders(); /** * Returns all headers previously added with {@link #addHeader(String name, String value)}. * * @return {@code Map
} */ Map getAllHeaders(); /** * Sets the resolver that will be used to look up host names. To chain multiple resolvers, wrap a list * of resolvers in a {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver}. * * @param resolver host name resolver */ void setHostNameResolver(AdvancedHostResolver resolver); /** * Returns the current host name resolver. * * @return current host name resolver */ AdvancedHostResolver getHostNameResolver(); /** * Waits for existing network traffic to stop, and for the specified quietPeriod to elapse. Returns true if there is no network traffic * for the quiet period within the specified timeout, otherwise returns false. * * @param quietPeriod amount of time after which network traffic will be considered "stopped" * @param timeout maximum amount of time to wait for network traffic to stop * @param timeUnit TimeUnit for the quietPeriod and timeout * @return true if network traffic is stopped, otherwise false */ boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUnit); /** * Sets an upstream proxy that this proxy will use to connect to external hosts. * * @param chainedProxyAddress address and port of the upstream proxy, or null to remove an upstream proxy */ void setChainedProxy(InetSocketAddress chainedProxyAddress); /** * Returns the address and port of the upstream proxy. * * @return address and port of the upstream proxy, or null of there is none. */ InetSocketAddress getChainedProxy(); /** * Adds a new filter factory (request/response interceptor) to the beginning of the HttpFilters chain. *

* Usage note: The actual filter (interceptor) instance is created on every request by implementing the * {@link HttpFiltersSource#filterRequest(io.netty.handler.codec.http.HttpRequest, io.netty.channel.ChannelHandlerContext)} method and returning an * {@link org.littleshoot.proxy.HttpFilters} instance (typically, a subclass of {@link org.littleshoot.proxy.HttpFiltersAdapter}). * To disable or bypass a filter on a per-request basis, the filterRequest() method may return null. * * @param filterFactory factory to generate HttpFilters */ void addFirstHttpFilterFactory(HttpFiltersSource filterFactory); /** * Adds a new filter factory (request/response interceptor) to the end of the HttpFilters chain. *

* Usage note: The actual filter (interceptor) instance is created on every request by implementing the * {@link HttpFiltersSource#filterRequest(io.netty.handler.codec.http.HttpRequest, io.netty.channel.ChannelHandlerContext)} method and returning an * {@link org.littleshoot.proxy.HttpFilters} instance (typically, a subclass of {@link org.littleshoot.proxy.HttpFiltersAdapter}). * To disable or bypass a filter on a per-request basis, the filterRequest() method may return null. * * @param filterFactory factory to generate HttpFilters */ void addLastHttpFilterFactory(HttpFiltersSource filterFactory); /** * Adds a new ResponseFilter that can be used to examine and manipulate the response before sending it to the client. * * @param filter filter instance */ void addResponseFilter(ResponseFilter filter); /** * Adds a new RequestFilter that can be used to examine and manipulate the request before sending it to the server. * * @param filter filter instance */ void addRequestFilter(RequestFilter filter); /** * Completely disables MITM for this proxy server. The proxy will no longer intercept HTTPS requests, but they will * still be pass-through proxied. This option must be set before the proxy is started; otherwise an IllegalStateException will be thrown. * * @param mitmDisabled when true, MITM capture will be disabled * @throws java.lang.IllegalStateException if the proxy is already started */ void setMitmDisabled(boolean mitmDisabled); /** * Sets the MITM manager, which is responsible for generating forged SSL certificates to present to clients. By default, * BrowserMob Proxy uses the ca-certificate-rsa.cer root certificate for impersonation. See the documentation at * {@link net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager} and {@link net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager.Builder} * for details on customizing the root and server certificate generation. * * @param mitmManager MITM manager to use */ void setMitmManager(MitmManager mitmManager); /** * Disables verification of all upstream servers' SSL certificates. All upstream servers will be trusted, even if they * do not present valid certificates signed by certification authorities in the JDK's trust store. This option * exposes the proxy to MITM attacks and should only be used when testing in trusted environments. * * @param trustAllServers when true, disables upstream server certificate verification */ void setTrustAllServers(boolean trustAllServers); /** * Sets the {@link TrustSource} that contains trusted root certificate authorities that will be used to validate * upstream servers' certificates. When null, disables certificate validation (see warning at {@link #setTrustAllServers(boolean)}). * * @param trustSource TrustSource containing root CAs, or null to disable upstream server validation */ void setTrustSource(TrustSource trustSource); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java ================================================ package net.lightbody.bmp; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.MapMaker; import net.lightbody.bmp.client.ClientUtil; import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarLog; import net.lightbody.bmp.core.har.HarNameVersion; import net.lightbody.bmp.core.har.HarPage; import net.lightbody.bmp.filters.AddHeadersFilter; import net.lightbody.bmp.filters.AutoBasicAuthFilter; import net.lightbody.bmp.filters.BlacklistFilter; import net.lightbody.bmp.filters.BrowserMobHttpFilterChain; import net.lightbody.bmp.filters.HarCaptureFilter; import net.lightbody.bmp.filters.HttpConnectHarCaptureFilter; import net.lightbody.bmp.filters.HttpsHostCaptureFilter; import net.lightbody.bmp.filters.HttpsOriginalHostCaptureFilter; import net.lightbody.bmp.filters.LatencyFilter; import net.lightbody.bmp.filters.RegisterRequestFilter; import net.lightbody.bmp.filters.RequestFilter; import net.lightbody.bmp.filters.RequestFilterAdapter; import net.lightbody.bmp.filters.ResolvedHostnameCacheFilter; import net.lightbody.bmp.filters.ResponseFilter; import net.lightbody.bmp.filters.ResponseFilterAdapter; import net.lightbody.bmp.filters.RewriteUrlFilter; import net.lightbody.bmp.filters.UnregisterRequestFilter; import net.lightbody.bmp.filters.WhitelistFilter; import net.lightbody.bmp.mitm.KeyStoreFileCertificateSource; import net.lightbody.bmp.mitm.TrustSource; import net.lightbody.bmp.mitm.keys.ECKeyGenerator; import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; import net.lightbody.bmp.proxy.ActivityMonitor; import net.lightbody.bmp.proxy.BlacklistEntry; import net.lightbody.bmp.proxy.CaptureType; import net.lightbody.bmp.proxy.RewriteRule; import net.lightbody.bmp.proxy.Whitelist; import net.lightbody.bmp.proxy.auth.AuthType; import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; import net.lightbody.bmp.proxy.dns.DelegatingHostResolver; import net.lightbody.bmp.util.BrowserMobHttpUtil; import net.lightbody.bmp.util.BrowserMobProxyUtil; import org.littleshoot.proxy.ChainedProxy; import org.littleshoot.proxy.ChainedProxyAdapter; import org.littleshoot.proxy.ChainedProxyManager; import org.littleshoot.proxy.HttpFilters; import org.littleshoot.proxy.HttpFiltersSource; import org.littleshoot.proxy.HttpFiltersSourceAdapter; import org.littleshoot.proxy.HttpProxyServer; import org.littleshoot.proxy.HttpProxyServerBootstrap; import org.littleshoot.proxy.MitmManager; import org.littleshoot.proxy.impl.DefaultHttpProxyServer; import org.littleshoot.proxy.impl.ProxyUtils; import org.littleshoot.proxy.impl.ThreadPoolConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; /** * A LittleProxy-based implementation of {@link net.lightbody.bmp.BrowserMobProxy}. */ public class BrowserMobProxyServer implements BrowserMobProxy { private static final Logger log = LoggerFactory.getLogger(BrowserMobProxyServer.class); private static final HarNameVersion HAR_CREATOR_VERSION = new HarNameVersion("BrowserMob Proxy", BrowserMobProxyUtil.getVersionString()); /* Default MITM resources */ private static final String RSA_KEYSTORE_RESOURCE = "/sslSupport/ca-keystore-rsa.p12"; private static final String EC_KEYSTORE_RESOURCE = "/sslSupport/ca-keystore-ec.p12"; private static final String KEYSTORE_TYPE = "PKCS12"; private static final String KEYSTORE_PRIVATE_KEY_ALIAS = "key"; private static final String KEYSTORE_PASSWORD = "password"; /** * The default pseudonym to use when adding the Via header to proxied requests. */ public static final String VIA_HEADER_ALIAS = "browsermobproxy"; /** * True only after the proxy has been successfully started. */ private final AtomicBoolean started = new AtomicBoolean(false); /** * True only after the proxy has been successfully started, then successfully stopped or aborted. */ private final AtomicBoolean stopped = new AtomicBoolean(false); /** * Tracks the current page count, for use when auto-generating HAR page names. */ private final AtomicInteger harPageCount = new AtomicInteger(0); /** * When true, MITM will be disabled. The proxy will no longer intercept HTTPS requests, but they will still be proxied. */ private volatile boolean mitmDisabled = false; /** * The MITM manager that will be used for HTTPS requests. */ private volatile MitmManager mitmManager; /** * The list of filterFactories that will generate the filters that implement browsermob-proxy behavior. */ private final List filterFactories = new CopyOnWriteArrayList<>(); /** * List of rejected URL patterns */ private volatile Collection blacklistEntries = new CopyOnWriteArrayList<>(); /** * List of URLs to rewrite */ private volatile CopyOnWriteArrayList rewriteRules = new CopyOnWriteArrayList<>(); /** * The LittleProxy instance that performs all proxy operations. */ private volatile HttpProxyServer proxyServer; /** * No capture types are enabled by default. */ private volatile EnumSet harCaptureTypes = EnumSet.noneOf(CaptureType.class); /** * The current HAR being captured. */ private volatile Har har; /** * The current HarPage to which new requests will be associated. */ private volatile HarPage currentHarPage; /** * Maximum bandwidth to consume when reading responses from servers. */ private volatile long readBandwidthLimitBps; /** * Maximum bandwidth to consume when writing requests to servers. */ private volatile long writeBandwidthLimitBps; /** * List of accepted URL patterns. Unlisted URL patterns will be rejected with the response code contained in the Whitelist. */ private final AtomicReference whitelist = new AtomicReference<>(Whitelist.WHITELIST_DISABLED); /** * Additional headers that will be sent with every request. The map is declared as a ConcurrentMap to indicate that writes may be performed * by other threads concurrently (e.g. due to an incoming REST call), but the concurrencyLevel is set to 1 because modifications to the * additionalHeaders are rare, and in most cases happen only once, at start-up. */ private volatile ConcurrentMap additionalHeaders = new MapMaker().concurrencyLevel(1).makeMap(); /** * The amount of time to wait while connecting to a server. */ private volatile int connectTimeoutMs; /** * The amount of time a connection to a server can remain idle while receiving data from the server. */ private volatile int idleConnectionTimeoutSec; /** * The amount of time to wait before forwarding the response to the client. */ private volatile int latencyMs; /** * Set to true once the HAR capture filter has been added to the filter chain. */ private final AtomicBoolean harCaptureFilterEnabled = new AtomicBoolean(false); /** * The address of an upstream chained proxy to route traffic through. */ private volatile InetSocketAddress upstreamProxyAddress; /** * The chained proxy manager that manages upstream proxies. */ private volatile ChainedProxyManager chainedProxyManager; /** * The address of the network interface from which the proxy will initiate connections. */ private volatile InetAddress serverBindAddress; /** * The TrustSource that will be used to validate servers' certificates. If null, will not validate server certificates. */ private volatile TrustSource trustSource = TrustSource.defaultTrustSource(); /** * When true, use Elliptic Curve keys and certificates when impersonating upstream servers. */ private volatile boolean useEcc = false; /** * Resolver to use when resolving hostnames to IP addresses. This is a bridge between {@link org.littleshoot.proxy.HostResolver} and * {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver}. It allows the resolvers to be changed on-the-fly without re-bootstrapping the * littleproxy server. The default resolver (native JDK resolver) can be changed using {@link #setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)} and * supplying one of the pre-defined resolvers in {@link ClientUtil}, such as {@link ClientUtil#createDnsJavaWithNativeFallbackResolver()} * or {@link ClientUtil#createDnsJavaResolver()}. You can also build your own resolver, or use {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver} * to chain together multiple DNS resolvers. */ private final DelegatingHostResolver delegatingResolver = new DelegatingHostResolver(ClientUtil.createNativeCacheManipulatingResolver()); private final ActivityMonitor activityMonitor = new ActivityMonitor(); /** * The acceptor and worker thread configuration for the Netty thread pools. */ private volatile ThreadPoolConfiguration threadPoolConfiguration; /** * A mapping of hostnames to base64-encoded Basic auth credentials that will be added to the Authorization header for * matching requests. */ private final ConcurrentMap basicAuthCredentials = new MapMaker() .concurrencyLevel(1) .makeMap(); /** * Base64-encoded credentials to use to authenticate with the upstream proxy. */ private volatile String chainedProxyCredentials; public BrowserMobProxyServer() { } @Override public void start(int port, InetAddress clientBindAddress, InetAddress serverBindAddress) { boolean notStarted = started.compareAndSet(false, true); if (!notStarted) { throw new IllegalStateException("Proxy server is already started. Not restarting."); } InetSocketAddress clientBindSocket; if (clientBindAddress == null) { // if no client bind address was specified, bind to the wildcard address clientBindSocket = new InetSocketAddress(port); } else { clientBindSocket = new InetSocketAddress(clientBindAddress, port); } this.serverBindAddress = serverBindAddress; // initialize all the default BrowserMob filter factories that provide core BMP functionality addBrowserMobFilters(); HttpProxyServerBootstrap bootstrap = DefaultHttpProxyServer.bootstrap() .withFiltersSource(new HttpFiltersSource() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext channelHandlerContext) { return new BrowserMobHttpFilterChain(BrowserMobProxyServer.this, originalRequest, channelHandlerContext); } @Override public int getMaximumRequestBufferSizeInBytes() { return getMaximumRequestBufferSize(); } @Override public int getMaximumResponseBufferSizeInBytes() { return getMaximumResponseBufferSize(); } }) .withServerResolver(delegatingResolver) .withAddress(clientBindSocket) .withConnectTimeout(connectTimeoutMs) .withIdleConnectionTimeout(idleConnectionTimeoutSec) .withProxyAlias(VIA_HEADER_ALIAS); if (serverBindAddress != null) { bootstrap.withNetworkInterface(new InetSocketAddress(serverBindAddress, 0)); } if (!mitmDisabled) { if (mitmManager == null) { mitmManager = ImpersonatingMitmManager.builder() .rootCertificateSource(new KeyStoreFileCertificateSource( KEYSTORE_TYPE, useEcc ? EC_KEYSTORE_RESOURCE : RSA_KEYSTORE_RESOURCE, KEYSTORE_PRIVATE_KEY_ALIAS, KEYSTORE_PASSWORD)) .serverKeyGenerator(useEcc ? new ECKeyGenerator() : new RSAKeyGenerator()) .trustSource(trustSource) .build(); } bootstrap.withManInTheMiddle(mitmManager); } if (readBandwidthLimitBps > 0 || writeBandwidthLimitBps > 0) { bootstrap.withThrottling(readBandwidthLimitBps, writeBandwidthLimitBps); } if (chainedProxyManager != null) { bootstrap.withChainProxyManager(chainedProxyManager); } else if (upstreamProxyAddress != null) { bootstrap.withChainProxyManager(new ChainedProxyManager() { @Override public void lookupChainedProxies(HttpRequest httpRequest, Queue chainedProxies) { final InetSocketAddress upstreamProxy = upstreamProxyAddress; if (upstreamProxy != null) { chainedProxies.add(new ChainedProxyAdapter() { @Override public InetSocketAddress getChainedProxyAddress() { return upstreamProxy; } @Override public void filterRequest(HttpObject httpObject) { String chainedProxyAuth = chainedProxyCredentials; if (chainedProxyAuth != null) { if (httpObject instanceof HttpRequest) { HttpHeaders.addHeader((HttpRequest)httpObject, HttpHeaders.Names.PROXY_AUTHORIZATION, "Basic " + chainedProxyAuth); } } } }); } } }); } if (threadPoolConfiguration != null) { bootstrap.withThreadPoolConfiguration(threadPoolConfiguration); } proxyServer = bootstrap.start(); } @Override public boolean isStarted() { return started.get(); } @Override public void start(int port) { this.start(port, null, null); } @Override public void start(int port, InetAddress bindAddress) { this.start(port, bindAddress, null); } @Override public void start() { this.start(0); } @Override public void stop() { stop(true); } @Override public void abort() { stop(false); } protected void stop(boolean graceful) { if (isStarted()) { if (stopped.compareAndSet(false, true)) { if (proxyServer != null) { if (graceful) { proxyServer.stop(); } else { proxyServer.abort(); } } else { log.warn("Attempted to stop proxy server, but proxy was never successfully started."); } } else { throw new IllegalStateException("Proxy server is already stopped. Cannot re-stop."); } } else { throw new IllegalStateException("Proxy server has not been started"); } } @Override public InetAddress getClientBindAddress() { if (started.get()) { return proxyServer.getListenAddress().getAddress(); } else { return null; } } @Override public int getPort() { if (started.get()) { return proxyServer.getListenAddress().getPort(); } else { return 0; } } @Override public InetAddress getServerBindAddress() { return serverBindAddress; } @Override public Har getHar() { return har; } @Override public Har newHar() { return newHar(null); } @Override public Har newHar(String initialPageRef) { return newHar(initialPageRef, null); } @Override public Har newHar(String initialPageRef, String initialPageTitle) { Har oldHar = getHar(); addHarCaptureFilter(); harPageCount.set(0); this.har = new Har(new HarLog(HAR_CREATOR_VERSION)); newPage(initialPageRef, initialPageTitle); return oldHar; } @Override public void setHarCaptureTypes(Set harCaptureSettings) { if (harCaptureSettings == null || harCaptureSettings.isEmpty()) { harCaptureTypes = EnumSet.noneOf(CaptureType.class); } else { harCaptureTypes = EnumSet.copyOf(harCaptureSettings); } } @Override public void setHarCaptureTypes(CaptureType... captureTypes) { if (captureTypes == null) { setHarCaptureTypes(EnumSet.noneOf(CaptureType.class)); } else { setHarCaptureTypes(EnumSet.copyOf(Arrays.asList(captureTypes))); } } @Override public EnumSet getHarCaptureTypes() { return EnumSet.copyOf(harCaptureTypes); } @Override public void enableHarCaptureTypes(Set captureTypes) { harCaptureTypes.addAll(captureTypes); } @Override public void enableHarCaptureTypes(CaptureType... captureTypes) { if (captureTypes == null) { enableHarCaptureTypes(EnumSet.noneOf(CaptureType.class)); } else { enableHarCaptureTypes(EnumSet.copyOf(Arrays.asList(captureTypes))); } } @Override public void disableHarCaptureTypes(Set captureTypes) { harCaptureTypes.removeAll(captureTypes); } @Override public void disableHarCaptureTypes(CaptureType... captureTypes) { if (captureTypes == null) { disableHarCaptureTypes(EnumSet.noneOf(CaptureType.class)); } else { disableHarCaptureTypes(EnumSet.copyOf(Arrays.asList(captureTypes))); } } @Override public Har newPage() { return newPage(null); } @Override public Har newPage(String pageRef) { return newPage(pageRef, null); } @Override public Har newPage(String pageRef, String pageTitle) { if (har == null) { throw new IllegalStateException("No HAR exists for this proxy. Use newHar() to create a new HAR before calling newPage()."); } Har endOfPageHar = null; if (currentHarPage != null) { String currentPageRef = currentHarPage.getId(); // end the previous page, so that page-wide timings are populated endPage(); // the interface requires newPage() to return the Har as it was immediately after the previous page was ended. endOfPageHar = BrowserMobProxyUtil.copyHarThroughPageRef(har, currentPageRef); } if (pageRef == null) { pageRef = "Page " + harPageCount.getAndIncrement(); } if (pageTitle == null) { pageTitle = pageRef; } HarPage newPage = new HarPage(pageRef, pageTitle); har.getLog().addPage(newPage); currentHarPage = newPage; return endOfPageHar; } @Override public Har endHar() { Har oldHar = getHar(); // end the page and populate timings endPage(); this.har = null; return oldHar; } @Override public void setReadBandwidthLimit(long bytesPerSecond) { this.readBandwidthLimitBps = bytesPerSecond; if (isStarted()) { proxyServer.setThrottle(this.readBandwidthLimitBps, this.writeBandwidthLimitBps); } } @Override public long getReadBandwidthLimit() { return readBandwidthLimitBps; } @Override public void setWriteBandwidthLimit(long bytesPerSecond) { this.writeBandwidthLimitBps = bytesPerSecond; if (isStarted()) { proxyServer.setThrottle(this.readBandwidthLimitBps, this.writeBandwidthLimitBps); } } @Override public long getWriteBandwidthLimit() { return writeBandwidthLimitBps; } public void endPage() { if (har == null) { throw new IllegalStateException("No HAR exists for this proxy. Use newHar() to create a new HAR."); } HarPage previousPage = this.currentHarPage; this.currentHarPage = null; if (previousPage == null) { return; } previousPage.getPageTimings().setOnLoad(new Date().getTime() - previousPage.getStartedDateTime().getTime()); } @Override public void addHeaders(Map headers) { ConcurrentMap newHeaders = new MapMaker().concurrencyLevel(1).makeMap(); newHeaders.putAll(headers); this.additionalHeaders = newHeaders; } @Override public void setLatency(long latency, TimeUnit timeUnit) { this.latencyMs = (int) TimeUnit.MILLISECONDS.convert(latency, timeUnit); } @Override public void autoAuthorization(String domain, String username, String password, AuthType authType) { switch (authType) { case BASIC: // base64 encode the "username:password" string String base64EncodedCredentials = BrowserMobHttpUtil.base64EncodeBasicCredentials(username, password); basicAuthCredentials.put(domain, base64EncodedCredentials); break; default: throw new UnsupportedOperationException("AuthType " + authType + " is not supported for HTTP Authorization"); } } @Override public void stopAutoAuthorization(String domain) { basicAuthCredentials.remove(domain); } @Override public void chainedProxyAuthorization(String username, String password, AuthType authType) { switch (authType) { case BASIC: chainedProxyCredentials = BrowserMobHttpUtil.base64EncodeBasicCredentials(username, password); break; default: throw new UnsupportedOperationException("AuthType " + authType + " is not supported for Proxy Authorization"); } } @Override public void setConnectTimeout(int connectTimeout, TimeUnit timeUnit) { this.connectTimeoutMs = (int) TimeUnit.MILLISECONDS.convert(connectTimeout, timeUnit); if (isStarted()) { proxyServer.setConnectTimeout((int) TimeUnit.MILLISECONDS.convert(connectTimeout, timeUnit)); } } /** * The LittleProxy implementation only allows idle connection timeouts to be specified in seconds. idleConnectionTimeouts greater than * 0 but less than 1 second will be set to 1 second; otherwise, values will be truncated (i.e. 1500ms will become 1s). */ @Override public void setIdleConnectionTimeout(int idleConnectionTimeout, TimeUnit timeUnit) { long timeout = TimeUnit.SECONDS.convert(idleConnectionTimeout, timeUnit); if (timeout == 0 && idleConnectionTimeout > 0) { this.idleConnectionTimeoutSec = 1; } else { this.idleConnectionTimeoutSec = (int) timeout; } if (isStarted()) { proxyServer.setIdleConnectionTimeout(idleConnectionTimeoutSec); } } @Override public void setRequestTimeout(int requestTimeout, TimeUnit timeUnit) { //TODO: implement Request Timeouts using LittleProxy. currently this only sets an idle connection timeout, if the idle connection // timeout is higher than the specified requestTimeout. if (idleConnectionTimeoutSec == 0 || idleConnectionTimeoutSec > TimeUnit.SECONDS.convert(requestTimeout, timeUnit)) { setIdleConnectionTimeout(requestTimeout, timeUnit); } } @Override public void rewriteUrl(String pattern, String replace) { rewriteRules.add(new RewriteRule(pattern, replace)); } @Override public void rewriteUrls(Map rewriteRules) { List newRules = new ArrayList<>(rewriteRules.size()); for (Map.Entry rewriteRule : rewriteRules.entrySet()) { RewriteRule newRule = new RewriteRule(rewriteRule.getKey(), rewriteRule.getValue()); newRules.add(newRule); } this.rewriteRules = new CopyOnWriteArrayList<>(newRules); } @Override public void clearRewriteRules() { rewriteRules.clear(); } @Override public void blacklistRequests(String pattern, int responseCode) { blacklistEntries.add(new BlacklistEntry(pattern, responseCode)); } @Override public void blacklistRequests(String pattern, int responseCode, String method) { blacklistEntries.add(new BlacklistEntry(pattern, responseCode, method)); } @Override public void setBlacklist(Collection blacklist) { this.blacklistEntries = new CopyOnWriteArrayList<>(blacklist); } @Override public Collection getBlacklist() { return Collections.unmodifiableCollection(blacklistEntries); } @Override public boolean isWhitelistEnabled() { return whitelist.get().isEnabled(); } @Override public Collection getWhitelistUrls() { ImmutableList.Builder builder = ImmutableList.builder(); for (Pattern pattern : whitelist.get().getPatterns()) { builder.add(pattern.pattern()); } return builder.build(); } @Override public int getWhitelistStatusCode() { return whitelist.get().getStatusCode(); } @Override public void clearBlacklist() { blacklistEntries.clear(); } @Override public void whitelistRequests(Collection urlPatterns, int statusCode) { this.whitelist.set(new Whitelist(urlPatterns, statusCode)); } @Override public void addWhitelistPattern(String urlPattern) { // to make sure this method is threadsafe, we need to guarantee that the "snapshot" of the whitelist taken at the beginning // of the method has not been replaced by the time we have constructed a new whitelist at the end of the method boolean whitelistUpdated = false; while (!whitelistUpdated) { Whitelist currentWhitelist = this.whitelist.get(); if (!currentWhitelist.isEnabled()) { throw new IllegalStateException("Whitelist is disabled. Cannot add patterns to a disabled whitelist."); } // retrieve the response code and list of patterns from the current whitelist, the construct a new list of patterns that contains // all of the old whitelist's patterns + this new pattern int statusCode = currentWhitelist.getStatusCode(); List newPatterns = new ArrayList<>(currentWhitelist.getPatterns().size() + 1); for (Pattern pattern : currentWhitelist.getPatterns()) { newPatterns.add(pattern.pattern()); } newPatterns.add(urlPattern); // create a new (immutable) Whitelist object with the new pattern list and status code Whitelist newWhitelist = new Whitelist(newPatterns, statusCode); // replace the current whitelist with the new whitelist only if the current whitelist has not changed since we started whitelistUpdated = this.whitelist.compareAndSet(currentWhitelist, newWhitelist); } } /** * Whitelist the specified request patterns, returning the specified responseCode for non-whitelisted * requests. * * @param patterns regular expression strings matching URL patterns to whitelist. if empty or null, * the whitelist will be enabled but will not match any URLs. * @param responseCode the HTTP response code to return for non-whitelisted requests */ public void whitelistRequests(String[] patterns, int responseCode) { if (patterns == null || patterns.length == 0) { this.enableEmptyWhitelist(responseCode); } else { this.whitelistRequests(Arrays.asList(patterns), responseCode); } } @Override public void enableEmptyWhitelist(int statusCode) { whitelist.set(new Whitelist(statusCode)); } @Override public void disableWhitelist() { whitelist.set(Whitelist.WHITELIST_DISABLED); } @Override public void addHeader(String name, String value) { additionalHeaders.put(name, value); } @Override public void removeHeader(String name) { additionalHeaders.remove(name); } @Override public void removeAllHeaders() { additionalHeaders.clear(); } @Override public Map getAllHeaders() { return ImmutableMap.copyOf(additionalHeaders); } @Override public void setHostNameResolver(AdvancedHostResolver resolver) { delegatingResolver.setResolver(resolver); } @Override public AdvancedHostResolver getHostNameResolver() { return delegatingResolver.getResolver(); } @Override public boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUnit) { return activityMonitor.waitForQuiescence(quietPeriod, timeout, timeUnit); } /** * Instructs this proxy to route traffic through an upstream proxy. Proxy chaining is not compatible with man-in-the-middle * SSL, so HAR capture will be disabled for HTTPS traffic when using an upstream proxy. *

* Note: Using {@link #setChainedProxyManager(ChainedProxyManager)} will supersede any value set by this method. * * @param chainedProxyAddress address of the upstream proxy */ @Override public void setChainedProxy(InetSocketAddress chainedProxyAddress) { upstreamProxyAddress = chainedProxyAddress; } @Override public InetSocketAddress getChainedProxy() { return upstreamProxyAddress; } /** * Allows access to the LittleProxy {@link ChainedProxyManager} for fine-grained control of the chained proxies. To enable a single * chained proxy, {@link BrowserMobProxy#setChainedProxy(InetSocketAddress)} is generally more convenient. * * @param chainedProxyManager chained proxy manager to enable */ public void setChainedProxyManager(ChainedProxyManager chainedProxyManager) { if (isStarted()) { throw new IllegalStateException("Cannot configure chained proxy manager after proxy has started."); } this.chainedProxyManager = chainedProxyManager; } /** * Configures the Netty thread pool used by the LittleProxy back-end. See {@link ThreadPoolConfiguration} for details. * * @param threadPoolConfiguration thread pool configuration to use */ public void setThreadPoolConfiguration(ThreadPoolConfiguration threadPoolConfiguration) { if (isStarted()) { throw new IllegalStateException("Cannot configure thread pool after proxy has started."); } this.threadPoolConfiguration = threadPoolConfiguration; } @Override public void addFirstHttpFilterFactory(HttpFiltersSource filterFactory) { filterFactories.add(0, filterFactory); } @Override public void addLastHttpFilterFactory(HttpFiltersSource filterFactory) { filterFactories.add(filterFactory); } /** * Note: The current implementation of this method forces a maximum response size of 2 MiB. To adjust the maximum response size, or * to disable aggregation (which disallows access to the {@link net.lightbody.bmp.util.HttpMessageContents}), you may add the filter source * directly: addFirstHttpFilterFactory(new ResponseFilterAdapter.FilterSource(filter, bufferSizeInBytes)); */ @Override public void addResponseFilter(ResponseFilter filter) { addLastHttpFilterFactory(new ResponseFilterAdapter.FilterSource(filter)); } /** * Note: The current implementation of this method forces a maximum request size of 2 MiB. To adjust the maximum request size, or * to disable aggregation (which disallows access to the {@link net.lightbody.bmp.util.HttpMessageContents}), you may add the filter source * directly: addFirstHttpFilterFactory(new RequestFilterAdapter.FilterSource(filter, bufferSizeInBytes)); */ @Override public void addRequestFilter(RequestFilter filter) { addFirstHttpFilterFactory(new RequestFilterAdapter.FilterSource(filter)); } @Override public Map getRewriteRules() { ImmutableMap.Builder builder = ImmutableMap.builder(); for (RewriteRule rewriteRule : rewriteRules) { builder.put(rewriteRule.getPattern().pattern(), rewriteRule.getReplace()); } return builder.build(); } @Override public void removeRewriteRule(String urlPattern) { // normally removing elements from the list we are iterating over would not be possible, but since this is a CopyOnWriteArrayList // the iterator it returns is a "snapshot" of the list that will not be affected by removal (and that does not support removal, either) for (RewriteRule rewriteRule : rewriteRules) { if (rewriteRule.getPattern().pattern().equals(urlPattern)) { rewriteRules.remove(rewriteRule); } } } public boolean isStopped() { return stopped.get(); } public HarPage getCurrentHarPage() { return currentHarPage; } public void addHttpFilterFactory(HttpFiltersSource filterFactory) { filterFactories.add(filterFactory); } public List getFilterFactories() { return filterFactories; } @Override public void setMitmDisabled(boolean mitmDisabled) throws IllegalStateException { if (isStarted()) { throw new IllegalStateException("Cannot disable MITM after the proxy has been started"); } this.mitmDisabled = mitmDisabled; } @Override public void setMitmManager(MitmManager mitmManager) { this.mitmManager = mitmManager; } @Override public void setTrustAllServers(boolean trustAllServers) { if (isStarted()) { throw new IllegalStateException("Cannot disable upstream server verification after the proxy has been started"); } if (trustAllServers) { trustSource = null; } else { if (trustSource == null) { trustSource = TrustSource.defaultTrustSource(); } } } @Override public void setTrustSource(TrustSource trustSource) { if (isStarted()) { throw new IllegalStateException("Cannot change TrustSource after proxy has been started"); } this.trustSource = trustSource; } public boolean isMitmDisabled() { return this.mitmDisabled; } public void setUseEcc(boolean useEcc) { this.useEcc = useEcc; } /** * Adds the basic browsermob-proxy filters, except for the relatively-expensive HAR capture filter. */ protected void addBrowserMobFilters() { addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new ResolvedHostnameCacheFilter(originalRequest, ctx); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new RegisterRequestFilter(originalRequest, ctx, activityMonitor); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new HttpsOriginalHostCaptureFilter(originalRequest, ctx); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new BlacklistFilter(originalRequest, ctx, getBlacklist()); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { Whitelist currentWhitelist = whitelist.get(); return new WhitelistFilter(originalRequest, ctx, isWhitelistEnabled(), currentWhitelist.getStatusCode(), currentWhitelist.getPatterns()); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new AutoBasicAuthFilter(originalRequest, ctx, basicAuthCredentials); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new RewriteUrlFilter(originalRequest, ctx, rewriteRules); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new HttpsHostCaptureFilter(originalRequest, ctx); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest) { return new AddHeadersFilter(originalRequest, additionalHeaders); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest) { return new LatencyFilter(originalRequest, latencyMs); } }); addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new UnregisterRequestFilter(originalRequest, ctx, activityMonitor); } }); } private int getMaximumRequestBufferSize() { int maxBufferSize = 0; for (HttpFiltersSource source : filterFactories) { int requestBufferSize = source.getMaximumRequestBufferSizeInBytes(); if (requestBufferSize > maxBufferSize) { maxBufferSize = requestBufferSize; } } return maxBufferSize; } private int getMaximumResponseBufferSize() { int maxBufferSize = 0; for (HttpFiltersSource source : filterFactories) { int requestBufferSize = source.getMaximumResponseBufferSizeInBytes(); if (requestBufferSize > maxBufferSize) { maxBufferSize = requestBufferSize; } } return maxBufferSize; } /** * Enables the HAR capture filter if it has not already been enabled. The filter will be added to the end of the filter chain. * The HAR capture filter is relatively expensive, so this method is only called when a HAR is requested. */ protected void addHarCaptureFilter() { if (harCaptureFilterEnabled.compareAndSet(false, true)) { // the HAR capture filter is (relatively) expensive, so only enable it when a HAR is being captured. furthermore, // restricting the HAR capture filter to requests where the HAR exists, as well as excluding HTTP CONNECTs // from the HAR capture filter, greatly simplifies the filter code. addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { Har har = getHar(); if (har != null && !ProxyUtils.isCONNECT(originalRequest)) { return new HarCaptureFilter(originalRequest, ctx, har, getCurrentHarPage() == null ? null : getCurrentHarPage().getId(), getHarCaptureTypes()); } else { return null; } } }); // HTTP CONNECTs are a special case, since they require special timing and error handling addHttpFilterFactory(new HttpFiltersSourceAdapter() { @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { Har har = getHar(); if (har != null && ProxyUtils.isCONNECT(originalRequest)) { return new HttpConnectHarCaptureFilter(originalRequest, ctx, har, getCurrentHarPage() == null ? null : getCurrentHarPage().getId()); } else { return null; } } }); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/client/ClientUtil.java ================================================ package net.lightbody.bmp.client; import com.google.common.collect.ImmutableList; import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; import net.lightbody.bmp.proxy.dns.ChainedHostResolver; import net.lightbody.bmp.proxy.dns.DnsJavaResolver; import net.lightbody.bmp.proxy.dns.NativeCacheManipulatingResolver; import net.lightbody.bmp.proxy.dns.NativeResolver; import java.net.InetAddress; import java.net.UnknownHostException; //import org.openqa.selenium.Proxy; /** * A utility class with convenience methods for clients using BrowserMob Proxy in embedded mode. */ public class ClientUtil { /** * Creates a {@link net.lightbody.bmp.proxy.dns.NativeCacheManipulatingResolver} instance that can be used when * calling {@link net.lightbody.bmp.BrowserMobProxy#setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)}. * * @return a new NativeCacheManipulatingResolver */ public static AdvancedHostResolver createNativeCacheManipulatingResolver() { return new NativeCacheManipulatingResolver(); } /** * Creates a {@link net.lightbody.bmp.proxy.dns.NativeResolver} instance that does not support cache manipulation that can be used when * calling {@link net.lightbody.bmp.BrowserMobProxy#setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)}. * * @return a new NativeResolver */ public static AdvancedHostResolver createNativeResolver() { return new NativeResolver(); } /** * Creates a {@link net.lightbody.bmp.proxy.dns.DnsJavaResolver} instance that can be used when * calling {@link net.lightbody.bmp.BrowserMobProxy#setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)}. * * @return a new DnsJavaResolver */ public static AdvancedHostResolver createDnsJavaResolver() { return new DnsJavaResolver(); } /** * Creates a {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver} instance that first attempts to resolve a hostname using a * {@link net.lightbody.bmp.proxy.dns.DnsJavaResolver}, then uses {@link net.lightbody.bmp.proxy.dns.NativeCacheManipulatingResolver}. * Can be used when calling {@link net.lightbody.bmp.BrowserMobProxy#setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)}. * * @return a new ChainedHostResolver that resolves addresses first using a DnsJavaResolver, then using a NativeCacheManipulatingResolver */ public static AdvancedHostResolver createDnsJavaWithNativeFallbackResolver() { return new ChainedHostResolver(ImmutableList.of(new DnsJavaResolver(), new NativeCacheManipulatingResolver())); } /** * Creates a Selenium Proxy object from the BrowserMobProxy instance. The BrowserMobProxy must be started. Retrieves the address * of the Proxy using {@link #getConnectableAddress()}. * * @param browserMobProxy started BrowserMobProxy instance to read connection information from * @return a Selenium Proxy instance, configured to use the BrowserMobProxy instance as its proxy server * @throws IllegalStateException if the proxy has not been started. */ // public static org.openqa.selenium.Proxy createSeleniumProxy(BrowserMobProxy browserMobProxy) { // return createSeleniumProxy(browserMobProxy, getConnectableAddress()); // } // // /** // * Creates a Selenium Proxy object from the BrowserMobProxy instance, using the specified connectableAddress as the Selenium Proxy object's // * proxy address. Determines the port using {@link net.lightbody.bmp.BrowserMobProxy#getPort()}. The BrowserMobProxy must be started. // * // * @param browserMobProxy started BrowserMobProxy instance to read the port from // * @param connectableAddress the network address the Selenium Proxy will use to reach this BrowserMobProxy instance // * @return a Selenium Proxy instance, configured to use the BrowserMobProxy instance as its proxy server // * @throws java.lang.IllegalStateException if the proxy has not been started. // */ // public static org.openqa.selenium.Proxy createSeleniumProxy(BrowserMobProxy browserMobProxy, InetAddress connectableAddress) { // return createSeleniumProxy(new InetSocketAddress(connectableAddress, browserMobProxy.getPort())); // } // // /** // * Creates a Selenium Proxy object using the specified connectableAddressAndPort as the HTTP proxy server. // * // * @param connectableAddressAndPort the network address (or hostname) and port the Selenium Proxy will use to reach its // * proxy server (the InetSocketAddress may be unresolved). // * @return a Selenium Proxy instance, configured to use the specified address and port as its proxy server // */ // public static org.openqa.selenium.Proxy createSeleniumProxy(InetSocketAddress connectableAddressAndPort) { // Proxy proxy = new Proxy(); // proxy.setProxyType(Proxy.ProxyType.MANUAL); // // String proxyStr = String.format("%s:%d", connectableAddressAndPort.getHostString(), connectableAddressAndPort.getPort()); // proxy.setHttpProxy(proxyStr); // proxy.setSslProxy(proxyStr); // // return proxy; // } /** * Attempts to retrieve a "connectable" address for this device that other devices on the network can use to connect to a local proxy. * This is a "reasonable guess" that is suitable in many (but not all) common scenarios. * TODO: define the algorithm used to discover a "connectable" local host * @return a "reasonable guess" at an address that can be used by other machines on the network to reach this host */ public static InetAddress getConnectableAddress() { try { return InetAddress.getLocalHost(); } catch (UnknownHostException e) { throw new RuntimeException("Could not resolve localhost", e); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/Har.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.Writer; public class Har { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private volatile HarLog log; public Har() { } public Har(HarLog log) { this.log = log; } public HarLog getLog() { return log; } public void setLog(HarLog log) { this.log = log; } public void writeTo(Writer writer) throws IOException { OBJECT_MAPPER.writeValue(writer, this); } public void writeTo(OutputStream os) throws IOException { OBJECT_MAPPER.writeValue(os, this); } public void writeTo(File file) throws IOException { OBJECT_MAPPER.writeValue(file, this); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarCache.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarCache { private volatile HarCacheStatus beforeRequest; private volatile HarCacheStatus afterRequest; public HarCacheStatus getBeforeRequest() { return beforeRequest; } public void setBeforeRequest(HarCacheStatus beforeRequest) { this.beforeRequest = beforeRequest; } public HarCacheStatus getAfterRequest() { return afterRequest; } public void setAfterRequest(HarCacheStatus afterRequest) { this.afterRequest = afterRequest; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarCacheStatus.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import net.lightbody.bmp.core.json.ISO8601DateFormatter; import java.util.Date; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarCacheStatus { private volatile Date expires; private volatile Date lastAccess; private volatile String eTag; private volatile int hitCount; private volatile String comment = ""; @JsonSerialize(using = ISO8601DateFormatter.class) public Date getExpires() { return expires; } public void setExpires(Date expires) { this.expires = expires; } @JsonSerialize(using = ISO8601DateFormatter.class) public Date getLastAccess() { return lastAccess; } public void setLastAccess(Date lastAccess) { this.lastAccess = lastAccess; } public String geteTag() { return eTag; } public void seteTag(String eTag) { this.eTag = eTag; } public int getHitCount() { return hitCount; } public void setHitCount(int hitCount) { this.hitCount = hitCount; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarContent.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarContent { private volatile long size; private volatile Long compression; // mimeType is required; though it shouldn't be set to null, if it is, it still needs to be included to comply with the HAR spec @JsonInclude(JsonInclude.Include.ALWAYS) private volatile String mimeType = ""; private volatile String text; private volatile String encoding; private volatile String comment = ""; private volatile byte[] binaryContent = new byte[0]; public byte[] getBinaryContent() { return binaryContent; } public void setBinaryContent(byte[] binaryContent) { this.binaryContent = binaryContent; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public Long getCompression() { return compression; } public void setCompression(Long compression) { this.compression = compression; } public String getMimeType() { return mimeType; } public void setMimeType(String mimeType) { this.mimeType = mimeType; } public String getText() { return text; } public void setText(String text) { this.text = text; } public String getEncoding() { return encoding; } public void setEncoding(String encoding) { this.encoding = encoding; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarCookie.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import net.lightbody.bmp.core.json.ISO8601WithTDZDateFormatter; import java.net.URLDecoder; import java.util.Date; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarCookie { private volatile String name; private volatile String value; private volatile String path; private volatile String domain; private volatile Date expires; private volatile Boolean httpOnly; private volatile Boolean secure; private volatile String comment = ""; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public String getDecodeValue() { try{ return URLDecoder.decode(value); }catch (Exception e) { return value; } } public void setValue(String value) { this.value = value; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } @JsonSerialize(using = ISO8601WithTDZDateFormatter.class) public Date getExpires() { return expires; } public void setExpires(Date expires) { this.expires = expires; } public Boolean getHttpOnly() { return httpOnly; } public void setHttpOnly(Boolean httpOnly) { this.httpOnly = httpOnly; } public Boolean getSecure() { return secure; } public void setSecure(Boolean secure) { this.secure = secure; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } @Override public String toString() { return "HarCookie{" + "name='" + name + '\'' + ", value='" + value + '\'' + ", path='" + path + '\'' + ", domain='" + domain + '\'' + ", expires=" + expires + ", httpOnly=" + httpOnly + ", secure=" + secure + ", comment='" + comment + '\'' + '}'; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarEntry.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import net.lightbody.bmp.core.json.ISO8601WithTDZDateFormatter; import java.util.Date; import java.util.concurrent.TimeUnit; import me.moxun.dreamcatcher.connector.reporter.NetworkEventReporterImpl; @JsonInclude(JsonInclude.Include.NON_NULL) @JsonAutoDetect public class HarEntry { private volatile String id; private volatile String pageref; private volatile Date startedDateTime; private volatile HarRequest request; private volatile HarResponse response; private volatile HarCache cache = new HarCache(); private volatile HarTimings timings = new HarTimings(); private volatile String serverIPAddress; private volatile String connection; private volatile String comment = ""; private volatile double requestTime; private volatile double totalTime; public HarEntry(String pageref) { this.pageref = pageref; } public double getRequestTime() { return requestTime; } public double getTotalTime() { return totalTime; } public void setId(String id) { this.id = id; } public String getId() { return id; } public String getPageref() { return pageref; } public void setPageref(String pageref) { this.pageref = pageref; } @JsonSerialize(using = ISO8601WithTDZDateFormatter.class) public Date getStartedDateTime() { return startedDateTime; } public void setStartedDateTime(Date startedDateTime) { this.startedDateTime = startedDateTime; this.requestTime = NetworkEventReporterImpl.now(); } public void responseFinish() { this.totalTime = (NetworkEventReporterImpl.now() - requestTime) * 1000.0; } /** * Retrieves the time for this HarEntry in milliseconds. To retrieve the time in another time unit, use {@link #getTime(TimeUnit)}. * Rather than storing the time directly, calculate the time from the HarTimings as required in the HAR spec. * From https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html, * section 4.2.16 timings:

     Following must be true in case there are no -1 values (entry is an object in log.entries) :

     entry.time == entry.timings.blocked + entry.timings.dns +
     entry.timings.connect + entry.timings.send + entry.timings.wait +
     entry.timings.receive;
     
* @return time for this HAR entry, in milliseconds */ public long getTime() { return getTime(TimeUnit.MILLISECONDS); } /** * Retrieve the time for this HarEntry in the specified timeUnit. See {@link #getTime()} for details. * * @param timeUnit units of time to return * @return time for this har entry */ public long getTime(TimeUnit timeUnit) { HarTimings timings = getTimings(); if (timings == null) { return -1; } long timeNanos = 0; if (timings.getBlocked(TimeUnit.NANOSECONDS) > 0) { timeNanos += timings.getBlocked(TimeUnit.NANOSECONDS); } if (timings.getDns(TimeUnit.NANOSECONDS) > 0) { timeNanos += timings.getDns(TimeUnit.NANOSECONDS); } if (timings.getConnect(TimeUnit.NANOSECONDS) > 0) { timeNanos += timings.getConnect(TimeUnit.NANOSECONDS); } if (timings.getSend(TimeUnit.NANOSECONDS) > 0) { timeNanos += timings.getSend(TimeUnit.NANOSECONDS); } if (timings.getWait(TimeUnit.NANOSECONDS) > 0) { timeNanos += timings.getWait(TimeUnit.NANOSECONDS); } if (timings.getReceive(TimeUnit.NANOSECONDS) > 0) { timeNanos += timings.getReceive(TimeUnit.NANOSECONDS); } return timeUnit.convert(timeNanos, TimeUnit.NANOSECONDS); } public HarRequest getRequest() { return request; } public void setRequest(HarRequest request) { this.request = request; } public HarResponse getResponse() { return response; } public void setResponse(HarResponse response) { this.response = response; } public HarCache getCache() { return cache; } public void setCache(HarCache cache) { this.cache = cache; } public HarTimings getTimings() { return timings; } public void setTimings(HarTimings timings) { this.timings = timings; } public String getServerIPAddress() { return serverIPAddress; } public void setServerIPAddress(String serverIPAddress) { this.serverIPAddress = serverIPAddress; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getConnection() { return connection; } public void setConnection(String connection) { this.connection = connection; } @Override public String toString() { final StringBuffer sb = new StringBuffer("HarEntry{"); sb.append("cache=").append(cache); sb.append(", pageref='").append(pageref).append('\''); sb.append(", startedDateTime=").append(startedDateTime); sb.append(", request=").append(request); sb.append(", response=").append(response); sb.append(", timings=").append(timings); sb.append(", serverIPAddress='").append(serverIPAddress).append('\''); sb.append(", connection='").append(connection).append('\''); sb.append(", comment='").append(comment).append('\''); sb.append('}'); return sb.toString(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarLog.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarLog { private final String version = "1.2"; private volatile HarNameVersion creator; private volatile HarNameVersion browser; private final List pages = new CopyOnWriteArrayList(); private final List entries = new CopyOnWriteArrayList(); private volatile String comment = ""; private final int MAX_SIZE = 50; private long entryCount = 0; public HarLog() { } public HarLog(HarNameVersion creator) { this.creator = creator; } public void addPage(HarPage page) { pages.add(page); if (pages.size() > MAX_SIZE) { pages.remove(0); } } public void addEntry(HarEntry entry) { entryCount++; entries.add(entry); if (entries.size() > MAX_SIZE) { entries.remove(0); } } public String getVersion() { return version; } public HarNameVersion getCreator() { return creator; } public void setCreator(HarNameVersion creator) { this.creator = creator; } public HarNameVersion getBrowser() { return browser; } public void setBrowser(HarNameVersion browser) { this.browser = browser; } public List getPages() { return pages; } public List getEntries() { return entries; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public long getEntryCount() { return entryCount; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarNameValuePair.java ================================================ package net.lightbody.bmp.core.har; import java.net.URLDecoder; public final class HarNameValuePair { private final String name; private final String value; public HarNameValuePair(String name, String value) { this.name = name; this.value = value; } public String getName() { return name; } public String getValue() { return value; } public String getDecodeValue(){ try { return URLDecoder.decode(value); }catch (Exception e){ return value; } } public String toString() { return name + "=" + value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; HarNameValuePair that = (HarNameValuePair) o; if (name != null ? !name.equals(that.name) : that.name != null) return false; if (value != null ? !value.equals(that.value) : that.value != null) return false; return true; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (value != null ? value.hashCode() : 0); return result; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarNameVersion.java ================================================ package net.lightbody.bmp.core.har; public class HarNameVersion { private final String name; private final String version; private volatile String comment = ""; public HarNameVersion(String name, String version) { this.name = name; this.version = version; } public String getName() { return name; } public String getVersion() { return version; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarPage.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import net.lightbody.bmp.core.json.ISO8601WithTDZDateFormatter; import java.util.Date; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarPage { private volatile String id; private volatile Date startedDateTime; private volatile String title = ""; private final HarPageTimings pageTimings = new HarPageTimings(); private volatile String comment = ""; public HarPage() { } public HarPage(String id) { this(id, ""); } public HarPage(String id, String title) { this.id = id; this.title = title; startedDateTime = new Date(); } public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonSerialize(using = ISO8601WithTDZDateFormatter.class) public Date getStartedDateTime() { return startedDateTime; } public void setStartedDateTime(Date startedDateTime) { this.startedDateTime = startedDateTime; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public HarPageTimings getPageTimings() { return pageTimings; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarPageTimings.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarPageTimings { private volatile Long onContentLoad; private volatile Long onLoad; private volatile String comment = ""; public HarPageTimings() { } public HarPageTimings(Long onContentLoad, Long onLoad) { this.onContentLoad = onContentLoad; this.onLoad = onLoad; } public Long getOnContentLoad() { return onContentLoad; } public void setOnContentLoad(Long onContentLoad) { this.onContentLoad = onContentLoad; } public Long getOnLoad() { return onLoad; } public void setOnLoad(Long onLoad) { this.onLoad = onLoad; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarPostData.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarPostData { private volatile String mimeType; private volatile List params; private volatile String text; private volatile String comment = ""; public String getMimeType() { return mimeType; } public void setMimeType(String mimeType) { this.mimeType = mimeType; } public List getParams() { return params; } public void setParams(List params) { this.params = params; } public String getText() { return text; } public void setText(String text) { if(text != null && text.length()>100000){ text = "HarPostData is too large! Size:"+text.length(); } this.text = text; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarPostDataParam.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarPostDataParam { private volatile String name; private volatile String value; private volatile String fileName; private volatile String contentType; private volatile String comment = ""; public HarPostDataParam() { } public HarPostDataParam(String name, String value) { this.name = name; this.value = value; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarRequest.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarRequest { private volatile String method; private volatile String url; private volatile String httpVersion; private final List cookies = new CopyOnWriteArrayList(); private final List headers = new CopyOnWriteArrayList(); private final List queryString = new CopyOnWriteArrayList(); private volatile HarPostData postData; private volatile long headersSize; // Odd grammar in spec private volatile long bodySize; private volatile String comment = ""; private volatile HarContent content = new HarContent(); public HarRequest() { } public HarRequest(String method, String url, String httpVersion) { this.method = method; this.url = url; this.httpVersion = httpVersion; } public HarContent getContent() { return content; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getHttpVersion() { return httpVersion; } public void setHttpVersion(String httpVersion) { this.httpVersion = httpVersion; } public List getCookies() { return cookies; } public List getHeaders() { return headers; } public List getQueryString() { return queryString; } public HarPostData getPostData() { return postData; } public void setPostData(HarPostData postData) { this.postData = postData; } public long getHeadersSize() { return headersSize; } public void setHeadersSize(long headersSize) { this.headersSize = headersSize; } public long getBodySize() { return bodySize; } public void setBodySize(long bodySize) { this.bodySize = bodySize; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarResponse.java ================================================ package net.lightbody.bmp.core.har; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @JsonInclude(JsonInclude.Include.NON_NULL) public class HarResponse { private volatile int status; private volatile String statusText; private volatile String httpVersion; private final List cookies = new CopyOnWriteArrayList(); private final List headers = new CopyOnWriteArrayList(); private final HarContent content = new HarContent(); private volatile String redirectURL = ""; /* the values of headersSize and bodySize are set to -1 by default, in accordance with the HAR spec: headersSize [number] - Total number of bytes from the start of the HTTP request message until (and including) the double CRLF before the body. Set to -1 if the info is not available. bodySize [number] - Size of the request body (POST data payload) in bytes. Set to -1 if the info is not available. */ private volatile long headersSize = -1; private volatile long bodySize = -1; private volatile String comment = ""; /** * A custom field indicating that an error occurred, such as DNS resolution failure. */ @JsonProperty("_error") private volatile String error; public HarResponse() { } public HarResponse(int status, String statusText, String httpVersion) { this.status = status; this.statusText = statusText; this.httpVersion = httpVersion; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getStatusText() { return statusText; } public void setStatusText(String statusText) { this.statusText = statusText; } public String getHttpVersion() { return httpVersion; } public void setHttpVersion(String httpVersion) { this.httpVersion = httpVersion; } public List getCookies() { return cookies; } public List getHeaders() { return headers; } public HarContent getContent() { return content; } public String getRedirectURL() { return redirectURL; } public void setRedirectURL(String redirectURL) { this.redirectURL = redirectURL; } public long getHeadersSize() { return headersSize; } public void setHeadersSize(long headersSize) { this.headersSize = headersSize; } public long getBodySize() { return bodySize; } public void setBodySize(long bodySize) { this.bodySize = bodySize; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public String getError() { return error; } public void setError(String error) { this.error = error; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/har/HarTimings.java ================================================ package net.lightbody.bmp.core.har; import java.util.concurrent.TimeUnit; public class HarTimings { // optional values are initialized to -1, which indicates they do not apply to the current request, according to the HAR spec private volatile long blockedNanos = -1; private volatile long dnsNanos = -1; private volatile long connectNanos = -1; private volatile long sendNanos; private volatile long waitNanos; private volatile long receiveNanos; private volatile long sslNanos = -1; private volatile String comment = ""; public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } // the following getters and setters take a TimeUnit parameter, to allow finer precision control when no marshalling to JSON public long getBlocked(TimeUnit timeUnit) { if (blockedNanos == -1) { return 0; } else { return timeUnit.convert(blockedNanos, TimeUnit.NANOSECONDS); } } public void setBlocked(long blocked, TimeUnit timeUnit) { if (blocked == -1) { this.blockedNanos = -1; } else { this.blockedNanos = TimeUnit.NANOSECONDS.convert(blocked, timeUnit); } } public long getDns(TimeUnit timeUnit) { if (dnsNanos == -1) { return 0; } else { return timeUnit.convert(dnsNanos, TimeUnit.NANOSECONDS); } } public void setDns(long dns, TimeUnit timeUnit) { if (dns == -1) { this.dnsNanos = -1; } else{ this.dnsNanos = TimeUnit.NANOSECONDS.convert(dns, timeUnit); } } public long getConnect(TimeUnit timeUnit) { if (connectNanos == -1) { return 0; } else { return timeUnit.convert(connectNanos, TimeUnit.NANOSECONDS); } } public void setConnect(long connect, TimeUnit timeUnit) { if (connect == -1) { this.connectNanos = -1; } else { this.connectNanos = TimeUnit.NANOSECONDS.convert(connect, timeUnit); } } /* According to the HAR spec: The send, wait and receive timings are not optional and must have non-negative values. */ public long getSend(TimeUnit timeUnit) { return timeUnit.convert(sendNanos, TimeUnit.NANOSECONDS); } public void setSend(long send, TimeUnit timeUnit) { this.sendNanos = TimeUnit.NANOSECONDS.convert(send, timeUnit); } public long getWait(TimeUnit timeUnit) { return timeUnit.convert(waitNanos, TimeUnit.NANOSECONDS); } public void setWait(long wait, TimeUnit timeUnit) { this.waitNanos = TimeUnit.NANOSECONDS.convert(wait, timeUnit); } public long getReceive(TimeUnit timeUnit) { return timeUnit.convert(receiveNanos, TimeUnit.NANOSECONDS); } public void setReceive(long receive, TimeUnit timeUnit) { this.receiveNanos = TimeUnit.NANOSECONDS.convert(receive, timeUnit); } public long getSsl(TimeUnit timeUnit) { if (sslNanos == -1) { return 0; } else { return timeUnit.convert(sslNanos, TimeUnit.NANOSECONDS); } } public void setSsl(long ssl, TimeUnit timeUnit) { if (ssl == -1) { this.sslNanos = -1; } else { this.sslNanos = TimeUnit.NANOSECONDS.convert(ssl, timeUnit); } } // the following getters and setters assume TimeUnit.MILLISECOND precision. this allows jackson to generate ms values (in accordance // with the HAR spec), and also preserves compatibility with the legacy methods. optional methods are also declared as Long instead of // long (even though they always have values), to preserve compatibility. in general, the getters/setters which take TimeUnits // should always be preferred. public Long getBlocked() { return getBlocked(TimeUnit.MILLISECONDS); } public void setBlocked(long blocked) { setBlocked(blocked, TimeUnit.MILLISECONDS); } public Long getDns() { return getDns(TimeUnit.MILLISECONDS); } public void setDns(long dns) { setDns(dns, TimeUnit.MILLISECONDS); } public Long getConnect() { return getConnect(TimeUnit.MILLISECONDS); } public void setConnect(long connect) { setConnect(connect, TimeUnit.MILLISECONDS); } public long getSend() { return getSend(TimeUnit.MILLISECONDS); } public void setSend(long send) { setSend(send, TimeUnit.MILLISECONDS); } public long getWait() { return getWait(TimeUnit.MILLISECONDS); } public void setWait(long wait) { setWait(wait, TimeUnit.MILLISECONDS); } public long getReceive() { return getReceive(TimeUnit.MILLISECONDS); } public void setReceive(long receive) { setReceive(receive, TimeUnit.MILLISECONDS); } public Long getSsl() { return getSsl(TimeUnit.MILLISECONDS); } public void setSsl(long ssl) { setSsl(ssl, TimeUnit.MILLISECONDS); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/json/ISO8601DateFormatter.java ================================================ package net.lightbody.bmp.core.json; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.text.DateFormat; import java.util.Date; public class ISO8601DateFormatter extends JsonSerializer { public final static ISO8601DateFormatter instance = new ISO8601DateFormatter(); @Override public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { DateFormat df = (DateFormat) provider.getConfig().getDateFormat().clone(); jgen.writeString(df.format(value)); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/core/json/ISO8601WithTDZDateFormatter.java ================================================ package net.lightbody.bmp.core.json; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; /** * * @author Damien Jubeau * Allows Date Format to be compliant with Har 1.2 Spec : ISO 8601 with Time Zone Designator * @see https://github.com/lightbody/browsermob-proxy/issues/44 * */ public class ISO8601WithTDZDateFormatter extends JsonSerializer { @Override public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { jgen.writeString(format(value)); } private String format(Date date) { final String ISO8061_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; final SimpleDateFormat sdf = new SimpleDateFormat(ISO8061_FORMAT); return sdf.format(date); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/exception/DecompressionException.java ================================================ package net.lightbody.bmp.exception; /** * Indicates that an error occurred while decompressing content. */ public class DecompressionException extends RuntimeException { private static final long serialVersionUID = 8666473793514307564L; public DecompressionException() { } public DecompressionException(String message) { super(message); } public DecompressionException(String message, Throwable cause) { super(message, cause); } public DecompressionException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/exception/UnsupportedCharsetException.java ================================================ package net.lightbody.bmp.exception; /** * A checked exception wrapper for {@link java.nio.charset.UnsupportedCharsetException}. This exception is checked to prevent * situations where an unsupported character set in e.g. a Content-Type header causes the proxy to fail completely, rather * than fallback to some suitable default behavior, such as not parsing the text contents of a message. */ public class UnsupportedCharsetException extends Exception { public UnsupportedCharsetException(java.nio.charset.UnsupportedCharsetException e) { super(e); if (e == null) { throw new IllegalArgumentException("net.lightbody.bmp.exception.UnsupportedCharsetException must be initialized with a non-null instance of java.nio.charset.UnsupportedCharsetException"); } } /** * @return the underlying {@link java.nio.charset.UnsupportedCharsetException} that this exception wraps. */ public java.nio.charset.UnsupportedCharsetException getUnsupportedCharsetExceptionCause() { return (java.nio.charset.UnsupportedCharsetException) this.getCause(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/AddHeadersFilter.java ================================================ package net.lightbody.bmp.filters; import org.littleshoot.proxy.HttpFiltersAdapter; import java.util.Collections; import java.util.Map; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * Adds the headers specified in the constructor to this request. The filter does not make a defensive copy of the map, so there is no guarantee * that the map at the time of construction will contain the same values when the filter is actually invoked, if the map is modified concurrently. */ public class AddHeadersFilter extends HttpFiltersAdapter { private final Map additionalHeaders; public AddHeadersFilter(HttpRequest originalRequest, Map additionalHeaders) { super(originalRequest); if (additionalHeaders != null) { this.additionalHeaders = additionalHeaders; } else { this.additionalHeaders = Collections.emptyMap(); } } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; for (Map.Entry header : additionalHeaders.entrySet()) { httpRequest.headers().add(header.getKey(), header.getValue()); } } return null; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/AutoBasicAuthFilter.java ================================================ package net.lightbody.bmp.filters; import org.littleshoot.proxy.impl.ProxyUtils; import java.util.Map; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * A filter that adds Basic authentication information to non-CONNECT requests. Takes a map of domain names to base64-encoded * Basic auth credentials as a constructor parameter. If a key in the map matches the hostname of a filtered request, an Authorization * header will be added to the request. *

* The Authorization header itself is specified in RFC 7235, section 4.2: https://tools.ietf.org/html/rfc7235#section-4.2 * The Basic authentication scheme is specified in RFC 2617, section 2: https://tools.ietf.org/html/rfc2617#section-2 */ public class AutoBasicAuthFilter extends HttpsAwareFiltersAdapter { private final Map credentialsByHostname; public AutoBasicAuthFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Map credentialsByHostname) { super(originalRequest, ctx); this.credentialsByHostname = credentialsByHostname; } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (credentialsByHostname.isEmpty()) { return null; } if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; // providing authorization during a CONNECT is generally not useful if (ProxyUtils.isCONNECT(httpRequest)) { return null; } String hostname = getHost(httpRequest); // if there is an entry in the credentials map matching this hostname, add the credentials to the request String base64CredentialsForHostname = credentialsByHostname.get(hostname); if (base64CredentialsForHostname != null) { httpRequest.headers().add(HttpHeaders.Names.AUTHORIZATION, "Basic " + base64CredentialsForHostname); } } return null; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/BlacklistFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.proxy.BlacklistEntry; import java.util.Collection; import java.util.Collections; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; /** * Applies blacklist entries to this request. The filter does not make a defensive copy of the blacklist entries, so there is no guarantee * that the blacklist at the time of construction will contain the same values when the filter is actually invoked, if the entries are modified concurrently. */ public class BlacklistFilter extends HttpsAwareFiltersAdapter { private final Collection blacklistedUrls; public BlacklistFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Collection blacklistedUrls) { super(originalRequest, ctx); if (blacklistedUrls != null) { this.blacklistedUrls = blacklistedUrls; } else { this.blacklistedUrls = Collections.emptyList(); } } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; String url = getFullUrl(httpRequest); for (BlacklistEntry entry : blacklistedUrls) { if (HttpMethod.CONNECT.equals(httpRequest.getMethod()) && entry.getHttpMethodPattern() == null) { // do not allow CONNECTs to be blacklisted unless a method pattern is explicitly specified continue; } if (entry.matches(url, httpRequest.getMethod().name())) { HttpResponseStatus status = HttpResponseStatus.valueOf(entry.getStatusCode()); HttpResponse resp = new DefaultFullHttpResponse(httpRequest.getProtocolVersion(), status); HttpHeaders.setContentLength(resp, 0L); return resp; } } } return null; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/BrowserMobHttpFilterChain.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.BrowserMobProxyServer; import org.littleshoot.proxy.HttpFilters; import org.littleshoot.proxy.HttpFiltersAdapter; import org.littleshoot.proxy.HttpFiltersSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; /** * The filter "driver" that delegates to all chained filters specified by the proxy server. */ public class BrowserMobHttpFilterChain extends HttpFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(BrowserMobHttpFilterChain.class); private final BrowserMobProxyServer proxyServer; private final List filters; public BrowserMobHttpFilterChain(BrowserMobProxyServer proxyServer, HttpRequest originalRequest, ChannelHandlerContext ctx) { super(originalRequest, ctx); this.proxyServer = proxyServer; if (proxyServer.getFilterFactories() != null) { filters = new ArrayList<>(proxyServer.getFilterFactories().size()); // instantiate all HttpFilters using the proxy's filter factories for (HttpFiltersSource filterFactory : proxyServer.getFilterFactories()) { HttpFilters filter = filterFactory.filterRequest(originalRequest, ctx); // allow filter factories to avoid adding a filter on a per-request basis by returning a null // HttpFilters instance if (filter != null) { filters.add(filter); } } } else { filters = Collections.emptyList(); } } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (proxyServer.isStopped()) { log.warn("Aborting request to {} because proxy is stopped", originalRequest.getUri()); HttpResponse abortedResponse = new DefaultFullHttpResponse(originalRequest.getProtocolVersion(), HttpResponseStatus.SERVICE_UNAVAILABLE); HttpHeaders.setContentLength(abortedResponse, 0L); return abortedResponse; } for (HttpFilters filter : filters) { try { HttpResponse filterResponse = filter.clientToProxyRequest(httpObject); if (filterResponse != null) { // if we are short-circuiting the response to an HttpRequest, update ModifiedRequestAwareFilter instances // with this (possibly) modified HttpRequest before returning the short-circuit response if (httpObject instanceof HttpRequest) { updateFiltersWithModifiedResponse((HttpRequest) httpObject); } return filterResponse; } } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } // if this httpObject is the HTTP request, set the modified request object on all ModifiedRequestAwareFilter // instances, so they have access to all modifications the request filters made while filtering if (httpObject instanceof HttpRequest) { updateFiltersWithModifiedResponse((HttpRequest) httpObject); } return null; } @Override public HttpResponse proxyToServerRequest(HttpObject httpObject) { for (HttpFilters filter : filters) { try { HttpResponse filterResponse = filter.proxyToServerRequest(httpObject); if (filterResponse != null) { return filterResponse; } } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } return null; } @Override public void proxyToServerRequestSending() { for (HttpFilters filter : filters) { try { filter.proxyToServerRequestSending(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public HttpObject serverToProxyResponse(HttpObject httpObject) { HttpObject processedHttpObject = httpObject; for (HttpFilters filter : filters) { try { processedHttpObject = filter.serverToProxyResponse(processedHttpObject); if (processedHttpObject == null) { return null; } } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } return processedHttpObject; } @Override public void serverToProxyResponseTimedOut() { for (HttpFilters filter : filters) { try { filter.serverToProxyResponseTimedOut(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void serverToProxyResponseReceiving() { for (HttpFilters filter : filters) { try { filter.serverToProxyResponseReceiving(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public InetSocketAddress proxyToServerResolutionStarted(String resolvingServerHostAndPort) { InetSocketAddress overrideAddress = null; String newServerHostAndPort = resolvingServerHostAndPort; for (HttpFilters filter : filters) { try { InetSocketAddress filterResult = filter.proxyToServerResolutionStarted(newServerHostAndPort); if (filterResult != null) { overrideAddress = filterResult; newServerHostAndPort = filterResult.getHostString() + ":" + filterResult.getPort(); } } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } return overrideAddress; } @Override public void proxyToServerResolutionFailed(String hostAndPort) { for (HttpFilters filter : filters) { try { filter.proxyToServerResolutionFailed(hostAndPort); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void proxyToServerResolutionSucceeded(String serverHostAndPort, InetSocketAddress resolvedRemoteAddress) { for (HttpFilters filter : filters) { try { filter.proxyToServerResolutionSucceeded(serverHostAndPort, resolvedRemoteAddress); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } super.proxyToServerResolutionSucceeded(serverHostAndPort, resolvedRemoteAddress); } @Override public void proxyToServerConnectionStarted() { for (HttpFilters filter : filters) { try { filter.proxyToServerConnectionStarted(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void proxyToServerConnectionSSLHandshakeStarted() { for (HttpFilters filter : filters) { try { filter.proxyToServerConnectionSSLHandshakeStarted(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void proxyToServerConnectionFailed() { for (HttpFilters filter : filters) { try { filter.proxyToServerConnectionFailed(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void proxyToServerConnectionSucceeded(ChannelHandlerContext serverCtx) { for (HttpFilters filter : filters) { try { filter.proxyToServerConnectionSucceeded(serverCtx); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void proxyToServerRequestSent() { for (HttpFilters filter : filters) { try { filter.proxyToServerRequestSent(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public void serverToProxyResponseReceived() { for (HttpFilters filter : filters) { try { filter.serverToProxyResponseReceived(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } @Override public HttpObject proxyToClientResponse(HttpObject httpObject) { HttpObject processedHttpObject = httpObject; for (HttpFilters filter : filters) { try { processedHttpObject = filter.proxyToClientResponse(processedHttpObject); if (processedHttpObject == null) { return null; } } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } return processedHttpObject; } @Override public void proxyToServerConnectionQueued() { for (HttpFilters filter : filters) { try { filter.proxyToServerConnectionQueued(); } catch (RuntimeException e) { log.warn("Filter in filter chain threw exception. Filter method may have been aborted.", e); } } } /** * Updates {@link ModifiedRequestAwareFilter} filters with the final, modified request after all request filters have * processed the request. * * @param modifiedRequest the modified HttpRequest after all filters have finished processing it */ private void updateFiltersWithModifiedResponse(HttpRequest modifiedRequest) { for (HttpFilters filter : filters) { if (filter instanceof ModifiedRequestAwareFilter) { ModifiedRequestAwareFilter requestCaptureFilter = (ModifiedRequestAwareFilter) filter; try { requestCaptureFilter.setModifiedHttpRequest(modifiedRequest); } catch (RuntimeException e) { log.warn("ModifiedRequestAwareFilter in filter chain threw exception while setting modified HTTP request.", e); } } } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/ClientRequestCaptureFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.BrowserMobHttpUtil; import org.littleshoot.proxy.HttpFiltersAdapter; import java.io.ByteArrayOutputStream; import java.io.IOException; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; /** * This filter captures requests from the client (headers and content). *

* The filter can be used in one of three ways: (1) directly, by adding the filter to the filter chain; (2) by subclassing * the filter and overriding its filter methods; or (3) by invoking the filter directly from within another filter (see * {@link net.lightbody.bmp.filters.HarCaptureFilter} for an example of the latter). */ public class ClientRequestCaptureFilter extends HttpFiltersAdapter { /** * Populated by clientToProxyRequest() when processing the HttpRequest object. Unlike originalRequest, * this represents the "real" request that is being sent to the server, including headers. */ private volatile HttpRequest httpRequest; /** * Populated by clientToProxyRequest() when processing the HttpContent objects. If the request is chunked, * it will be populated across multiple calls to clientToProxyRequest(). */ private final ByteArrayOutputStream requestContents = new ByteArrayOutputStream(); /** * Populated by clientToProxyRequest() when processing the LastHttpContent. */ private volatile HttpHeaders trailingHeaders; public ClientRequestCaptureFilter(HttpRequest originalRequest) { super(originalRequest); } public ClientRequestCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx) { super(originalRequest, ctx); } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { this.httpRequest = (HttpRequest) httpObject; } if (httpObject instanceof HttpContent) { HttpContent httpContent = (HttpContent) httpObject; storeRequestContent(httpContent); if (httpContent instanceof LastHttpContent) { LastHttpContent lastHttpContent = (LastHttpContent) httpContent; trailingHeaders = lastHttpContent .trailingHeaders(); } } return null; } protected void storeRequestContent(HttpContent httpContent) { ByteBuf bufferedContent = httpContent.content(); byte[] content = BrowserMobHttpUtil.extractReadableBytes(bufferedContent); try { requestContents.write(content); } catch (IOException e) { // can't happen } } public HttpRequest getHttpRequest() { return httpRequest; } public byte[] getFullRequestContents() { return requestContents.toByteArray(); } public HttpHeaders getTrailingHeaders() { return trailingHeaders; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/HarCaptureFilter.java ================================================ package net.lightbody.bmp.filters; import android.util.Log; import com.google.common.collect.ImmutableList; import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarCookie; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarNameValuePair; import net.lightbody.bmp.core.har.HarPostData; import net.lightbody.bmp.core.har.HarPostDataParam; import net.lightbody.bmp.core.har.HarRequest; import net.lightbody.bmp.core.har.HarResponse; import net.lightbody.bmp.exception.UnsupportedCharsetException; import net.lightbody.bmp.filters.support.HttpConnectTiming; import net.lightbody.bmp.filters.util.HarCaptureUtil; import net.lightbody.bmp.proxy.CaptureType; import net.lightbody.bmp.util.BrowserMobHttpUtil; import org.littleshoot.proxy.impl.ProxyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.Charset; import java.util.Calendar; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ClientCookieDecoder; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import me.moxun.dreamcatcher.wrapper.DCRequest; import me.moxun.dreamcatcher.wrapper.DCResponse; import me.moxun.dreamcatcher.wrapper.ProxyManager; public class HarCaptureFilter extends HttpsAwareFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(HarCaptureFilter.class); /** * The currently active HAR at the time the current request is received. */ private final Har har; /** * The harEntry is created when this filter is constructed and is shared by both the clientToProxyRequest * and serverToProxyResponse methods. It is added to the HarLog when the request is received from the client. */ @Deprecated private final HarEntry harEntry; private DCRequest harRequest; private DCResponse harResponse; private ProxyManager proxyManager; /** * The requestCaptureFilter captures all request content, including headers, trailing headers, and content. The HarCaptureFilter * delegates to it when the clientToProxyRequest() callback is invoked. If this request does not need content capture, the * ClientRequestCaptureFilter filter will not be instantiated and will not capture content. */ private final ClientRequestCaptureFilter requestCaptureFilter; /** * Like requestCaptureFilter above, HarCaptureFilter delegates to responseCaptureFilter to capture response contents. If content capture * is not required for this request, the filter will not be instantiated or invoked. */ private final ServerResponseCaptureFilter responseCaptureFilter; /** * The CaptureType data types to capture in this request. */ private final EnumSet dataToCapture; /** * Populated by proxyToServerResolutionStarted when DNS resolution starts. If any previous filters already resolved the address, their resolution time * will not be included in this time. */ private volatile long dnsResolutionStartedNanos; private volatile long connectionQueuedNanos; private volatile long connectionStartedNanos; private volatile long sendStartedNanos; private volatile long sendFinishedNanos; private volatile long responseReceiveStartedNanos; /** * The address of the client making the request. Captured in the constructor and used when calculating and capturing ssl handshake and connect * timing information for SSL connections. */ private final InetSocketAddress clientAddress; /** * Request body size is determined by the actual size of the data the client sends. The filter does not use the Content-Length header to determine request size. */ private final AtomicInteger requestBodySize = new AtomicInteger(0); /** * Response body size is determined by the actual size of the data the server sends. */ private final AtomicInteger responseBodySize = new AtomicInteger(0); /** * The "real" original request, as captured by the {@link #clientToProxyRequest(io.netty.handler.codec.http.HttpObject)} method. */ private volatile HttpRequest capturedOriginalRequest; /** * True if this filter instance processed a {@link #proxyToServerResolutionSucceeded(String, InetSocketAddress)} call, indicating * that the hostname was resolved and populated in the HAR (if this is not a CONNECT). */ private volatile boolean addressResolved = false; /** * Create a new instance of the HarCaptureFilter that will capture request and response information. If no har is specified in the * constructor, this filter will do nothing. *

* Regardless of the CaptureTypes specified in dataToCapture, the HarCaptureFilter will always capture: *

    *
  • Request and response sizes
  • *
  • HTTP request and status lines
  • *
  • Page timing information
  • *
* * @param originalRequest the original HttpRequest from the HttpFiltersSource factory * @param har a reference to the ProxyServer's current HAR file at the time this request is received (can be null if HAR capture is not required) * @param currentPageRef the ProxyServer's currentPageRef at the time this request is received from the client * @param dataToCapture the data types to capture for this request. null or empty set indicates only basic information will be * captured (see {@link net.lightbody.bmp.proxy.CaptureType} for information on data collected for each CaptureType) */ public HarCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Har har, String currentPageRef, Set dataToCapture) { super(originalRequest, ctx); if (har == null) { throw new IllegalStateException("Attempted har capture when har is null"); } if (ProxyUtils.isCONNECT(originalRequest)) { throw new IllegalStateException("Attempted har capture for HTTP CONNECT request"); } this.clientAddress = (InetSocketAddress) ctx.channel().remoteAddress(); if (dataToCapture != null && !dataToCapture.isEmpty()) { this.dataToCapture = EnumSet.copyOf(dataToCapture); } else { this.dataToCapture = EnumSet.noneOf(CaptureType.class); } // we may need to capture both the request and the response, so set up the request/response filters and delegate to them when // the corresponding filter methods are invoked. to save time and memory, only set up the capturing filters when // we actually need to capture the data. if (this.dataToCapture.contains(CaptureType.REQUEST_CONTENT) || this.dataToCapture.contains(CaptureType.REQUEST_BINARY_CONTENT)) { requestCaptureFilter = new ClientRequestCaptureFilter(originalRequest); } else { requestCaptureFilter = null; } if (this.dataToCapture.contains(CaptureType.RESPONSE_CONTENT) || this.dataToCapture.contains(CaptureType.RESPONSE_BINARY_CONTENT)) { responseCaptureFilter = new ServerResponseCaptureFilter(originalRequest, true); } else { responseCaptureFilter = null; } this.har = har; this.proxyManager = ProxyManager.newInstance(); this.harEntry = new HarEntry(currentPageRef); harEntry.setId(proxyManager.getDCRequestId()); this.harRequest = new DCRequest(harEntry); this.harRequest.attachBodyHelper(this.proxyManager.getRequestBodyHelper()); this.harResponse = new DCResponse(harEntry); } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { // if a ServerResponseCaptureFilter is configured, delegate to it to collect the client request. if it is not // configured, we still need to capture basic information (timings, possibly client headers, etc.), just not content. Log.e("Capture", "clientToProxyRequest " + harEntry.getId()); if (requestCaptureFilter != null) { requestCaptureFilter.clientToProxyRequest(httpObject); } if (httpObject instanceof HttpRequest) { // link the object up now, before we make the request, so that if we get cut off (ie: favicon.ico request and browser shuts down) // we still have the attempt associated, even if we never got a response harEntry.setStartedDateTime(new Date()); har.getLog().addEntry(harEntry); HttpRequest httpRequest = (HttpRequest) httpObject; this.capturedOriginalRequest = httpRequest; // associate this request's DCRequest object with the har entry HarRequest request = createHarRequestForHttpRequest(httpRequest); harEntry.setRequest(request); // create a "no response received" DCResponse, in case the connection is interrupted, terminated, or the response is not received // for any other reason. having a "default" DCResponse prevents us from generating an invalid HAR. HarResponse defaultHarResponse = HarCaptureUtil.createHarResponseForFailure(); defaultHarResponse.setError(HarCaptureUtil.getNoResponseReceivedErrorMessage()); harEntry.setResponse(defaultHarResponse); captureQueryParameters(httpRequest); // not capturing user agent: in many cases, it doesn't make sense to capture at the HarLog level, since the proxy could be // serving requests from many different clients with various user agents. clients can turn on the REQUEST_HEADERS capture type // in order to capture the User-Agent header, if desired. captureRequestHeaderSize(httpRequest); if (dataToCapture.contains(CaptureType.REQUEST_COOKIES)) { captureRequestCookies(httpRequest); } if (dataToCapture.contains(CaptureType.REQUEST_HEADERS)) { captureRequestHeaders(httpRequest); } // The HTTP CONNECT to the proxy server establishes the SSL connection to the remote server, but the // HTTP CONNECT is not recorded in a separate HarEntry (except in case of error). Instead, the ssl and // connect times are recorded in the first request between the client and remote server after the HTTP CONNECT. captureConnectTiming(); } if (httpObject instanceof HttpContent) { HttpContent httpContent = (HttpContent) httpObject; captureRequestSize(httpContent); } if (httpObject instanceof LastHttpContent) { LastHttpContent lastHttpContent = (LastHttpContent) httpObject; if (dataToCapture.contains(CaptureType.REQUEST_HEADERS)) { captureTrailingHeaders(lastHttpContent); } if (dataToCapture.contains(CaptureType.REQUEST_CONTENT)) { captureRequestContent(requestCaptureFilter.getHttpRequest(), requestCaptureFilter.getFullRequestContents()); } harRequest.getRequest().setBodySize(requestBodySize.get()); } return null; } @Override public HttpObject serverToProxyResponse(HttpObject httpObject) { // if a ServerResponseCaptureFilter is configured, delegate to it to collect the server's response. if it is not // configured, we still need to capture basic information (timings, HTTP status, etc.), just not content. Log.e("Capture", "serverToProxyResponse " + harEntry.getId()); if (responseCaptureFilter != null) { responseCaptureFilter.serverToProxyResponse(httpObject); } if (httpObject instanceof HttpResponse) { HttpResponse httpResponse = (HttpResponse) httpObject; captureResponse(httpResponse); } if (httpObject instanceof HttpContent) { HttpContent httpContent = (HttpContent) httpObject; captureResponseSize(httpContent); } if (httpObject instanceof LastHttpContent) { if (dataToCapture.contains(CaptureType.RESPONSE_CONTENT)) { captureResponseContent(responseCaptureFilter.getHttpResponse(), responseCaptureFilter.getFullResponseContents()); } harResponse.getResponse().setBodySize(responseBodySize.get()); } proxyManager.responseHeadersReceived(harResponse); return super.serverToProxyResponse(httpObject); } @Override public void serverToProxyResponseTimedOut() { // replace any existing DCResponse that was created if the server sent a partial response Log.e("Capture", "serverToProxyResponseTimedOut " + harEntry.getId()); HarResponse response = HarCaptureUtil.createHarResponseForFailure(); harEntry.setResponse(response); response.setError(HarCaptureUtil.getResponseTimedOutErrorMessage()); proxyManager.httpExchangeFailed(response.getError()); // include this timeout time in the HarTimings object long timeoutTimestampNanos = System.nanoTime(); // if the proxy started to send the request but has not yet finished, we are currently "sending" if (sendStartedNanos > 0L && sendFinishedNanos == 0L) { harEntry.getTimings().setSend(timeoutTimestampNanos - sendStartedNanos, TimeUnit.NANOSECONDS); } // if the entire request was sent but the proxy has not begun receiving the response, we are currently "waiting" else if (sendFinishedNanos > 0L && responseReceiveStartedNanos == 0L) { harEntry.getTimings().setWait(timeoutTimestampNanos - sendFinishedNanos, TimeUnit.NANOSECONDS); } // if the proxy has already begun to receive the response, we are currenting "receiving" else if (responseReceiveStartedNanos > 0L) { harEntry.getTimings().setReceive(timeoutTimestampNanos - responseReceiveStartedNanos, TimeUnit.NANOSECONDS); } } /** * Creates a DCRequest object using the method, url, and HTTP version of the specified request. * * @param httpRequest HTTP request on which the DCRequest will be based * @return a new DCRequest object */ private HarRequest createHarRequestForHttpRequest(HttpRequest httpRequest) { // the HAR spec defines the request.url field as: // url [string] - Absolute URL of the request (fragments are not included). // the URI on the httpRequest may only identify the path of the resource, so find the full URL. // the full URL consists of the scheme + host + port (if non-standard) + path + query params + fragment. String url = getFullUrl(httpRequest); return new HarRequest(httpRequest.getMethod().toString(), url, httpRequest.getProtocolVersion().text()); } //TODO: add unit tests for these utility-like capture() methods protected void captureQueryParameters(HttpRequest httpRequest) { // capture query parameters. it is safe to assume the query string is UTF-8, since it "should" be in US-ASCII (a subset of UTF-8), // but sometimes does include UTF-8 characters. Log.e("InnerHandle", "captureQueryParameters " + harEntry.getId()); QueryStringDecoder queryStringDecoder = null; queryStringDecoder = new QueryStringDecoder(httpRequest.getUri(), Charset.forName("UTF-8")); try { for (Map.Entry> entry : queryStringDecoder.parameters().entrySet()) { for (String value : entry.getValue()) { harEntry.getRequest().getQueryString().add(new HarNameValuePair(entry.getKey(), value)); } } } catch (IllegalArgumentException e) { // QueryStringDecoder will throw an IllegalArgumentException if it cannot interpret a query string. rather than cause the entire request to // fail by propagating the exception, simply skip the query parameter capture. harEntry.setComment("Unable to decode query parameters on URI: " + httpRequest.getUri()); log.info("Unable to decode query parameters on URI: " + httpRequest.getUri(), e); } } protected void captureRequestHeaderSize(HttpRequest httpRequest) { Log.e("InnerHandle", "captureRequestHeaderSize " + harEntry.getId()); String requestLine = httpRequest.getMethod().toString() + ' ' + httpRequest.getUri() + ' ' + httpRequest.getProtocolVersion().toString(); // +2 => CRLF after status line, +4 => header/data separation long requestHeadersSize = requestLine.length() + 6; HttpHeaders headers = httpRequest.headers(); requestHeadersSize += BrowserMobHttpUtil.getHeaderSize(headers); harRequest.getRequest().setHeadersSize(requestHeadersSize); } protected void captureRequestCookies(HttpRequest httpRequest) { Log.e("InnerHandle", "captureRequestCookies " + harEntry.getId()); String cookieHeader = httpRequest.headers().get(HttpHeaders.Names.COOKIE); if (cookieHeader == null) { return; } Set cookies = ServerCookieDecoder.LAX.decode(cookieHeader); for (Cookie cookie : cookies) { HarCookie harCookie = new HarCookie(); harCookie.setName(cookie.name()); harCookie.setValue(cookie.value()); harRequest.getRequest().getCookies().add(harCookie); harRequest.addHeader(cookie.name(), cookie.value()); } } protected void captureRequestHeaders(HttpRequest httpRequest) { Log.e("InnerHandle", "captureRequestHeaders " + harEntry.getId()); HttpHeaders headers = httpRequest.headers(); captureHeaders(headers); } protected void captureTrailingHeaders(LastHttpContent lastHttpContent) { Log.e("InnerHandle", "captureTrailingHeaders " + harEntry.getId()); HttpHeaders headers = lastHttpContent.trailingHeaders(); captureHeaders(headers); } protected void captureHeaders(HttpHeaders headers) { Log.e("InnerHandle", "captureHeaders " + harEntry.getId()); for (Map.Entry header : headers.entries()) { harRequest.getRequest().getHeaders().add(new HarNameValuePair(header.getKey(), header.getValue())); harRequest.addHeader(header.getKey(), header.getValue()); } } protected void captureRequestContent(HttpRequest httpRequest, byte[] fullMessage) { Log.e("InnerHandle", "captureRequestContent " + harEntry.getId()); if (fullMessage.length == 0) { harRequest.getRequest().getContent().setText("Empty body"); return; } harRequest.getRequest().getContent().setBinaryContent(fullMessage); harRequest.getRequest().getContent().setText(fullMessage.length + " bytes"); String contentType = HttpHeaders.getHeader(httpRequest, HttpHeaders.Names.CONTENT_TYPE); if (contentType == null) { log.warn("No content type specified in request to {}. Content will be treated as {}", httpRequest.getUri(), BrowserMobHttpUtil.UNKNOWN_CONTENT_TYPE); contentType = BrowserMobHttpUtil.UNKNOWN_CONTENT_TYPE; } HarPostData postData = new HarPostData(); harRequest.getRequest().setPostData(postData); postData.setMimeType(contentType); boolean urlEncoded; if (contentType.startsWith(HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED)) { urlEncoded = true; } else { urlEncoded = false; } Charset charset; try { charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentType); } catch (UnsupportedCharsetException e) { log.warn("Found unsupported character set in Content-Type header '{}' in HTTP request to {}. Content will not be captured in HAR.", contentType, httpRequest.getUri(), e); return; } if (charset == null) { // no charset specified, so use the default -- but log a message since this might not encode the data correctly charset = BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET; log.debug("No charset specified; using charset {} to decode contents to {}", charset, httpRequest.getUri()); } if (urlEncoded) { String textContents = BrowserMobHttpUtil.getContentAsString(fullMessage, charset); QueryStringDecoder queryStringDecoder = new QueryStringDecoder(textContents, charset, false); ImmutableList.Builder paramBuilder = ImmutableList.builder(); for (Map.Entry> entry : queryStringDecoder.parameters().entrySet()) { for (String value : entry.getValue()) { paramBuilder.add(new HarPostDataParam(entry.getKey(), value)); } } harRequest.getRequest().getPostData().setParams(paramBuilder.build()); } else { //TODO: implement capture of files and multipart form data // not URL encoded, so let's grab the body of the POST and capture that String postBody = BrowserMobHttpUtil.getContentAsString(fullMessage, charset); harRequest.getRequest().getPostData().setText(postBody); } } protected void captureResponseContent(HttpResponse httpResponse, byte[] fullMessage) { // force binary if the content encoding is not supported Log.e("InnerHandle", "captureResponseContent " + harEntry.getId()); boolean forceBinary = false; if (fullMessage.length != 0) { harResponse.getResponse().getContent().setBinaryContent(fullMessage); } String contentType = HttpHeaders.getHeader(httpResponse, HttpHeaders.Names.CONTENT_TYPE); if (contentType == null) { log.warn("No content type specified in response from {}. Content will be treated as {}", originalRequest.getUri(), BrowserMobHttpUtil.UNKNOWN_CONTENT_TYPE); contentType = BrowserMobHttpUtil.UNKNOWN_CONTENT_TYPE; } if (responseCaptureFilter.isResponseCompressed() && !responseCaptureFilter.isDecompressionSuccessful()) { log.warn("Unable to decompress content with encoding: {}. Contents will be encoded as base64 binary data.", responseCaptureFilter.getContentEncoding()); forceBinary = true; } Charset charset; try { charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentType); } catch (UnsupportedCharsetException e) { log.warn("Found unsupported character set in Content-Type header '{}' in HTTP response from {}. Content will not be captured in HAR.", contentType, originalRequest.getUri(), e); return; } if (charset == null) { // no charset specified, so use the default -- but log a message since this might not encode the data correctly charset = BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET; log.debug("No charset specified; using charset {} to decode contents from {}", charset, originalRequest.getUri()); } if (!forceBinary && BrowserMobHttpUtil.hasTextualContent(contentType)) { String text = BrowserMobHttpUtil.getContentAsString(fullMessage, charset); harResponse.getResponse().getContent().setText(text); } else if (dataToCapture.contains(CaptureType.RESPONSE_BINARY_CONTENT)) { harResponse.getResponse().getContent().setText("Binary Content " + fullMessage.length + " bytes"); } harResponse.getResponse().getContent().setSize(fullMessage.length); } protected void captureResponse(HttpResponse httpResponse) { Log.e("InnerHandle", "captureResponse " + harEntry.getId()); HarResponse response = new HarResponse(httpResponse.getStatus().code(), httpResponse.getStatus().reasonPhrase(), httpResponse.getProtocolVersion().text()); harEntry.setResponse(response); captureResponseHeaderSize(httpResponse); captureResponseMimeType(httpResponse); if (dataToCapture.contains(CaptureType.RESPONSE_COOKIES)) { captureResponseCookies(httpResponse); } if (dataToCapture.contains(CaptureType.RESPONSE_HEADERS)) { captureResponseHeaders(httpResponse); } if (BrowserMobHttpUtil.isRedirect(httpResponse)) { captureRedirectUrl(httpResponse); } } protected void captureResponseMimeType(HttpResponse httpResponse) { Log.e("InnerHandle", "captureResponseMimeType " + harEntry.getId()); String contentType = HttpHeaders.getHeader(httpResponse, HttpHeaders.Names.CONTENT_TYPE); // don't set the mimeType to null, since mimeType is a required field if (contentType != null) { harResponse.getResponse().getContent().setMimeType(contentType); } } protected void captureResponseCookies(HttpResponse httpResponse) { Log.e("InnerHandle", "captureResponseCookies " + harEntry.getId()); List setCookieHeaders = httpResponse.headers().getAll(HttpHeaders.Names.SET_COOKIE); if (setCookieHeaders == null) { return; } for (String setCookieHeader : setCookieHeaders) { Cookie cookie = ClientCookieDecoder.LAX.decode(setCookieHeader); if (cookie == null) { return; } HarCookie harCookie = new HarCookie(); harCookie.setName(cookie.name()); harCookie.setValue(cookie.value()); // comment is no longer supported in the netty ClientCookieDecoder harCookie.setDomain(cookie.domain()); harCookie.setHttpOnly(cookie.isHttpOnly()); harCookie.setPath(cookie.path()); harCookie.setSecure(cookie.isSecure()); if (cookie.maxAge() > 0) { // use a Calendar with the current timestamp + maxAge seconds. the locale of the calendar is irrelevant, // since we are dealing with timestamps. Calendar expires = Calendar.getInstance(); // zero out the milliseconds, since maxAge is in seconds expires.set(Calendar.MILLISECOND, 0); // we can't use Calendar.add, since that only takes ints. TimeUnit.convert handles second->millisecond // overflow reasonably well by returning the result as Long.MAX_VALUE. expires.setTimeInMillis(expires.getTimeInMillis() + TimeUnit.MILLISECONDS.convert(cookie.maxAge(), TimeUnit.SECONDS)); harCookie.setExpires(expires.getTime()); } harResponse.getResponse().getCookies().add(harCookie); harResponse.addHeader(harCookie.getName(), harCookie.getValue()); } } protected void captureResponseHeaderSize(HttpResponse httpResponse) { Log.e("InnerHandle", "captureResponseHeaderSize " + harEntry.getId()); String statusLine = httpResponse.getProtocolVersion().toString() + ' ' + httpResponse.getStatus().toString(); // +2 => CRLF after status line, +4 => header/data separation long responseHeadersSize = statusLine.length() + 6; HttpHeaders headers = httpResponse.headers(); responseHeadersSize += BrowserMobHttpUtil.getHeaderSize(headers); harResponse.getResponse().setHeadersSize(responseHeadersSize); } protected void captureResponseHeaders(HttpResponse httpResponse) { Log.e("InnerHandle", "captureResponseHeaders " + harEntry.getId()); HttpHeaders headers = httpResponse.headers(); for (Map.Entry header : headers.entries()) { harResponse.getResponse().getHeaders().add(new HarNameValuePair(header.getKey(), header.getValue())); harResponse.addHeader(header.getKey(), header.getValue()); } } protected void captureRedirectUrl(HttpResponse httpResponse) { Log.e("InnerHandle", "captureRedirectUrl " + harEntry.getId()); String locationHeaderValue = HttpHeaders.getHeader(httpResponse, HttpHeaders.Names.LOCATION); if (locationHeaderValue != null) { harResponse.getResponse().setRedirectURL(locationHeaderValue); } } /** * Adds the size of this httpContent to the requestBodySize. * * @param httpContent HttpContent to size */ protected void captureRequestSize(HttpContent httpContent) { Log.e("InnerHandle", "captureRequestSize " + harEntry.getId()); ByteBuf bufferedContent = httpContent.content(); int contentSize = bufferedContent.readableBytes(); requestBodySize.addAndGet(contentSize); } /** * Adds the size of this httpContent to the responseBodySize. * * @param httpContent HttpContent to size */ protected void captureResponseSize(HttpContent httpContent) { Log.e("InnerHandle", "captureResponseSize " + harEntry.getId()); ByteBuf bufferedContent = httpContent.content(); int contentSize = bufferedContent.readableBytes(); responseBodySize.addAndGet(contentSize); proxyManager.dataReceived(contentSize); } /** * Populates ssl and connect timing info in the HAR if an entry for this client and server exist in the cache. */ protected void captureConnectTiming() { Log.e("InnerHandle", "captureConnectTiming " + harEntry.getId()); HttpConnectTiming httpConnectTiming = HttpConnectHarCaptureFilter.consumeConnectTimingForConnection(clientAddress); if (httpConnectTiming != null) { harEntry.getTimings().setSsl(httpConnectTiming.getSslHandshakeTimeNanos(), TimeUnit.NANOSECONDS); harEntry.getTimings().setConnect(httpConnectTiming.getConnectTimeNanos(), TimeUnit.NANOSECONDS); harEntry.getTimings().setBlocked(httpConnectTiming.getBlockedTimeNanos(), TimeUnit.NANOSECONDS); harEntry.getTimings().setDns(httpConnectTiming.getDnsTimeNanos(), TimeUnit.NANOSECONDS); } } /** * Populates the serverIpAddress field of the harEntry using the internal hostname->IP address cache. * * @param httpRequest HTTP request to take the hostname from */ protected void populateAddressFromCache(HttpRequest httpRequest) { Log.e("InnerHandle", "populateAddressFromCache " + harEntry.getId()); String serverHost = getHost(httpRequest); if (serverHost != null && !serverHost.isEmpty()) { String resolvedAddress = ResolvedHostnameCacheFilter.getPreviouslyResolvedAddressForHost(serverHost); if (resolvedAddress != null) { harEntry.setServerIPAddress(resolvedAddress); } else { // the resolvedAddress may be null if the ResolvedHostnameCacheFilter has expired the entry (which is unlikely), // or in the far more common case that the proxy is using a chained proxy to connect to connect to the // remote host. since the chained proxy handles IP address resolution, the IP address in the HAR must be blank. log.trace("Unable to find cached IP address for host: {}. IP address in HAR entry will be blank.", serverHost); } } else { log.warn("Unable to identify host from request uri: {}", httpRequest.getUri()); } } @Override public InetSocketAddress proxyToServerResolutionStarted(String resolvingServerHostAndPort) { Log.e("Capture", "proxyToServerResolutionStarted " + harEntry.getId()); dnsResolutionStartedNanos = System.nanoTime(); // resolution started means the connection is no longer queued, so populate 'blocked' time if (connectionQueuedNanos > 0L) { harEntry.getTimings().setBlocked(dnsResolutionStartedNanos - connectionQueuedNanos, TimeUnit.NANOSECONDS); } else { harEntry.getTimings().setBlocked(0L, TimeUnit.NANOSECONDS); } return null; } @Override public void proxyToServerResolutionFailed(String hostAndPort) { Log.e("Capture", "proxyToServerResolutionFailed " + harEntry.getId()); HarResponse response = HarCaptureUtil.createHarResponseForFailure(); harEntry.setResponse(response); response.setError(HarCaptureUtil.getResolutionFailedErrorMessage(hostAndPort)); // record the amount of time we attempted to resolve the hostname in the HarTimings object if (dnsResolutionStartedNanos > 0L) { harEntry.getTimings().setDns(System.nanoTime() - dnsResolutionStartedNanos, TimeUnit.NANOSECONDS); } } @Override public void proxyToServerResolutionSucceeded(String serverHostAndPort, InetSocketAddress resolvedRemoteAddress) { Log.e("Capture", "proxyToServerResolutionSucceeded " + harEntry.getId()); long dnsResolutionFinishedNanos = System.nanoTime(); if (dnsResolutionStartedNanos > 0L) { harEntry.getTimings().setDns(dnsResolutionFinishedNanos - dnsResolutionStartedNanos, TimeUnit.NANOSECONDS); } else { harEntry.getTimings().setDns(0L, TimeUnit.NANOSECONDS); } // the address *should* always be resolved at this point InetAddress resolvedAddress = resolvedRemoteAddress.getAddress(); if (resolvedAddress != null) { addressResolved = true; harEntry.setServerIPAddress(resolvedAddress.getHostAddress()); } } @Override public void proxyToServerConnectionQueued() { Log.e("Capture", "proxyToServerConnectionQueued " + harEntry.getId()); this.connectionQueuedNanos = System.nanoTime(); } @Override public void proxyToServerConnectionStarted() { Log.e("Capture", "proxyToServerConnectionStarted " + harEntry.getId()); this.connectionStartedNanos = System.nanoTime(); } @Override public void proxyToServerConnectionFailed() { Log.e("Capture", "proxyToServerConnectionFailed " + harEntry.getId()); HarResponse response = HarCaptureUtil.createHarResponseForFailure(); harEntry.setResponse(response); response.setError(HarCaptureUtil.getConnectionFailedErrorMessage()); // record the amount of time we attempted to connect in the HarTimings object if (connectionStartedNanos > 0L) { harEntry.getTimings().setConnect(System.nanoTime() - connectionStartedNanos, TimeUnit.NANOSECONDS); } proxyManager.httpExchangeFailed(response.getError()); } @Override public void proxyToServerConnectionSucceeded(ChannelHandlerContext serverCtx) { Log.e("Capture", "proxyToServerConnectionSucceeded " + harEntry.getId()); long connectionSucceededTimeNanos = System.nanoTime(); // make sure the previous timestamp was captured, to avoid setting an absurd value in the har (see serverToProxyResponseReceiving()) if (connectionStartedNanos > 0L) { harEntry.getTimings().setConnect(connectionSucceededTimeNanos - connectionStartedNanos, TimeUnit.NANOSECONDS); } else { harEntry.getTimings().setConnect(0L, TimeUnit.NANOSECONDS); } } @Override public void proxyToServerRequestSending() { Log.e("Capture", "proxyToServerRequestSending " + harEntry.getId()); this.sendStartedNanos = System.nanoTime(); // if the hostname was not resolved (and thus the IP address populated in the har) during this request, populate the IP address from the cache if (!addressResolved) { populateAddressFromCache(capturedOriginalRequest); } proxyManager.requestWillBeSent(harRequest); } @Override public void proxyToServerRequestSent() { Log.e("Capture", "proxyToServerRequestSent " + harEntry.getId()); this.sendFinishedNanos = System.nanoTime(); // make sure the previous timestamp was captured, to avoid setting an absurd value in the har (see serverToProxyResponseReceiving()) if (sendStartedNanos > 0L) { harEntry.getTimings().setSend(sendFinishedNanos - sendStartedNanos, TimeUnit.NANOSECONDS); } else { harEntry.getTimings().setSend(0L, TimeUnit.NANOSECONDS); } proxyManager.dataSent(harRequest); } @Override public void serverToProxyResponseReceiving() { Log.e("Capture", "serverToProxyResponseReceiving " + harEntry.getId()); this.responseReceiveStartedNanos = System.nanoTime(); // started to receive response, so populate the 'wait' time. if we started receiving a response from the server before we finished // sending (for example, the server replied with a 404 while we were uploading a large file), there was no wait time, so // make sure the wait is set to 0. if (sendFinishedNanos > 0L && sendFinishedNanos < responseReceiveStartedNanos) { harEntry.getTimings().setWait(responseReceiveStartedNanos - sendFinishedNanos, TimeUnit.NANOSECONDS); } else { harEntry.getTimings().setWait(0L, TimeUnit.NANOSECONDS); } } @Override public void serverToProxyResponseReceived() { Log.e("Capture", "serverToProxyResponseReceived " + harEntry.getId()); long responseReceivedNanos = System.nanoTime(); // like the wait time, the receive time requires that the serverToProxyResponseReceiving() method be called before this method is invoked. // typically that should happen, but it has been reported (https://github.com/lightbody/browsermob-proxy/issues/288) that it // sometimes does not. therefore, to be safe, make sure responseReceiveStartedNanos is populated before setting the receive time. if (responseReceiveStartedNanos > 0L) { harEntry.getTimings().setReceive(responseReceivedNanos - responseReceiveStartedNanos, TimeUnit.NANOSECONDS); } else { harEntry.getTimings().setReceive(0L, TimeUnit.NANOSECONDS); } proxyManager.interpretResponseStream(harResponse); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/HttpConnectHarCaptureFilter.java ================================================ package net.lightbody.bmp.filters; import com.google.common.cache.CacheBuilder; import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarRequest; import net.lightbody.bmp.core.har.HarResponse; import net.lightbody.bmp.core.har.HarTimings; import net.lightbody.bmp.filters.support.HttpConnectTiming; import net.lightbody.bmp.filters.util.HarCaptureUtil; import net.lightbody.bmp.util.HttpUtil; import org.littleshoot.proxy.impl.ProxyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Date; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * This filter captures HAR data for HTTP CONNECT requests. CONNECTs are "meta" requests that must be made before HTTPS * requests, but are not populated as separate requests in the HAR. Most information from HTTP CONNECTs (such as SSL * handshake time, dns resolution time, etc.) is populated in the HAR entry for the first "true" request following the * CONNECT. This filter captures the timing-related information and makes it available to subsequent filters through * static methods. This filter also handles HTTP CONNECT errors and creates HAR entries for those errors, since there * would otherwise not be any record in the HAR of the error (if the CONNECT fails, there will be no subsequent "real" * request in which to record the error). * */ public class HttpConnectHarCaptureFilter extends HttpsAwareFiltersAdapter implements ModifiedRequestAwareFilter { private static final Logger log = LoggerFactory.getLogger(HttpConnectHarCaptureFilter.class); /** * The currently active HAR at the time the current request is received. */ private final Har har; /** * The currently active page ref at the time the current request is received. */ private final String currentPageRef; /** * The time this CONNECT began. Used to populate the HAR entry in case of failure. */ private volatile Date requestStartTime; /** * True if this filter instance processed a {@link #proxyToServerResolutionSucceeded(String, InetSocketAddress)} call, indicating * that the hostname was resolved and populated in the HAR (if this is not a CONNECT). */ // private volatile boolean addressResolved = false; private volatile InetAddress resolvedAddress; /** * Populated by proxyToServerResolutionStarted when DNS resolution starts. If any previous filters already resolved the address, their resolution time * will not be included in this time. See {@link HarCaptureFilter#dnsResolutionStartedNanos}. */ private volatile long dnsResolutionStartedNanos; private volatile long dnsResolutionFinishedNanos; private volatile long connectionQueuedNanos; private volatile long connectionStartedNanos; private volatile long connectionSucceededTimeNanos; private volatile long sendStartedNanos; private volatile long sendFinishedNanos; private volatile long responseReceiveStartedNanos; private volatile long sslHandshakeStartedNanos; /** * The address of the client making the request. Captured in the constructor and used when calculating and capturing ssl handshake and connect * timing information for SSL connections. */ private final InetSocketAddress clientAddress; /** * Stores HTTP CONNECT timing information for this request, if it is an HTTP CONNECT. */ private final HttpConnectTiming httpConnectTiming; /** * The maximum amount of time to save timing information between an HTTP CONNECT and the subsequent HTTP request. Typically this is done * immediately, but if for some reason it is not (e.g. due to a client crash or dropped connection), the timing information will be * kept for this long before being evicted to prevent a memory leak. If a subsequent request does come through after eviction, it will still * be recorded, but the timing information will not be populated in the HAR. */ private static final int HTTP_CONNECT_TIMING_EVICTION_SECONDS = 60; /** * Concurrency of the httpConnectTiming map. Should be approximately equal to the maximum number of simultaneous connection * attempts (but not necessarily simultaneous connections). A lower value will inhibit performance. * TODO: tune this value for a large number of concurrent requests. develop a non-cache-based mechanism of passing ssl timings to subsequent requests. */ private static final int HTTP_CONNECT_TIMING_CONCURRENCY_LEVEL = 50; /** * Stores SSL connection timing information from HTTP CONNNECT requests. This timing information is stored in the first HTTP request * after the CONNECT, not in the CONNECT itself, so it needs to be stored across requests. * * This is the only state stored across multiple requests. */ private static final ConcurrentMap httpConnectTimes = CacheBuilder.newBuilder() .expireAfterWrite(HTTP_CONNECT_TIMING_EVICTION_SECONDS, TimeUnit.SECONDS) .concurrencyLevel(HTTP_CONNECT_TIMING_CONCURRENCY_LEVEL) .build() .asMap(); private volatile HttpRequest modifiedHttpRequest; public HttpConnectHarCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Har har, String currentPageRef) { super(originalRequest, ctx); if (har == null) { throw new IllegalStateException("Attempted har capture when har is null"); } if (!ProxyUtils.isCONNECT(originalRequest)) { throw new IllegalStateException("Attempted HTTP CONNECT har capture on non-HTTP CONNECT request"); } this.har = har; this.currentPageRef = currentPageRef; this.clientAddress = (InetSocketAddress) ctx.channel().remoteAddress(); // create and cache an HTTP CONNECT timing object to capture timing-related information this.httpConnectTiming = new HttpConnectTiming(); httpConnectTimes.put(clientAddress, httpConnectTiming); } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { // store the CONNECT start time in case of failure, so we can populate the HarEntry with it requestStartTime = new Date(); } return null; } @Override public void proxyToServerResolutionFailed(String hostAndPort) { // since this is a CONNECT, which is not handled by the HarCaptureFilter, we need to create and populate the // entire HarEntry and add it to this har. HarEntry harEntry = createHarEntryForFailedCONNECT(HarCaptureUtil.getResolutionFailedErrorMessage(hostAndPort)); har.getLog().addEntry(harEntry); // record the amount of time we attempted to resolve the hostname in the HarTimings object if (dnsResolutionStartedNanos > 0L) { harEntry.getTimings().setDns(System.nanoTime() - dnsResolutionStartedNanos, TimeUnit.NANOSECONDS); } httpConnectTimes.remove(clientAddress); } @Override public void proxyToServerConnectionFailed() { // since this is a CONNECT, which is not handled by the HarCaptureFilter, we need to create and populate the // entire HarEntry and add it to this har. HarEntry harEntry = createHarEntryForFailedCONNECT(HarCaptureUtil.getConnectionFailedErrorMessage()); har.getLog().addEntry(harEntry); // record the amount of time we attempted to connect in the HarTimings object if (connectionStartedNanos > 0L) { harEntry.getTimings().setConnect(System.nanoTime() - connectionStartedNanos, TimeUnit.NANOSECONDS); } httpConnectTimes.remove(clientAddress); } @Override public void proxyToServerConnectionSucceeded(ChannelHandlerContext serverCtx) { this.connectionSucceededTimeNanos = System.nanoTime(); if (connectionStartedNanos > 0L) { httpConnectTiming.setConnectTimeNanos(connectionSucceededTimeNanos - connectionStartedNanos); } else { httpConnectTiming.setConnectTimeNanos(0L); } if (sslHandshakeStartedNanos > 0L) { httpConnectTiming.setSslHandshakeTimeNanos(connectionSucceededTimeNanos - sslHandshakeStartedNanos); } else { httpConnectTiming.setSslHandshakeTimeNanos(0L); } } @Override public void proxyToServerConnectionSSLHandshakeStarted() { this.sslHandshakeStartedNanos = System.nanoTime(); } @Override public void serverToProxyResponseTimedOut() { HarEntry harEntry = createHarEntryForFailedCONNECT(HarCaptureUtil.getResponseTimedOutErrorMessage()); har.getLog().addEntry(harEntry); // include this timeout time in the HarTimings object long timeoutTimestampNanos = System.nanoTime(); // if the proxy started to send the request but has not yet finished, we are currently "sending" if (sendStartedNanos > 0L && sendFinishedNanos == 0L) { harEntry.getTimings().setSend(timeoutTimestampNanos - sendStartedNanos, TimeUnit.NANOSECONDS); } // if the entire request was sent but the proxy has not begun receiving the response, we are currently "waiting" else if (sendFinishedNanos > 0L && responseReceiveStartedNanos == 0L) { harEntry.getTimings().setWait(timeoutTimestampNanos - sendFinishedNanos, TimeUnit.NANOSECONDS); } // if the proxy has already begun to receive the response, we are currenting "receiving" else if (responseReceiveStartedNanos > 0L) { harEntry.getTimings().setReceive(timeoutTimestampNanos - responseReceiveStartedNanos, TimeUnit.NANOSECONDS); } } @Override public void proxyToServerConnectionQueued() { this.connectionQueuedNanos = System.nanoTime(); } @Override public InetSocketAddress proxyToServerResolutionStarted(String resolvingServerHostAndPort) { dnsResolutionStartedNanos = System.nanoTime(); if (connectionQueuedNanos > 0L) { httpConnectTiming.setBlockedTimeNanos(dnsResolutionStartedNanos - connectionQueuedNanos); } else { httpConnectTiming.setBlockedTimeNanos(0L); } return null; } @Override public void proxyToServerResolutionSucceeded(String serverHostAndPort, InetSocketAddress resolvedRemoteAddress) { this.dnsResolutionFinishedNanos = System.nanoTime(); if (dnsResolutionStartedNanos > 0L) { httpConnectTiming.setDnsTimeNanos(dnsResolutionFinishedNanos - dnsResolutionStartedNanos); } else { httpConnectTiming.setDnsTimeNanos(0L); } // the address *should* always be resolved at this point this.resolvedAddress = resolvedRemoteAddress.getAddress(); } @Override public void proxyToServerConnectionStarted() { this.connectionStartedNanos = System.nanoTime(); } @Override public void proxyToServerRequestSending() { this.sendStartedNanos = System.nanoTime(); } @Override public void proxyToServerRequestSent() { this.sendFinishedNanos = System.nanoTime(); } @Override public void serverToProxyResponseReceiving() { this.responseReceiveStartedNanos = System.nanoTime(); } /** * Populates timing information in the specified harEntry for failed rquests. Populates as much timing information * as possible, up to the point of failure. * * @param harEntry HAR entry to populate timing information in */ private void populateTimingsForFailedCONNECT(HarEntry harEntry) { HarTimings timings = harEntry.getTimings(); if (connectionQueuedNanos > 0L && dnsResolutionStartedNanos > 0L) { timings.setBlocked(dnsResolutionStartedNanos - connectionQueuedNanos, TimeUnit.NANOSECONDS); } if (dnsResolutionStartedNanos > 0L && dnsResolutionFinishedNanos > 0L) { timings.setDns(dnsResolutionFinishedNanos - dnsResolutionStartedNanos, TimeUnit.NANOSECONDS); } if (connectionStartedNanos > 0L && connectionSucceededTimeNanos > 0L) { timings.setConnect(connectionSucceededTimeNanos - connectionStartedNanos, TimeUnit.NANOSECONDS); if (sslHandshakeStartedNanos > 0L) { timings.setSsl(connectionSucceededTimeNanos - this.sslHandshakeStartedNanos, TimeUnit.NANOSECONDS); } } if (sendStartedNanos > 0L && sendFinishedNanos >= 0L) { timings.setSend(sendFinishedNanos - sendStartedNanos, TimeUnit.NANOSECONDS); } if (sendFinishedNanos > 0L && responseReceiveStartedNanos >= 0L) { timings.setWait(responseReceiveStartedNanos - sendFinishedNanos, TimeUnit.NANOSECONDS); } // since this method is for HTTP CONNECT failures only, we can't populate a "received" time, since that would // require the CONNECT to be successful, in which case this method wouldn't be called. } /** * Creates a {@link HarEntry} for a failed CONNECT request. Initializes and populates the entry, including the * {@link HarRequest}, {@link HarResponse}, and {@link HarTimings}. (Note: only successful timing information is * populated in the timings object; the calling method must populate the timing information for the final, failed * step. For example, if DNS resolution failed, this method will populate the network 'blocked' time, but not the DNS * time.) Populates the specified errorMessage in the {@link HarResponse}'s error field. * * @param errorMessage error message to place in the har response * @return a new HAR entry */ private HarEntry createHarEntryForFailedCONNECT(String errorMessage) { HarEntry harEntry = new HarEntry(currentPageRef); harEntry.setStartedDateTime(requestStartTime); HarRequest request = createRequestForFailedConnect(originalRequest); harEntry.setRequest(request); HarResponse response = HarCaptureUtil.createHarResponseForFailure(); harEntry.setResponse(response); response.setError(errorMessage); populateTimingsForFailedCONNECT(harEntry); populateServerIpAddress(harEntry); return harEntry; } private void populateServerIpAddress(HarEntry harEntry) { // populate the server IP address if it was resolved as part of this request. otherwise, populate the IP address from the cache. if (resolvedAddress != null) { harEntry.setServerIPAddress(resolvedAddress.getHostAddress()); } else { String serverHost = HttpUtil.getHostFromRequest(modifiedHttpRequest); if (serverHost != null && !serverHost.isEmpty()) { String resolvedAddress = ResolvedHostnameCacheFilter.getPreviouslyResolvedAddressForHost(serverHost); if (resolvedAddress != null) { harEntry.setServerIPAddress(resolvedAddress); } else { // the resolvedAddress may be null if the ResolvedHostnameCacheFilter has expired the entry (which is unlikely), // or in the far more common case that the proxy is using a chained proxy to connect to connect to the // remote host. since the chained proxy handles IP address resolution, the IP address in the HAR must be blank. log.trace("Unable to find cached IP address for host: {}. IP address in HAR entry will be blank.", serverHost); } } else { log.warn("Unable to identify host from request uri: {}", modifiedHttpRequest.getUri()); } } } /** * Creates a new {@link HarRequest} object for this failed HTTP CONNECT. Does not populate fields within the request, * such as the error message. * * @param httpConnectRequest the HTTP CONNECT request that failed * @return a new HAR request object */ private HarRequest createRequestForFailedConnect(HttpRequest httpConnectRequest) { String url = getFullUrl(httpConnectRequest); return new HarRequest(httpConnectRequest.getMethod().toString(), url, httpConnectRequest.getProtocolVersion().text()); } /** * Retrieves and removes (thus "consumes") the SSL timing information from the connection cache for the specified address. * * @param clientAddress the address of the client connection that established the HTTP tunnel * @return the timing information for the tunnel previously established from the clientAddress */ public static HttpConnectTiming consumeConnectTimingForConnection(InetSocketAddress clientAddress) { return httpConnectTimes.remove(clientAddress); } @Override public void setModifiedHttpRequest(HttpRequest modifiedHttpRequest) { this.modifiedHttpRequest = modifiedHttpRequest; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/HttpsAwareFiltersAdapter.java ================================================ package net.lightbody.bmp.filters; import com.google.common.net.HostAndPort; import net.lightbody.bmp.util.BrowserMobHttpUtil; import net.lightbody.bmp.util.HttpUtil; import org.littleshoot.proxy.HttpFiltersAdapter; import org.littleshoot.proxy.impl.ProxyUtils; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.Attribute; import io.netty.util.AttributeKey; /** * The HttpsAwareFiltersAdapter exposes the original host and the "real" host (after filter modifications) to filters for HTTPS * requets. HTTPS requests do not normally contain the host in the URI, and the Host header may be missing or spoofed. *

* Note: The {@link #getHttpsRequestHostAndPort()} and {@link #getHttpsOriginalRequestHostAndPort()} methods can only be * called when the request is an HTTPS request. Otherwise they will throw an IllegalStateException. */ public class HttpsAwareFiltersAdapter extends HttpFiltersAdapter { public static final String IS_HTTPS_ATTRIBUTE_NAME = "isHttps"; public static final String HOST_ATTRIBUTE_NAME = "host"; public static final String ORIGINAL_HOST_ATTRIBUTE_NAME = "originalHost"; public HttpsAwareFiltersAdapter(HttpRequest originalRequest, ChannelHandlerContext ctx) { super(originalRequest, ctx); } /** * Returns true if this is an HTTPS request. * * @return true if https, false if http */ public boolean isHttps() { Attribute isHttpsAttr = ctx.attr(AttributeKey.valueOf(IS_HTTPS_ATTRIBUTE_NAME)); Boolean isHttps = isHttpsAttr.get(); if (isHttps == null) { return false; } else { return isHttps; } } /** * Returns the full, absolute URL of the specified request for both HTTP and HTTPS URLs. The request may reflect * modifications from this or other filters. This filter instance must be currently handling the specified request; * otherwise the results are undefined. * * @param modifiedRequest a possibly-modified version of the request currently being processed * @return the full URL of the request, including scheme, host, port, path, and query parameters */ public String getFullUrl(HttpRequest modifiedRequest) { // special case: for HTTPS requests, the full URL is scheme (https://) + the URI of this request if (ProxyUtils.isCONNECT(modifiedRequest)) { // CONNECT requests contain the default port, even if it isn't specified on the request. String hostNoDefaultPort = BrowserMobHttpUtil.removeMatchingPort(modifiedRequest.getUri(), 443); return "https://" + hostNoDefaultPort; } // To get the full URL, we need to retrieve the Scheme, Host + Port, Path, and Query Params from the request. // If the request URI starts with http:// or https://, it is already a full URL and can be returned directly. if (HttpUtil.startsWithHttpOrHttps(modifiedRequest.getUri())) { return modifiedRequest.getUri(); } // The URI did not include the scheme and host, so examine the request to obtain them: // Scheme: the scheme (HTTP/HTTPS) are based on the type of connection, obtained from isHttps() // Host and Port: available for HTTP and HTTPS requests using the getHostAndPort() helper method. // Path + Query Params: since the request URI doesn't start with the scheme, we can safely assume that the URI // contains only the path and query params. String hostAndPort = getHostAndPort(modifiedRequest); String path = modifiedRequest.getUri(); String url; if (isHttps()) { url = "https://" + hostAndPort + path; } else { url = "http://" + hostAndPort + path; } return url; } /** * Returns the full, absolute URL of the original request from the client for both HTTP and HTTPS URLs. The URL * will not reflect modifications from this or other filters. * * @return the full URL of the original request, including scheme, host, port, path, and query parameters */ public String getOriginalUrl() { return getFullUrl(originalRequest); } /** * Returns the hostname (but not the port) the specified request for both HTTP and HTTPS requests. The request may reflect * modifications from this or other filters. This filter instance must be currently handling the specified request; * otherwise the results are undefined. * * @param modifiedRequest a possibly-modified version of the request currently being processed * @return hostname of the specified request, without the port */ public String getHost(HttpRequest modifiedRequest) { String serverHost; if (isHttps()) { HostAndPort hostAndPort = HostAndPort.fromString(getHttpsRequestHostAndPort()); serverHost = hostAndPort.getHostText(); } else { serverHost = HttpUtil.getHostFromRequest(modifiedRequest); } return serverHost; } /** * Returns the host and port of the specified request for both HTTP and HTTPS requests. The request may reflect * modifications from this or other filters. This filter instance must be currently handling the specified request; * otherwise the results are undefined. * * @param modifiedRequest a possibly-modified version of the request currently being processed * @return host and port of the specified request */ public String getHostAndPort(HttpRequest modifiedRequest) { // For HTTP requests, the host and port can be read from the request itself using the URI and/or // Host header. for HTTPS requests, the host and port are not available in the request. by using the // getHttpsRequestHostAndPort() helper method, we can retrieve the host and port for HTTPS requests. if (isHttps()) { return getHttpsRequestHostAndPort(); } else { return HttpUtil.getHostAndPortFromRequest(modifiedRequest); } } /** * Returns the host and port of this HTTPS request, including any modifications by other filters. * * @return host and port of this HTTPS request * @throws IllegalStateException if this is not an HTTPS request */ private String getHttpsRequestHostAndPort() throws IllegalStateException { if (!isHttps()) { throw new IllegalStateException("Request is not HTTPS. Cannot get host and port on non-HTTPS request using this method."); } Attribute hostnameAttr = ctx.attr(AttributeKey.valueOf(HOST_ATTRIBUTE_NAME)); return hostnameAttr.get(); } /** * Returns the original host and port of this HTTPS request, as sent by the client. Does not reflect any modifications * by other filters. * TODO: evaluate this (unused) method and its capture mechanism in HttpsOriginalHostCaptureFilter; remove if not useful. * * @return host and port of this HTTPS request * @throws IllegalStateException if this is not an HTTPS request */ private String getHttpsOriginalRequestHostAndPort() throws IllegalStateException { if (!isHttps()) { throw new IllegalStateException("Request is not HTTPS. Cannot get original host and port on non-HTTPS request using this method."); } Attribute hostnameAttr = ctx.attr(AttributeKey.valueOf(ORIGINAL_HOST_ATTRIBUTE_NAME)); return hostnameAttr.get(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/HttpsHostCaptureFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.BrowserMobHttpUtil; import org.littleshoot.proxy.HttpFiltersAdapter; import org.littleshoot.proxy.impl.ProxyUtils; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.Attribute; import io.netty.util.AttributeKey; /** * Captures the host for HTTPS requests and stores the value in the ChannelHandlerContext for use by {@link HttpsAwareFiltersAdapter} * filters. This filter reads the host from the HttpRequest during the HTTP CONNECT call, and therefore MUST be invoked * after any other filters which modify the host. * Note: If the request uses the default HTTPS port (443), it will be removed from the hostname captured by this filter. */ public class HttpsHostCaptureFilter extends HttpFiltersAdapter { public HttpsHostCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx) { super(originalRequest, ctx); } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; if (ProxyUtils.isCONNECT(httpRequest)) { Attribute hostname = ctx.attr(AttributeKey.valueOf(HttpsAwareFiltersAdapter.HOST_ATTRIBUTE_NAME)); String hostAndPort = httpRequest.getUri(); // CONNECT requests contain the port, even when using the default port. a sensible default is to remove the // default port, since in most cases it is not explicitly specified and its presence (in a HAR file, for example) // would be unexpected. String hostNoDefaultPort = BrowserMobHttpUtil.removeMatchingPort(hostAndPort, 443); hostname.set(hostNoDefaultPort); } } return null; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/HttpsOriginalHostCaptureFilter.java ================================================ package net.lightbody.bmp.filters; import org.littleshoot.proxy.impl.ProxyUtils; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.Attribute; import io.netty.util.AttributeKey; /** * Captures the original host for HTTPS requests and stores the value in the ChannelHandlerContext for use by {@link HttpsAwareFiltersAdapter} * filters. This filter sets the isHttps attribute on the ChannelHandlerContext during the HTTP CONNECT and therefore MUST be invoked before * any other filters calling any of the methods in {@link HttpsAwareFiltersAdapter}. * This filter extends {@link HttpsHostCaptureFilter} and so also sets the host attribute on the channel for use by filters * that modify the original host during the CONNECT. If the hostname is modified by filters, it will be overwritten when the {@link HttpsHostCaptureFilter} * is processed later in the filter chain. */ public class HttpsOriginalHostCaptureFilter extends HttpsHostCaptureFilter { public HttpsOriginalHostCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx) { super(originalRequest, ctx); // if this is an HTTP CONNECT, set the isHttps attribute on the ChannelHandlerConect and capture the hostname from the original request. // capturing the original host (and the remapped/modified host in clientToProxyRequest() below) guarantees that we will // have the "true" host, rather than relying on the Host header in subsequent requests (which may be absent or spoofed by malicious clients). if (ProxyUtils.isCONNECT(originalRequest)) { Attribute originalHostAttr = ctx.attr(AttributeKey.valueOf(HttpsAwareFiltersAdapter.ORIGINAL_HOST_ATTRIBUTE_NAME)); String hostAndPort = originalRequest.getUri(); originalHostAttr.set(hostAndPort); Attribute isHttpsAttr = ctx.attr(AttributeKey.valueOf(HttpsAwareFiltersAdapter.IS_HTTPS_ATTRIBUTE_NAME)); isHttpsAttr.set(true); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/LatencyFilter.java ================================================ package net.lightbody.bmp.filters; import org.littleshoot.proxy.HttpFiltersAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * Adds latency to a response before sending it to the client. This filter always adds the specified latency, even if the latency * between the proxy and the remote server already exceeds this value. */ public class LatencyFilter extends HttpFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(HttpFiltersAdapter.class); private final int latencyMs; public LatencyFilter(HttpRequest originalRequest, int latencyMs) { super(originalRequest); this.latencyMs = latencyMs; } @Override public HttpObject proxyToClientResponse(HttpObject httpObject) { if (httpObject instanceof HttpResponse) { if (latencyMs > 0) { try { TimeUnit.MILLISECONDS.sleep(latencyMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.warn("Interrupted while adding latency to response", e); } } } return super.proxyToClientResponse(httpObject); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/ModifiedRequestAwareFilter.java ================================================ package net.lightbody.bmp.filters; import io.netty.handler.codec.http.HttpRequest; /** * Indicates that a filter wishes to capture the final HttpRequest that is sent to the server, reflecting all * modifications from request filters. {@link BrowserMobHttpFilterChain#clientToProxyRequest(io.netty.handler.codec.http.HttpObject)} * will invoke the {@link #setModifiedHttpRequest(HttpRequest)} method after all filters have processed the initial * {@link HttpRequest} object. */ public interface ModifiedRequestAwareFilter { /** * Notifies implementing classes of the modified HttpRequest that will be sent to the server, reflecting all * modifications from filters. * * @param modifiedHttpRequest the modified HttpRequest sent to the server */ void setModifiedHttpRequest(HttpRequest modifiedHttpRequest); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/RegisterRequestFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.proxy.ActivityMonitor; import org.littleshoot.proxy.HttpFiltersAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * Registers this request with the {@link net.lightbody.bmp.proxy.ActivityMonitor} when the HttpRequest is received from the client. */ public class RegisterRequestFilter extends HttpFiltersAdapter { private final ActivityMonitor activityMonitor; public RegisterRequestFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, ActivityMonitor activityMonitor) { super(originalRequest, ctx); this.activityMonitor = activityMonitor; } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { activityMonitor.requestStarted(); } return super.clientToProxyRequest(httpObject); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/RequestFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * A functional interface to simplify modification and manipulation of requests. */ public interface RequestFilter { /** * Implement this method to filter an HTTP request. The HTTP method, URI, headers, etc. are available in the {@code request} parameter, * while the contents of the message are available in the {@code contents} parameter. The request can be modified directly, while the * contents may be modified using the {@link HttpMessageContents#setTextContents(String)} or {@link HttpMessageContents#setBinaryContents(byte[])} * methods. The request can be "short-circuited" by returning a non-null value. * * @param request The request object, including method, URI, headers, etc. Modifications to the request object will be reflected in the request sent to the server. * @param contents The request contents. * @param messageInfo Additional information relating to the HTTP message. * @return if the return value is non-null, the proxy will suppress the request and send the specified response to the client immediately */ HttpResponse filterRequest(HttpRequest request, HttpMessageContents contents, HttpMessageInfo messageInfo); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/RequestFilterAdapter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; import org.littleshoot.proxy.HttpFilters; import org.littleshoot.proxy.HttpFiltersSourceAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * A filter adapter for {@link RequestFilter} implementations. Executes the filter when the {@link HttpFilters#clientToProxyRequest(HttpObject)} * method is invoked. */ public class RequestFilterAdapter extends HttpsAwareFiltersAdapter { private final RequestFilter requestFilter; public RequestFilterAdapter(HttpRequest originalRequest, ChannelHandlerContext ctx, RequestFilter requestFilter) { super(originalRequest, ctx); this.requestFilter = requestFilter; } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { // only filter when the original HttpRequest comes through. the RequestFilterAdapter is not designed to filter // any subsequent HttpContents. if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; HttpMessageContents contents; if (httpObject instanceof FullHttpMessage) { FullHttpMessage httpContent = (FullHttpMessage) httpObject; contents = new HttpMessageContents(httpContent); } else { // the HTTP object is not a FullHttpMessage, which means that message contents are not available on this request and cannot be modified. contents = null; } HttpMessageInfo messageInfo = new HttpMessageInfo(originalRequest, ctx, isHttps(), getFullUrl(httpRequest), getOriginalUrl()); HttpResponse response = requestFilter.filterRequest(httpRequest, contents, messageInfo); if (response != null) { return response; } } return null; } /** * A {@link HttpFiltersSourceAdapter} for {@link RequestFilterAdapter}s. By default, this FilterSource enables HTTP message aggregation * and sets a maximum request buffer size of 2 MiB. */ public static class FilterSource extends HttpFiltersSourceAdapter { private static final int DEFAULT_MAXIMUM_REQUEST_BUFFER_SIZE = 2097152; private final RequestFilter filter; private final int maximumRequestBufferSizeInBytes; /** * Creates a new filter source that will invoke the specified filter and uses the {@link #DEFAULT_MAXIMUM_REQUEST_BUFFER_SIZE} as * the maximum buffer size. * * @param filter RequestFilter to invoke */ public FilterSource(RequestFilter filter) { this.filter = filter; this.maximumRequestBufferSizeInBytes = DEFAULT_MAXIMUM_REQUEST_BUFFER_SIZE; } /** * Creates a new filter source that will invoke the specified filter and uses the maximumRequestBufferSizeInBytes as the maximum * buffer size. Set maximumRequestBufferSizeInBytes to 0 to disable aggregation. If message aggregation is disabled, * the {@link HttpMessageContents} will not be available for modification. (Note: HTTP message aggregation will * be enabled if any filter has a maximum request or response buffer size greater than 0. See * {@link org.littleshoot.proxy.HttpFiltersSource#getMaximumRequestBufferSizeInBytes()} for details.) * * @param filter RequestFilter to invoke * @param maximumRequestBufferSizeInBytes maximum buffer size when aggregating Requests for filtering */ public FilterSource(RequestFilter filter, int maximumRequestBufferSizeInBytes) { this.filter = filter; this.maximumRequestBufferSizeInBytes = maximumRequestBufferSizeInBytes; } @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new RequestFilterAdapter(originalRequest, ctx, filter); } @Override public int getMaximumRequestBufferSizeInBytes() { return maximumRequestBufferSizeInBytes; } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/ResolvedHostnameCacheFilter.java ================================================ package net.lightbody.bmp.filters; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.net.HostAndPort; import org.littleshoot.proxy.HttpFiltersAdapter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; /** * Caches hostname resolutions reported by the {@link org.littleshoot.proxy.HttpFilters#proxyToServerResolutionSucceeded(String, InetSocketAddress)} * filter method. Allows access to the resolved IP address on subsequent requests, when the address is not re-resolved because * the connection has already been established. */ public class ResolvedHostnameCacheFilter extends HttpFiltersAdapter { /** * The maximum amount of time to save host name resolution information. This is done in order to populate the server IP address field in the * har. Unfortunately there is not currently any way to determine the remote IP address of a keep-alive connection in a filter, so caching the * resolved hostnames gives a generally-reasonable best guess. */ private static final int RESOLVED_ADDRESSES_EVICTION_SECONDS = 600; /** * Concurrency of the resolvedAddresses map. Should be approximately equal to the maximum number of simultaneous connection * attempts (but not necessarily simultaneous connections). A lower value will inhibit performance. */ private static final int RESOLVED_ADDRESSES_CONCURRENCY_LEVEL = 50; /** * A {@code Map} that provides a reasonable estimate of the upstream server's IP address for keep-alive connections. * The expiration time is renewed after each access, rather than after each write, so if the connection is consistently kept alive and used, * the cached IP address will not be evicted. */ private static final Cache resolvedAddresses = CacheBuilder.newBuilder() .expireAfterAccess(RESOLVED_ADDRESSES_EVICTION_SECONDS, TimeUnit.SECONDS) .concurrencyLevel(RESOLVED_ADDRESSES_CONCURRENCY_LEVEL) .build(); public ResolvedHostnameCacheFilter(HttpRequest originalRequest, ChannelHandlerContext ctx) { super(originalRequest, ctx); } @Override public void proxyToServerResolutionSucceeded(String serverHostAndPort, InetSocketAddress resolvedRemoteAddress) { // the address *should* always be resolved at this point InetAddress resolvedAddress = resolvedRemoteAddress.getAddress(); if (resolvedAddress != null) { // place the resolved host into the hostname cache, so subsequent requests will be able to identify the IP address HostAndPort parsedHostAndPort = HostAndPort.fromString(serverHostAndPort); String host = parsedHostAndPort.getHostText(); if (host != null && !host.isEmpty()) { resolvedAddresses.put(host, resolvedAddress.getHostAddress()); } } } /** * Returns the (cached) address that was previously resolved for the specified host. * * @param host hostname that was previously resolved (without a port) * @return the resolved IP address for the host, or null if the resolved address is not in the cache */ public static String getPreviouslyResolvedAddressForHost(String host) { return resolvedAddresses.getIfPresent(host); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/ResponseFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; import io.netty.handler.codec.http.HttpResponse; /** * A functional interface to simplify modification and manipulation of responses. */ public interface ResponseFilter { /** * Implement this method to filter an HTTP response. The URI, headers, status line, etc. are available in the {@code response} parameter, * while the contents of the message are available in the {@code contents} parameter. The response can be modified directly, while the * contents may be modified using the {@link HttpMessageContents#setTextContents(String)} or {@link HttpMessageContents#setBinaryContents(byte[])} * methods. * * @param response The response object, including URI, headers, status line, etc. Modifications to the response object will be reflected in the client response. * @param contents The response contents. * @param messageInfo Additional information relating to the HTTP message. */ void filterResponse(HttpResponse response, HttpMessageContents contents, HttpMessageInfo messageInfo); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/ResponseFilterAdapter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; import org.littleshoot.proxy.HttpFilters; import org.littleshoot.proxy.HttpFiltersSourceAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * A filter adapter for {@link ResponseFilter} implementations. Executes the filter when the {@link HttpFilters#serverToProxyResponse(HttpObject)} * method is invoked. */ public class ResponseFilterAdapter extends HttpsAwareFiltersAdapter implements ModifiedRequestAwareFilter { private final ResponseFilter responseFilter; /** * The final HttpRequest sent to the server, reflecting all modifications from request filters. */ private HttpRequest modifiedHttpRequest; public ResponseFilterAdapter(HttpRequest originalRequest, ChannelHandlerContext ctx, ResponseFilter responseFilter) { super(originalRequest, ctx); this.responseFilter = responseFilter; } @Override public HttpObject serverToProxyResponse(HttpObject httpObject) { // only filter when the original HttpResponse comes through. the ResponseFilterAdapter is not designed to filter // any subsequent HttpContents. if (httpObject instanceof HttpResponse) { HttpResponse httpResponse = (HttpResponse) httpObject; HttpMessageContents contents; if (httpObject instanceof FullHttpMessage) { FullHttpMessage httpContent = (FullHttpMessage) httpObject; contents = new HttpMessageContents(httpContent); } else { // the HTTP object is not a FullHttpMessage, which means that message contents will not be available on this response and cannot be modified. contents = null; } HttpMessageInfo messageInfo = new HttpMessageInfo(originalRequest, ctx, isHttps(), getFullUrl(modifiedHttpRequest), getOriginalUrl()); responseFilter.filterResponse(httpResponse, contents, messageInfo); } return super.serverToProxyResponse(httpObject); } @Override public void setModifiedHttpRequest(HttpRequest modifiedHttpRequest) { this.modifiedHttpRequest = modifiedHttpRequest; } /** * A {@link HttpFiltersSourceAdapter} for {@link ResponseFilterAdapter}s. By default, this FilterSource enables HTTP message aggregation * and sets a maximum response buffer size of 2 MiB. */ public static class FilterSource extends HttpFiltersSourceAdapter { private static final int DEFAULT_MAXIMUM_RESPONSE_BUFFER_SIZE = 2097152; private final ResponseFilter filter; private final int maximumResponseBufferSizeInBytes; /** * Creates a new filter source that will invoke the specified filter and uses the {@link #DEFAULT_MAXIMUM_RESPONSE_BUFFER_SIZE} as * the maximum buffer size. * * @param filter ResponseFilter to invoke */ public FilterSource(ResponseFilter filter) { this.filter = filter; this.maximumResponseBufferSizeInBytes = DEFAULT_MAXIMUM_RESPONSE_BUFFER_SIZE; } /** * Creates a new filter source that will invoke the specified filter and uses the maximumResponseBufferSizeInBytes as the maximum * buffer size. Set maximumResponseBufferSizeInBytes to 0 to disable aggregation. If message aggregation is disabled, * the {@link HttpMessageContents} will not be available for modification. (Note: HTTP message aggregation will * be enabled if any filter has a maximum request or response buffer size greater than 0. See * {@link org.littleshoot.proxy.HttpFiltersSource#getMaximumResponseBufferSizeInBytes()} for details.) * * @param filter ResponseFilter to invoke * @param maximumResponseBufferSizeInBytes maximum buffer size when aggregating responses for filtering */ public FilterSource(ResponseFilter filter, int maximumResponseBufferSizeInBytes) { this.filter = filter; this.maximumResponseBufferSizeInBytes = maximumResponseBufferSizeInBytes; } @Override public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) { return new ResponseFilterAdapter(originalRequest, ctx, filter); } @Override public int getMaximumResponseBufferSizeInBytes() { return maximumResponseBufferSizeInBytes; } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/RewriteUrlFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.proxy.RewriteRule; import net.lightbody.bmp.util.BrowserMobHttpUtil; import net.lightbody.bmp.util.HttpUtil; import org.littleshoot.proxy.impl.ProxyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URISyntaxException; import java.util.Collection; import java.util.Collections; import java.util.regex.Matcher; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * Applies rewrite rules to the specified request. If a rewrite rule matches, the request's URI will be overwritten with the rewritten URI. * The filter does not make a defensive copy of the rewrite rule collection, so there is no guarantee * that the collection at the time of construction will contain the same values when the filter is actually invoked, if the collection is * modified concurrently. */ public class RewriteUrlFilter extends HttpsAwareFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(RewriteUrlFilter.class); private final Collection rewriteRules; public RewriteUrlFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Collection rewriterules) { super(originalRequest, ctx); if (rewriterules != null) { this.rewriteRules = rewriterules; } else { this.rewriteRules = Collections.emptyList(); } } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; // if this is a CONNECT request, don't bother applying the rewrite rules, since CONNECT rewriting is not supported if (ProxyUtils.isCONNECT(httpRequest)) { return null; } String originalUrl = getFullUrl(httpRequest); String rewrittenUrl = originalUrl; boolean rewroteUri = false; for (RewriteRule rule : rewriteRules) { Matcher matcher = rule.getPattern().matcher(rewrittenUrl); if (matcher.matches()) { rewrittenUrl = matcher.replaceAll(rule.getReplace()); rewroteUri = true; } } if (rewroteUri) { // if the URI in the request contains the scheme, host, and port, the request's URI can be replaced // with the rewritten URI. if not (for example, on HTTPS requests), strip the scheme, host, and port from // the rewritten URL before replacing the URI on the request. String uriFromRequest = httpRequest.getUri(); if (HttpUtil.startsWithHttpOrHttps(uriFromRequest)) { httpRequest.setUri(rewrittenUrl); } else { try { String resource = BrowserMobHttpUtil.getRawPathAndParamsFromUri(rewrittenUrl); httpRequest.setUri(resource); } catch (URISyntaxException e) { // the rewritten URL couldn't be parsed, possibly due to the rewrite rule mangling the URL. log // a warning message and replace the resource on the request with the full, rewritten URL. log.warn("Unable to determine path from rewritten URL. Request URL will be set to the full rewritten URL instead of the resource's path.\n\tOriginal URL: {}\n\tRewritten URL: {}", originalUrl, rewrittenUrl, e); httpRequest.setUri(rewrittenUrl); } } // determine if the hostname and/or port has been changed by the rewrite rule. if so, update the Host // header for HTTP requests. for HTTPS requests, log a warning, since hostname and port cannot be changed // by rewrite rules. String originalHostAndPort = null; try { originalHostAndPort = HttpUtil.getHostAndPortFromUri(originalUrl); } catch (URISyntaxException e) { // for some reason we couldn't determine the original host and port from the original URL. log a warning, // and allow the Host header to be forcibly updated to the rewritten host and port. log.warn("Unable to determine host and port from original URL. Host header will be set to rewritten URL's host and port.\n\tOriginal URL: {}\n\tRewritten URL: {}", originalUrl, rewrittenUrl, e); } String modifiedHostAndPort = null; try { modifiedHostAndPort = HttpUtil.getHostAndPortFromUri(rewrittenUrl); } catch (URISyntaxException e) { log.warn("Unable to determine host and port from rewritten URL. Host header will not be updated.\n\tOriginal URL: {}\n\tRewritten URL: {}", originalUrl, rewrittenUrl, e); } // if the modifiedHostAndPort was parsed successfully and is different from the originalHostAndPort, update the Host header if (modifiedHostAndPort != null && !modifiedHostAndPort.equals(originalHostAndPort)) { if (isHttps()) { // for HTTPS requests we cannot modify the host and port, since we are always reusing a persistent connection. log.warn("Cannot rewrite the host or port of an HTTPS connection.\n\tHost and port from original request: {}\n\tRewritten host and port: {}", originalHostAndPort, modifiedHostAndPort); } else { // only modify the Host header if it already exists if (httpRequest.headers().contains(HttpHeaders.Names.HOST)) { HttpHeaders.setHost(httpRequest, modifiedHostAndPort); } } } } } return null; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.util.BrowserMobHttpUtil; import org.littleshoot.proxy.HttpFiltersAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; /** * This filter captures responses from the server (headers and content). The filter can also decompress contents if desired. *

* The filter can be used in one of three ways: (1) directly, by adding the filter to the filter chain; (2) by subclassing * the filter and overriding its filter methods; or (3) by invoking the filter directly from within another filter (see * {@link net.lightbody.bmp.filters.HarCaptureFilter} for an example of the latter). */ public class ServerResponseCaptureFilter extends HttpFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(ServerResponseCaptureFilter.class); /** * Populated by serverToProxyResponse() when processing the HttpResponse object */ private volatile HttpResponse httpResponse; /** * Populated by serverToProxyResponse() as it receives HttpContent responses. If the response is chunked, it will * be populated across multiple calls to proxyToServerResponse(). */ private final ByteArrayOutputStream rawResponseContents = new ByteArrayOutputStream(); /** * Populated when processing the LastHttpContent. If the response is compressed and decompression is requested, * this contains the entire decompressed response. Otherwise it contains the raw response. */ private volatile byte[] fullResponseContents; /** * Populated by serverToProxyResponse() when it processes the LastHttpContent object. */ private volatile HttpHeaders trailingHeaders; /** * Set to true when processing the LastHttpContent if the server indicates there is a content encoding. */ private volatile boolean responseCompressed; /** * Set to true when processing the LastHttpContent if decompression was requested and successful. */ private volatile boolean decompressionSuccessful; /** * Populated when processing the LastHttpContent. */ private volatile String contentEncoding; /** * User option indicating compressed content should be uncompressed. */ private final boolean decompressEncodedContent; public ServerResponseCaptureFilter(HttpRequest originalRequest, boolean decompressEncodedContent) { super(originalRequest); this.decompressEncodedContent = decompressEncodedContent; } public ServerResponseCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, boolean decompressEncodedContent) { super(originalRequest, ctx); this.decompressEncodedContent = decompressEncodedContent; } @Override public HttpObject serverToProxyResponse(HttpObject httpObject) { if (httpObject instanceof HttpResponse) { httpResponse = (HttpResponse) httpObject; captureContentEncoding(httpResponse); } if (httpObject instanceof HttpContent) { HttpContent httpContent = (HttpContent) httpObject; storeResponseContent(httpContent); if (httpContent instanceof LastHttpContent) { LastHttpContent lastContent = (LastHttpContent) httpContent; captureTrailingHeaders(lastContent); captureFullResponseContents(); } } return super.serverToProxyResponse(httpObject); } protected void captureFullResponseContents() { // start by setting fullResponseContent to the raw, (possibly) compressed byte stream. replace it // with the decompressed bytes if decompression is successful. fullResponseContents = getRawResponseContents(); // if the content is compressed, we need to decompress it. but don't use // the netty HttpContentCompressor/Decompressor in the pipeline because we don't actually want it to // change the message sent to the client if (contentEncoding != null) { responseCompressed = true; if (decompressEncodedContent) { decompressContents(); } else { // will not decompress response } } else { // no compression responseCompressed = false; } } protected void decompressContents() { if (contentEncoding.equals(HttpHeaders.Values.GZIP)) { try { fullResponseContents = BrowserMobHttpUtil.decompressContents(getRawResponseContents()); decompressionSuccessful = true; } catch (RuntimeException e) { log.warn("Failed to decompress response with encoding type " + contentEncoding + " when decoding request from " + originalRequest.getUri(), e); } } else { log.warn("Cannot decode unsupported content encoding type {}", contentEncoding); } } protected void captureContentEncoding(HttpResponse httpResponse) { contentEncoding = HttpHeaders.getHeader(httpResponse, HttpHeaders.Names.CONTENT_ENCODING); } protected void captureTrailingHeaders(LastHttpContent lastContent) { trailingHeaders = lastContent.trailingHeaders(); // technically, the Content-Encoding header can be in a trailing header, although this is excruciatingly uncommon if (trailingHeaders != null) { String trailingContentEncoding = trailingHeaders.get(HttpHeaders.Names.CONTENT_ENCODING); if (trailingContentEncoding != null) { contentEncoding = trailingContentEncoding; } } } protected void storeResponseContent(HttpContent httpContent) { ByteBuf bufferedContent = httpContent.content(); byte[] content = BrowserMobHttpUtil.extractReadableBytes(bufferedContent); try { rawResponseContents.write(content); } catch (IOException e) { // can't happen } } public HttpResponse getHttpResponse() { return httpResponse; } /** * Returns the contents of the entire response. If the contents were compressed, decompressEncodedContent is true, and * decompression was successful, this method returns the decompressed contents. * * @return entire response contents, decompressed if possible */ public byte[] getFullResponseContents() { return fullResponseContents; } /** * Returns the raw contents of the entire response, without decompression. * * @return entire response contents, without decompression */ public byte[] getRawResponseContents() { return rawResponseContents.toByteArray(); } public HttpHeaders getTrailingHeaders() { return trailingHeaders; } public boolean isResponseCompressed() { return responseCompressed; } /** * @return true if decompression is both enabled and successful */ public boolean isDecompressionSuccessful() { if (!decompressEncodedContent) { return false; } return decompressionSuccessful; } public String getContentEncoding() { return contentEncoding; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/UnregisterRequestFilter.java ================================================ package net.lightbody.bmp.filters; import net.lightbody.bmp.proxy.ActivityMonitor; import org.littleshoot.proxy.HttpFiltersAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.LastHttpContent; /** * Unregisters this request with the {@link net.lightbody.bmp.proxy.ActivityMonitor} when the LastHttpContent is sent to the client. */ public class UnregisterRequestFilter extends HttpFiltersAdapter { private final ActivityMonitor activityMonitor; public UnregisterRequestFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, ActivityMonitor activityMonitor) { super(originalRequest, ctx); this.activityMonitor = activityMonitor; } @Override public HttpObject proxyToClientResponse(HttpObject httpObject) { if (httpObject instanceof LastHttpContent) { activityMonitor.requestFinished(); } return super.proxyToClientResponse(httpObject); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/WhitelistFilter.java ================================================ package net.lightbody.bmp.filters; import org.littleshoot.proxy.impl.ProxyUtils; import java.util.Collection; import java.util.Collections; import java.util.regex.Pattern; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; /** * Checks this request against the whitelist, and returns the modified response if the request is not in the whitelist. The filter does not * make a defensive copy of the whitelist URLs, so there is no guarantee that the whitelist URLs at the time of construction will contain the * same values when the filter is actually invoked, if the URL collection is modified concurrently. */ public class WhitelistFilter extends HttpsAwareFiltersAdapter { private final boolean whitelistEnabled; private final int whitelistResponseCode; private final Collection whitelistUrls; public WhitelistFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, boolean whitelistEnabled, int whitelistResponseCode, Collection whitelistUrls) { super(originalRequest, ctx); this.whitelistEnabled = whitelistEnabled; this.whitelistResponseCode = whitelistResponseCode; if (whitelistUrls != null) { this.whitelistUrls = whitelistUrls; } else { this.whitelistUrls = Collections.emptyList(); } } @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (!whitelistEnabled) { return null; } if (httpObject instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) httpObject; // do not allow HTTP CONNECTs to be short-circuited if (ProxyUtils.isCONNECT(httpRequest)) { return null; } boolean urlWhitelisted = false; String url = getFullUrl(httpRequest); for (Pattern pattern : whitelistUrls) { if (pattern.matcher(url).matches()) { urlWhitelisted = true; break; } } if (!urlWhitelisted) { HttpResponseStatus status = HttpResponseStatus.valueOf(whitelistResponseCode); HttpResponse resp = new DefaultFullHttpResponse(httpRequest.getProtocolVersion(), status); HttpHeaders.setContentLength(resp, 0L); return resp; } } return null; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/support/HttpConnectTiming.java ================================================ package net.lightbody.bmp.filters.support; /** * Holds the connection-related timing information from an HTTP CONNECT request, so it can be added to the HAR timings for the first * "real" request to the same host. The HTTP CONNECT and the "real" HTTP requests are processed in different HarCaptureFilter instances. *

* Note: The connect time must include the ssl time. According to the HAR spec at https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.htm:

 ssl [number, optional] (new in 1.2) - Time required for SSL/TLS negotiation. If this field is defined then the time is also
 included in the connect field (to ensure backward compatibility with HAR 1.1). Use -1 if the timing does not apply to the
 current request.
 
*/ public class HttpConnectTiming { private volatile long blockedTimeNanos = -1; private volatile long dnsTimeNanos = -1; private volatile long connectTimeNanos = -1; private volatile long sslHandshakeTimeNanos = -1; public void setConnectTimeNanos(long connectTimeNanos) { this.connectTimeNanos = connectTimeNanos; } public void setSslHandshakeTimeNanos(long sslHandshakeTimeNanos) { this.sslHandshakeTimeNanos = sslHandshakeTimeNanos; } public void setBlockedTimeNanos(long blockedTimeNanos) { this.blockedTimeNanos = blockedTimeNanos; } public void setDnsTimeNanos(long dnsTimeNanos) { this.dnsTimeNanos = dnsTimeNanos; } public long getConnectTimeNanos() { return connectTimeNanos; } public long getSslHandshakeTimeNanos() { return sslHandshakeTimeNanos; } public long getBlockedTimeNanos() { return blockedTimeNanos; } public long getDnsTimeNanos() { return dnsTimeNanos; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/filters/util/HarCaptureUtil.java ================================================ package net.lightbody.bmp.filters.util; import net.lightbody.bmp.core.har.HarResponse; /** * Static utility methods for {@link net.lightbody.bmp.filters.HarCaptureFilter} and {@link net.lightbody.bmp.filters.HttpConnectHarCaptureFilter}. */ public class HarCaptureUtil { /** * The HTTP version string in the {@link HarResponse} for failed requests. */ public static final String HTTP_VERSION_STRING_FOR_FAILURE = "unknown"; /** * The HTTP status code in the {@link HarResponse} for failed requests. */ public static final int HTTP_STATUS_CODE_FOR_FAILURE = 0; /** * The HTTP status text/reason phrase in the {@link HarResponse} for failed requests. */ public static final String HTTP_REASON_PHRASE_FOR_FAILURE = ""; /** * The error message that will be populated in the _error field of the {@link HarResponse} due to a name * lookup failure. */ private static final String RESOLUTION_FAILED_ERROR_MESSAGE = "Unable to resolve host: "; /** * The error message that will be populated in the _error field of the {@link HarResponse} due to a * connection failure. */ private static final String CONNECTION_FAILED_ERROR_MESSAGE = "Unable to connect to host"; /** * The error message that will be populated in the _error field of the {@link HarResponse} when the proxy fails to * receive a response in a timely manner. */ private static final String RESPONSE_TIMED_OUT_ERROR_MESSAGE = "Response timed out"; /** * The error message that will be populated in the _error field of the {@link HarResponse} when no response is received * from the server for any reason other than a server response timeout. */ private static final String NO_RESPONSE_RECEIVED_ERROR_MESSAGE = "No response received"; /** * Creates a DCResponse object for failed requests. Normally the DCResponse is populated when the response is received * from the server, but if the request fails due to a name resolution issue, connection problem, timeout, etc., no * DCResponse would otherwise be created. * * @return a new DCResponse object with invalid HTTP status code (0) and version string ("unknown") */ public static HarResponse createHarResponseForFailure() { return new HarResponse(HTTP_STATUS_CODE_FOR_FAILURE, HTTP_REASON_PHRASE_FOR_FAILURE, HTTP_VERSION_STRING_FOR_FAILURE); } /** * Returns the error message for the HAR response when DNS resolution fails. * * @param hostAndPort the host and port of the address lookup that failed * @return the resolution failed error message */ public static String getResolutionFailedErrorMessage(String hostAndPort) { return RESOLUTION_FAILED_ERROR_MESSAGE + hostAndPort; } /** * Returns the error message for the HAR response when the connection fails. * * @return the connection failed error message */ public static String getConnectionFailedErrorMessage() { return CONNECTION_FAILED_ERROR_MESSAGE; } /** * Returns the error message for the HAR response when the response from the server times out. * * @return the response timed out error message */ public static String getResponseTimedOutErrorMessage() { return RESPONSE_TIMED_OUT_ERROR_MESSAGE; } /** * Returns the error message for the HAR response when no response was received from the server (e.g. when the * browser is closed). * * @return the no response received error message */ public static String getNoResponseReceivedErrorMessage() { return NO_RESPONSE_RECEIVED_ERROR_MESSAGE; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/CertificateAndKey.java ================================================ package net.lightbody.bmp.mitm; import java.security.PrivateKey; import java.security.cert.X509Certificate; /** * A simple container for an X.509 certificate and its corresponding private key. */ public class CertificateAndKey { private final X509Certificate certificate; private final PrivateKey privateKey; public CertificateAndKey(X509Certificate certificate, PrivateKey privateKey) { this.certificate = certificate; this.privateKey = privateKey; } public X509Certificate getCertificate() { return certificate; } public PrivateKey getPrivateKey() { return privateKey; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/CertificateAndKeySource.java ================================================ package net.lightbody.bmp.mitm; /** * A CertificateAndKeySource generates {@link CertificateAndKey}s, i.e. the root certificate and private key used * to sign impersonated certificates of upstream servers. Implementations of this interface load impersonation materials * from various sources, including Java KeyStores, JKS files, etc., or generate them on-the-fly. */ public interface CertificateAndKeySource { /** * Loads a certificate and its corresponding private key. Every time this method is called, it should return the same * certificate and private key (although it may be a different {@link CertificateAndKey} instance). * * @return certificate and its corresponding private key */ CertificateAndKey load(); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/CertificateInfo.java ================================================ package net.lightbody.bmp.mitm; import java.util.Collections; import java.util.Date; import java.util.List; /** * Container for X.509 Certificate information. */ public class CertificateInfo { private String commonName; private String organization; private String organizationalUnit; private String email; private String locality; private String state; private String countryCode; private Date notBefore; private Date notAfter; private List subjectAlternativeNames = Collections.emptyList(); public String getCommonName() { return commonName; } public String getOrganization() { return organization; } public String getOrganizationalUnit() { return organizationalUnit; } public Date getNotBefore() { return notBefore; } public Date getNotAfter() { return notAfter; } public String getEmail() { return email; } public String getLocality() { return locality; } public String getState() { return state; } public String getCountryCode() { return countryCode; } public List getSubjectAlternativeNames() { return subjectAlternativeNames; } public CertificateInfo commonName(String commonName) { this.commonName = commonName; return this; } public CertificateInfo organization(String organization) { this.organization = organization; return this; } public CertificateInfo organizationalUnit(String organizationalUnit) { this.organizationalUnit = organizationalUnit; return this; } public CertificateInfo notBefore(Date notBefore) { this.notBefore = notBefore; return this; } public CertificateInfo notAfter(Date notAfter) { this.notAfter = notAfter; return this; } public CertificateInfo email(String email) { this.email = email; return this; } public CertificateInfo locality(String locality) { this.locality = locality; return this; } public CertificateInfo state(String state) { this.state = state; return this; } public CertificateInfo countryCode(String countryCode) { this.countryCode = countryCode; return this; } public CertificateInfo subjectAlternativeNames(List subjectAlternativeNames) { this.subjectAlternativeNames = subjectAlternativeNames; return this; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java ================================================ package net.lightbody.bmp.mitm; import java.security.cert.X509Certificate; import java.util.List; /** * A functional interface to allow customization of the certificates generated by the * {@link net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager}. */ public interface CertificateInfoGenerator { /** * Generate a certificate for the specified hostnames, optionally using parameters from the originalCertificate. * * @param hostnames the hostnames to generate the certificate for, which may include wildcards * @param originalCertificate original X.509 certificate sent by the upstream server, which may be null * @return CertificateInfo to be used to create an X509Certificate for the specified hostnames */ CertificateInfo generate(List hostnames, X509Certificate originalCertificate); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/ExistingCertificateSource.java ================================================ package net.lightbody.bmp.mitm; import java.security.PrivateKey; import java.security.cert.X509Certificate; /** * A simple adapter that produces a {@link CertificateAndKey} from existing {@link X509Certificate} and {@link PrivateKey} * java objects. */ public class ExistingCertificateSource implements CertificateAndKeySource { private final X509Certificate rootCertificate; private final PrivateKey privateKey; public ExistingCertificateSource(X509Certificate rootCertificate, PrivateKey privateKey) { if (rootCertificate == null) { throw new IllegalArgumentException("CA root certificate cannot be null"); } if (privateKey == null) { throw new IllegalArgumentException("Private key cannot be null"); } this.rootCertificate = rootCertificate; this.privateKey = privateKey; } @Override public CertificateAndKey load() { return new CertificateAndKey(rootCertificate, privateKey); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/HostnameCertificateInfoGenerator.java ================================================ package net.lightbody.bmp.mitm; import java.security.cert.X509Certificate; import java.util.Date; import java.util.List; /** * A {@link CertificateInfoGenerator} that uses only a hostname to populate a new {@link CertificateInfo}. The * values in the upstream server's original X.509 certificate will be ignored. */ public class HostnameCertificateInfoGenerator implements CertificateInfoGenerator { /** * The 'O' to use for the impersonated server certificate when doing "simple" certificate impersonation (i.e. * not copying values from actual server certificate). */ private static final String DEFAULT_IMPERSONATED_CERT_ORG = "Impersonated Certificate"; /** * The 'O' to use for the impersonated server certificate when doing "simple" certificate impersonation. */ private static final String DEFAULT_IMPERSONATED_CERT_ORG_UNIT = "LittleProxy MITM"; @Override public CertificateInfo generate(List hostnames, X509Certificate originalCertificate) { if (hostnames == null || hostnames.size() < 1) { throw new IllegalArgumentException("Cannot create X.509 certificate without server hostname"); } // take the first entry as the CN String commonName = hostnames.get(0); return new CertificateInfo() .commonName(commonName) .organization(DEFAULT_IMPERSONATED_CERT_ORG) .organizationalUnit(DEFAULT_IMPERSONATED_CERT_ORG_UNIT) .notBefore(getNotBefore()) .notAfter(getNotAfter()) .subjectAlternativeNames(hostnames); } /** * Returns the default Not Before date for impersonated certificates. Defaults to the current date minus 1 year. */ protected Date getNotBefore() { return new Date(System.currentTimeMillis() - 365L * 24L * 60L * 60L * 1000L); } /** * Returns the default Not After date for impersonated certificates. Defaults to the current date plus 1 year. */ protected Date getNotAfter() { return new Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/KeyStoreCertificateSource.java ================================================ package net.lightbody.bmp.mitm; import net.lightbody.bmp.mitm.exception.CertificateSourceException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableEntryException; import java.security.cert.X509Certificate; /** * A {@link CertificateAndKeySource} that loads the root certificate and private key from a Java KeyStore. The * KeyStore must contain a certificate and a private key, specified by the privateKeyAlias value. The KeyStore must * already be loaded and initialized; to load the KeyStore from a file or classpath resource, use * {@link KeyStoreFileCertificateSource}, {@link PemFileCertificateSource}, or a custom * implementation of {@link CertificateAndKeySource}. */ public class KeyStoreCertificateSource implements CertificateAndKeySource { private final KeyStore keyStore; private final String keyStorePassword; private final String privateKeyAlias; public KeyStoreCertificateSource(KeyStore keyStore, String privateKeyAlias, String keyStorePassword) { if (keyStore == null) { throw new IllegalArgumentException("KeyStore cannot be null"); } if (privateKeyAlias == null) { throw new IllegalArgumentException("Private key alias cannot be null"); } if (keyStorePassword == null) { throw new IllegalArgumentException("KeyStore password cannot be null"); } this.keyStore = keyStore; this.keyStorePassword = keyStorePassword; this.privateKeyAlias = privateKeyAlias; } @Override public CertificateAndKey load() { try { KeyStore.Entry entry; try { entry = keyStore.getEntry(privateKeyAlias, new KeyStore.PasswordProtection(keyStorePassword.toCharArray())); } catch (UnrecoverableEntryException e) { throw new CertificateSourceException("Unable to load private key with alias " + privateKeyAlias + " from KeyStore. Verify the KeyStore password is correct.", e); } if (entry == null) { throw new CertificateSourceException("Unable to find entry in keystore with alias: " + privateKeyAlias); } if (!(entry instanceof KeyStore.PrivateKeyEntry)) { throw new CertificateSourceException("Entry in KeyStore with alias " + privateKeyAlias + " did not contain a private key entry"); } KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) entry; PrivateKey privateKey = privateKeyEntry.getPrivateKey(); if (!(privateKeyEntry.getCertificate() instanceof X509Certificate)) { throw new CertificateSourceException("Certificate for private key in KeyStore was not an X509Certificate. Private key alias: " + privateKeyAlias + ". Certificate type: " + (privateKeyEntry.getCertificate() != null ? privateKeyEntry.getCertificate().getClass().getName() : null)); } X509Certificate x509Certificate = (X509Certificate) privateKeyEntry.getCertificate(); return new CertificateAndKey(x509Certificate, privateKey); } catch (KeyStoreException | NoSuchAlgorithmException e) { throw new CertificateSourceException("Error accessing keyStore", e); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java ================================================ package net.lightbody.bmp.mitm; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import net.lightbody.bmp.mitm.exception.CertificateSourceException; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; /** * Loads a KeyStore from a file or classpath resource. If configured with a File object, attempts to load the KeyStore * from the specified file. Otherwise, attempts to load the KeyStore from a classpath resource. */ public class KeyStoreFileCertificateSource implements CertificateAndKeySource { private static final Logger log = LoggerFactory.getLogger(KeyStoreFileCertificateSource.class); private final String keyStoreClasspathResource; private final File keyStoreFile; private final String keyStoreType; private final String keyStorePassword; private final String privateKeyAlias; private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); private final Supplier certificateAndKey = Suppliers.memoize(new Supplier() { @Override public CertificateAndKey get() { return loadKeyStore(); } }); /** * Creates a {@link CertificateAndKeySource} that loads an existing {@link KeyStore} from a classpath resource. * @param keyStoreType the KeyStore type, such as PKCS12 or JKS * @param keyStoreClasspathResource classpath resource to load (for example, "/keystore.jks") * @param privateKeyAlias the alias of the private key in the KeyStore * @param keyStorePassword te KeyStore password */ public KeyStoreFileCertificateSource(String keyStoreType, String keyStoreClasspathResource, String privateKeyAlias, String keyStorePassword) { if (keyStoreClasspathResource == null) { throw new IllegalArgumentException("The classpath location of the KeyStore cannot be null"); } if (keyStoreType == null) { throw new IllegalArgumentException("KeyStore type cannot be null"); } if (privateKeyAlias == null) { throw new IllegalArgumentException("Alias of the private key in the KeyStore cannot be null"); } this.keyStoreClasspathResource = keyStoreClasspathResource; this.keyStoreFile = null; this.keyStoreType = keyStoreType; this.keyStorePassword = keyStorePassword; this.privateKeyAlias = privateKeyAlias; } /** * Creates a {@link CertificateAndKeySource} that loads an existing {@link KeyStore} from a classpath resource. * @param keyStoreType the KeyStore type, such as PKCS12 or JKS * @param keyStoreFile KeyStore file to load * @param privateKeyAlias the alias of the private key in the KeyStore * @param keyStorePassword te KeyStore password */ public KeyStoreFileCertificateSource(String keyStoreType, File keyStoreFile, String privateKeyAlias, String keyStorePassword) { if (keyStoreFile == null) { throw new IllegalArgumentException("The KeyStore file cannot be null"); } if (keyStoreType == null) { throw new IllegalArgumentException("KeyStore type cannot be null"); } if (privateKeyAlias == null) { throw new IllegalArgumentException("Alias of the private key in the KeyStore cannot be null"); } this.keyStoreFile = keyStoreFile; this.keyStoreClasspathResource = null; this.keyStoreType = keyStoreType; this.keyStorePassword = keyStorePassword; this.privateKeyAlias = privateKeyAlias; } /** * Override the default {@link SecurityProviderTool} used to load the KeyStore. */ public KeyStoreFileCertificateSource certificateTool(SecurityProviderTool securityProviderTool) { this.securityProviderTool = securityProviderTool; return this; } @Override public CertificateAndKey load() { return certificateAndKey.get(); } /** * Loads the {@link CertificateAndKey} from the KeyStore using the {@link SecurityProviderTool}. */ private CertificateAndKey loadKeyStore() { // load the KeyStore from the file or classpath resource, then delegate to a KeyStoreCertificateSource KeyStore keyStore; if (keyStoreFile != null) { keyStore = securityProviderTool.loadKeyStore(keyStoreFile, keyStoreType, keyStorePassword); } else { // copy the classpath resource to a temporary file and load the keystore from that temp file File tempKeyStoreFile = null; try{ InputStream keystoreAsStream = KeyStoreFileCertificateSource.class.getResourceAsStream(keyStoreClasspathResource); tempKeyStoreFile = File.createTempFile("keystore", "temp"); FileUtils.copyInputStreamToFile(keystoreAsStream, tempKeyStoreFile); keyStore = securityProviderTool.loadKeyStore(tempKeyStoreFile, keyStoreType, keyStorePassword); } catch (IOException e) { throw new CertificateSourceException("Unable to open KeyStore classpath resource: " + keyStoreClasspathResource, e); } finally { if (tempKeyStoreFile != null) { try { FileUtils.forceDelete(tempKeyStoreFile); } catch (IOException e) { log.warn("Unable to delete temporary KeyStore file: {}.", tempKeyStoreFile.getAbsoluteFile()); } } } } KeyStoreCertificateSource keyStoreCertificateSource = new KeyStoreCertificateSource(keyStore, privateKeyAlias, keyStorePassword); return keyStoreCertificateSource.load(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java ================================================ package net.lightbody.bmp.mitm; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.mitm.util.EncryptionUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.StringReader; import java.security.PrivateKey; import java.security.cert.X509Certificate; /** * Loads impersonation materials from two separate, PEM-encoded files: a CA root certificate and its corresponding * private key. */ public class PemFileCertificateSource implements CertificateAndKeySource { private static final Logger log = LoggerFactory.getLogger(PemFileCertificateSource.class); private final File certificateFile; private final File privateKeyFile; private final String privateKeyPassword; private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); private final Supplier certificateAndKey = Suppliers.memoize(new Supplier() { @Override public CertificateAndKey get() { return loadCertificateAndKeyFiles(); } }); /** * Creates a {@link CertificateAndKeySource} that loads the certificate and private key from PEM files. * * @param certificateFile PEM-encoded file containing the root certificate * @param privateKeyFile PEM-encoded file continaing the certificate's private key * @param privateKeyPassword password for the private key */ public PemFileCertificateSource(File certificateFile, File privateKeyFile, String privateKeyPassword) { this.certificateFile = certificateFile; this.privateKeyFile = privateKeyFile; this.privateKeyPassword = privateKeyPassword; } /** * Override the default {@link SecurityProviderTool} used to load the PEM files. */ public PemFileCertificateSource certificateTool(SecurityProviderTool securityProviderTool) { this.securityProviderTool = securityProviderTool; return this; } @Override public CertificateAndKey load() { return certificateAndKey.get(); } private CertificateAndKey loadCertificateAndKeyFiles() { if (certificateFile == null) { throw new IllegalArgumentException("PEM root certificate file cannot be null"); } if (privateKeyFile == null) { throw new IllegalArgumentException("PEM private key file cannot be null"); } if (privateKeyPassword == null) { log.warn("Attempting to load private key from file without password. Private keys should be password-protected."); } String pemEncodedCertificate = EncryptionUtil.readPemStringFromFile(certificateFile); X509Certificate certificate = securityProviderTool.decodePemEncodedCertificate(new StringReader(pemEncodedCertificate)); String pemEncodedPrivateKey = EncryptionUtil.readPemStringFromFile(privateKeyFile); PrivateKey privateKey = securityProviderTool.decodePemEncodedPrivateKey(new StringReader(pemEncodedPrivateKey), privateKeyPassword); return new CertificateAndKey(certificate, privateKey); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java ================================================ package net.lightbody.bmp.mitm; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import net.lightbody.bmp.mitm.keys.KeyGenerator; import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.mitm.util.EncryptionUtil; import net.lightbody.bmp.mitm.util.MitmConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.KeyPair; import java.security.KeyStore; import java.text.SimpleDateFormat; import java.util.Date; /** * A {@link CertificateAndKeySource} that dynamically generates a CA root certificate and private key. The certificate * and key will only be generated once; all subsequent calls to {@link #load()} will return the same materials. To save * the generated certificate and/or private key for installation in a browser or other client, use one of the encode * or save methods: *
    *
  • {@link #encodeRootCertificateAsPem()}
  • *
  • {@link #encodePrivateKeyAsPem(String)}
  • *
  • {@link #saveRootCertificateAsPemFile(File)}
  • *
  • {@link #savePrivateKeyAsPemFile(File, String)}
  • *
  • {@link #saveRootCertificateAndKey(String, File, String, String)}
  • *
*/ public class RootCertificateGenerator implements CertificateAndKeySource { private static final Logger log = LoggerFactory.getLogger(RootCertificateGenerator.class); private final CertificateInfo rootCertificateInfo; private final String messageDigest; private final KeyGenerator keyGenerator; private final SecurityProviderTool securityProviderTool; /** * The default algorithm to use when encrypting objects in PEM files (such as private keys). */ private static final String DEFAULT_PEM_ENCRYPTION_ALGORITHM = "AES-128-CBC"; /** * The new root certificate and private key are generated only once, even across multiple calls to {@link #load()}}, * to allow users to save the new generated root certificate for use in browsers/other HTTP clients. */ private final Supplier generatedCertificateAndKey = Suppliers.memoize(new Supplier() { @Override public CertificateAndKey get() { return generateRootCertificate(); } }); public RootCertificateGenerator(CertificateInfo rootCertificateInfo, String messageDigest, KeyGenerator keyGenerator, SecurityProviderTool securityProviderTool) { if (rootCertificateInfo == null) { throw new IllegalArgumentException("CA root certificate cannot be null"); } if (messageDigest == null) { throw new IllegalArgumentException("Message digest cannot be null"); } if (keyGenerator == null) { throw new IllegalArgumentException("Key generator cannot be null"); } if (securityProviderTool == null) { throw new IllegalArgumentException("Certificate tool cannot be null"); } this.rootCertificateInfo = rootCertificateInfo; this.messageDigest = messageDigest; this.keyGenerator = keyGenerator; this.securityProviderTool = securityProviderTool; } @Override public CertificateAndKey load() { // only generate the materials once, so they can can be saved if desired return generatedCertificateAndKey.get(); } /** * Generates a new CA root certificate and private key. * * @return new root certificate and private key */ private CertificateAndKey generateRootCertificate() { long generationStart = System.currentTimeMillis(); // create the public and private key pair that will be used to sign the generated certificate KeyPair caKeyPair = keyGenerator.generate(); // delegate the creation and signing of the X.509 certificate to the certificate tool CertificateAndKey certificateAndKey = securityProviderTool.createCARootCertificate( rootCertificateInfo, caKeyPair, messageDigest); long generationFinished = System.currentTimeMillis(); log.info("Generated CA root certificate and private key in {}ms. Key generator: {}. Signature algorithm: {}.", generationFinished - generationStart, keyGenerator, messageDigest); return certificateAndKey; } /** * Returns the generated root certificate as a PEM-encoded String. */ public String encodeRootCertificateAsPem() { return securityProviderTool.encodeCertificateAsPem(generatedCertificateAndKey.get().getCertificate()); } /** * Returns the generated private key as a PEM-encoded String, encrypted using the specified password and the * {@link #DEFAULT_PEM_ENCRYPTION_ALGORITHM}. * * @param privateKeyPassword password to use to encrypt the private key */ public String encodePrivateKeyAsPem(String privateKeyPassword) { return securityProviderTool.encodePrivateKeyAsPem(generatedCertificateAndKey.get().getPrivateKey(), privateKeyPassword, DEFAULT_PEM_ENCRYPTION_ALGORITHM); } /** * Saves the root certificate as PEM-encoded data to the specified file. */ public void saveRootCertificateAsPemFile(File file) { String pemEncodedCertificate = securityProviderTool.encodeCertificateAsPem(generatedCertificateAndKey.get().getCertificate()); EncryptionUtil.writePemStringToFile(file, pemEncodedCertificate); } /** * Saves the private key as PEM-encoded data to a file, using the specified password to encrypt the private key and * the {@link #DEFAULT_PEM_ENCRYPTION_ALGORITHM}. If the password is null, the private key will be stored unencrypted. * In general, private keys should not be stored unencrypted. * * @param file file to save the private key to * @param passwordForPrivateKey password to protect the private key */ public void savePrivateKeyAsPemFile(File file, String passwordForPrivateKey) { String pemEncodedPrivateKey = securityProviderTool.encodePrivateKeyAsPem(generatedCertificateAndKey.get().getPrivateKey(), passwordForPrivateKey, DEFAULT_PEM_ENCRYPTION_ALGORITHM); EncryptionUtil.writePemStringToFile(file, pemEncodedPrivateKey); } /** * Saves the generated certificate and private key as a file, using the specified password to protect the key store. * * @param keyStoreType the KeyStore type, such as PKCS12 or JKS * @param file file to export the root certificate and private key to * @param privateKeyAlias alias for the private key in the KeyStore * @param password password for the private key and the KeyStore */ public void saveRootCertificateAndKey(String keyStoreType, File file, String privateKeyAlias, String password) { CertificateAndKey certificateAndKey = generatedCertificateAndKey.get(); KeyStore keyStore = securityProviderTool.createRootCertificateKeyStore(keyStoreType, certificateAndKey, privateKeyAlias, password); securityProviderTool.saveKeyStore(file, keyStore, password); } /** * Convenience method to return a new {@link Builder} instance. */ public static Builder builder() { return new Builder(); } /** * A Builder for {@link RootCertificateGenerator}s. Initialized with suitable default values suitable for most purposes. */ public static class Builder { private CertificateInfo certificateInfo = new CertificateInfo() .commonName(getDefaultCommonName()) .organization("CA dynamically generated by LittleProxy") .notBefore(new Date(System.currentTimeMillis() - 365L * 24L * 60L * 60L * 1000L)) .notAfter(new Date(System.currentTimeMillis() + 365L * 24L * 60L * 60L * 1000L)); private KeyGenerator keyGenerator = new RSAKeyGenerator(); private String messageDigest = MitmConstants.DEFAULT_MESSAGE_DIGEST; private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); /** * Certificate info to use to generate the root certificate. Reasonable default values will be used if certificate * info is not supplied. */ public Builder certificateInfo(CertificateInfo certificateInfo) { this.certificateInfo = certificateInfo; return this; } /** * The {@link KeyGenerator} that will be used to generate the root certificate's public and private keys. */ public Builder keyGenerator(KeyGenerator keyGenerator) { this.keyGenerator = keyGenerator; return this; } /** * The message digest that will be used when self-signing the root certificates. */ public Builder messageDigest(String messageDigest) { this.messageDigest = messageDigest; return this; } /** * The {@link SecurityProviderTool} implementation that will be used to generate certificates. */ public Builder certificateTool(SecurityProviderTool securityProviderTool) { this.securityProviderTool = securityProviderTool; return this; } public RootCertificateGenerator build() { return new RootCertificateGenerator(certificateInfo, messageDigest, keyGenerator, securityProviderTool); } } /** * Creates a default CN field for a certificate, using the hostname of this machine and the current time. */ private static String getDefaultCommonName() { String hostname; try { hostname = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { hostname = "localhost"; } SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz"); String currentDateTime = dateFormat.format(new Date()); String defaultCN = "Generated CA (" + hostname + ") " + currentDateTime; // CN fields can only be 64 characters return defaultCN.length() <= 64 ? defaultCN : defaultCN.substring(0, 63); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/TrustSource.java ================================================ package net.lightbody.bmp.mitm; import com.google.common.collect.ObjectArrays; import com.google.common.io.Files; import net.lightbody.bmp.mitm.exception.UncheckedIOException; import net.lightbody.bmp.mitm.util.TrustUtil; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.security.KeyStore; import java.security.cert.X509Certificate; import java.util.List; /** * A source of trusted root certificate authorities. Provides static methods to obtain default trust sources: *
    *
  • {@link #defaultTrustSource()}- both the built-in and JVM-trusted CAs
  • *
  • {@link #javaTrustSource()} - only default CAs trusted by the JVM
  • *
  • {@link #builtinTrustSource()} - only built-in trusted CAs (ultimately derived from Firefox's trust list)
  • *
* * Custom TrustSources can be built by starting with {@link #empty()}, then calling the various add() methods to add * PEM-encoded files and Strings, KeyStores, and X509Certificates to the TrustSource. For example: *

* * TrustSource customTrustSource = TrustSource.empty() * .add(myX509Certificate) * .add(pemFileContainingMyCA) * .add(javaKeyStore); * *

* Note: This class is immutable, so calls to add() will return a new instance, rather than modifying the existing instance. */ public class TrustSource { /** * The default TrustSource. To obtain this TrustSource, use {@link #defaultTrustSource()}. */ private static final TrustSource DEFAULT_TRUST_SOURCE = TrustSource.javaTrustSource().add(TrustSource.builtinTrustSource()); /** * The root CAs this TrustSource trusts. */ private final X509Certificate[] trustedCAs; /** * Creates a TrustSource that contains no trusted certificates. For public use, see {@link #empty()}. */ protected TrustSource() { this(TrustUtil.EMPTY_CERTIFICATE_ARRAY); } /** * Creates a TrustSource that considers only the specified certificates as "trusted". For public use, * use {@link #empty()} followed by {@link #add(X509Certificate...)}. * * @param trustedCAs root CAs to trust */ protected TrustSource(X509Certificate... trustedCAs) { if (trustedCAs == null) { this.trustedCAs = TrustUtil.EMPTY_CERTIFICATE_ARRAY; } else { this.trustedCAs = trustedCAs; } } /** * Returns the X509 certificates considered "trusted" by this TrustSource. This method will not return null, but * may return an empty array. */ public X509Certificate[] getTrustedCAs() { return trustedCAs; } /** * Returns a TrustSource that contains no trusted CAs. Can be used in conjunction with the add() methods to build * a TrustSource containing custom CAs from a variety of sources (PEM files, KeyStores, etc.). */ public static TrustSource empty() { return new TrustSource(); } /** * Returns a TrustSource containing the default trusted CAs. By default, contains both the JVM's trusted CAs and the * built-in trusted CAs (Firefox's trusted CAs). */ public static TrustSource defaultTrustSource() { return DEFAULT_TRUST_SOURCE; } /** * Returns a TrustSource containing only the builtin trusted CAs and does not include the JVM's trusted CAs. * See {@link TrustUtil#getBuiltinTrustedCAs()}. */ public static TrustSource builtinTrustSource() { return new TrustSource(TrustUtil.getBuiltinTrustedCAs()); } /** * Returns a TrustSource containing the default CAs trusted by this JVM. See {@link TrustUtil#getJavaTrustedCAs()}. */ public static TrustSource javaTrustSource() { return new TrustSource(TrustUtil.getJavaTrustedCAs()); } /** * Returns a new TrustSource containing the same trusted CAs as this TrustSource, plus zero or more CAs contained in * the PEM-encoded String. The String may contain multiple certificates and may contain comments or other non-PEM-encoded * text, as long as the PEM-encoded certificates are delimited by appropriate BEGIN_CERTIFICATE and END_CERTIFICATE * text blocks. * * @param trustedPemEncodedCAs String containing PEM-encoded certificates to trust * @return a new TrustSource containing this TrustSource's trusted CAs plus the CAs in the specified String */ public TrustSource add(String trustedPemEncodedCAs) { if (trustedPemEncodedCAs == null) { throw new IllegalArgumentException("PEM-encoded trusted CA String cannot be null"); } X509Certificate[] trustedCertificates = TrustUtil.readX509CertificatesFromPem(trustedPemEncodedCAs); return add(trustedCertificates); } /** * Returns a new TrustSource containing the same trusted CAs as this TrustSource, plus zero or more additional * trusted X509Certificates. If trustedCertificates is null or empty, returns this same TrustSource. * * @param trustedCertificates X509Certificates of CAs to trust * @return a new TrustSource containing this TrustSource's trusted CAs plus the specified CAs */ public TrustSource add(X509Certificate... trustedCertificates) { if (trustedCertificates == null || trustedCertificates.length == 0) { return this; } X509Certificate[] newTrustedCAs = ObjectArrays.concat(trustedCAs, trustedCertificates, X509Certificate.class); return new TrustSource(newTrustedCAs); } /** * Returns a new TrustSource containing the same trusted CAs as this TrustSource, plus all trusted certificate * entries from the specified trustStore. This method will only add trusted certificate entries from the specified * KeyStore (i.e. entries of type {@link KeyStore.TrustedCertificateEntry}; private keys will be * ignored. The trustStore may be in JKS or PKCS12 format. * * @param trustStore keystore containing trusted certificate entries * @return a new TrustSource containing this TrustSource's trusted CAs plus trusted certificate entries from the keystore */ public TrustSource add(KeyStore trustStore) { if (trustStore == null) { throw new IllegalArgumentException("Trust store cannot be null"); } List trustedCertificates = TrustUtil.extractTrustedCertificateEntries(trustStore); return add(trustedCertificates.toArray(new X509Certificate[0])); } /** * Returns a new TrustSource containing the same trusted CAs as this TrustSource, plus zero or more CAs contained in * the PEM-encoded File. The File may contain multiple certificates and may contain comments or other non-PEM-encoded * text, as long as the PEM-encoded certificates are delimited by appropriate BEGIN_CERTIFICATE and END_CERTIFICATE * text blocks. The file may contain UTF-8 characters, but the PEM-encoded certificate data itself must be US-ASCII. * * @param trustedCAPemFile File containing PEM-encoded certificates * @return a new TrustSource containing this TrustSource's trusted CAs plus the CAs in the specified String */ public TrustSource add(File trustedCAPemFile) { if (trustedCAPemFile == null) { throw new IllegalArgumentException("Trusted CA file cannot be null"); } String pemFileContents; try { pemFileContents = Files.toString(trustedCAPemFile, Charset.forName("UTF-8")); } catch (IOException e) { throw new UncheckedIOException("Unable to read file containing PEM-encoded trusted CAs: " + trustedCAPemFile.getAbsolutePath(), e); } return add(pemFileContents); } /** * Returns a new TrustSource containing the same trusted CAs as this TrustSource, plus the trusted CAs in the specified * TrustSource. * * @param trustSource TrustSource to combine with this TrustSource * @return a new TrustSource containing both TrustSources' trusted CAs */ public TrustSource add(TrustSource trustSource) { if (trustSource == null) { throw new IllegalArgumentException("TrustSource cannot be null"); } return add(trustSource.getTrustedCAs()); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/CertificateCreationException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates a problem creating a certificate (server or CA). */ public class CertificateCreationException extends RuntimeException { private static final long serialVersionUID = 592999944486567944L; public CertificateCreationException() { } public CertificateCreationException(String message) { super(message); } public CertificateCreationException(String message, Throwable cause) { super(message, cause); } public CertificateCreationException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/CertificateSourceException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates that a {@link net.lightbody.bmp.mitm.CertificateAndKeySource} encountered an error while loading a * certificate and/or private key from a KeyStore, PEM file, or other source. */ public class CertificateSourceException extends RuntimeException { private static final long serialVersionUID = 6195838041376082083L; public CertificateSourceException() { } public CertificateSourceException(String message) { super(message); } public CertificateSourceException(String message, Throwable cause) { super(message, cause); } public CertificateSourceException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/ExportException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates an error occurred while exporting/serializing a certificate, private key, KeyStore, etc. */ public class ExportException extends RuntimeException { private static final long serialVersionUID = -3505301862887355206L; public ExportException() { } public ExportException(String message) { super(message); } public ExportException(String message, Throwable cause) { super(message, cause); } public ExportException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/ImportException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates that an error occurred while importing a certificate, private key, or KeyStore. */ public class ImportException extends RuntimeException { private static final long serialVersionUID = 584414535648926010L; public ImportException() { } public ImportException(String message) { super(message); } public ImportException(String message, Throwable cause) { super(message, cause); } public ImportException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/KeyGeneratorException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates an exception occurred while generating a key pair. */ public class KeyGeneratorException extends RuntimeException { private static final long serialVersionUID = 7607159769324427808L; public KeyGeneratorException() { } public KeyGeneratorException(String message) { super(message); } public KeyGeneratorException(String message, Throwable cause) { super(message, cause); } public KeyGeneratorException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/KeyStoreAccessException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates an error occurred while accessing a java KeyStore. */ public class KeyStoreAccessException extends RuntimeException { private static final long serialVersionUID = -5560417886988154298L; public KeyStoreAccessException() { } public KeyStoreAccessException(String message) { super(message); } public KeyStoreAccessException(String message, Throwable cause) { super(message, cause); } public KeyStoreAccessException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/MitmException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates a general problem occurred while attempting to man-in-the-middle communications between the client and the * upstream server. */ public class MitmException extends RuntimeException { private static final long serialVersionUID = -1960691906515767537L; public MitmException() { } public MitmException(String message) { super(message); } public MitmException(String message, Throwable cause) { super(message, cause); } public MitmException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/SslContextInitializationException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates an error occurred while attempting to create a new {@link javax.net.ssl.SSLContext}. */ public class SslContextInitializationException extends RuntimeException { private static final long serialVersionUID = 6744059714710316821L; public SslContextInitializationException() { } public SslContextInitializationException(String message) { super(message); } public SslContextInitializationException(String message, Throwable cause) { super(message, cause); } public SslContextInitializationException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/TrustSourceException.java ================================================ package net.lightbody.bmp.mitm.exception; /** * Indicates that an error occurred while attempting to create or populate a {@link net.lightbody.bmp.mitm.TrustSource}. */ public class TrustSourceException extends RuntimeException { public TrustSourceException() { } public TrustSourceException(String message) { super(message); } public TrustSourceException(String message, Throwable cause) { super(message, cause); } public TrustSourceException(Throwable cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/exception/UncheckedIOException.java ================================================ package net.lightbody.bmp.mitm.exception; import java.io.IOException; /** * A convenience exception that wraps checked {@link IOException}s. (The built-in java.io.UncheckedIOException is only * available on Java 8.) */ public class UncheckedIOException extends RuntimeException { public UncheckedIOException(String message, IOException cause) { super(message, cause); } public UncheckedIOException(IOException cause) { super(cause); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/keys/ECKeyGenerator.java ================================================ package net.lightbody.bmp.mitm.keys; import net.lightbody.bmp.mitm.exception.KeyGeneratorException; import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.spec.ECGenParameterSpec; /** * A {@link KeyGenerator} that creates Elliptic Curve key pairs. */ public class ECKeyGenerator implements KeyGenerator { private static final String EC_KEY_GEN_ALGORITHM = "EC"; private static final String DEFAULT_NAMED_CURVE = "secp256r1"; private final String namedCurve; /** * Create a {@link KeyGenerator} that will create EC key pairs using the secp256r1 named curve (NIST P-256) * supported by modern web browsers. */ public ECKeyGenerator() { this.namedCurve = DEFAULT_NAMED_CURVE; } /** * Create a {@link KeyGenerator} that will create EC key pairs using the specified named curve. */ public ECKeyGenerator(String namedCurve) { this.namedCurve = namedCurve; } @Override public KeyPair generate() { // obtain an EC key pair generator for the specified named curve KeyPairGenerator generator; try { generator = KeyPairGenerator.getInstance(EC_KEY_GEN_ALGORITHM); ECGenParameterSpec ecName = new ECGenParameterSpec(namedCurve); generator.initialize(ecName); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { throw new KeyGeneratorException("Unable to generate EC public/private key pair using named curve: " + namedCurve, e); } return generator.generateKeyPair(); } @Override public String toString() { return "EC (" + namedCurve + ")"; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/keys/KeyGenerator.java ================================================ package net.lightbody.bmp.mitm.keys; import java.security.KeyPair; /** * A functional interface for key pair generators. */ public interface KeyGenerator { /** * Generates a new public/private key pair. This method should not cache or reuse any previously-generated key pairs. * * @return a new public/private key pair */ KeyPair generate(); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/keys/RSAKeyGenerator.java ================================================ package net.lightbody.bmp.mitm.keys; import net.lightbody.bmp.mitm.exception.KeyGeneratorException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; /** * A {@link KeyGenerator} that creates RSA key pairs. */ public class RSAKeyGenerator implements KeyGenerator { private static final String RSA_KEY_GEN_ALGORITHM = "RSA"; /** * Use a default RSA key size of 2048, since Chrome, Firefox, and possibly other browsers have begun to distrust * certificates signed with 1024-bit RSA keys. */ private static final int DEFAULT_KEY_SIZE = 2048; private final int keySize; /** * Create a {@link KeyGenerator} that will create a 2048-bit RSA key pair. */ public RSAKeyGenerator() { this.keySize = DEFAULT_KEY_SIZE; } /** * Create a {@link KeyGenerator} that will create an RSA key pair of the specified keySize. */ public RSAKeyGenerator(int keySize) { this.keySize = keySize; } @Override public KeyPair generate() { // obtain an RSA key pair generator for the specified key size KeyPairGenerator generator; try { generator = KeyPairGenerator.getInstance(RSA_KEY_GEN_ALGORITHM); generator.initialize(keySize); } catch (NoSuchAlgorithmException e) { throw new KeyGeneratorException("Unable to generate " + keySize + "-bit RSA public/private key pair", e); } return generator.generateKeyPair(); } @Override public String toString() { return "RSA (" + keySize + ")"; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java ================================================ package net.lightbody.bmp.mitm.manager; import android.os.Build; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateAndKeySource; import net.lightbody.bmp.mitm.CertificateInfo; import net.lightbody.bmp.mitm.CertificateInfoGenerator; import net.lightbody.bmp.mitm.HostnameCertificateInfoGenerator; import net.lightbody.bmp.mitm.RootCertificateGenerator; import net.lightbody.bmp.mitm.TrustSource; import net.lightbody.bmp.mitm.exception.MitmException; import net.lightbody.bmp.mitm.exception.SslContextInitializationException; import net.lightbody.bmp.mitm.keys.ECKeyGenerator; import net.lightbody.bmp.mitm.keys.KeyGenerator; import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; import net.lightbody.bmp.mitm.stats.CertificateGenerationStatistics; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.mitm.util.EncryptionUtil; import net.lightbody.bmp.mitm.util.MitmConstants; import net.lightbody.bmp.mitm.util.SslUtil; import net.lightbody.bmp.util.HttpUtil; import org.littleshoot.proxy.MitmManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.security.KeyPair; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSession; import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SupportedCipherSuiteFilter; /** * An {@link MitmManager} that will create SSLEngines for clients that present impersonated certificates for upstream servers. The impersonated * certificates will be signed using the certificate and private key specified in an {@link #rootCertificateSource}. The impersonated server * certificates will be created by the {@link #securityProviderTool} based on the {@link CertificateInfo} returned by the {@link #certificateInfoGenerator}. */ public class ImpersonatingMitmManager implements MitmManager { private static final Logger log = LoggerFactory.getLogger(ImpersonatingMitmManager.class); /** * Cipher suites allowed on proxy connections to upstream servers. */ private final List serverCipherSuites; /** * Cipher suites allowed on client connections to the proxy. */ private final List clientCipherSuites; /** * The SSLContext that will be used for communications with all upstream servers. This can be reused, so store it as a lazily-loaded singleton. */ private final Supplier upstreamServerSslContext = Suppliers.memoize(new Supplier() { @Override public SslContext get() { return SslUtil.getUpstreamServerSslContext(serverCipherSuites, trustSource); } }); /** * Cache for impersonating netty SslContexts. SslContexts can be safely reused, so caching the impersonating contexts avoids * repeatedly re-impersonating upstream servers. */ private final Cache sslContextCache; /** * Generator used to create public and private keys for the server certificates. */ private final KeyGenerator serverKeyGenerator; /** * The source of the CA's {@link CertificateAndKey} that will be used to sign generated server certificates. */ private final CertificateAndKeySource rootCertificateSource; /** * The message digest used to sign the server certificate, such as SHA512. * See https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest for information * on supported message digests. */ private final String serverCertificateMessageDigest; /** * The source of trusted root CAs. May be null, which disables all upstream certificate validation. Disabling upstream * certificate validation allows attackers to intercept communciations and should only be used during testing. */ private final TrustSource trustSource; /** * Utility used to generate {@link CertificateInfo} objects when impersonating an upstream server. */ private final CertificateInfoGenerator certificateInfoGenerator; /** * Tool implementation that is used to generate, sign, and otherwise manipulate server certificates. */ private final SecurityProviderTool securityProviderTool; /** * The CA root root certificate used to sign generated server certificates. {@link CertificateAndKeySource#load()} * is only called once to retrieve the CA root certificate, which will be used to impersonate all server certificates. */ private Supplier rootCertificate = Suppliers.memoize(new Supplier() { @Override public CertificateAndKey get() { return rootCertificateSource.load(); } }); /** * Simple server certificate generation statistics. */ private final CertificateGenerationStatistics statistics = new CertificateGenerationStatistics(); /** * Creates a new ImpersonatingMitmManager. In general, use {@link ImpersonatingMitmManager.Builder} * to construct new instances. */ public ImpersonatingMitmManager(CertificateAndKeySource rootCertificateSource, KeyGenerator serverKeyGenerator, String serverMessageDigest, TrustSource trustSource, int sslContextCacheConcurrencyLevel, long cacheExpirationIntervalMs, SecurityProviderTool securityProviderTool, CertificateInfoGenerator certificateInfoGenerator, Collection serverCipherSuites, Collection clientCipherSuites) { if (rootCertificateSource == null) { throw new IllegalArgumentException("CA root certificate source cannot be null"); } if (serverKeyGenerator == null) { throw new IllegalArgumentException("Server key generator cannot be null"); } if (serverMessageDigest == null) { throw new IllegalArgumentException("Server certificate message digest cannot be null"); } if (securityProviderTool == null) { throw new IllegalArgumentException("The certificate tool implementation cannot be null"); } if (certificateInfoGenerator == null) { throw new IllegalArgumentException("Certificate info generator cannot be null"); } this.rootCertificateSource = rootCertificateSource; this.trustSource = trustSource; this.serverCertificateMessageDigest = serverMessageDigest; this.serverKeyGenerator = serverKeyGenerator; this.sslContextCache = CacheBuilder.newBuilder() .concurrencyLevel(sslContextCacheConcurrencyLevel) .expireAfterAccess(cacheExpirationIntervalMs, TimeUnit.MILLISECONDS) .build(); this.securityProviderTool = securityProviderTool; this.certificateInfoGenerator = certificateInfoGenerator; this.serverCipherSuites = ImmutableList.copyOf(serverCipherSuites); log.debug("Allowed ciphers for proxy connections to upstream servers (some ciphers may not be available): {}", serverCipherSuites); this.clientCipherSuites = ImmutableList.copyOf(clientCipherSuites); log.debug("Allowed ciphers for client connections to proxy (some ciphers may not be available): {}", clientCipherSuites); } @Override public SSLEngine serverSslEngine() { try { SSLEngine sslEngine = upstreamServerSslContext.get().newEngine(ByteBufAllocator.DEFAULT); return sslEngine; } catch (RuntimeException e) { throw new MitmException("Error creating SSLEngine for connection to upstream server", e); } } @Override public SSLEngine serverSslEngine(String peerHost, int peerPort) { try { SSLEngine sslEngine = upstreamServerSslContext.get().newEngine(ByteBufAllocator.DEFAULT, peerHost, peerPort); // support SNI by setting the endpoint identification algorithm. this requires Java 7+. SSLParameters sslParams = new SSLParameters(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { sslParams.setEndpointIdentificationAlgorithm("HTTPS"); } sslEngine.setSSLParameters(sslParams); return sslEngine; } catch (RuntimeException e) { throw new MitmException("Error creating SSLEngine for connection to upstream server: " + peerHost + ":" + peerPort, e); } } @Override public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession sslSession) { String requestedHostname = HttpUtil.getHostFromRequest(httpRequest); try { SslContext ctx = getHostnameImpersonatingSslContext(requestedHostname, sslSession); return ctx.newEngine(ByteBufAllocator.DEFAULT); } catch (RuntimeException e) { throw new MitmException("Error creating SSLEngine for connection to client to impersonate upstream host: " + requestedHostname, e); } } /** * Retrieves an SSLContext that impersonates the specified hostname. If an impersonating SSLContext has already been * created for this hostname and is stored in the cache, it will be reused. Otherwise, a certificate will be created * which impersonates the specified hostname. * * @param hostnameToImpersonate the hostname for which the impersonated SSLContext is being requested * @param sslSession the upstream server SSLSession * @return SSLContext which will present an impersonated certificate */ private SslContext getHostnameImpersonatingSslContext(final String hostnameToImpersonate, final SSLSession sslSession) { try { return sslContextCache.get(hostnameToImpersonate, new Callable() { @Override public SslContext call() throws Exception { return createImpersonatingSslContext(sslSession, hostnameToImpersonate); } }); } catch (ExecutionException e) { throw new SslContextInitializationException("An error occurred while impersonating the remote host: " + hostnameToImpersonate, e); } //TODO: generate wildcard certificates, rather than one certificate per host, to reduce the number of certs generated } /** * Creates an SSLContext that will present an impersonated certificate for the specified hostname to the client. * This is a convenience method for {@link #createImpersonatingSslContext(CertificateInfo)} that generates the * {@link CertificateInfo} from the specified hostname using the {@link #certificateInfoGenerator}. * * @param sslSession sslSession between the proxy and the upstream server * @param hostnameToImpersonate hostname (supplied by the client's HTTP CONNECT) that will be impersonated * @return an SSLContext presenting a certificate matching the hostnameToImpersonate */ private SslContext createImpersonatingSslContext(SSLSession sslSession, String hostnameToImpersonate) { // get the upstream server's certificate so the certificateInfoGenerator can (optionally) use it to construct a forged certificate X509Certificate originalCertificate = SslUtil.getServerCertificate(sslSession); // get the CertificateInfo that will be used to populate the impersonated X509Certificate CertificateInfo certificateInfo = certificateInfoGenerator.generate(Collections.singletonList(hostnameToImpersonate), originalCertificate); SslContext sslContext = createImpersonatingSslContext(certificateInfo); return sslContext; } /** * Generates an {@link SslContext} using an impersonated certificate containing the information in the specified * certificateInfo. * * @param certificateInfo certificate information to impersonate * @return an SslContext that will present the impersonated certificate to the client */ private SslContext createImpersonatingSslContext(CertificateInfo certificateInfo) { long impersonationStart = System.currentTimeMillis(); // generate a public and private key pair for the forged certificate. the SslContext will send the impersonated certificate to clients // to impersonate the real upstream server, and will use the private key to encrypt the channel. KeyPair serverKeyPair = serverKeyGenerator.generate(); // get the CA root certificate and private key that will be used to sign the forced certificate X509Certificate caRootCertificate = rootCertificate.get().getCertificate(); PrivateKey caPrivateKey = rootCertificate.get().getPrivateKey(); if (caRootCertificate == null || caPrivateKey == null) { throw new IllegalStateException("A CA root certificate and private key are required to sign a server certificate. Root certificate was: " + caRootCertificate + ". Private key was: " + caPrivateKey); } // determine if the server private key was signed with an RSA private key. though TLS no longer requires the server // certificate to use the same private key type as the root certificate, Java bug JDK-8136442 prevents Java from creating a opening an SSL socket // if the CA and server certificates are not of the same type. see https://bugs.openjdk.java.net/browse/JDK-8136442 // note this only applies to RSA CAs signing EC server certificates; Java seems to properly handle EC CAs signing // RSA server certificates. if (EncryptionUtil.isEcKey(serverKeyPair.getPrivate()) && EncryptionUtil.isRsaKey(caPrivateKey)) { log.warn("CA private key is an RSA key and impersonated server private key is an Elliptic Curve key. JDK bug 8136442 may prevent the proxy server from creating connections to clients due to 'no cipher suites in common'."); } // create the forged server certificate and sign it with the root certificate and private key CertificateAndKey impersonatedCertificateAndKey = securityProviderTool.createServerCertificate( certificateInfo, caRootCertificate, caPrivateKey, serverKeyPair, serverCertificateMessageDigest); X509Certificate[] certChain = {impersonatedCertificateAndKey.getCertificate(), caRootCertificate}; SslContext sslContext; try { sslContext = SslContextBuilder.forServer(impersonatedCertificateAndKey.getPrivateKey(), certChain) .ciphers(clientCipherSuites, SupportedCipherSuiteFilter.INSTANCE) .build(); } catch (SSLException e) { throw new MitmException("Error creating SslContext for connection to client using impersonated certificate and private key", e); } long impersonationFinish = System.currentTimeMillis(); statistics.certificateCreated(impersonationStart, impersonationFinish); log.debug("Impersonated certificate for {} in {}ms", certificateInfo.getCommonName(), impersonationFinish - impersonationStart); return sslContext; } /** * Returns basic certificate generation statistics for this MitmManager. */ public CertificateGenerationStatistics getStatistics() { return this.statistics; } /** * Convenience method to return a new {@link Builder} instance default default values: a {@link RootCertificateGenerator} * that dynamically generates an RSA root certificate and RSA server certificates. */ public static Builder builder() { return new Builder(); } /** * Convenience method to return a new {@link Builder} instance that will dynamically create EC root certificates and * EC server certificates, but otherwise uses default values. */ public static Builder builderWithECC() { return new Builder() .serverKeyGenerator(new ECKeyGenerator()) .rootCertificateSource(RootCertificateGenerator.builder() .keyGenerator(new ECKeyGenerator()) .build()); } /** * A Builder for {@link ImpersonatingMitmManager}s. Initialized with suitable default values suitable for most purposes. */ public static class Builder { private CertificateAndKeySource rootCertificateSource = RootCertificateGenerator.builder().build(); private KeyGenerator serverKeyGenerator = new RSAKeyGenerator(); private TrustSource trustSource = TrustSource.defaultTrustSource(); private int cacheConcurrencyLevel = 8; private long cacheExpirationIntervalMs = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES); private String serverMessageDigest = MitmConstants.DEFAULT_MESSAGE_DIGEST; private SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); private CertificateInfoGenerator certificateInfoGenerator = new HostnameCertificateInfoGenerator(); private Collection serverCiphers; private Collection clientCiphers; /** * The source of the CA root certificate that will be used to sign the impersonated server certificates. Custom * certificates can be used by supplying an implementation of {@link CertificateAndKeySource}, such as * {@link net.lightbody.bmp.mitm.PemFileCertificateSource}. Alternatively, a new root certificate can be generated * and saved (for later import into browsers) using {@link RootCertificateGenerator}. * * @param certificateAndKeySource impersonation materials source to use */ public Builder rootCertificateSource(CertificateAndKeySource certificateAndKeySource) { this.rootCertificateSource = certificateAndKeySource; return this; } /** * The message digest that will be used when signing server certificates with the root certificate's private key. */ public Builder serverMessageDigest(String serverMessageDigest) { this.serverMessageDigest = serverMessageDigest; return this; } /** * When true, no upstream certificate verification will be performed. This will make it possible for * attackers to MITM communications with the upstream server, so use trustAllServers only when testing. * Calling this method with 'true' will remove any trustSource set with {@link #trustSource(TrustSource)}. * Calling this method with 'false' has no effect unless trustAllServers was previously called with 'true'. * To set a specific TrustSource, use {@link #trustSource(TrustSource)}. */ public Builder trustAllServers(boolean trustAllServers) { if (trustAllServers) { this.trustSource = null; } else { // if the TrustSource was previously removed, restore it to the default. otherwise keep the existing TrustSource. if (this.trustSource == null) { this.trustSource = TrustSource.defaultTrustSource(); } } return this; } /** * The TrustSource that supplies the trusted root CAs used to validate upstream servers' certificates. */ public Builder trustSource(TrustSource trustSource) { this.trustSource = trustSource; return this; } /** * The {@link KeyGenerator} that will be used to generate the server public and private keys. */ public Builder serverKeyGenerator(KeyGenerator serverKeyGenerator) { this.serverKeyGenerator = serverKeyGenerator; return this; } /** * The concurrency level for the SSLContext cache. Increase this beyond the default value for high-volume proxy servers. */ public Builder cacheConcurrencyLevel(int cacheConcurrencyLevel) { this.cacheConcurrencyLevel = cacheConcurrencyLevel; return this; } /** * The length of time SSLContexts with forged certificates will be kept in the cache. */ public Builder cacheExpirationInterval(long cacheExpirationInterval, TimeUnit timeUnit) { this.cacheExpirationIntervalMs = TimeUnit.MILLISECONDS.convert(cacheExpirationInterval, timeUnit); return this; } /** * The {@link CertificateInfoGenerator} that will populate {@link CertificateInfo} objects containing certificate data for * forced X509Certificates. */ public Builder certificateInfoGenerator(CertificateInfoGenerator certificateInfoGenerator) { this.certificateInfoGenerator = certificateInfoGenerator; return this; } /** * The cipher suites allowed on connections to upstream servers. Cipher suite names should be specified in Java * format, rather than OpenSSL format (e.g., TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384), even when using OpenSSL. * Ciphers will be preferred in the order they are returned by the collection's iterator. */ public Builder serverCiphers(Collection serverCiphers) { this.serverCiphers = serverCiphers; return this; } /** * The cipher suites allowed on client connections to the proxy. Cipher suite names should be specified in Java * format, rather than OpenSSL format (e.g., TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384), even when using OpenSSL. * Ciphers will be preferred in the order they are returned by the collection's iterator. */ public Builder clientCiphers(Collection clientCiphers) { this.clientCiphers = clientCiphers; return this; } /** * The {@link SecurityProviderTool} implementation that will be used to generate certificates. */ public Builder certificateTool(SecurityProviderTool securityProviderTool) { this.securityProviderTool = securityProviderTool; return this; } public ImpersonatingMitmManager build() { if (clientCiphers == null) { clientCiphers = SslUtil.getDefaultCipherList(); } if (serverCiphers == null) { serverCiphers = SslUtil.getDefaultCipherList(); } return new ImpersonatingMitmManager( rootCertificateSource, serverKeyGenerator, serverMessageDigest, trustSource, cacheConcurrencyLevel, cacheExpirationIntervalMs, securityProviderTool, certificateInfoGenerator, serverCiphers, clientCiphers ); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/stats/CertificateGenerationStatistics.java ================================================ package net.lightbody.bmp.mitm.stats; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; /** * Tracks basic certificate generation statistics. */ public class CertificateGenerationStatistics { private AtomicLong certificateGenerationTimeMs = new AtomicLong(); private AtomicInteger certificatesGenerated = new AtomicInteger(); private AtomicLong firstCertificateGeneratedTimestamp = new AtomicLong(); /** * Records a certificate generation that started at startTimeMs and completed at finishTimeMs. */ public void certificateCreated(long startTimeMs, long finishTimeMs) { certificatesGenerated.incrementAndGet(); certificateGenerationTimeMs.addAndGet(finishTimeMs - startTimeMs); // record the timestamp of the first certificate generation firstCertificateGeneratedTimestamp.compareAndSet(0L, System.currentTimeMillis()); } /** * Returns the total number of certificates created. */ public int getCertificatesGenerated() { return certificatesGenerated.get(); } /** * Returns the total number of ms spent generating all certificates. */ public long getTotalCertificateGenerationTimeMs() { return certificateGenerationTimeMs.get(); } /** * Returns the average number of ms per certificate generated. */ public long getAvgCertificateGenerationTimeMs() { if (certificatesGenerated.get() > 0) { return certificateGenerationTimeMs.get() / certificatesGenerated.get(); } else { return 0L; } } /** * Returns the timestamp (ms since epoch) when the first certificate was generated, or 0 if none have been generated. */ public long firstCertificateGeneratedTimestamp() { return firstCertificateGeneratedTimestamp.get(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java ================================================ package net.lightbody.bmp.mitm.tools; import com.google.common.net.InetAddresses; import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateInfo; import net.lightbody.bmp.mitm.exception.CertificateCreationException; import net.lightbody.bmp.mitm.exception.ExportException; import net.lightbody.bmp.mitm.exception.ImportException; import net.lightbody.bmp.mitm.util.EncryptionUtil; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.cert.CertIOException; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.bc.BcX509ExtensionUtils; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMDecryptorProvider; import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMEncryptor; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringWriter; import java.math.BigInteger; import java.security.Key; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import javax.net.ssl.KeyManager; public class BouncyCastleSecurityProviderTool implements SecurityProviderTool { static { Security.addProvider(new BouncyCastleProvider()); } /** * The size of certificate serial numbers, in bits. */ private static final int CERTIFICATE_SERIAL_NUMBER_SIZE = 160; @Override public CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, X509Certificate caRootCertificate, PrivateKey caPrivateKey, KeyPair serverKeyPair, String messageDigest) { // make sure certificateInfo contains all fields necessary to generate the certificate if (certificateInfo.getCommonName() == null) { throw new IllegalArgumentException("Must specify CN for server certificate"); } if (certificateInfo.getNotBefore() == null) { throw new IllegalArgumentException("Must specify Not Before for server certificate"); } if (certificateInfo.getNotAfter() == null) { throw new IllegalArgumentException("Must specify Not After for server certificate"); } // create the subject for the new server certificate. when impersonating an upstream server, this should contain // the hostname of the server we are trying to impersonate in the CN field X500Name serverCertificateSubject = createX500NameForCertificate(certificateInfo); // get the algorithm that will be used to sign the new certificate, which is a combination of the message digest // and the digital signature from the CA's private key String signatureAlgorithm = EncryptionUtil.getSignatureAlgorithm(messageDigest, caPrivateKey); // get a ContentSigner with our CA private key that will be used to sign the new server certificate ContentSigner signer = getCertificateSigner(caPrivateKey, signatureAlgorithm); // generate a serial number for the new certificate. serial numbers only need to be unique within our // certification authority; a large random integer will satisfy that requirement. BigInteger serialNumber = EncryptionUtil.getRandomBigInteger(CERTIFICATE_SERIAL_NUMBER_SIZE); // create the X509Certificate using Bouncy Castle. the BC X509CertificateHolder can be converted to a JCA X509Certificate. X509CertificateHolder certificateHolder; try { certificateHolder = new JcaX509v3CertificateBuilder(caRootCertificate, serialNumber, certificateInfo.getNotBefore(), certificateInfo.getNotAfter(), serverCertificateSubject, serverKeyPair.getPublic()) .addExtension(Extension.subjectAlternativeName, false, getDomainNameSANsAsASN1Encodable(certificateInfo.getSubjectAlternativeNames())) .addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(serverKeyPair.getPublic())) .addExtension(Extension.basicConstraints, false, new BasicConstraints(false)) .build(signer); } catch (CertIOException e) { throw new CertificateCreationException("Error creating new server certificate", e); } // convert the Bouncy Castle certificate holder into a JCA X509Certificate X509Certificate serverCertificate = convertToJcaCertificate(certificateHolder); return new CertificateAndKey(serverCertificate, serverKeyPair.getPrivate()); } @Override public KeyStore createServerKeyStore(String keyStoreType, CertificateAndKey serverCertificateAndKey, X509Certificate rootCertificate, String privateKeyAlias, String password) { throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); } @Override public KeyStore createRootCertificateKeyStore(String keyStoreType, CertificateAndKey rootCertificateAndKey, String privateKeyAlias, String password) { throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); } @Override public CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo, KeyPair keyPair, String messageDigest) { if (certificateInfo.getNotBefore() == null) { throw new IllegalArgumentException("Must specify Not Before for server certificate"); } if (certificateInfo.getNotAfter() == null) { throw new IllegalArgumentException("Must specify Not After for server certificate"); } // create the X500Name that will be both the issuer and the subject of the new root certificate X500Name issuer = createX500NameForCertificate(certificateInfo); BigInteger serial = EncryptionUtil.getRandomBigInteger(CERTIFICATE_SERIAL_NUMBER_SIZE); PublicKey rootCertificatePublicKey = keyPair.getPublic(); String signatureAlgorithm = EncryptionUtil.getSignatureAlgorithm(messageDigest, keyPair.getPrivate()); // this is a CA root certificate, so it is self-signed ContentSigner selfSigner = getCertificateSigner(keyPair.getPrivate(), signatureAlgorithm); ASN1EncodableVector extendedKeyUsages = new ASN1EncodableVector(); extendedKeyUsages.add(KeyPurposeId.id_kp_serverAuth); extendedKeyUsages.add(KeyPurposeId.id_kp_clientAuth); extendedKeyUsages.add(KeyPurposeId.anyExtendedKeyUsage); X509CertificateHolder certificateHolder; try { certificateHolder = new JcaX509v3CertificateBuilder( issuer, serial, certificateInfo.getNotBefore(), certificateInfo.getNotAfter(), issuer, rootCertificatePublicKey) .addExtension(Extension.subjectKeyIdentifier, false, createSubjectKeyIdentifier(rootCertificatePublicKey)) .addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) .addExtension(Extension.keyUsage, false, new KeyUsage( KeyUsage.keyCertSign | KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.cRLSign)) .addExtension(Extension.extendedKeyUsage, false, new DERSequence(extendedKeyUsages)) .build(selfSigner); } catch (CertIOException e) { throw new CertificateCreationException("Error creating root certificate", e); } // convert the Bouncy Castle X590CertificateHolder to a JCA cert X509Certificate cert = convertToJcaCertificate(certificateHolder); return new CertificateAndKey(cert, keyPair.getPrivate()); } @Override public String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm) { if (passwordForPrivateKey == null) { throw new IllegalArgumentException("You must specify a password when serializing a private key"); } PEMEncryptor encryptor = new JcePEMEncryptorBuilder(encryptionAlgorithm) .build(passwordForPrivateKey.toCharArray()); return encodeObjectAsPemString(privateKey, encryptor); } @Override public String encodeCertificateAsPem(Certificate certificate) { return encodeObjectAsPemString(certificate, null); } @Override public PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password) { try { PEMParser pemParser = new PEMParser(privateKeyReader); Object keyPair = pemParser.readObject(); // retrieve the PrivateKeyInfo from the returned keyPair object. if the key is encrypted, it needs to be // decrypted using the specified password first. PrivateKeyInfo keyInfo; if (keyPair instanceof PEMEncryptedKeyPair) { if (password == null) { throw new ImportException("Unable to import private key. Key is encrypted, but no password was provided."); } PEMDecryptorProvider decryptor = new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptor); keyInfo = decryptedKeyPair.getPrivateKeyInfo(); } else { keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); } return new JcaPEMKeyConverter().getPrivateKey(keyInfo); } catch (IOException e) { throw new ImportException("Unable to read PEM-encoded PrivateKey", e); } } @Override public X509Certificate decodePemEncodedCertificate(Reader certificateReader) { // JCA provides this functionality already, but it can be easily implemented using BC as well throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); } @Override public KeyStore loadKeyStore(File file, String keyStoreType, String password) { throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); } @Override public void saveKeyStore(File file, KeyStore keyStore, String keystorePassword) { throw new UnsupportedOperationException("BouncyCastle implementation does not implement this method"); } @Override public KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword) { return new KeyManager[0]; } /** * Creates an X500Name based on the specified certificateInfo. * * @param certificateInfo information to populate the X500Name with * @return a new X500Name object for use as a subject or issuer */ private static X500Name createX500NameForCertificate(CertificateInfo certificateInfo) { X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE); if (certificateInfo.getCommonName() != null) { x500NameBuilder.addRDN(BCStyle.CN, certificateInfo.getCommonName()); } if (certificateInfo.getOrganization() != null) { x500NameBuilder.addRDN(BCStyle.O, certificateInfo.getOrganization()); } if (certificateInfo.getOrganizationalUnit() != null) { x500NameBuilder.addRDN(BCStyle.OU, certificateInfo.getOrganizationalUnit()); } if (certificateInfo.getEmail() != null) { x500NameBuilder.addRDN(BCStyle.E, certificateInfo.getEmail()); } if (certificateInfo.getLocality() != null) { x500NameBuilder.addRDN(BCStyle.L, certificateInfo.getLocality()); } if (certificateInfo.getState() != null) { x500NameBuilder.addRDN(BCStyle.ST, certificateInfo.getState()); } if (certificateInfo.getCountryCode() != null) { x500NameBuilder.addRDN(BCStyle.C, certificateInfo.getCountryCode()); } // TODO: Add more X.509 certificate fields as needed return x500NameBuilder.build(); } /** * Converts a list of domain name Subject Alternative Names into ASN1Encodable GeneralNames objects, for use with * the Bouncy Castle certificate builder. * * @param subjectAlternativeNames domain name SANs to convert * @return a GeneralNames instance that includes the specifie dsubjectAlternativeNames as DNS name fields */ private static GeneralNames getDomainNameSANsAsASN1Encodable(List subjectAlternativeNames) { List encodedSANs = new ArrayList<>(subjectAlternativeNames.size()); for (String subjectAlternativeName : subjectAlternativeNames) { // IP addresses use the IP Address tag instead of the DNS Name tag in the SAN list boolean isIpAddress = InetAddresses.isInetAddress(subjectAlternativeName); GeneralName generalName = new GeneralName(isIpAddress ? GeneralName.iPAddress : GeneralName.dNSName, subjectAlternativeName); encodedSANs.add(generalName); } return new GeneralNames(encodedSANs.toArray(new GeneralName[encodedSANs.size()])); } /** * Creates a ContentSigner that can be used to sign certificates with the given private key and signature algorithm. * * @param certAuthorityPrivateKey the private key to use to sign certificates * @param signatureAlgorithm the algorithm to use to sign certificates * @return a ContentSigner */ private static ContentSigner getCertificateSigner(PrivateKey certAuthorityPrivateKey, String signatureAlgorithm) { try { return new JcaContentSignerBuilder(signatureAlgorithm) .build(certAuthorityPrivateKey); } catch (OperatorCreationException e) { throw new CertificateCreationException("Unable to create ContentSigner using signature algorithm: " + signatureAlgorithm, e); } } /** * Converts a Bouncy Castle X509CertificateHolder into a JCA X590Certificate. * * @param bouncyCastleCertificate BC X509CertificateHolder * @return JCA X509Certificate */ private static X509Certificate convertToJcaCertificate(X509CertificateHolder bouncyCastleCertificate) { try { return new JcaX509CertificateConverter() .getCertificate(bouncyCastleCertificate); } catch (CertificateException e) { throw new CertificateCreationException("Unable to convert X590CertificateHolder to JCA X590Certificate", e); } } /** * Creates the SubjectKeyIdentifier for a Bouncy Castle X590CertificateHolder. * * @param key public key to identify * @return SubjectKeyIdentifier for the specified key */ private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) { SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(key.getEncoded()); return new BcX509ExtensionUtils().createSubjectKeyIdentifier(publicKeyInfo); } /** * Encodes the specified security object in PEM format, using the specified encryptor. If the encryptor is null, * the object will not be encrypted in the generated String. * * @param object object to encrypt (certificate, private key, etc.) * @param encryptor engine to encrypt the resulting PEM String, or null if no encryption should be used * @return a PEM-encoded String */ private static String encodeObjectAsPemString(Object object, PEMEncryptor encryptor) { StringWriter stringWriter = new StringWriter(); try { JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); pemWriter.writeObject(object, encryptor); pemWriter.flush(); } catch (IOException e) { throw new ExportException("Unable to generate PEM string representing object", e); } return stringWriter.toString(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java ================================================ package net.lightbody.bmp.mitm.tools; import com.google.common.io.CharStreams; import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateInfo; import net.lightbody.bmp.mitm.exception.ImportException; import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; import net.lightbody.bmp.mitm.util.KeyStoreUtil; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.charset.Charset; import java.security.KeyPair; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import javax.net.ssl.KeyManager; /** * A {@link SecurityProviderTool} implementation that uses the default system Security provider where possible, but uses the * Bouncy Castle provider for operations that the JCA does not provide or implement (e.g. certificate generation and signing). */ public class DefaultSecurityProviderTool implements SecurityProviderTool { private final SecurityProviderTool bouncyCastle = new BouncyCastleSecurityProviderTool(); @Override public CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo, KeyPair keyPair, String messageDigest) { return bouncyCastle.createCARootCertificate(certificateInfo, keyPair, messageDigest); } @Override public CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, X509Certificate caRootCertificate, PrivateKey caPrivateKey, KeyPair serverKeyPair, String messageDigest) { return bouncyCastle.createServerCertificate(certificateInfo, caRootCertificate, caPrivateKey, serverKeyPair, messageDigest); } @Override public KeyStore createServerKeyStore(String keyStoreType, CertificateAndKey serverCertificateAndKey, X509Certificate rootCertificate, String privateKeyAlias, String password) { if (password == null) { throw new IllegalArgumentException("KeyStore password cannot be null"); } if (privateKeyAlias == null) { throw new IllegalArgumentException("Private key alias cannot be null"); } // create a KeyStore containing the impersonated certificate's private key and a certificate chain with the // impersonated cert and our root certificate KeyStore impersonatedCertificateKeyStore = KeyStoreUtil.createEmptyKeyStore(keyStoreType, null); // create the certificate chain back for the impersonated certificate back to the root certificate Certificate[] chain = {serverCertificateAndKey.getCertificate(), rootCertificate}; try { // place the impersonated certificate and its private key in the KeyStore impersonatedCertificateKeyStore.setKeyEntry(privateKeyAlias, serverCertificateAndKey.getPrivateKey(), password.toCharArray(), chain); } catch (KeyStoreException e) { throw new KeyStoreAccessException("Error storing impersonated certificate and private key in KeyStore", e); } return impersonatedCertificateKeyStore; } @Override public KeyStore createRootCertificateKeyStore(String keyStoreType, CertificateAndKey rootCertificateAndKey, String privateKeyAlias, String password) { return KeyStoreUtil.createRootCertificateKeyStore(keyStoreType, rootCertificateAndKey.getCertificate(), privateKeyAlias, rootCertificateAndKey.getPrivateKey(), password, null); } @Override public String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm) { return bouncyCastle.encodePrivateKeyAsPem(privateKey, passwordForPrivateKey, encryptionAlgorithm); } @Override public String encodeCertificateAsPem(Certificate certificate) { return bouncyCastle.encodeCertificateAsPem(certificate); } @Override public PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password) { return bouncyCastle.decodePemEncodedPrivateKey(privateKeyReader, password); } @Override public X509Certificate decodePemEncodedCertificate(Reader certificateReader) { // JCA supports reading PEM-encoded X509Certificates fairly easily, so there is no need to use BC to read the cert Certificate certificate; // the JCA CertificateFactory takes an InputStream, so convert the reader to a stream first. converting to a String first // is not ideal, but is relatively straightforward. (PEM certificates should only contain US_ASCII-compatible characters.) try { InputStream certificateAsStream = new ByteArrayInputStream(CharStreams.toString(certificateReader).getBytes(Charset.forName("US-ASCII"))); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); certificate = certificateFactory.generateCertificate(certificateAsStream); } catch (CertificateException | IOException e) { throw new ImportException("Unable to read PEM-encoded X509Certificate", e); } if (!(certificate instanceof X509Certificate)) { throw new ImportException("Attempted to import non-X.509 certificate as X.509 certificate"); } return (X509Certificate) certificate; } /** * Loads the KeyStore from the specified InputStream. The InputStream is not closed after the KeyStore has been read. * * @param file file containing a KeyStore * @param keyStoreType KeyStore type, such as "JKS" or "PKCS12" * @param password password of the KeyStore * @return KeyStore loaded from the input stream */ @Override public KeyStore loadKeyStore(File file, String keyStoreType, String password) { KeyStore keyStore; try { keyStore = KeyStore.getInstance(keyStoreType); } catch (KeyStoreException e) { throw new KeyStoreAccessException("Unable to get KeyStore instance of type: " + keyStoreType, e); } try { InputStream keystoreAsStream = new FileInputStream(file); keyStore.load(keystoreAsStream, password.toCharArray()); } catch (IOException e) { throw new ImportException("Unable to read KeyStore from file: " + file.getName(), e); } catch (CertificateException | NoSuchAlgorithmException e) { throw new ImportException("Error while reading KeyStore", e); } return keyStore; } /** * Exports the keyStore to the specified file. * * @param file file to save the KeyStore to * @param keyStore KeyStore to export * @param keystorePassword the password for the KeyStore */ @Override public void saveKeyStore(File file, KeyStore keyStore, String keystorePassword) { try { FileOutputStream fos = new FileOutputStream(file); keyStore.store(fos, keystorePassword.toCharArray()); } catch (CertificateException | NoSuchAlgorithmException | IOException | KeyStoreException e) { throw new KeyStoreAccessException("Unable to save KeyStore to file: " + file.getName(), e); } } @Override public KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword) { return KeyStoreUtil.getKeyManagers(keyStore, keyStorePassword, null, null); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java ================================================ package net.lightbody.bmp.mitm.tools; import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateInfo; import java.io.File; import java.io.Reader; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import javax.net.ssl.KeyManager; /** * Generic interface for functionality provided by a Security Provider. */ public interface SecurityProviderTool { /** * Creates a new self-signed CA root certificate, suitable for use signing new server certificates. * * @param certificateInfo certificate info to populate in the new root cert * @param keyPair root certificate's public and private keys * @param messageDigest digest to use when signing the new root certificate, such as SHA512 * @return a new root certificate and private key */ CertificateAndKey createCARootCertificate(CertificateInfo certificateInfo, KeyPair keyPair, String messageDigest); /** * Creates a new server X.509 certificate using the serverKeyPair. The new certificate will be populated with * information from the specified certificateInfo and will be signed using the specified caPrivateKey and messageDigest. * * @param certificateInfo basic X.509 certificate info that will be used to create the server certificate * @param caRootCertificate root certificate that will be used to populate the issuer field of the server certificate * @param serverKeyPair server's public and private keys * @param messageDigest message digest to use when signing the server certificate, such as SHA512 * @param caPrivateKey root certificate private key that will be used to sign the server certificate * @return a new server certificate and its private key */ CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, X509Certificate caRootCertificate, PrivateKey caPrivateKey, KeyPair serverKeyPair, String messageDigest); /** * Assembles a Java KeyStore containing a server's certificate, private key, and the certificate authority's certificate, * which can be used to create an {@link javax.net.ssl.SSLContext}. * * @param keyStoreType the KeyStore type, such as JKS or PKCS12 * @param serverCertificateAndKey certificate and private key for the server, which will be placed in the KeyStore * @param rootCertificate CA root certificate of the private key that signed the server certificate * @param privateKeyAlias alias to assign the private key (with accompanying certificate chain) to in the KeyStore * @param password password for the new KeyStore and private key * @return a new KeyStore with the server's certificate and password-protected private key */ KeyStore createServerKeyStore(String keyStoreType, CertificateAndKey serverCertificateAndKey, X509Certificate rootCertificate, String privateKeyAlias, String password); /** * Assembles a Java KeyStore containing a CA root certificate and its private key. * * @param keyStoreType the KeyStore type, such as JKS or PKCS12 * @param rootCertificateAndKey certification authority's root certificate and private key, which will be placed in the KeyStore * @param privateKeyAlias alias to assign the private key (with accompanying certificate chain) to in the KeyStore * @param password password for the new KeyStore and private key * @return a new KeyStore with the root certificate and password-protected private key */ KeyStore createRootCertificateKeyStore(String keyStoreType, CertificateAndKey rootCertificateAndKey, String privateKeyAlias, String password); /** * Encodes a private key in PEM format, encrypting it with the specified password. The private key will be encrypted * using the specified algorithm. * * @param privateKey private key to encode * @param passwordForPrivateKey password to protect the private key * @param encryptionAlgorithm algorithm to use to encrypt the private key * @return PEM-encoded private key as a String */ String encodePrivateKeyAsPem(PrivateKey privateKey, String passwordForPrivateKey, String encryptionAlgorithm); /** * Encodes a certificate in PEM format. * * @param certificate certificate to encode * @return PEM-encoded certificate as a String */ String encodeCertificateAsPem(Certificate certificate); /** * Decodes a PEM-encoded private key into a {@link PrivateKey}. The password may be null if the PEM-encoded private key * is not password-encrypted. * * @param privateKeyReader a reader for a PEM-encoded private key * @param password password protecting the private key @return the decoded private key */ PrivateKey decodePemEncodedPrivateKey(Reader privateKeyReader, String password); /** * Decodes a PEM-encoded X.509 Certificate into a {@link X509Certificate}. * * @param certificateReader a reader for a PEM-encoded certificate * @return the decoded X.509 certificate */ X509Certificate decodePemEncodedCertificate(Reader certificateReader); /** * Loads a Java KeyStore object from a file. * * @param file KeyStore file to load * @param keyStoreType KeyStore type (PKCS12, JKS, etc.) * @param password the KeyStore password * @return an initialized Java KeyStore object */ KeyStore loadKeyStore(File file, String keyStoreType, String password); /** * Saves a Java KeyStore to a file, protecting it with the specified password. * * @param file file to save the KeyStore to * @param keyStore KeyStore to save * @param keystorePassword password for the KeyStore */ void saveKeyStore(File file, KeyStore keyStore, String keystorePassword); /** * Retrieve the KeyManagers for the specified KeyStore. * * @param keyStore the KeyStore to retrieve KeyManagers from * @param keyStorePassword the KeyStore password * @return KeyManagers for the specified KeyStore */ KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureExtendedTrustManager.java ================================================ package net.lightbody.bmp.mitm.trustmanager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.Socket; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import me.moxun.dreamcatcher.misc.X509ExtendedTrustManager; import io.netty.util.internal.EmptyArrays; /** * An {@link X509ExtendedTrustManager} and {@link javax.net.ssl.X509TrustManager} that will accept all server and client * certificates. Before accepting a certificate, the InsecureExtendedTrustManager uses the default X509ExtendedTrustManager * to determine if the certificate would otherwise be trusted, and logs a debug-level message if it is not trusted. */ public class InsecureExtendedTrustManager extends X509ExtendedTrustManager { private static final Logger log = LoggerFactory.getLogger(InsecureExtendedTrustManager.class); /** * The default extended trust manager, which will be used to determine if certificates would otherwise be trusted. */ protected static final X509ExtendedTrustManager DEFAULT_EXTENDED_TRUST_MANAGER = getDefaultExtendedTrustManager(); /** * An {@link X509ExtendedTrustManager} that does no certificate validation whatsoever. */ private static final X509ExtendedTrustManager NOOP_EXTENDED_TRUST_MANAGER = new X509ExtendedTrustManager() { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { } @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { } @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return EmptyArrays.EMPTY_X509_CERTIFICATES; } }; @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { try { DEFAULT_EXTENDED_TRUST_MANAGER.checkClientTrusted(x509Certificates, s, socket); } catch (Exception e) { log.debug("Accepting an untrusted client certificate: {}", x509Certificates[0].getSubjectDN(), e); } } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { try { DEFAULT_EXTENDED_TRUST_MANAGER.checkServerTrusted(x509Certificates, s, socket); } catch (Exception e) { log.debug("Accepting an untrusted server certificate: {}", x509Certificates[0].getSubjectDN(), e); } } @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { try { DEFAULT_EXTENDED_TRUST_MANAGER.checkClientTrusted(x509Certificates, s, sslEngine); } catch (Exception e) { log.debug("Accepting an untrusted client certificate: {}", x509Certificates[0].getSubjectDN(), e); } } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException { try { DEFAULT_EXTENDED_TRUST_MANAGER.checkServerTrusted(x509Certificates, s, sslEngine); } catch (Exception e) { log.debug("Accepting an untrusted server certificate: {}", x509Certificates[0].getSubjectDN(), e); } } @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { try { DEFAULT_EXTENDED_TRUST_MANAGER.checkClientTrusted(x509Certificates, s); } catch (Exception e) { log.debug("Accepting an untrusted client certificate: {}", x509Certificates[0].getSubjectDN(), e); } } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { try { DEFAULT_EXTENDED_TRUST_MANAGER.checkServerTrusted(x509Certificates, s); } catch (Exception e) { log.debug("Accepting an untrusted server certificate: {}", x509Certificates[0].getSubjectDN(), e); } } @Override public X509Certificate[] getAcceptedIssuers() { return EmptyArrays.EMPTY_X509_CERTIFICATES; } /** * Returns the JDK's default X509ExtendedTrustManager, or a no-op trust manager if the default cannot be found. */ private static X509ExtendedTrustManager getDefaultExtendedTrustManager() { TrustManagerFactory trustManagerFactory; try { trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // initialize the TrustManagerFactory with the default KeyStore trustManagerFactory.init((KeyStore) null); } catch (NoSuchAlgorithmException | KeyStoreException e) { log.debug("Unable to initialize default TrustManagerFactory. Using no-op X509ExtendedTrustManager.", e); return NOOP_EXTENDED_TRUST_MANAGER; } // find the X509ExtendedTrustManager in the list of registered trust managers for (TrustManager tm : trustManagerFactory.getTrustManagers()) { if (tm instanceof X509ExtendedTrustManager) { return (X509ExtendedTrustManager) tm; } } // no default X509ExtendedTrustManager found, so return a no-op log.debug("No default X509ExtendedTrustManager found. Using no-op."); return NOOP_EXTENDED_TRUST_MANAGER; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureTrustManagerFactory.java ================================================ /* * Copyright 2014 The Netty Project, * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package net.lightbody.bmp.mitm.trustmanager; import java.security.KeyStore; import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import me.moxun.dreamcatcher.misc.X509ExtendedTrustManager; import io.netty.handler.ssl.util.SimpleTrustManagerFactory; /** * Note: This is a modified version of {@link io.netty.handler.ssl.util.InsecureTrustManagerFactory} from Netty * 4.0.36. Unlike the netty version, this class returns an {@link X509ExtendedTrustManager} instead of an * {@link javax.net.ssl.X509TrustManager} instance, which allows us to bypass additional certificate validations. *

* An insecure {@link TrustManagerFactory} that trusts all X.509 certificates without any verification. *

* NOTE: * Never use this {@link TrustManagerFactory} in production. * It is purely for testing purposes, and thus it is very insecure. *

*/ public class InsecureTrustManagerFactory extends SimpleTrustManagerFactory { public static final TrustManagerFactory INSTANCE = new InsecureTrustManagerFactory(); public static final X509ExtendedTrustManager tm = new InsecureExtendedTrustManager(); @Override protected void engineInit(KeyStore keyStore) throws Exception { } @Override protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { } @Override protected TrustManager[] engineGetTrustManagers() { return new TrustManager[]{tm}; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java ================================================ package net.lightbody.bmp.mitm.util; import net.lightbody.bmp.mitm.exception.ExportException; import net.lightbody.bmp.mitm.exception.ImportException; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.Charset; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.interfaces.DSAKey; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.util.Random; import javax.crypto.Cipher; /** * A collection of simple JCA-related utilities. */ public class EncryptionUtil { /** * Creates a signature algorithm string using the specified message digest and the encryption type corresponding * to the supplied signingKey. Useful when generating the signature algorithm to be used to sign server certificates * using the CA root certificate's signingKey. *

* For example, if the root certificate has an RSA private key, and you * wish to use the SHA256 message digest, this method will return the string "SHA256withRSA". See the * "Signature Algorithms" section of http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html * for a list of JSSE-supported signature algorithms. * * @param messageDigest digest to use to sign the certificate, such as SHA512 * @param signingKey private key that will be used to sign the certificate * @return a JCA-compatible signature algorithm */ public static String getSignatureAlgorithm(String messageDigest, Key signingKey) { return messageDigest + "with" + getDigitalSignatureType(signingKey); } /** * Returns the type of digital signature used with the specified signing key. * * @param signingKey private key that will be used to sign a certificate (or something else) * @return a string representing the digital signature type (ECDSA, RSA, etc.) */ public static String getDigitalSignatureType(Key signingKey) { if (signingKey instanceof ECKey) { return "ECDSA"; } else if (signingKey instanceof RSAKey) { return "RSA"; } else if (signingKey instanceof DSAKey) { return "DSA"; } else { throw new IllegalArgumentException("Cannot determine digital signature encryption type for unknown key type: " + signingKey.getClass().getCanonicalName()); } } /** * Creates a random BigInteger greater than 0 with the specified number of bits. * * @param bits number of bits to generate * @return random BigInteger */ public static BigInteger getRandomBigInteger(int bits) { return new BigInteger(bits, new Random()); } /** * Returns true if the key is an RSA public or private key. */ public static boolean isRsaKey(Key key) { return "RSA".equals(key.getAlgorithm()); } /** * Returns true if the key is an elliptic curve public or private key. */ public static boolean isEcKey(Key key) { return "EC".equals(key.getAlgorithm()); } /** * Convenience method to write PEM data to a file. The file will be encoded in the US_ASCII character set. * * @param file file to write to * @param pemDataToWrite PEM data to write to the file */ public static void writePemStringToFile(File file, String pemDataToWrite) { try { FileUtils.write(file, pemDataToWrite); } catch (IOException e) { throw new ExportException("Unable to write PEM string to file: " + file.getName(), e); } } /** * Convenience method to read PEM data from a file. The file encoding must be US_ASCII. * * @param file file to read from * @return PEM data from file */ public static String readPemStringFromFile(File file) { try { byte[] fileContents = FileUtils.readFileToByteArray(file); return new String(fileContents, Charset.forName("US-ASCII")); } catch (IOException e) { throw new ImportException("Unable to read PEM-encoded data from file: " + file.getName()); } } /** * Determines if unlimited-strength cryptography is allowed, i.e. if this JRE has then the unlimited strength policy * files installed. * * @return true if unlimited strength cryptography is allowed, otherwise false */ public static boolean isUnlimitedStrengthAllowed() { try { return Cipher.getMaxAllowedKeyLength("AES") >= 256; } catch (NoSuchAlgorithmException e) { return false; } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java ================================================ package net.lightbody.bmp.mitm.util; import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; /** * Utility for loading, saving, and manipulating {@link KeyStore}s. */ public class KeyStoreUtil { /** * Creates and initializes an empty KeyStore using the specified keyStoreType. * * @param keyStoreType type of key store to initialize, or null to use the system default * @param provider JCA provider to use, or null to use the system default * @return a new KeyStore */ public static KeyStore createEmptyKeyStore(String keyStoreType, String provider) { if (keyStoreType == null) { keyStoreType = "BKS"; } KeyStore keyStore; try { if (provider == null) { keyStore = KeyStore.getInstance(keyStoreType); } else { keyStore = KeyStore.getInstance(keyStoreType, provider); } keyStore.load(null, null); } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException | IOException e) { throw new KeyStoreAccessException("Error creating or initializing new KeyStore of type: " + keyStoreType, e); } return keyStore; } /** * Creates a new KeyStore containing the specified root certificate and private key. * * @param keyStoreType type of the generated KeyStore, such as PKCS12 or JKS * @param certificate root certificate to add to the KeyStore * @param privateKeyAlias alias for the private key in the KeyStore * @param privateKey private key to add to the KeyStore * @param privateKeyPassword password for the private key * @param provider JCA provider to use, or null to use the system default * @return new KeyStore containing the root certificate and private key */ public static KeyStore createRootCertificateKeyStore(String keyStoreType, X509Certificate certificate, String privateKeyAlias, PrivateKey privateKey, String privateKeyPassword, String provider) { if (privateKeyPassword == null) { throw new IllegalArgumentException("Must specify a KeyStore password"); } KeyStore newKeyStore = KeyStoreUtil.createEmptyKeyStore(keyStoreType, provider); try { newKeyStore.setKeyEntry(privateKeyAlias, privateKey, privateKeyPassword.toCharArray(), new Certificate[]{certificate}); } catch (KeyStoreException e) { throw new KeyStoreAccessException("Unable to store certificate and private key in KeyStore", e); } return newKeyStore; } /** * Retrieve the KeyManagers for the specified KeyStore. * * @param keyStore the KeyStore to retrieve KeyManagers from * @param keyStorePassword the KeyStore password * @param keyManagerAlgorithm key manager algorithm to use, or null to use the system default * @param provider JCA provider to use, or null to use the system default * @return KeyManagers for the specified KeyStore */ public static KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword, String keyManagerAlgorithm, String provider) { if (keyManagerAlgorithm == null) { keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); } try { KeyManagerFactory kmf; if (provider == null) { kmf = KeyManagerFactory.getInstance(keyManagerAlgorithm); } else { kmf = KeyManagerFactory.getInstance(keyManagerAlgorithm, provider); } kmf.init(keyStore, keyStorePassword.toCharArray()); return kmf.getKeyManagers(); } catch (NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException | NoSuchProviderException e) { throw new KeyStoreAccessException("Unable to get KeyManagers for KeyStore", e); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java ================================================ package net.lightbody.bmp.mitm.util; /** * Default values for basic MITM properties. */ public class MitmConstants { /** * The default message digest to use when signing certificates (CA or server). On 64-bit systems this is set to * SHA512, on 32-bit systems this is SHA256. On 64-bit systems, SHA512 generally performs better than SHA256; see * this question for details: http://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256. SHA384 is * SHA512 with a smaller output size. */ public static final String DEFAULT_MESSAGE_DIGEST = is32BitJvm() ? "SHA256": "SHA384"; /** * The default {@link java.security.KeyStore} type to use when creating KeyStores (e.g. for impersonated server * certificates). PKCS12 is widely supported. */ public static final String DEFAULT_KEYSTORE_TYPE = "PKCS12"; /** * Uses the non-portable system property sun.arch.data.model to help determine if we are running on a 32-bit JVM. * Since the majority of modern systems are 64 bits, this method "assumes" 64 bits and only returns true if * sun.arch.data.model explicitly indicates a 32-bit JVM. * * @return true if we can determine definitively that this is a 32-bit JVM, otherwise false */ private static boolean is32BitJvm() { Integer bits = Integer.getInteger("sun.arch.data.model"); return bits != null && bits == 32; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java ================================================ package net.lightbody.bmp.mitm.util; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.io.CharStreams; import net.lightbody.bmp.mitm.TrustSource; import net.lightbody.bmp.mitm.exception.SslContextInitializationException; import net.lightbody.bmp.mitm.trustmanager.InsecureTrustManagerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SupportedCipherSuiteFilter; /** * Utility for creating SSLContexts. */ public class SslUtil { private static final Logger log = LoggerFactory.getLogger(SslUtil.class); /** * Classpath resource containing a list of default ciphers. */ private static final String DEFAULT_CIPHERS_LIST_RESOURCE = "/default-ciphers.txt"; /** * The default cipher list to prefer when creating client or server connections. Stored as a lazily-loaded singleton * due to the relatively expensive initialization time, especially when determining the enabled JDK ciphers. * If OpenSsl support is enabled, this simply returns the list provided by {@link #getBuiltInCipherList()}. * If OpenSsl is not available, retrieves the default ciphers enabled on java SSLContexts. If the enabled JDK cipher * list cannot be read, returns the list provided by {@link #getBuiltInCipherList()}. */ private static final Supplier> defaultCipherList = Suppliers.memoize(new Supplier>() { @Override public List get() { List ciphers; if (OpenSsl.isAvailable()) { // TODO: consider switching to the list of all available ciphers using OpenSsl.availableCipherSuites() ciphers = getBuiltInCipherList(); } else { ciphers = getEnabledJdkCipherSuites(); if (ciphers.isEmpty()) { // could not retrieve the list of enabled ciphers from the JDK SSLContext, so use the hard-coded list ciphers = getBuiltInCipherList(); } } return ciphers; } }); /** * Creates a netty SslContext for use when connecting to upstream servers. Retrieves the list of trusted root CAs * from the trustSource. When trustSource is true, no upstream certificate verification will be performed. * This will make it possible for attackers to MITM communications with the upstream server, so always * supply an appropriate trustSource except in extraordinary circumstances (e.g. testing with dynamically-generated * certificates). * * @param cipherSuites cipher suites to allow when connecting to the upstream server * @param trustSource the trust store that will be used to validate upstream servers' certificates, or null to accept all upstream server certificates * @return an SSLContext to connect to upstream servers with */ public static SslContext getUpstreamServerSslContext(Collection cipherSuites, TrustSource trustSource) { SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); if (trustSource == null) { log.warn("Disabling upstream server certificate verification. This will allow attackers to intercept communications with upstream servers."); sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); } else { sslContextBuilder.trustManager(trustSource.getTrustedCAs()); } sslContextBuilder.ciphers(cipherSuites, SupportedCipherSuiteFilter.INSTANCE); try { return sslContextBuilder.build(); } catch (SSLException e) { throw new SslContextInitializationException("Error creating new SSL context for connection to upstream server", e); } } /** * Returns the X509Certificate for the server this session is connected to. The certificate may be null. * * @param sslSession SSL session connected to upstream server * @return the X.509 certificate from the upstream server, or null if no certificate is available */ public static X509Certificate getServerCertificate(SSLSession sslSession) { Certificate[] peerCertificates; try { peerCertificates = sslSession.getPeerCertificates(); } catch (SSLPeerUnverifiedException e) { peerCertificates = null; } if (peerCertificates != null && peerCertificates.length > 0) { Certificate peerCertificate = peerCertificates[0]; if (peerCertificate != null && peerCertificate instanceof X509Certificate) { return (X509Certificate) peerCertificates[0]; } } // no X.509 certificate was found for this server return null; } /** * Returns the list of default "enabled" ciphers for server TLS connections, as reported by the default Java security provider. * This is most likely a subset of "available" ciphers. * * @return list of default server ciphers, or an empty list if the default cipher list cannot be loaded */ public static List getEnabledJdkCipherSuites() { try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, null); String[] defaultCiphers = sslContext.getServerSocketFactory().getDefaultCipherSuites(); return Arrays.asList(defaultCiphers); } catch (Throwable t) { log.info("Unable to load default JDK server cipher list from SSLContext"); // log the actual exception for debugging log.debug("An error occurred while initializing an SSLContext or ServerSocketFactory", t); return Collections.emptyList(); } } /** * Returns a reasonable default cipher list for new client and server SSL connections. Not all of the ciphers may be supported * by the underlying SSL implementation (OpenSsl or JDK). The default list itself may also vary between OpenSsl and JDK * implementations. See {@link #defaultCipherList} for implementation details. * * @return default ciphers for client and server connections */ public static List getDefaultCipherList() { return defaultCipherList.get(); } /** * Returns ciphers from the hard-coded list of "reasonable" default ciphers in {@link #DEFAULT_CIPHERS_LIST_RESOURCE}. * * @return ciphers from the {@link #DEFAULT_CIPHERS_LIST_RESOURCE} */ public static List getBuiltInCipherList() { try (InputStream cipherListStream = SslUtil.class.getResourceAsStream(DEFAULT_CIPHERS_LIST_RESOURCE)) { if (cipherListStream == null) { return Collections.emptyList(); } Reader reader = new InputStreamReader(cipherListStream, Charset.forName("UTF-8")); return CharStreams.readLines(reader); } catch (IOException e) { return Collections.emptyList(); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/mitm/util/TrustUtil.java ================================================ package net.lightbody.bmp.mitm.util; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; import net.lightbody.bmp.mitm.exception.TrustSourceException; import net.lightbody.bmp.mitm.exception.UncheckedIOException; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.util.ClasspathResourceUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.StringReader; import java.nio.charset.Charset; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; /** * Utility class for interacting with the default trust stores on this JVM. */ public class TrustUtil { private static final Logger log = LoggerFactory.getLogger(TrustUtil.class); /** * Regex that matches a single certificate within a PEM file containing (potentially multiple) certificates. */ private static final Pattern CA_PEM_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----", Pattern.DOTALL); /** * The file containing the built-in list of trusted CAs. */ private static final String DEFAULT_TRUSTED_CA_RESOURCE = "/cacerts.pem"; /** * Empty X509 certificate array, useful for indicating an empty root CA trust store. */ public static final X509Certificate[] EMPTY_CERTIFICATE_ARRAY = new X509Certificate[0]; /** * Security provider used to transform PEM files into Certificates. * TODO: Modify the architecture of TrustUtil and TrustSource so that they do not need a hard-coded SecurityProviderTool. */ private static final SecurityProviderTool securityProviderTool = new DefaultSecurityProviderTool(); /** * Singleton for the list of CAs trusted by Java by default. */ private static final Supplier javaTrustedCAs = Suppliers.memoize(new Supplier() { @Override public X509Certificate[] get() { X509TrustManager defaultTrustManager = getDefaultJavaTrustManager(); X509Certificate[] defaultJavaTrustedCerts = defaultTrustManager.getAcceptedIssuers(); if (defaultJavaTrustedCerts != null) { return defaultJavaTrustedCerts; } else { return EMPTY_CERTIFICATE_ARRAY; } } }); /** * Singleton for the built-in list of trusted CAs. */ private static final Supplier builtinTrustedCAs = Suppliers.memoize(new Supplier() { @Override public X509Certificate[] get() { try { // the file may contain UTF-8 characters, but the PEM-encoded certificate data itself must be US-ASCII String allCAs = ClasspathResourceUtil.classpathResourceToString(DEFAULT_TRUSTED_CA_RESOURCE, Charset.forName("UTF-8")); return readX509CertificatesFromPem(allCAs); } catch (UncheckedIOException e) { log.warn("Unable to load built-in trusted CAs; no built-in CAs will be trusted", e); return new X509Certificate[0]; } } }); /** * Returns the built-in list of trusted CAs. This is a copy of cURL's list (https://curl.haxx.se/ca/cacert.pem), which is * ultimately derived from Firefox/NSS' list of trusted CAs. */ public static X509Certificate[] getBuiltinTrustedCAs() { return builtinTrustedCAs.get(); } /** * Returns the list of root CAs trusted by default in this JVM, according to the TrustManager returned by * {@link #getDefaultJavaTrustManager()}. */ public static X509Certificate[] getJavaTrustedCAs() { return javaTrustedCAs.get(); } /** * Parses a String containing zero or more PEM-encoded X509 certificates into an array of {@link X509Certificate}. * Everything outside of BEGIN CERTIFICATE and END CERTIFICATE lines will be ignored. * * @param pemEncodedCAs a String containing PEM-encoded certficiates * @return array containing certificates in the String */ public static X509Certificate[] readX509CertificatesFromPem(String pemEncodedCAs) { List certificates = new ArrayList<>(500); Matcher pemMatcher = CA_PEM_PATTERN.matcher(pemEncodedCAs); while (pemMatcher.find()) { String singleCAPem = pemMatcher.group(); X509Certificate certificate = readSingleX509Certificate(singleCAPem); certificates.add(certificate); } return certificates.toArray(new X509Certificate[0]); } /** * Parses a single PEM-encoded X509 certificate into an {@link X509Certificate}. * * @param x509CertificateAsPem PEM-encoded X509 certificate * @return parsed Java X509Certificate */ public static X509Certificate readSingleX509Certificate(String x509CertificateAsPem) { return securityProviderTool.decodePemEncodedCertificate(new StringReader(x509CertificateAsPem)); } /** * Returns a new instance of the default TrustManager for this JVM. Uses the default JVM trust store, which is * generally the cacerts file in JAVA_HOME/jre/lib/security, but this can be overridden using JVM parameters. */ public static X509TrustManager getDefaultJavaTrustManager() { TrustManagerFactory tmf; try { tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // initializing the trust store with a null KeyStore will load the default JVM trust store tmf.init((KeyStore) null); } catch (NoSuchAlgorithmException | KeyStoreException e) { throw new TrustSourceException("Unable to retrieve default TrustManagerFactory", e); } // Get hold of the default trust manager for (TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { return (X509TrustManager) tm; } } // didn't find an X509TrustManager throw new TrustSourceException("No X509TrustManager found"); } /** * Extracts the {@link KeyStore.TrustedCertificateEntry}s from the specified KeyStore. All other entry * types, including private keys, will be ignored. * * @param trustStore keystore containing trusted certificate entries * @return the trusted certificate entries in the specified keystore */ public static List extractTrustedCertificateEntries(KeyStore trustStore) { try { Enumeration aliases = trustStore.aliases(); List keyStoreAliases = Collections.list(aliases); List trustedCertificates = new ArrayList<>(keyStoreAliases.size()); for (String alias : keyStoreAliases) { if (trustStore.entryInstanceOf(alias, KeyStore.TrustedCertificateEntry.class)) { Certificate certificate = trustStore.getCertificate(alias); if (!(certificate instanceof X509Certificate)) { log.debug("Skipping non-X509Certificate in KeyStore. Certificate type: {}", certificate.getType()); continue; } trustedCertificates.add((X509Certificate) certificate); } } return trustedCertificates; } catch (KeyStoreException e) { throw new KeyStoreAccessException("Error occurred while retrieving trusted CAs from KeyStore", e); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/ActivityMonitor.java ================================================ package net.lightbody.bmp.proxy; import com.google.common.util.concurrent.Monitor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; /** * Tracks active and total requests on a proxy, and provides a mechanism to wait for active requests to finish. * See {@link net.lightbody.bmp.proxy.ActivityMonitor#waitForQuiescence(long, long, TimeUnit)}. */ public class ActivityMonitor { private final AtomicInteger activeRequests = new AtomicInteger(0); private final AtomicInteger totalRequests = new AtomicInteger(0); private final AtomicLong lastRequestFinishedNanos = new AtomicLong(System.nanoTime()); private final Monitor monitor = new Monitor(); private final Monitor.Guard requestNotActive = new Monitor.Guard(monitor) { @Override public boolean isSatisfied() { return activeRequests.get() == 0; } }; private final Monitor.Guard requestActive = new Monitor.Guard(monitor) { @Override public boolean isSatisfied() { return activeRequests.get() > 0; } }; public void requestStarted() { int previousCount = activeRequests.getAndIncrement(); totalRequests.incrementAndGet(); if (previousCount == 0) { // previously there were no active requests, but now there are -- signal to any waitForQuiescence threads that they need to // begin waiting again monitor.enter(); monitor.leave(); } } public void requestFinished() { int newCount = activeRequests.decrementAndGet(); lastRequestFinishedNanos.set(System.nanoTime()); if (newCount == 0) { // there are no active requests, so signal to any waitForQuiescence threads that they can begin waiting for their quietPeriod monitor.enter(); monitor.leave(); } } public int getActiveRequests() { return activeRequests.get(); } public int getTotalRequests() { return totalRequests.get(); } public boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUnit) { // the minRequestFinishTime is the earliest possible time the current or last request "could" finish. if there is no active // request, this is simply the lastRequestFinishedNanos time. if there is an active request, it is "now". this helps avoid waiting // for quiescence if there is an active request and the timeout is less than the quietPeriod. long minRequestFinishTime; if (activeRequests.get() == 0) { if (timeUnit.convert(System.nanoTime() - lastRequestFinishedNanos.get(), TimeUnit.NANOSECONDS) >= quietPeriod) { return true; } else { minRequestFinishTime = lastRequestFinishedNanos.get(); } } else { minRequestFinishTime = System.nanoTime(); } // record the maximum time we can wait until (the current time + the timeout), which will allow us to avoid waiting for // quiescence if it is not possible to satisfy the quietPeriod before the waitUntil time elapses long waitUntil = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeout, timeUnit); while (minRequestFinishTime + TimeUnit.NANOSECONDS.convert(quietPeriod, timeUnit) <= waitUntil) { // the maximum amount of time we can wait for active requests to finish that will still allow us to wait for quiescence // for the quietPeriod. long maxWaitTimeForActiveRequests = waitUntil - System.nanoTime() - TimeUnit.NANOSECONDS.convert(quietPeriod, timeUnit); // wait for active requests to finish boolean success = monitor.enterWhenUninterruptibly(requestNotActive, maxWaitTimeForActiveRequests, TimeUnit.NANOSECONDS); if (!success) { // timed out waiting for active requests to finish return false; } monitor.leave(); // the time needed to monitor for new active requests is whenever the last request finished + the quiet period. this may be less // than the actual quiet period if no requests were active when entering waitForQuiescence, but the quietPeriod has not yet elapsed // since the last request. long waitForNewRequests = lastRequestFinishedNanos.get() - System.nanoTime() + TimeUnit.NANOSECONDS.convert(quietPeriod, timeUnit); // if the quietPeriod has already elapsed since the last request, no need to wait any longer if (waitForNewRequests < 0) { return true; } // wait for new requests to come in. if a new request comes in, the loop will restart, waiting for active requests to complete. boolean requestsActive = monitor.enterWhenUninterruptibly(requestActive, waitForNewRequests, TimeUnit.NANOSECONDS); if (requestsActive) { // a request became active, so we need to wait for all requests to finish again monitor.leave(); continue; } else { return true; } } return false; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/BlacklistEntry.java ================================================ package net.lightbody.bmp.proxy; import java.util.regex.Pattern; /** * An entry in the Blacklist, consisting of a regular expression to match the URL, an HTTP status code, and a regular expression * to match the HTTP method. */ public class BlacklistEntry { private final Pattern urlPattern; private final int statusCode; private final Pattern httpMethodPattern; /** * Creates a new BlacklistEntry with no HTTP method matching (i.e. all methods will match). * * @param urlPattern URL pattern to blacklist * @param statusCode HTTP status code to return for blacklisted URL */ public BlacklistEntry(String urlPattern, int statusCode) { this(urlPattern, statusCode, null); } /** * Creates a new BlacklistEntry which will match both a URL and an HTTP method * * @param urlPattern URL pattern to blacklist * @param statusCode status code to return for blacklisted URL * @param httpMethodPattern HTTP method to match (e.g. GET, PUT, PATCH, etc.) */ public BlacklistEntry(String urlPattern, int statusCode, String httpMethodPattern) { this.urlPattern = Pattern.compile(urlPattern); this.statusCode = statusCode; if (httpMethodPattern == null || httpMethodPattern.isEmpty()) { this.httpMethodPattern = null; } else { this.httpMethodPattern = Pattern.compile(httpMethodPattern); } } /** * Determines if this BlacklistEntry matches the given URL. Attempts to match both the URL and the * HTTP method. * * @param url possibly-blacklisted URL * @param httpMethod HTTP method this URL is being accessed with * @return true if the URL matches this BlacklistEntry */ public boolean matches(String url, String httpMethod) { if (httpMethodPattern != null) { return urlPattern.matcher(url).matches() && httpMethodPattern.matcher(httpMethod).matches(); } else { return urlPattern.matcher(url).matches(); } } public Pattern getUrlPattern() { return urlPattern; } public int getStatusCode() { return statusCode; } public Pattern getHttpMethodPattern() { return httpMethodPattern; } @Deprecated /** * @deprecated use {@link #getUrlPattern()} */ public Pattern getPattern() { return getUrlPattern(); } @Deprecated /** * @deprecated use {@link #getStatusCode()} */ public int getResponseCode() { return getStatusCode(); } @Deprecated /** * @deprecated use {@link #getHttpMethodPattern()} */ public Pattern getMethod() { return getHttpMethodPattern(); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/CaptureType.java ================================================ package net.lightbody.bmp.proxy; import java.util.EnumSet; /** * Data types that the proxy can capture. Data types are organized into two broad categories, REQUEST_* and * RESPONSE_*, corresponding to client requests and server responses. */ public enum CaptureType { /** * HTTP request headers, including trailing headers. */ REQUEST_HEADERS, /** * HTTP Cookies sent with the request. */ REQUEST_COOKIES, /** * Non-binary HTTP request content, such as post data or other text-based request payload. * See {@link net.lightbody.bmp.util.BrowserMobHttpUtil#hasTextualContent(String)} for a list of Content-Types that * are considered non-binary. * */ REQUEST_CONTENT, /** * Binary HTTP request content, such as file uploads, or any unrecognized request payload. */ REQUEST_BINARY_CONTENT, /** * HTTP response headers, including trailing headers. */ RESPONSE_HEADERS, /** * Set-Cookie headers sent with the response. */ RESPONSE_COOKIES, /** * Non-binary HTTP response content (typically, HTTP body content). * See {@link net.lightbody.bmp.util.BrowserMobHttpUtil#hasTextualContent(String)} for a list of Content-Types that * are considered non-binary. */ RESPONSE_CONTENT, /** * Binary HTTP response content, such as image files, or any unrecognized response payload. */ RESPONSE_BINARY_CONTENT; // the following groups of capture types are private so that clients do not accidentally modify these sets (EnumSets are not immutable) private static final EnumSet REQUEST_CAPTURE_TYPES = EnumSet.of(REQUEST_HEADERS, REQUEST_CONTENT, REQUEST_BINARY_CONTENT, REQUEST_COOKIES); private static final EnumSet RESPONSE_CAPTURE_TYPES = EnumSet.of(RESPONSE_HEADERS, RESPONSE_CONTENT, RESPONSE_BINARY_CONTENT, RESPONSE_COOKIES); private static final EnumSet HEADER_CAPTURE_TYPES = EnumSet.of(REQUEST_HEADERS, RESPONSE_HEADERS); private static final EnumSet NON_BINARY_CONTENT_CAPTURE_TYPES = EnumSet.of(REQUEST_CONTENT, RESPONSE_CONTENT); private static final EnumSet BINARY_CONTENT_CAPTURE_TYPES = EnumSet.of(REQUEST_BINARY_CONTENT, RESPONSE_BINARY_CONTENT); private static final EnumSet ALL_CONTENT_CAPTURE_TYPES = EnumSet.of(REQUEST_CONTENT, RESPONSE_CONTENT, REQUEST_BINARY_CONTENT, RESPONSE_BINARY_CONTENT); private static final EnumSet COOKIE_CAPTURE_TYPES = EnumSet.of(REQUEST_COOKIES, RESPONSE_COOKIES); /** * @return Set of CaptureTypes for requests. */ public static EnumSet getRequestCaptureTypes() { return EnumSet.copyOf(REQUEST_CAPTURE_TYPES); } /** * @return Set of CaptureTypes for responses. */ public static EnumSet getResponseCaptureTypes() { return EnumSet.copyOf(RESPONSE_CAPTURE_TYPES); } /** * @return Set of CaptureTypes for headers. */ public static EnumSet getHeaderCaptureTypes() { return EnumSet.copyOf(HEADER_CAPTURE_TYPES); } /** * @return Set of CaptureTypes for non-binary content. */ public static EnumSet getNonBinaryContentCaptureTypes() { return EnumSet.copyOf(NON_BINARY_CONTENT_CAPTURE_TYPES); } /** * @return Set of CaptureTypes for binary content. */ public static EnumSet getBinaryContentCaptureTypes() { return EnumSet.copyOf(BINARY_CONTENT_CAPTURE_TYPES); } /** * @return Set of CaptureTypes for both binary and non-binary content. */ public static EnumSet getAllContentCaptureTypes() { return EnumSet.copyOf(ALL_CONTENT_CAPTURE_TYPES); } /** * @return Set of CaptureTypes for cookies. */ public static EnumSet getCookieCaptureTypes() { return EnumSet.copyOf(COOKIE_CAPTURE_TYPES); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/RewriteRule.java ================================================ package net.lightbody.bmp.proxy; import java.util.regex.Pattern; /** * Container for a URL rewrite rule pattern and replacement string. */ public class RewriteRule { private final Pattern pattern; private final String replace; public RewriteRule(String pattern, String replace) { this.pattern = Pattern.compile(pattern); this.replace = replace; } public Pattern getPattern() { return pattern; } public String getReplace() { return replace; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RewriteRule that = (RewriteRule) o; if (!pattern.equals(that.pattern)) return false; if (!replace.equals(that.replace)) return false; return true; } @Override public int hashCode() { int result = pattern.hashCode(); result = 31 * result + replace.hashCode(); return result; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/Whitelist.java ================================================ package net.lightbody.bmp.proxy; import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A URL whitelist. This object is immutable and the list of matching patterns and the HTTP status code is unmodifiable * after creation. Enabling, disabling, or modifying the whitelist can be safely and easily accomplished by updating the * whitelist reference to a new whitelist. */ public class Whitelist { private final List patterns; private final int statusCode; private final boolean enabled; /** * A disabled Whitelist. */ public static final Whitelist WHITELIST_DISABLED = new Whitelist(); /** * Creates an empty, disabled Whitelist. */ public Whitelist() { this.patterns = Collections.emptyList(); this.statusCode = -1; this.enabled = false; } /** * Creates an empty, enabled whitelist with the specified response code. * * @param statusCode the response code that the (enabled) Whitelist will return for all URLs. */ public Whitelist(int statusCode) { this.patterns = Collections.emptyList(); this.statusCode = statusCode; this.enabled = true; } /** * @deprecated use {@link #Whitelist(Collection, int)} */ @Deprecated public Whitelist(String[] patterns, int statusCode) { this(patterns == null ? null : Arrays.asList(patterns), statusCode); } /** * Creates a whitelist for the specified patterns, returning the given statusCode when a URL does not match one of the patterns. * A null or empty collection will result in an empty whitelist. * * @param patterns URL-matching regular expression patterns to whitelist * @param statusCode the HTTP status code to return when a request URL matches a whitelist pattern */ public Whitelist(Collection patterns, int statusCode) { if (patterns == null || patterns.isEmpty()) { this.patterns = Collections.emptyList(); } else { ImmutableList.Builder builder = ImmutableList.builder(); for (String pattern : patterns) { builder.add(Pattern.compile(pattern)); } this.patterns = builder.build(); } this.statusCode = statusCode; this.enabled = true; } /** * @return true if this whitelist is enabled, otherwise false */ public boolean isEnabled() { return enabled; } /** * @return regular expression patterns describing the URLs that should be whitelisted, or an empty collection if the whitelist is disabled */ public Collection getPatterns() { return this.patterns; } /** * @return HTTP status code returned by the whitelist, or -1 if the whitelist is disabled */ public int getStatusCode() { return statusCode; } /** * @deprecated use {@link #getStatusCode()} */ @Deprecated public int getResponseCode() { return getStatusCode(); } /** * Returns true if the specified URL matches a whitelisted URL regular expression. If the whitelist is disabled, this * method always returns false. * * @param url URL to match against the whitelist * @return true if the whitelist is enabled and the URL matched an entry in the whitelist, otherwise false */ public boolean matches(String url) { if (!enabled) { return false; } for (Pattern pattern : getPatterns()) { Matcher matcher = pattern.matcher(url); if (matcher.matches()) { return true; } } return false; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/auth/AuthType.java ================================================ package net.lightbody.bmp.proxy.auth; /** * Authentication types support by BrowserMobProxy. */ public enum AuthType { BASIC, // TODO: determine if we can actually do NTLM authentication NTLM } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractHostNameRemapper.java ================================================ package net.lightbody.bmp.proxy.dns; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** * Base class that provides host name remapping capabilities for AdvancedHostResolvers. Subclasses must implement {@link #resolveRemapped(String)} * instead of {@link net.lightbody.bmp.proxy.dns.HostResolver#resolve(String)}, which takes the remapped host as the input parameter. */ public abstract class AbstractHostNameRemapper implements AdvancedHostResolver { /** * Host name remappings, maintained as a reference to an ImmutableMap. The ImmutableMap type is specified explicitly because ImmutableMap * guarantees the iteration order of the map's entries. Specifying ImmutableMap also makes clear that the underlying map will never change, * and that any modifications to the host name remappings will result in an entirely new map. * * The current implementation does not actually use any of the special features of AtomicReference, but it does rely on synchronizing on * the AtomicReference when performing write operations. It could be replaced by a volatile reference to a Map and separate lock object. */ private final AtomicReference> remappedHostNames = new AtomicReference<>(ImmutableMap.of()); @Override public void remapHosts(Map hostRemappings) { synchronized (remappedHostNames) { ImmutableMap newRemappings = ImmutableMap.copyOf(hostRemappings); remappedHostNames.set(newRemappings); } } @Override public void remapHost(String originalHost, String remappedHost) { synchronized (remappedHostNames) { Map currentHostRemappings = remappedHostNames.get(); // use a LinkedHashMap to build the new remapping, to avoid duplicate key issues if the originalHost is already in the map Map builderMap = Maps.newLinkedHashMap(currentHostRemappings); builderMap.remove(originalHost); builderMap.put(originalHost, remappedHost); ImmutableMap newRemappings = ImmutableMap.copyOf(builderMap); remappedHostNames.set(newRemappings); } } @Override public void removeHostRemapping(String originalHost) { synchronized (remappedHostNames) { Map currentHostRemappings = remappedHostNames.get(); if (currentHostRemappings.containsKey(originalHost)) { // use a LinkedHashMap to build the new remapping, to take advantage of the remove() method Map builderMap = Maps.newLinkedHashMap(currentHostRemappings); builderMap.remove(originalHost); ImmutableMap newRemappings = ImmutableMap.copyOf(builderMap); remappedHostNames.set(newRemappings); } } } @Override public void clearHostRemappings() { synchronized (remappedHostNames) { remappedHostNames.set(ImmutableMap.of()); } } @Override public Map getHostRemappings() { return remappedHostNames.get(); } @Override public Collection getOriginalHostnames(String remappedHost) { //TODO: implement this using a reverse mapping multimap that is guarded by the same lock as remappedHostNames, since this method will likely be called // very often when forging certificates List originalHostnames = new ArrayList<>(); Map currentRemappings = remappedHostNames.get(); for (Map.Entry entry : currentRemappings.entrySet()) { if (entry.getValue().equals(remappedHost)) { originalHostnames.add(entry.getKey()); } } return originalHostnames; } /** * Applies this class's host name remappings to the specified original host, returning the remapped host name (if any), or the originalHost * if there is no remapped host name. * * @param originalHost original host name to resolve * @return a remapped host, or the original host if no mapping exists */ public String applyRemapping(String originalHost) { String remappedHost = remappedHostNames.get().get(originalHost); if (remappedHost != null) { return remappedHost; } else { return originalHost; } } /** * Resolves the specified remapped host. Subclasses should provide resolution by implementing this method, rather than overriding * {@link net.lightbody.bmp.proxy.dns.HostResolver#resolve(String)}. * * @param remappedHost remapped hostname to resolve * @return resolved InetAddresses, or an empty list if no addresses were found */ public abstract Collection resolveRemapped(String remappedHost); /** * Retrieves the remapped hostname and resolves it using {@link #resolveRemapped(String)}. * * @param originalHost original hostname to resolve * @return InetAddresses resolved from the remapped hostname, or an empty list if no addresses were found */ @Override public Collection resolve(String originalHost) { String remappedHost = applyRemapping(originalHost); return resolveRemapped(remappedHost); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/AdvancedHostResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import java.util.Collection; import java.util.Map; import java.util.concurrent.TimeUnit; /** * This interface defines the "core" DNS-manipulation functionality that BrowserMob Proxy supports, in addition to the basic name resolution * capability defined in {@link net.lightbody.bmp.proxy.dns.HostResolver}. AdvancedHostResolvers should apply any remappings before attempting * to resolve the hostname in the {@link HostResolver#resolve(String)} method. */ public interface AdvancedHostResolver extends HostResolver { /** * Replaces the host remappings in the existing list of remappings (if any) with the specified remappings. The remappings will be * applied in the order specified by the Map's iterator. *

* Note: The original hostnames must exactly match the requested hostname. It is not a domain or regular expression match. * * @param hostRemappings Map of {@code } */ void remapHosts(Map hostRemappings); /** * Remaps an individual host. If there are any existing remappings, the new remapping will be applied last, after all existing * remappings are applied. If there is already a remapping for the specified originalHost, it will be removed before * the new remapping is added to the end of the host remapping list (and will therefore be the last remapping applied). * * @param originalHost Original host to remap. Must exactly match the requested hostname (not a domain or regular expression match). * @param remappedHost hostname that will replace originalHost */ void remapHost(String originalHost, String remappedHost); /** * Removes the specified host remapping. If the remapping does not exist, this method has no effect. * * @param originalHost currently-remapped hostname */ void removeHostRemapping(String originalHost); /** * Removes all hostname remappings. */ void clearHostRemappings(); /** * Returns all host remappings in effect. Iterating over the returned Map is guaranteed to return remappings in the order in which the * remappings are actually applied. * * @return Map of {@code } */ Map getHostRemappings(); /** * Returns the original address or addresses that are remapped to the specified remappedHost. Iterating over the returned Collection is * guaranteed to return original mappings in the order in which the remappings are applied. * * @param remappedHost remapped hostname * @return original hostnames that are remapped to the specified remappedHost, or an empty Collection if no remapping is defined to the remappedHost */ Collection getOriginalHostnames(String remappedHost); /** * Clears both the positive (successful DNS lookups) and negative (failed DNS lookups) cache. */ void clearDNSCache(); /** * Sets the positive (successful DNS lookup) timeout when making DNS lookups. *

* Note: The timeUnit parameter does not guarantee the specified precision; implementations may need to reduce precision, depending on the underlying * DNS implementation. For example, the Oracle JVM's DNS cache only supports timeouts in whole seconds, so specifying a timeout of 1200ms will result * in a timeout of 1 second. * * @param timeout maximum lookup time * @param timeUnit units of the timeout value */ void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit); /** * Sets the negative (failed DNS lookup) timeout when making DNS lookups. *

* Note: The timeUnit parameter does not guarantee the specified precision; implementations may need to reduce precision, depending on the underlying * DNS implementation. For example, the Oracle JVM's DNS cache only supports timeouts in whole seconds, so specifying a timeout of 1200ms will result * in a timeout of 1 second. * * @param timeout maximum lookup time * @param timeUnit units of the timeout value */ void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/BasicHostResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import java.util.Collection; import java.util.Map; import java.util.concurrent.TimeUnit; /** * An {@link AdvancedHostResolver} that throws UnsupportedOperationException on all methods except {@link HostResolver#resolve(String)}. * Use this class to supply a {@link HostResolver} to {@link net.lightbody.bmp.BrowserMobProxy#setHostNameResolver(AdvancedHostResolver)} * if you do not need {@link AdvancedHostResolver} functionality. */ public abstract class BasicHostResolver implements AdvancedHostResolver { @Override public void remapHosts(Map hostRemappings) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public void remapHost(String originalHost, String remappedHost) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public void removeHostRemapping(String originalHost) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public void clearHostRemappings() { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public Map getHostRemappings() { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public Collection getOriginalHostnames(String remappedHost) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public void clearDNSCache() { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } @Override public void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/ChainedHostResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import com.google.common.collect.ImmutableList; import java.net.InetAddress; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * An {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} that applies the AdvancedHostResolver methods to multiple implementations. Methods * are applied to the resolvers in the order specified when the ChainedHostResolver is constructed. AdvancedHostResolver methods that modify the * resolver are guaranteed to complete atomically over all resolvers. For example, if one thread makes a call to * {@link #resolve(String)} while another thread is remapping hosts using * {@link #remapHost(String, String)}, the call to {@link #resolve(String)} is guaranteed to * apply the newly-remapped hosts to all resolvers managed by this ChainedHostResolver, or to no resolvers, but the call to * {@link #resolve(String)} will never result in the host name remappings applied only to "some" of the chained resolvers. *

* For getter methods (all read-only methods except {@link #resolve(String)}), the ChainedHostResolver returns results from the first chained resolver. *

* The atomic write methods specified by AdvancedHostResolver are: *

    *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#remapHost(String, String)}
  • *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#remapHosts(Map)}
  • *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#removeHostRemapping(String)}
  • *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#clearHostRemappings()}
  • *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#setNegativeDNSCacheTimeout(int, TimeUnit)}
  • *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#setPositiveDNSCacheTimeout(int, TimeUnit)}
  • *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#clearDNSCache()}
  • *
*/ public class ChainedHostResolver implements AdvancedHostResolver { private final List resolvers; private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final Lock readLock = readWriteLock.readLock(); private final Lock writeLock = readWriteLock.writeLock(); /** * Creates a ChainedHostResolver that applies {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} methods to the specified resolvers * in the order specified by the collection's iterator. * * @param resolvers resolvers to invoke, in the order specified by the collection's iterator */ public ChainedHostResolver(Collection resolvers) { if (resolvers == null) { this.resolvers = Collections.emptyList(); } else { this.resolvers = ImmutableList.copyOf(resolvers); } } /** * Returns the resolvers used by this ChainedHostResolver. The iterator of the collection is guaranteed to return the resolvers in the order * in which they are queried. * * @return resolvers used by this ChainedHostResolver */ public Collection getResolvers() { return ImmutableList.copyOf(resolvers); } @Override public void remapHosts(Map hostRemappings) { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.remapHosts(hostRemappings); } } finally { writeLock.unlock(); } } @Override public void remapHost(String originalHost, String remappedHost) { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.remapHost(originalHost, remappedHost); } } finally { writeLock.unlock(); } } @Override public void removeHostRemapping(String originalHost) { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.removeHostRemapping(originalHost); } } finally { writeLock.unlock(); } } @Override public void clearHostRemappings() { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.clearHostRemappings(); } } finally { writeLock.unlock(); } } @Override public Map getHostRemappings() { readLock.lock(); try { if (resolvers.isEmpty()) { return Collections.emptyMap(); } else { return resolvers.get(0).getHostRemappings(); } } finally { readLock.unlock(); } } @Override public Collection getOriginalHostnames(String remappedHost) { readLock.lock(); try { if (resolvers.isEmpty()) { return Collections.emptyList(); } else { return resolvers.get(0).getOriginalHostnames(remappedHost); } } finally { readLock.unlock(); } } @Override public void clearDNSCache() { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.clearDNSCache(); } } finally { writeLock.unlock(); } } @Override public void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit) { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.setPositiveDNSCacheTimeout(timeout, timeUnit); } } finally { writeLock.unlock(); } } @Override public void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit) { writeLock.lock(); try { for (AdvancedHostResolver resolver : resolvers) { resolver.setNegativeDNSCacheTimeout(timeout, timeUnit); } } finally { writeLock.unlock(); } } @Override public Collection resolve(String host) { readLock.lock(); try { // attempt to resolve the host using all resolvers. returns the results from the first successful resolution. for (AdvancedHostResolver resolver : resolvers) { Collection results = resolver.resolve(host); if (!results.isEmpty()) { return results; } } // no resolvers returned results return Collections.emptyList(); } finally { readLock.unlock(); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/DelegatingHostResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import com.google.common.collect.Iterables; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Collection; /** * A LittleProxy HostResolver that delegates to the specified {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} instance. This class * serves as a bridge between {@link AdvancedHostResolver} and {@link org.littleshoot.proxy.HostResolver}. */ public class DelegatingHostResolver implements org.littleshoot.proxy.HostResolver { private volatile AdvancedHostResolver resolver; /** * Creates a new resolver that will delegate to the specified resolver. * * @param resolver HostResolver to delegate to */ public DelegatingHostResolver(AdvancedHostResolver resolver) { this.resolver = resolver; } public AdvancedHostResolver getResolver() { return resolver; } public void setResolver(AdvancedHostResolver resolver) { this.resolver = resolver; } @Override public InetSocketAddress resolve(String host, int port) throws UnknownHostException { Collection resolvedAddresses = resolver.resolve(host); if (!resolvedAddresses.isEmpty()) { InetAddress resolvedAddress = Iterables.get(resolvedAddresses, 0); return new InetSocketAddress(resolvedAddress, port); } // no address found by the resolver throw new UnknownHostException(host); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/DnsJavaResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import com.google.common.net.InetAddresses; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xbill.DNS.AAAARecord; import org.xbill.DNS.ARecord; import org.xbill.DNS.Cache; import org.xbill.DNS.DClass; import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; /** * An {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} that uses dnsjava to perform DNS lookups. This implementation provides full * cache manipulation capabilities. */ public class DnsJavaResolver extends AbstractHostNameRemapper implements AdvancedHostResolver { private static final Logger log = LoggerFactory.getLogger(DnsJavaResolver.class); /** * DNS cache used for dnsjava lookups. */ private final Cache cache = new Cache(); /** * Maximum number of times to retry a DNS lookup due to a failure to connect to the DNS server. */ private static final int DNS_NETWORK_FAILURE_RETRY_COUNT = 5; @Override public void clearDNSCache() { cache.clearCache(); } @Override public void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit) { cache.setMaxCache((int) TimeUnit.SECONDS.convert(timeout, timeUnit)); } @Override public void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit) { cache.setMaxNCache((int) TimeUnit.SECONDS.convert(timeout, timeUnit)); } @Override public Collection resolveRemapped(String remappedHost) { // special case for IP literals: return the InetAddress without doing a dnsjava lookup. dnsjava seems to handle ipv4 literals // reasonably well, but does not handle ipv6 literals (with or without [] brackets) correctly. // note this does not work properly for ipv6 literals with a scope identifier, which is a known issue for InetAddresses.isInetAddress(). // (dnsjava also handles the situation incorrectly) if (InetAddresses.isInetAddress(remappedHost)) { return Collections.singletonList(InetAddresses.forString(remappedHost)); } // retrieve IPv4 addresses, then retrieve IPv6 addresses only if no IPv4 addresses are found. the current implementation always uses the // first returned address, so there is no need to look for IPv6 addresses if an IPv4 address is found. Collection ipv4addresses = resolveHostByType(remappedHost, Type.A); if (!ipv4addresses.isEmpty()) { return ipv4addresses; } else { return resolveHostByType(remappedHost, Type.AAAA); } } /** * Resolves the specified host using dnsjava, retrieving addresses of the specified type. * * @param host hostname to resolve * @param type one of {@link Type}, typically {@link Type#A} (IPv4) or {@link Type#AAAA} (IPv6). * @return resolved addresses, or an empty collection if no addresses could be resolved */ protected Collection resolveHostByType(String host, int type) { Lookup lookup; try { lookup = new Lookup(host, type, DClass.IN); } catch (TextParseException e) { return Collections.emptyList(); } lookup.setCache(cache); // we set the retry count to -1 because we want the first execution not be counted as a retry. int retryCount = -1; Record[] records; // we iterate while the status is TRY_AGAIN and DNS_NETWORK_FAILURE_RETRY_COUNT is not exceeded do { records = lookup.run(); retryCount++; } while (lookup.getResult() == Lookup.TRY_AGAIN && retryCount < DNS_NETWORK_FAILURE_RETRY_COUNT); if (records == null) { // no records found, so could not resolve host return Collections.emptyList(); } // convert the records we found into IPv4/IPv6 InetAddress objects List addrList = new ArrayList(records.length); // the InetAddresses returned by dnsjava include the trailing dot, e.g. "www.google.com." -- use the passed-in (or remapped) host value instead for (Record record : records) { if (record instanceof ARecord) { ARecord ipv4Record = (ARecord) record; try { InetAddress resolvedAddress = InetAddress.getByAddress(host, ipv4Record.getAddress().getAddress()); addrList.add(resolvedAddress); } catch (UnknownHostException e) { // this should never happen, unless there is a bug in dnsjava log.warn("dnsjava resolver returned an invalid InetAddress for host: " + host, e); continue; } } else if (record instanceof AAAARecord) { AAAARecord ipv6Record = (AAAARecord) record; try { InetAddress resolvedAddress = InetAddress.getByAddress(host, ipv6Record.getAddress().getAddress()); addrList.add(resolvedAddress); } catch (UnknownHostException e) { // this should never happen, unless there is a bug in dnsjava log.warn("dnsjava resolver returned an invalid InetAddress for host: " + host, e); continue; } } } return addrList; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/HostResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import java.net.InetAddress; import java.util.Collection; /** * Defines the basic functionality that {@link net.lightbody.bmp.BrowserMobProxy} implementations require when resolving hostnames. */ public interface HostResolver { /** * Resolves a hostname to one or more IP addresses. The iterator over the returned Collection is recommended to reflect the ordering * returned by the underlying name lookup service. For example, if a DNS server returns three IP addresses, 1.1.1.1, 2.2.2.2, and * 3.3.3.3, corresponding to www.somehost.com, the returned Collection iterator is recommended to iterate in * the order [1.1.1.1, 2.2.2.2, 3.3.3.3]. * * @param host host to resolve * @return resolved InetAddresses, or an empty collection if no addresses were found */ Collection resolve(String host); } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/NativeCacheManipulatingResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.net.InetAddress; import java.util.LinkedHashMap; import java.util.concurrent.TimeUnit; /** * An {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} that provides native JVM lookup using {@link net.lightbody.bmp.proxy.dns.NativeResolver} * but also implements DNS cache manipulation functionality. *

* Important note: The Oracle JVM does not provide any public facility to manipulate the JVM's DNS cache. This class uses reflection to forcibly * manipulate the cache, which includes access to private class members that are not part of the published Java specification. As such, this * implementation is brittle and may break in a future Java release, or may not work on non-Oracle JVMs. If this implementation cannot * perform any of its operations due to a failure to find or set the relevant field using reflection, it will log a warning but will not * throw an exception. You are using this class at your own risk! JVM cache manipulation does not work on Windows -- this class will behave exactly * the same as {@link net.lightbody.bmp.proxy.dns.NativeResolver} on that platform. */ public class NativeCacheManipulatingResolver extends NativeResolver { private static final Logger log = LoggerFactory.getLogger(NativeCacheManipulatingResolver.class); @Override public void clearDNSCache() { // clear the DNS cache but replacing the LinkedHashMaps that contain the positive and negative caches on the // private static InetAddress.Cache inner class with new, empty maps try { Field positiveCacheField = InetAddress.class.getDeclaredField("addressCache"); positiveCacheField.setAccessible(true); Object positiveCacheInstance = positiveCacheField.get(null); Field negativeCacheField = InetAddress.class.getDeclaredField("negativeCache"); negativeCacheField.setAccessible(true); Object negativeCacheInstance = positiveCacheField.get(null); Class cacheClass = Class.forName("java.net.InetAddress$Cache"); Field cacheField = cacheClass.getDeclaredField("cache"); cacheField.setAccessible(true); cacheField.set(positiveCacheInstance, new LinkedHashMap()); cacheField.set(negativeCacheInstance, new LinkedHashMap()); } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) { log.warn("Unable to clear native JVM DNS cache", e); } } @Override public void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit) { try { Class inetAddressCachePolicyClass = Class.forName("sun.net.InetAddressCachePolicy"); Field positiveCacheTimeoutSeconds = inetAddressCachePolicyClass.getDeclaredField("cachePolicy"); positiveCacheTimeoutSeconds.setAccessible(true); if (timeout < 0) { positiveCacheTimeoutSeconds.setInt(null, -1); java.security.Security.setProperty("networkaddress.cache.ttl", "-1"); } else { positiveCacheTimeoutSeconds.setInt(null, (int) TimeUnit.SECONDS.convert(timeout, timeUnit)); java.security.Security.setProperty("networkaddress.cache.ttl", Long.toString(TimeUnit.SECONDS.convert(timeout, timeUnit))); } } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { log.warn("Unable to modify native JVM DNS cache timeouts", e); } } @Override public void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit) { try { Class inetAddressCachePolicyClass = Class.forName("sun.net.InetAddressCachePolicy"); Field negativeCacheTimeoutSeconds = inetAddressCachePolicyClass.getDeclaredField("negativeCachePolicy"); negativeCacheTimeoutSeconds.setAccessible(true); if (timeout < 0) { negativeCacheTimeoutSeconds.setInt(null, -1); java.security.Security.setProperty("networkaddress.cache.negative.ttl", "-1"); } else { negativeCacheTimeoutSeconds.setInt(null, (int) TimeUnit.SECONDS.convert(timeout, timeUnit)); java.security.Security.setProperty("networkaddress.cache.negative.ttl", Long.toString(TimeUnit.SECONDS.convert(timeout, timeUnit))); } } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { log.warn("Unable to modify native JVM DNS cache timeouts", e); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/proxy/dns/NativeResolver.java ================================================ package net.lightbody.bmp.proxy.dns; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.concurrent.TimeUnit; /** * An {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} that provides native JVM lookup using {@link InetAddress}. * This implementation does not provide any cache manipulation. Attempting to manipulate the DNS cache will result in a DEBUG-level * log statement and will not raise an exception. The {@link net.lightbody.bmp.proxy.dns.DnsJavaResolver} provides support for cache * manipulation. If you absolutely need to manipulate the native JVM DNS cache, see * {@link net.lightbody.bmp.proxy.dns.NativeCacheManipulatingResolver} for details. */ public class NativeResolver extends AbstractHostNameRemapper implements AdvancedHostResolver { private static final Logger log = LoggerFactory.getLogger(NativeResolver.class); @Override public void clearDNSCache() { log.debug("Cannot clear native JVM DNS Cache using this Resolver"); } @Override public void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit) { log.debug("Cannot change native JVM DNS cache timeout using this Resolver"); } @Override public void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit) { log.debug("Cannot change native JVM DNS cache timeout using this Resolver"); } @Override public Collection resolveRemapped(String remappedHost) { try { Collection addresses = Arrays.asList(InetAddress.getAllByName(remappedHost)); return addresses; } catch (UnknownHostException e) { return Collections.emptyList(); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java ================================================ package net.lightbody.bmp.util; import android.util.Base64; import com.google.common.net.HostAndPort; import com.google.common.net.MediaType; import net.lightbody.bmp.exception.DecompressionException; import net.lightbody.bmp.exception.UnsupportedCharsetException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Map; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** * Utility class with static methods for processing HTTP requests and responses. */ public class BrowserMobHttpUtil { private static final Logger log = LoggerFactory.getLogger(BrowserMobHttpUtil.class); /** * Default MIME content type if no Content-Type header is present. According to the HTTP 1.1 spec, section 7.2.1: *

     *     Any HTTP/1.1 message containing an entity-body SHOULD include a Content-Type header field defining the media
     *     type of that body. If and only if the media type is not given by a Content-Type field, the recipient MAY
     *     attempt to guess the media type via inspection of its content and/or the name extension(s) of the URI used to
     *     identify the resource. If the media type remains unknown, the recipient SHOULD treat it as
     *     type "application/octet-stream".
     * 
*/ public static final String UNKNOWN_CONTENT_TYPE = "application/octet-stream"; /** * The default charset when the Content-Type header does not specify a charset. From the HTTP 1.1 spec section 3.7.1: *
     *     When no explicit charset parameter is provided by the sender, media subtypes of the "text" type are defined to have a default
     *     charset value of "ISO-8859-1" when received via HTTP. Data in character sets other than "ISO-8859-1" or its subsets MUST be
     *     labeled with an appropriate charset value.
     * 
*/ public static final Charset DEFAULT_HTTP_CHARSET = Charset.forName("ISO-8859-1"); /** * Buffer size when decompressing content. */ public static final int DECOMPRESS_BUFFER_SIZE = 16192; /** * Returns the size of the headers, including the 2 CRLFs at the end of the header block. * * @param headers headers to size * @return length of the headers, in bytes */ public static long getHeaderSize(HttpHeaders headers) { long headersSize = 0; for (Map.Entry header : headers.entries()) { // +2 for ': ', +2 for new line headersSize += header.getKey().length() + header.getValue().length() + 4; } return headersSize; } /** * Decompresses the gzipped byte stream. * * @param fullMessage gzipped byte stream to decomress * @return decompressed bytes * @throws DecompressionException thrown if the fullMessage cannot be read or decompressed for any reason */ public static byte[] decompressContents(byte[] fullMessage) throws DecompressionException { InflaterInputStream gzipReader = null; ByteArrayOutputStream uncompressed; try { gzipReader = new GZIPInputStream(new ByteArrayInputStream(fullMessage)); uncompressed = new ByteArrayOutputStream(fullMessage.length); byte[] decompressBuffer = new byte[DECOMPRESS_BUFFER_SIZE]; int bytesRead; while ((bytesRead = gzipReader.read(decompressBuffer)) > -1) { uncompressed.write(decompressBuffer, 0, bytesRead); } fullMessage = uncompressed.toByteArray(); } catch (IOException e) { throw new DecompressionException("Unable to decompress response", e); } finally { try { if (gzipReader != null) { gzipReader.close(); } } catch (IOException e) { log.warn("Unable to close gzip stream", e); } } return fullMessage; } /** * Returns true if the content type string indicates textual content. Currently these are any Content-Types that start with one of the * following: *
     *     text/
     *     application/x-javascript
     *     application/javascript
     *     application/json
     *     application/xml
     *     application/xhtml+xml
     * 
* * @param contentType contentType string to parse * @return true if the content type is textual */ public static boolean hasTextualContent(String contentType) { return contentType != null && (contentType.startsWith("text/") || contentType.startsWith("application/x-javascript") || contentType.startsWith("application/javascript") || contentType.startsWith("application/json") || contentType.startsWith("application/xml") || contentType.startsWith("application/xhtml+xml") ); } /** * Extracts all readable bytes from the ByteBuf as a byte array. * * @param content ByteBuf to read * @return byte array containing the readable bytes from the ByteBuf */ public static byte[] extractReadableBytes(ByteBuf content) { byte[] binaryContent = new byte[content.readableBytes()]; content.markReaderIndex(); content.readBytes(binaryContent); content.resetReaderIndex(); return binaryContent; } /** * Converts the byte array into a String based on the specified charset. The charset cannot be null. * * @param content bytes to convert to a String * @param charset the character set of the content * @return String containing the converted content * @throws IllegalArgumentException if charset is null */ public static String getContentAsString(byte[] content, Charset charset) { if (charset == null) { throw new IllegalArgumentException("Charset cannot be null"); } return new String(content, charset); } /** * Reads the charset directly from the Content-Type header string. If the Content-Type header does not contain a charset, * is malformed or unparsable, or if the header is null or empty, this method returns null. * * @param contentTypeHeader the Content-Type header string; can be null or empty * @return the character set indicated in the contentTypeHeader, or null if the charset is not present or is not parsable * @throws UnsupportedCharsetException if there is a charset specified in the content-type header, but it is not supported on this platform */ public static Charset readCharsetInContentTypeHeader(String contentTypeHeader) throws UnsupportedCharsetException { if (contentTypeHeader == null || contentTypeHeader.isEmpty()) { return null; } MediaType mediaType; try { mediaType = MediaType.parse(contentTypeHeader); } catch (IllegalArgumentException e) { log.info("Unable to parse Content-Type header: {}. Content-Type header will be ignored.", contentTypeHeader, e); return null; } try { return mediaType.charset().orNull(); } catch (java.nio.charset.UnsupportedCharsetException e) { throw new UnsupportedCharsetException(e); } } /** * Retrieves the raw (unescaped) path + query string from the specified request. The returned path will not include * the scheme, host, or port. * * @param httpRequest HTTP request * @return the unescaped path + query string from the HTTP request * @throws URISyntaxException if the path could not be parsed (due to invalid characters in the URI, etc.) */ public static String getRawPathAndParamsFromRequest(HttpRequest httpRequest) throws URISyntaxException { // if this request's URI contains a full URI (including scheme, host, etc.), strip away the non-path components if (HttpUtil.startsWithHttpOrHttps(httpRequest.getUri())) { return getRawPathAndParamsFromUri(httpRequest.getUri()); } else { // to provide consistent validation behavior for URIs that contain a scheme and those that don't, attempt to parse // the URI, even though we discard the parsed URI object new URI(httpRequest.getUri()); return httpRequest.getUri(); } } /** * Retrieves the raw (unescaped) path and query parameters from the URI, stripping out the scheme, host, and port. * The path will begin with a leading '/'. For example, 'http://example.com/some/resource?param%20name=param%20value' * would return '/some/resource?param%20name=param%20value'. * * @param uriString the URI to parse, containing a scheme, host, port, path, and query parameters * @return the unescaped path and query parameters from the URI * @throws URISyntaxException if the specified URI is invalid or cannot be parsed */ public static String getRawPathAndParamsFromUri(String uriString) throws URISyntaxException { URI uri = new URI(uriString); String path = uri.getRawPath(); String query = uri.getRawQuery(); if (query != null) { return path + '?' + query; } else { return path; } } /** * Returns true if the specified response is an HTTP redirect response, i.e. a 300, 301, 302, 303, or 307. * * @param httpResponse HTTP response * @return true if the response is a redirect, otherwise false */ public static boolean isRedirect(HttpResponse httpResponse) { switch (httpResponse.getStatus().code()) { case 300: case 301: case 302: case 303: case 307: return true; default: return false; } } /** * Removes a port from a host+port if the string contains the specified port. If the host+port does not contain * a port, or contains another port, the string is returned unaltered. For example, if hostWithPort is the * string {@code www.website.com:443}, this method will return {@code www.website.com}. * * Note: The hostWithPort string is not a URI and should not contain a scheme or resource. This method does * not attempt to validate the specified host; it might throw IllegalArgumentException if there was a problem * parsing the hostname, but makes no guarantees. In general, it should be validated externally, if necessary. * * @param hostWithPort string containing a hostname and optional port * @param portNumber port to remove from the string * @return string with the specified port removed, or the original string if it did not contain the portNumber */ public static String removeMatchingPort(String hostWithPort, int portNumber) { HostAndPort parsedHostAndPort = HostAndPort.fromString(hostWithPort); if (parsedHostAndPort.hasPort() && parsedHostAndPort.getPort() == portNumber) { // HostAndPort.getHostText() strips brackets from ipv6 addresses, so reparse using fromHost return HostAndPort.fromHost(parsedHostAndPort.getHostText()).toString(); } else { return hostWithPort; } } /** * Base64-encodes the specified username and password for Basic Authorization for HTTP requests or upstream proxy * authorization. The format of Basic auth is "username:password" as a base64 string. * * @param username username to encode * @param password password to encode * @return a base-64 encoded string containing username:password */ public static String base64EncodeBasicCredentials(String username, String password) { String credentialsToEncode = username + ':' + password; // using UTF-8, which is the modern de facto standard, and which retains compatibility with US_ASCII for ASCII characters, // as required by RFC 7616, section 3: http://tools.ietf.org/html/rfc7617#section-3 byte[] credentialsAsUtf8Bytes = credentialsToEncode.getBytes(Charset.forName("UTF-8")); return Base64.encodeToString(credentialsAsUtf8Bytes,Base64.DEFAULT); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/BrowserMobProxyUtil.java ================================================ package net.lightbody.bmp.util; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarLog; import net.lightbody.bmp.core.har.HarPage; import net.lightbody.bmp.mitm.exception.UncheckedIOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.Charset; import java.util.HashSet; import java.util.Set; /** * General utility class for functionality and classes used mostly internally by BrowserMob Proxy. */ public class BrowserMobProxyUtil { private static final Logger log = LoggerFactory.getLogger(BrowserMobProxyUtil.class); /** * Classpath resource containing this build's version string. */ private static final String VERSION_CLASSPATH_RESOURCE = "/net/lightbody/bmp/version"; /** * Default value if the version string cannot be read. */ private static final String UNKNOWN_VERSION_STRING = "UNKNOWN-VERSION"; /** * Singleton version string loader. */ private static final Supplier version = Suppliers.memoize(new Supplier() { @Override public String get() { return readVersionFileOnClasspath(); } }); /** * Copies {@link HarEntry} and {@link HarPage} references from the specified har to a new har copy, up to and including * the specified pageRef. Does not perform a "deep copy", so any subsequent modification to the entries or pages will * be reflected in the copied har. * * @param har existing har to copy * @param pageRef last page ID to copy * @return copy of a {@link Har} with entries and pages from the original har, or null if the input har is null */ public static Har copyHarThroughPageRef(Har har, String pageRef) { if (har == null) { return null; } if (har.getLog() == null) { return new Har(); } // collect the page refs that need to be copied to new har copy. Set pageRefsToCopy = new HashSet(); for (HarPage page : har.getLog().getPages()) { pageRefsToCopy.add(page.getId()); if (pageRef.equals(page.getId())) { break; } } HarLog logCopy = new HarLog(); // copy every entry and page in the HarLog that matches a pageRefToCopy. since getEntries() and getPages() return // lists, we are guaranteed that we will iterate through the pages and entries in the proper order for (HarEntry entry : har.getLog().getEntries()) { if (pageRefsToCopy.contains(entry.getPageref())) { logCopy.addEntry(entry); } } for (HarPage page : har.getLog().getPages()) { if (pageRefsToCopy.contains(page.getId())) { logCopy.addPage(page); } } Har harCopy = new Har(); harCopy.setLog(logCopy); return harCopy; } /** * Returns the version of BrowserMob Proxy, e.g. "2.1.0". * * @return BMP version string */ public static String getVersionString() { return version.get(); } /** * Reads the version of this build from the classpath resource specified by {@link #VERSION_CLASSPATH_RESOURCE}. * * @return version string from the classpath version resource */ private static String readVersionFileOnClasspath() { String versionString; try { versionString = ClasspathResourceUtil.classpathResourceToString(VERSION_CLASSPATH_RESOURCE, Charset.forName("UTF-8")); } catch (UncheckedIOException e) { log.debug("Unable to load version from classpath resource: {}", VERSION_CLASSPATH_RESOURCE, e); return UNKNOWN_VERSION_STRING; } if (versionString.isEmpty()) { log.debug("Version file on classpath was empty or could not be read. Resource: {}", VERSION_CLASSPATH_RESOURCE); return UNKNOWN_VERSION_STRING; } return versionString; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/ClasspathResourceUtil.java ================================================ package net.lightbody.bmp.util; import com.google.common.io.CharStreams; import net.lightbody.bmp.mitm.exception.UncheckedIOException; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; /** * Utility class for dealing with classpath resources. */ public class ClasspathResourceUtil { /** * Retrieves a classpath resource using the {@link ClasspathResourceUtil} classloader and converts it to a String using the specified * character set. If any error occurs while reading the resource, this method throws * {@link net.lightbody.bmp.mitm.exception.UncheckedIOException}. If the classpath resource cannot be found, this * method throws a FileNotFoundException wrapped in an UncheckedIOException. * * @param resource classpath resource to load * @param charset charset to use to decode the classpath resource * @return a String * @throws UncheckedIOException if the classpath resource cannot be found or cannot be read for any reason */ public static String classpathResourceToString(String resource, Charset charset) throws UncheckedIOException { if (resource == null) { throw new IllegalArgumentException("Classpath resource to load cannot be null"); } if (charset == null) { throw new IllegalArgumentException("Character set cannot be null"); } try { InputStream resourceAsStream = ClasspathResourceUtil.class.getResourceAsStream(resource); if (resourceAsStream == null) { throw new UncheckedIOException(new FileNotFoundException("Unable to locate classpath resource: " + resource)); } // the classpath resource was found and opened. wrap it in a Reader and return its contents. Reader resourceReader = new InputStreamReader(resourceAsStream, charset); return CharStreams.toString(resourceReader); } catch (IOException e) { throw new UncheckedIOException("Error occurred while reading classpath resource", e); } } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/HttpMessageContents.java ================================================ package net.lightbody.bmp.util; import net.lightbody.bmp.exception.UnsupportedCharsetException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.Charset; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpHeaders; /** * Helper class to wrap the contents of an {@link io.netty.handler.codec.http.HttpMessage}. Contains convenience methods to extract and * manipulate the contents of the wrapped {@link io.netty.handler.codec.http.HttpMessage}. * * TODO: Currently this class only wraps FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well */ public class HttpMessageContents { private static final Logger log = LoggerFactory.getLogger(HttpMessageContents.class); private final FullHttpMessage httpMessage; // caches for contents, to avoid repeated re-extraction of data private volatile String textContents; private volatile byte[] binaryContents; public HttpMessageContents(FullHttpMessage httpMessage) { this.httpMessage = httpMessage; } /** * Replaces the contents of the wrapped HttpMessage with the specified text contents, encoding them in the character set specified by the * message's Content-Type header. Note that this method does not update the Content-Type header, so if the content type will change as a * result of this call, the Content-Type header should be updated before calling this method. * * @param newContents new message contents */ public void setTextContents(String newContents) { HttpObjectUtil.replaceTextHttpEntityBody(httpMessage, newContents); // replaced the contents, so clear the local cache textContents = null; binaryContents = null; } /** * Replaces the contents of the wrapped HttpMessage with the specified binary contents. Note that this method does not update the * Content-Type header, so if the content type will change as a result of this call, the Content-Type header should be updated before * calling this method. * * @param newBinaryContents new message contents */ public void setBinaryContents(byte[] newBinaryContents) { HttpObjectUtil.replaceBinaryHttpEntityBody(httpMessage, newBinaryContents); // replaced the contents, so clear the local cache binaryContents = null; textContents = null; } /** * Retrieves the contents of this message as a String, decoded according to the message's Content-Type header. This method caches * the contents, so repeated calls to this method should not incur a penalty; however, modifications to the message contents * outside of this class will result in stale data returned from this method. * * @return String representation of the entity body * @throws java.nio.charset.UnsupportedCharsetException if the character set declared in the message is not supported on this platform */ public String getTextContents() throws java.nio.charset.UnsupportedCharsetException { // avoid re-extracting the contents if this method is called repeatedly if (textContents == null) { textContents = HttpObjectUtil.extractHttpEntityBody(httpMessage); } return textContents; } /** * Retrieves the binary contents of this message. This method caches the contents, so repeated calls to this method should not incur a * penalty; however, modifications to the message contents outside of this class will result in stale data returned from this method. * * @return binary contents of the entity body */ public byte[] getBinaryContents() { // avoid re-extracting the contents if this method is called repeatedly if (binaryContents == null) { binaryContents = HttpObjectUtil.extractBinaryHttpEntityBody(httpMessage); } return binaryContents; } /** * Retrieves the Content-Type header of this message. If no Content-Type is present, returns the assumed default Content-Type (see * {@link BrowserMobHttpUtil#UNKNOWN_CONTENT_TYPE}). * * @return the message's content type */ public String getContentType() { String contentTypeHeader = HttpHeaders.getHeader(httpMessage, HttpHeaders.Names.CONTENT_TYPE); if (contentTypeHeader == null || contentTypeHeader.isEmpty()) { return BrowserMobHttpUtil.UNKNOWN_CONTENT_TYPE; } else { return contentTypeHeader; } } /** * Retrieves the character set of the entity body. If the Content-Type is not a textual type, this value is meaningless. * If no character set is specified, this method will return the default ISO-8859-1 character set. If the Content-Type * specifies a character set, but the character set is not supported on this platform, this method throws an * {@link java.nio.charset.UnsupportedCharsetException}. * * @return the entity body's character set * @throws java.nio.charset.UnsupportedCharsetException if the character set declared in the message is not supported on this platform */ public Charset getCharset() throws java.nio.charset.UnsupportedCharsetException { String contentTypeHeader = getContentType(); Charset charset = null; try { charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentTypeHeader); } catch (UnsupportedCharsetException e) { java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause(); log.error("Character set specified in Content-Type header is not supported on this platform. Content-Type header: {}", contentTypeHeader, cause); throw cause; } if (charset == null) { return BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET; } return charset; } /** * Returns true if this message's Content-Type header indicates that it contains a textual data type. See {@link BrowserMobHttpUtil#hasTextualContent(String)}. * * @return true if the Content-Type header is a textual type, otherwise false */ public boolean isText() { return BrowserMobHttpUtil.hasTextualContent(getContentType()); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/HttpMessageInfo.java ================================================ package net.lightbody.bmp.util; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; /** * Encapsulates additional HTTP message data passed to request and response filters. */ public class HttpMessageInfo { private final HttpRequest originalRequest; private final ChannelHandlerContext channelHandlerContext; private final boolean isHttps; private final String url; private final String originalUrl; public HttpMessageInfo(HttpRequest originalRequest, ChannelHandlerContext channelHandlerContext, boolean isHttps, String url, String originalUrl) { this.originalRequest = originalRequest; this.channelHandlerContext = channelHandlerContext; this.isHttps = isHttps; this.url = url; this.originalUrl = originalUrl; } /** * The original request from the client. Does not reflect any modifications from previous filters. */ public HttpRequest getOriginalRequest() { return originalRequest; } /** * The {@link ChannelHandlerContext} for this request's client connection. */ public ChannelHandlerContext getChannelHandlerContext() { return channelHandlerContext; } /** * Returns true if this is an HTTPS message. */ public boolean isHttps() { return isHttps; } /** * Returns the full, absolute URL of the original request from the client for both HTTP and HTTPS URLs. The URL * will not reflect modifications from this or other filters. */ public String getOriginalUrl() { return originalUrl; } /** * Returns the full, absolute URL of this request from the client for both HTTP and HTTPS URLs. The URL will reflect * modifications from filters. If this method is called while a request filter is processing, it will reflect any * modifications to the URL from all previous filters. */ public String getUrl() { return url; } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/HttpObjectUtil.java ================================================ package net.lightbody.bmp.util; import net.lightbody.bmp.exception.UnsupportedCharsetException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.Charset; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMessage; /** * Utility class to assist with manipulation of {@link io.netty.handler.codec.http.HttpObject} instances, including * {@link io.netty.handler.codec.http.HttpMessage} and {@link io.netty.handler.codec.http.HttpContent}. */ public class HttpObjectUtil { private static final Logger log = LoggerFactory.getLogger(HttpObjectUtil.class); /** * Replaces the entity body of the message with the specified contents. Encodes the message contents according to charset in the message's * Content-Type header, or uses {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET} if none is specified. * Note: If the charset of the message is not supported on this platform, this will throw an {@link java.nio.charset.UnsupportedCharsetException}. * * TODO: Currently this method only works for FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well * * @param message the HTTP message to manipulate * @param newContents the new entity body contents * @throws java.nio.charset.UnsupportedCharsetException if the charset in the message is not supported on this platform */ public static void replaceTextHttpEntityBody(FullHttpMessage message, String newContents) { // get the content type for this message so we can encode the newContents into a byte stream appropriately String contentTypeHeader = message.headers().get(HttpHeaders.Names.CONTENT_TYPE); Charset messageCharset; try { messageCharset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentTypeHeader); } catch (UnsupportedCharsetException e) { java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause() ; log.error("Found unsupported character set in Content-Type header '{}' while attempting to replace contents of HTTP message.", contentTypeHeader, cause); throw cause; } if (messageCharset == null) { messageCharset = BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET; log.warn("No character set declared in HTTP message. Replacing text using default charset {}.", messageCharset); } byte[] contentBytes = newContents.getBytes(messageCharset); replaceBinaryHttpEntityBody(message, contentBytes); } /** * Replaces an HTTP entity body with the specified binary contents. * TODO: Currently this method only works for FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well * * @param message the HTTP message to manipulate * @param newBinaryContents the new entity body contents */ public static void replaceBinaryHttpEntityBody(FullHttpMessage message, byte[] newBinaryContents) { message.content().resetWriterIndex(); // resize the buffer if needed, since the new message may be longer than the old one message.content().ensureWritable(newBinaryContents.length, true); message.content().writeBytes(newBinaryContents); // update the Content-Length header, since the size may have changed message.headers().set(HttpHeaders.Names.CONTENT_LENGTH, newBinaryContents.length); } /** * Extracts the entity body from an HTTP content object, according to the specified character set. The character set cannot be null. If * the character set is not specified or is unknown, you still must specify a suitable default charset (see {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET}). * * @param httpContent HTTP content object to extract the entity body from * @param charset character set of the entity body * @return String representation of the entity body * @throws IllegalArgumentException if the charset is null */ public static String extractHttpEntityBody(HttpContent httpContent, Charset charset) { if (charset == null) { throw new IllegalArgumentException("No charset specified when extracting the contents of an HTTP message"); } byte[] contentBytes = BrowserMobHttpUtil.extractReadableBytes(httpContent.content()); return new String(contentBytes, charset); } /** * Extracts the entity body from a FullHttpMessage, according to the character set in the message's Content-Type header. If the Content-Type * header is not present or does not specify a charset, assumes the ISO-8859-1 character set (see {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET}). * * @param httpMessage HTTP message to extract entity body from * @return String representation of the entity body * @throws java.nio.charset.UnsupportedCharsetException if there is a charset specified in the content-type header, but it is not supported */ public static String extractHttpEntityBody(FullHttpMessage httpMessage) { Charset charset; try { charset = getCharsetFromMessage(httpMessage); } catch (UnsupportedCharsetException e) { // the declared character set is not supported, so it is impossible to decode the contents of the message. log an error and throw an exception // to alert the client code. java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause(); String contentTypeHeader = HttpHeaders.getHeader(httpMessage, HttpHeaders.Names.CONTENT_TYPE); log.error("Cannot retrieve text contents of message because HTTP message declares a character set that is not supported on this platform. Content type header: {}.", contentTypeHeader, cause); throw cause; } return extractHttpEntityBody(httpMessage, charset); } /** * Derives the charset from the Content-Type header in the HttpMessage. If the Content-Type header is not present or does not contain * a character set, this method returns the ISO-8859-1 character set. See {@link BrowserMobHttpUtil#readCharsetInContentTypeHeader(String)} * for more details. * * @param httpMessage HTTP message to extract charset from * @return the charset associated with the HTTP message, or the default charset if none is present * @throws UnsupportedCharsetException if there is a charset specified in the content-type header, but it is not supported */ public static Charset getCharsetFromMessage(HttpMessage httpMessage) throws UnsupportedCharsetException { String contentTypeHeader = HttpHeaders.getHeader(httpMessage, HttpHeaders.Names.CONTENT_TYPE); Charset charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentTypeHeader); if (charset == null) { return BrowserMobHttpUtil.DEFAULT_HTTP_CHARSET; } return charset; } /** * Extracts the binary contents from an HTTP message. * * @param httpContent HTTP content object to extract the entity body from * @return binary contents of the HTTP message */ public static byte[] extractBinaryHttpEntityBody(HttpContent httpContent) { return BrowserMobHttpUtil.extractReadableBytes(httpContent.content()); } } ================================================ FILE: app/src/main/java/net/lightbody/bmp/util/HttpUtil.java ================================================ package net.lightbody.bmp.util; import com.google.common.net.HostAndPort; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Locale; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; /** * Contains utility methods for netty {@link HttpRequest} and related objects. */ public class HttpUtil { /** * Identify the host of an HTTP request. This method uses the URI of the request if possible, otherwise it attempts to find the host * in the request headers. * * @param httpRequest HTTP request to parse the host from * @return the host the request is connecting to, or null if no host can be found */ public static String getHostFromRequest(HttpRequest httpRequest) { // try to use the URI from the request first, if the URI starts with http:// or https://. checking for http/https avoids confusing // java's URI class when the request is for a malformed URL like '//some-resource'. String host = null; if (startsWithHttpOrHttps(httpRequest.getUri())) { try { URI uri = new URI(httpRequest.getUri()); host = uri.getHost(); } catch (URISyntaxException e) { } } // if there was no host in the URI, attempt to grab the host from the Host header if (host == null || host.isEmpty()) { host = parseHostHeader(httpRequest, false); } return host; } /** * Gets the host and port from the specified request. Returns the host and port from the request URI if available, * otherwise retrieves the host and port from the Host header. * * @param httpRequest HTTP request * @return host and port of the request */ public static String getHostAndPortFromRequest(HttpRequest httpRequest) { if (startsWithHttpOrHttps(httpRequest.getUri())) { try { return getHostAndPortFromUri(httpRequest.getUri()); } catch (URISyntaxException e) { // the URI could not be parsed, so return the host and port in the Host header } } return parseHostHeader(httpRequest, true); } /** * Returns true if the string starts with http:// or https://. * * @param uri string to evaluate * @return true if the string starts with http:// or https:// */ public static boolean startsWithHttpOrHttps(String uri) { if (uri == null) { return false; } // the scheme is case insensitive, according to RFC 7230, section 2.7.3: /* The scheme and host are case-insensitive and normally provided in lowercase; all other components are compared in a case-sensitive manner. */ String lowercaseUri = uri.toLowerCase(Locale.US); return lowercaseUri.startsWith("http://") || lowercaseUri.startsWith("https://"); } /** * Retrieves the host and port from the specified URI. * * @param uriString URI to retrieve the host and port from * @return the host and port from the URI as a String * @throws URISyntaxException if the specified URI is invalid or cannot be parsed */ public static String getHostAndPortFromUri(String uriString) throws URISyntaxException { URI uri = new URI(uriString); if (uri.getPort() == -1) { return uri.getHost(); } else { return HostAndPort.fromParts(uri.getHost(), uri.getPort()).toString(); } } /** * Retrieves the host and, optionally, the port from the specified request's Host header. * * @param httpRequest HTTP request * @param includePort when true, include the port * @return the host and, optionally, the port specified in the request's Host header */ private static String parseHostHeader(HttpRequest httpRequest, boolean includePort) { // this header parsing logic is adapted from ClientToProxyConnection#identifyHostAndPort. List hosts = httpRequest.headers().getAll(HttpHeaders.Names.HOST); if (!hosts.isEmpty()) { String hostAndPort = hosts.get(0); if (includePort) { return hostAndPort; } else { HostAndPort parsedHostAndPort = HostAndPort.fromString(hostAndPort); return parsedHostAndPort.getHostText(); } } else { return null; } } } ================================================ FILE: app/src/main/res/layout/activity_capture.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_main.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #3F51B5 #303F9F #FF4081 ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 16dp 16dp 16dp ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Dream Catcher Settings Open chrome://inspect in Chrome to start inspection ================================================ FILE: app/src/main/res/values/styles.xml ================================================