├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── jarRepositories.xml ├── jvm-http-proxy-agent.iml ├── kotlinc.xml ├── misc.xml ├── modules.xml ├── modules │ ├── java-agent.iml │ ├── java-agent.main.iml │ └── java-agent.test.iml └── vcs.xml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── r8-rules.txt ├── settings.gradle ├── src ├── main │ ├── java │ │ ├── reactor │ │ │ └── netty │ │ │ │ └── tcp │ │ │ │ └── ProxyProvider.java │ │ └── tech │ │ │ └── httptoolkit │ │ │ └── javaagent │ │ │ └── advice │ │ │ ├── OverrideAllProxySelectionAdvice.java │ │ │ ├── OverrideSslContextFieldAdvice.java │ │ │ ├── OverrideUrlConnectionProxyAdvice.java │ │ │ ├── ReturnProxyAddressAdvice.java │ │ │ ├── ReturnProxyAdvice.java │ │ │ ├── ReturnProxySelectorAdvice.java │ │ │ ├── ReturnSslContextAdvice.java │ │ │ ├── ReturnSslSocketFactoryAdvice.java │ │ │ ├── SkipMethodAdvice.java │ │ │ ├── akka │ │ │ ├── OverrideHttpSettingsAdvice.java │ │ │ ├── ResetOldGatewaysAdvice.java │ │ │ ├── ResetOldPoolsAdvice.java │ │ │ └── ResetPoolSetupAdvice.java │ │ │ ├── apacheclient │ │ │ ├── ApacheCustomSslProtocolSocketFactory.java │ │ │ ├── ApacheOverrideProxyHostFieldAdvice.java │ │ │ ├── ApacheReturnCustomSslProtocolSocketFactoryAdvice.java │ │ │ ├── ApacheSetConfigProxyHostAdvice.java │ │ │ ├── ApacheSetSslSocketFactoryAdvice.java │ │ │ ├── ApacheV4ReturnProxyRouteAdvice.java │ │ │ └── ApacheV5ReturnProxyRouteAdvice.java │ │ │ ├── asynchttpclient │ │ │ ├── AsyncHttpClientReturnProxySelectorAdvice.java │ │ │ ├── AsyncHttpClientReturnSslContextAdvice.java │ │ │ └── AsyncHttpResetSslEngineFactoryAdvice.java │ │ │ ├── jettyclient │ │ │ ├── JettyResetDestinationsAdvice.java │ │ │ ├── JettyReturnProxyConfigurationAdvice.java │ │ │ ├── JettyReturnSslContextFactoryV10Advice.java │ │ │ ├── JettyReturnSslContextFactoryV9Advice.java │ │ │ └── JettyV9StubContextFactory.java │ │ │ ├── ktor │ │ │ ├── KtorResetProxyFieldAdvice.java │ │ │ └── KtorResetTlsClientTrustAdvice.java │ │ │ ├── reactornetty │ │ │ ├── ReactorNettyResetAllConfigAdvice.java │ │ │ ├── ReactorNettyResetHttpClientSecureSslAdvice.java │ │ │ └── ReactorNettyV09ResetProxyProviderFieldAdvice.java │ │ │ └── vertxclient │ │ │ ├── VertxHttpClientReturnProxyConfigurationAdvice.java │ │ │ └── VertxNetClientOptionsSetTrustOptionsAdvice.java │ └── kotlin │ │ └── tech │ │ └── httptoolkit │ │ └── javaagent │ │ ├── AgentConfig.kt │ │ ├── AgentMain.kt │ │ ├── AkkaClientTransformers.kt │ │ ├── ApacheAsyncClientTransformer.kt │ │ ├── ApacheClientTransformers.kt │ │ ├── AsyncHttpClientConfigTransformers.kt │ │ ├── AttachMain.kt │ │ ├── ConstantProxySelector.kt │ │ ├── CustomSslContext.kt │ │ ├── HttpsUrlConnectionTransformer.kt │ │ ├── JavaClientTransformer.kt │ │ ├── JettyClientTransformer.kt │ │ ├── KtorCioTransformers.kt │ │ ├── OkHttpClientTransformers.kt │ │ ├── ProxySelectorTransformer.kt │ │ ├── ReactorNettyTransformers.kt │ │ ├── SslContextTransformer.kt │ │ ├── TransformationLogger.kt │ │ ├── UrlConnectionTransformer.kt │ │ ├── VertxHttpClientTransformer.kt │ │ └── VertxNetClientOptionsTransformer.kt └── test │ ├── kotlin │ └── IntegrationTests.kt │ └── resources │ ├── cert.jks │ └── cert.pem └── test-app ├── build.gradle └── src └── main └── java └── tech └── httptoolkit └── testapp ├── Main.java └── cases ├── AkkaHostClientCase.java ├── AkkaRequestClientCase.java ├── ApacheHttpAsyncClientV4Case.java ├── ApacheHttpAsyncClientV5Case.java ├── ApacheHttpClientV3Case.java ├── ApacheHttpClientV4Case.java ├── ApacheHttpClientV5Case.java ├── AsyncHttpClientCase.java ├── ClientCase.java ├── HttpUrlConnCase.java ├── JavaHttpClientCase.java ├── JettyClientCase.java ├── KtorCioCase.kt ├── OkHttpV2Case.java ├── OkHttpV4Case.java ├── RestEasyWithApacheHttpClientV4Case.java ├── RetrofitCase.java ├── SpringWebClientCase.java ├── VertxHttpClientCase.java └── VertxWebClientCase.java /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build & test 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Set up JRE 8 as libs for R8 12 | uses: actions/setup-java@v3 13 | with: 14 | java-version: 8 15 | distribution: adopt-hotspot 16 | 17 | - name: Set up JDK 14 for build 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: 14 21 | distribution: adopt-hotspot 22 | 23 | - name: Build & test the agent standalone 24 | run: ./gradlew quickTest 25 | 26 | - name: Build & test the full distributable 27 | run: ./gradlew distTest 28 | 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: distributables 32 | path: build/libs/*-dist.jar 33 | if-no-files-found: error 34 | 35 | - name: Publish tagged release 36 | uses: svenstaro/upload-release-action@v2 37 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 38 | with: 39 | repo_token: ${{ secrets.GITHUB_TOKEN }} 40 | file: build/libs/*-dist.jar 41 | file_glob: true 42 | tag: ${{ github.ref }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | build/ 26 | .idea/workspace.xml 27 | .idea/tasks.xml 28 | .gradle 29 | local.properties -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | http-proxy-agent -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/jvm-http-proxy-agent.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules/java-agent.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules/java-agent.main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/modules/java-agent.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jvm-http-proxy-agent 2 | 3 | > _Part of [HTTP Toolkit](https://httptoolkit.com): powerful tools for building, testing & debugging HTTP(S)_ 4 | 5 | A JVM agent that automatically forces a proxy for HTTP(S) connections, and trusts a given additional HTTPS certificate authority, for all major JVM HTTP clients. 6 | 7 | This agent lets you intercept all HTTP(S) from any JVM application automatically, with no code changes, so you can inspect, debug & mock this traffic using an HTTPS proxy, such as [HTTP Toolkit](https://httptoolkit.com) or any other HTTPS MitM proxy. 8 | 9 | You can either launch the application with this agent from the start, or it can attach to and take over HTTP(S) from an already running JVM application. 10 | 11 | Traffic can be captured from at least: 12 | 13 | - [x] Java's built-in HttpURLConnection 14 | - [x] Java 11's built-in HttpClient 15 | - [x] Apache HttpClient v3, v4 & v5 16 | - [x] Apache HttpAsyncClient v4 & v5 17 | - [x] OkHttp v2, v3 & v4 18 | - [x] Retrofit 19 | - [x] Jetty-Client v9, v10 & v11 20 | - [x] Async-Http-Client 21 | - [x] Reactor-Netty v0.9 & v1+ 22 | - [x] Spring WebClient 23 | - [x] Ktor-Client 24 | - [x] Akka-HTTP v10.1.6+ 25 | - [x] Vert.x HttpClient and WebClient 26 | 27 | This will also capture HTTP(S) from any downstream libraries based on each of these clients, and many other untested clients sharing similar implementations, and so should cover a very large percentage of HTTP client usage. 28 | 29 | This agent supports at least Oracle & OpenJDK v8+ when starting the application with the agent, or v11+ for application that the agent will attach to. 30 | 31 | It's likely that this supports many other HTTP client configurations & JVMs too. If you find a case that isn't supported, or isn't supported correctly, please [file an issue](https://github.com/httptoolkit/jvm-http-proxy-agent/issues/new). 32 | 33 | ## Usage 34 | 35 | This agent can either be attached when the process is started, or attached later to a running process. 36 | 37 | In each case, the agent must be configured with the proxy host (e.g. 127.0.0.1), the proxy port (e.g. 8000) and the absolute path to the HTTPS certificate to be trusted. 38 | 39 | ### Attaching at startup 40 | 41 | To attach at startup, pass this JAR using the `javaagent` option, e.g: 42 | 43 | ``` 44 | java -javaagent:./agent.jar="127.0.0.1|8000|/path/to/cert.pem" -jar ./application.jar 45 | ``` 46 | 47 | ### Attaching to a running process 48 | 49 | To attach to a process, first launch the target process, and then run: 50 | 51 | ``` 52 | java -jar ./agent.jar 1234 127.0.0.1 8000 /path/to/cert.pem 53 | ``` 54 | 55 | where 1234 is the pid of the target process. This will exit successfully & immediately if attachment succeeds, or with a non-zero exit code if not. 56 | 57 | You can also query the available JVM processes ids, like so: 58 | 59 | ``` 60 | > java -jar ./agent.jar list-targets 61 | 589739:./application.jar 62 | 404401:com.intellij.idea.Main 63 | 453889:org.jetbrains.kotlin.daemon.KotlinCompileDaemon --daemon-runFilesPath ... 64 | 413868:org.gradle.launcher.daemon.bootstrap.GradleDaemon 6.7 65 | ``` 66 | 67 | When attached from startup all clients will always be intercepted. When attached later, both newly created HTTP clients and already existing instances will be intercepted, but it's possible that in some cases already established connections may not be immediately captured. Typically though these will eventually close and be reconnected, and at that point the connection is always intercepted. 68 | 69 | ### Testing attachment capabilities 70 | 71 | Not all JDKs provide the instrumentation & attachment APIs required to support this process, although all Oracle & OpenJDK v9+ versions should do so. 72 | 73 | To check this, you can test whether the `java` in your $PATH is capable of attaching to and intercepting a target process using the self-test command, like so: 74 | 75 | ```bash 76 | java -Djdk.attach.allowAttachSelf=true -jar ./agent.jar self-test 77 | ``` 78 | 79 | ### Contributing 80 | 81 | Contributions are very welcome! Reports of scenarios that aren't currently supported are helpful (please [create an issue](https://github.com/httptoolkit/jvm-http-proxy-agent/issues/new), including any errors, and preferably steps to reproduce the issue) but patches to fix issues are even better. 82 | 83 | Be aware that for all contributors to HTTP Toolkit components, including this, [HTTP Toolkit Pro is totally free](https://github.com/httptoolkit/httptoolkit/#contributing-directly) - just [get in touch](https://httptoolkit.com/contact) after your contribution is accepted with the email you'd like to use to claim your Pro account. 84 | 85 | To contribute a patch: 86 | 87 | * Fork this repo 88 | * Clone your fork: `git clone git@github.com:$YOUR_GITHUB_USERNAME/jvm-http-proxy-agent.git` 89 | * Create a new branch: `git checkout -B my-contribution-branch` 90 | * Check the existing tests pass locally: `./gradlew quickTest` 91 | * N.B. this requires Java 11+ - while some features are supported in older JVM versions, development requires a modern JVM 92 | * For library-specific issues: 93 | * Add/edit a test case in [/test-app/src/main/java/tech/httptoolkit/testapp/cases](https://github.com/httptoolkit/jvm-http-proxy-agent/tree/main/test-app/src/main/java/tech/httptoolkit/testapp/cases) to reproduce your issue 94 | * Add that case to [the list](https://github.com/httptoolkit/jvm-http-proxy-agent/blob/459b931a2eebd486261f296418aa028e4b2fb7e9/test-app/src/main/java/tech/httptoolkit/testapp/Main.java#L17-L36) if you created a new case. 95 | * Check that `./gradlew quickTest` now fails. 96 | * For more general changes: 97 | * Either add a test case (as above) or add a new standalone test in https://github.com/httptoolkit/jvm-http-proxy-agent/blob/main/src/test/kotlin/IntegrationTests.kt 98 | * Make your changes within the [advice classes](https://github.com/httptoolkit/jvm-http-proxy-agent/tree/main/src/main/java/tech/httptoolkit/javaagent/advice) and [injection setup code](https://github.com/httptoolkit/jvm-http-proxy-agent/tree/main/src/main/kotlin/tech/httptoolkit/javaagent) to fix your issue/add your feature. 99 | * Test that `./gradlew quickTest` now passes. 100 | * If you've changed any functionality, consider adding it to the docs here. 101 | * Commit your change, push it, and open a PR here for review. 102 | 103 | If you have any issues, or if you want to discuss a change before working on it (recommended for large/complex changes), please [open an issue](https://github.com/httptoolkit/jvm-http-proxy-agent/issues/new). 104 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'net.sf.proguard:proguard-gradle:6.2.2' 7 | } 8 | } 9 | 10 | 11 | plugins { 12 | id 'java' 13 | id 'org.jetbrains.kotlin.jvm' version '1.6.21' 14 | id 'com.github.johnrengelman.shadow' version '7.1.2' 15 | } 16 | 17 | group 'tech.httptoolkit' 18 | version '1.3.8' 19 | 20 | repositories { 21 | mavenCentral() 22 | maven { 23 | url "https://maven.google.com/" 24 | } 25 | } 26 | 27 | configurations { 28 | r8 29 | } 30 | 31 | dependencies { 32 | implementation group: 'net.bytebuddy', name: 'byte-buddy-dep', version: '1.15.4' 33 | // byte buddy contains references to jna classes and without them the r8Jar step fails 34 | compileOnly group: 'net.java.dev.jna', name: 'jna', version: '5.8.0' 35 | 36 | // Dependencies we load only as part of rewriting them, iff the target app includes them: 37 | compileOnly group: 'commons-httpclient', name: 'commons-httpclient', version: '3.1' 38 | compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5' 39 | compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3' 40 | compileOnly group: 'org.eclipse.jetty', name: 'jetty-client', version: '11.0.1' 41 | compileOnly group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.2' 42 | compileOnly group: 'io.projectreactor.netty', name: 'reactor-netty', version: '1.0.4' 43 | compileOnly group: 'io.ktor', name: 'ktor-client-core', version: '1.5.2' 44 | compileOnly group: 'io.ktor', name: 'ktor-client-cio', version: '1.5.2' 45 | compileOnly group: 'com.typesafe.akka', name: 'akka-http-core_2.13', version: '10.2.4' 46 | compileOnly group: 'com.typesafe.akka', name: 'akka-actor_2.13', version: '2.6.13' 47 | compileOnly group: 'io.vertx', name: 'vertx-core', version: '4.2.2' 48 | 49 | // Test deps: 50 | testImplementation group: 'io.kotest', name: 'kotest-runner-junit5-jvm', version: '4.4.0' 51 | testImplementation group: 'io.kotest', name: 'kotest-assertions-core-jvm', version: '4.4.0' 52 | testImplementation "com.github.tomakehurst:wiremock-jre8:2.27.2" 53 | 54 | // Only used during the R8 build task 55 | r8 group: 'com.android.tools', name: 'r8', version: '2.1.75' 56 | } 57 | 58 | compileJava { 59 | sourceCompatibility = '1.8' 60 | targetCompatibility = '1.8' 61 | } 62 | 63 | compileKotlin { 64 | kotlinOptions { 65 | jvmTarget = "1.8" 66 | } 67 | } 68 | 69 | tasks.withType(Jar) { 70 | manifest { 71 | attributes 'Premain-Class': 'tech.httptoolkit.javaagent.HttpProxyAgent' 72 | attributes 'Agent-Class': 'tech.httptoolkit.javaagent.HttpProxyAgent' 73 | attributes 'Main-Class': 'tech.httptoolkit.javaagent.AttachMain' 74 | 75 | attributes 'Can-Redefine-Classes': 'true' 76 | attributes 'Can-Retransform-Classes': 'true' 77 | } 78 | } 79 | 80 | // First, we bundle everything into a workable standalone JAR, with all runtime source included plus 81 | // dependencies plus agent metadata: 82 | shadowJar { 83 | minimize() 84 | exclude '**/*.kotlin_metadata' 85 | exclude '**/*.kotlin_module' 86 | exclude '**/*.kotlin_builtins' 87 | exclude '**/module_info.class' 88 | exclude 'META-INF/maven/**' 89 | 90 | // We have to specifically exclude our reactor stub code here, because we don't want to the type 91 | // stubs that we've manually defined in our own source included here. 92 | exclude 'reactor/' 93 | } 94 | 95 | // As part of bundling the JAR, we relocate all dependencies into our namespace: 96 | import com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation 97 | task relocateShadowJar(type: ConfigureShadowRelocation) { 98 | target = tasks.shadowJar 99 | prefix = "tech.httptoolkit.relocated" 100 | } 101 | tasks.shadowJar.dependsOn tasks.relocateShadowJar 102 | 103 | // Then we take this bundled JAR and optimize it. This shrinks it dramatically, but also breaks it, because 104 | // bytebuddy depends on some of our source being unmodified by R8 (frames in advice classes get messed with). 105 | def r8File = new File("$buildDir/libs/$archivesBaseName-r8.jar") 106 | tasks.register('r8Jar', JavaExec) { task -> 107 | def rules = file('r8-rules.txt') 108 | task.dependsOn(tasks.shadowJar) 109 | task.outputs.file(r8File) 110 | task.inputs.files shadowJar.getArchiveFile() 111 | 112 | task.classpath(configurations.r8) 113 | task.main = 'com.android.tools.r8.R8' 114 | task.args = [ 115 | '--release', 116 | '--classfile', 117 | '--output', r8File.toString(), 118 | '--pg-conf', rules.toString() 119 | ] + (configurations.compileClasspath.filter { path -> 120 | // Include libs for a few runtime-only deps, so R8 can resolve them during optimization: 121 | path.getName().startsWith("jna-") || 122 | path.getName().startsWith("jetty-util-") || 123 | path.getName().startsWith("commons-httpclient-") || 124 | path.getName().startsWith("async-http-client-") 125 | }.collect {path -> 126 | ['--lib', path.toString()] 127 | }.flatten()) 128 | 129 | doFirst { 130 | def java8Home = System.getenv("JAVA_HOME_8_X64") 131 | if (java8Home == null || java8Home.empty) { 132 | throw new GradleException("\$JAVA_HOME_8_X64 must be set to build a minified distributable") 133 | } else { 134 | // AFAICT R8 only supports the Java 8 lib files. We require that to be available, configured by env 135 | task.args += "--lib" 136 | task.args += java8Home 137 | } 138 | 139 | task.args += shadowJar.getArchiveFile().get().asFile.toString() 140 | } 141 | } 142 | 143 | // Then we fix this, by taking the raw advice classes for our own source from the original bundled JAR (i.e. including 144 | // any relocated references) and combining that with the minified & optimized dependencies from R8, to get a single 145 | // bundled and 99% optimized JAR. 146 | task distJar(type: Jar) { 147 | dependsOn(tasks.shadowJar, tasks.r8Jar) 148 | archiveClassifier = 'dist' 149 | 150 | // Pull raw advice classes from the shadow JAR, unminified: 151 | from (zipTree(shadowJar.getArchiveFile())) { 152 | include "tech/httptoolkit/javaagent/advice/**/*" 153 | include "tech/httptoolkit/relocated/net/bytebuddy/agent/builder/**/*" 154 | } 155 | 156 | // Pull other source & bundled dependencies in their minified form, from R8: 157 | from (zipTree(r8Jar.outputs.files[0])) { 158 | exclude "tech/httptoolkit/javaagent/advice/**/*" 159 | exclude "tech/httptoolkit/relocated/net/bytebuddy/agent/builder/**/*" 160 | } 161 | } 162 | 163 | tasks.withType(Test) { 164 | // We need to build both JARs before the integration tests can run 165 | dependsOn('shadowJar') 166 | dependsOn(':test-app:shadowJar') 167 | useJUnitPlatform() 168 | outputs.upToDateWhen {false} 169 | 170 | testLogging { 171 | events "STARTED", "PASSED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR" 172 | } 173 | } 174 | 175 | task quickTest(type: Test) { 176 | environment 'TEST_JAR', tasks.shadowJar.getArchiveFile().get().asFile.toString() 177 | } 178 | 179 | task distTest(type: Test) { 180 | environment 'TEST_JAR', tasks.distJar.getArchiveFile().get().asFile.toString() 181 | dependsOn('distJar') 182 | } 183 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httptoolkit/jvm-http-proxy-agent/d43d27373aba210c9c4b564d9e22960d64a0766d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /r8-rules.txt: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -dontoptimize 3 | -allowaccessmodification 4 | -keepattributes SourceFile, LineNumberTable, *Annotation* 5 | 6 | -keep class tech.httptoolkit.relocated.net.bytebuddy.agent.builder.** { *; } 7 | 8 | -keep class tech.httptoolkit.javaagent.** { *; } 9 | -keep class tech.httptoolkit.relocated.net.bytebuddy.asm.** { *; } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'http-proxy-agent' 2 | include 'test-app' 3 | 4 | -------------------------------------------------------------------------------- /src/main/java/reactor/netty/tcp/ProxyProvider.java: -------------------------------------------------------------------------------- 1 | package reactor.netty.tcp; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | /** 6 | * A stub with the parts of the interface we need to support v0.9 of Reactor-Netty. We compile against this, but we 7 | * *don't* include this in the resulting JAR, so references instead resolve to the real implementation, when that 8 | * is present. This is required because we can't depend on both v0.9 and v1 in the same module, and this class has 9 | * moved packages between the two. 10 | */ 11 | public final class ProxyProvider { 12 | 13 | public static ProxyProvider.TypeSpec builder() { 14 | return new ProxyProvider.Build(); 15 | } 16 | 17 | ProxyProvider(ProxyProvider.Build builder) {} 18 | 19 | public enum Proxy { 20 | HTTP 21 | } 22 | 23 | static final class Build implements TypeSpec, AddressSpec, Builder { 24 | 25 | Build() {} 26 | 27 | public final Builder address(InetSocketAddress address) { 28 | return this; 29 | } 30 | 31 | public final AddressSpec type(Proxy type) { 32 | return this; 33 | } 34 | 35 | public ProxyProvider build() { 36 | return new ProxyProvider(this); 37 | } 38 | } 39 | 40 | public interface TypeSpec { 41 | AddressSpec type(Proxy type); 42 | } 43 | 44 | public interface AddressSpec { 45 | Builder address(InetSocketAddress address); 46 | } 47 | 48 | public interface Builder { 49 | ProxyProvider build(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/OverrideAllProxySelectionAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | import java.net.InetSocketAddress; 6 | import java.net.Proxy; 7 | import java.net.URI; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class OverrideAllProxySelectionAdvice { 12 | 13 | @Advice.OnMethodExit 14 | public static void selectProxy( 15 | @Advice.Argument(value = 0) URI uri, 16 | @Advice.Return(readOnly = false) List returnedProxies 17 | ) { 18 | String scheme = uri.getScheme(); 19 | 20 | boolean isHttp = scheme.equals("http") || scheme.equals("https"); 21 | 22 | // We read from our custom variables, since we can't access HttpProxyAgent from a bootstrapped 23 | // class, and we use namespaced properties to make this extra reliable: 24 | String proxyHost = System.getProperty("tech.httptoolkit.proxyHost"); 25 | int proxyPort = Integer.parseInt(System.getProperty("tech.httptoolkit.proxyPort")); 26 | 27 | boolean isRequestToProxy = uri.getHost().equals(proxyHost) && uri.getPort() == proxyPort; 28 | 29 | // For HTTP URIs going elsewhere, we override all proxy selection globally to go via our proxy: 30 | if (isHttp && !isRequestToProxy) { 31 | returnedProxies = Collections.singletonList( 32 | new Proxy(Proxy.Type.HTTP, 33 | new InetSocketAddress(proxyHost, proxyPort) 34 | ) 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/OverrideSslContextFieldAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import javax.net.ssl.SSLContext; 5 | import java.security.NoSuchAlgorithmException; 6 | 7 | public class OverrideSslContextFieldAdvice { 8 | 9 | @Advice.OnMethodEnter 10 | public static void beforeMethod( 11 | @Advice.FieldValue(value = "sslContext", readOnly = false) SSLContext sslContextField 12 | ) throws NoSuchAlgorithmException { 13 | sslContextField = SSLContext.getDefault(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/OverrideUrlConnectionProxyAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | import java.net.Proxy; 6 | import java.net.ProxySelector; 7 | import java.net.URI; 8 | 9 | public class OverrideUrlConnectionProxyAdvice { 10 | 11 | @Advice.OnMethodEnter 12 | public static void openConnection( 13 | @Advice.FieldValue(value = "protocol") String urlProtocol, 14 | @Advice.Argument(value = 0, readOnly = false) Proxy proxyArgument 15 | ) { 16 | if (urlProtocol.equals("http") || urlProtocol.equals("https")) { 17 | // We can't access HttpProxyAgent here or even thisd class, since we're in the bootstrap loader, but 18 | // we've already stored a proxy on ProxySelector for all URLs, so we can just use that directly: 19 | proxyArgument = ProxySelector.getDefault().select( 20 | URI.create("http://example.com") 21 | ).get(0); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ReturnProxyAddressAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import tech.httptoolkit.javaagent.HttpProxyAgent; 5 | 6 | import java.net.InetSocketAddress; 7 | import java.net.SocketAddress; 8 | 9 | public class ReturnProxyAddressAdvice { 10 | @Advice.OnMethodExit 11 | public static void proxy(@Advice.Return(readOnly = false) SocketAddress returnValue) { 12 | returnValue = new InetSocketAddress( 13 | HttpProxyAgent.getAgentProxyHost(), 14 | HttpProxyAgent.getAgentProxyPort() 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ReturnProxyAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import tech.httptoolkit.javaagent.HttpProxyAgent; 5 | 6 | import java.net.InetSocketAddress; 7 | import java.net.Proxy; 8 | 9 | public class ReturnProxyAdvice { 10 | @Advice.OnMethodExit 11 | public static void proxy(@Advice.Return(readOnly = false) Proxy returnValue) { 12 | returnValue = new Proxy(Proxy.Type.HTTP, new InetSocketAddress( 13 | HttpProxyAgent.getAgentProxyHost(), 14 | HttpProxyAgent.getAgentProxyPort() 15 | )); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ReturnProxySelectorAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | import java.net.ProxySelector; 6 | import java.util.Optional; 7 | 8 | public class ReturnProxySelectorAdvice { 9 | @Advice.OnMethodExit 10 | public static void proxy(@Advice.Return(readOnly = false) Optional returnValue) { 11 | returnValue = Optional.of(ProxySelector.getDefault()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ReturnSslContextAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | import javax.net.ssl.SSLContext; 6 | import java.security.NoSuchAlgorithmException; 7 | 8 | public class ReturnSslContextAdvice { 9 | @Advice.OnMethodExit 10 | public static void sslContext(@Advice.Return(readOnly = false) SSLContext returnValue) { 11 | try { 12 | returnValue = SSLContext.getDefault(); 13 | } catch (NoSuchAlgorithmException e) { 14 | throw new RuntimeException(e); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ReturnSslSocketFactoryAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | import javax.net.ssl.SSLContext; 6 | import javax.net.ssl.SSLSocketFactory; 7 | import java.security.NoSuchAlgorithmException; 8 | 9 | public class ReturnSslSocketFactoryAdvice { 10 | @Advice.OnMethodExit 11 | public static void sslSocketFactory(@Advice.Return(readOnly = false) SSLSocketFactory returnValue) { 12 | try { 13 | returnValue = SSLContext.getDefault().getSocketFactory(); 14 | } catch (NoSuchAlgorithmException e) { 15 | throw new RuntimeException(e); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/SkipMethodAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | // General purpose advice which skips a given method, returning the default value for its type 6 | // (so usually null) if there is a return value, and silently doing nothing. 7 | public class SkipMethodAdvice { 8 | @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) // => skip if we return true (or similar) 9 | public static boolean skipMethod() { 10 | return true; // Skip the method body entirely 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/akka/OverrideHttpSettingsAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.akka; 2 | 3 | import akka.http.javadsl.ClientTransport; 4 | import akka.http.javadsl.ConnectionContext; 5 | import akka.http.javadsl.settings.ClientConnectionSettings; 6 | import net.bytebuddy.asm.Advice; 7 | import net.bytebuddy.implementation.bytecode.assign.Assigner; 8 | import tech.httptoolkit.javaagent.HttpProxyAgent; 9 | 10 | import java.net.InetSocketAddress; 11 | import java.util.Arrays; 12 | 13 | public class OverrideHttpSettingsAdvice { 14 | 15 | public static final boolean hasHttpsSettingsMethod = 16 | Arrays.stream(ConnectionContext.class.getDeclaredMethods()) 17 | .anyMatch(method -> method.getName().equals("httpsClient")); 18 | 19 | public static final ConnectionContext interceptedConnectionContext = hasHttpsSettingsMethod 20 | // For 10.2+: 21 | ? ConnectionContext.httpsClient(HttpProxyAgent.getInterceptedSslContext()) 22 | // For everything before then: 23 | : ConnectionContext.https(HttpProxyAgent.getInterceptedSslContext()); 24 | 25 | @Advice.OnMethodEnter 26 | public static void beforeOutgoingConnection( 27 | @Advice.Argument(value = 2, readOnly = false, typing = Assigner.Typing.DYNAMIC) ClientConnectionSettings clientSettings, 28 | @Advice.Argument(value = 3, readOnly = false, typing = Assigner.Typing.DYNAMIC) ConnectionContext connectionContext 29 | ) { 30 | // Change all new outgoing connections to use the proxy: 31 | clientSettings = clientSettings.withTransport( 32 | ClientTransport.httpsProxy(new InetSocketAddress( 33 | HttpProxyAgent.getAgentProxyHost(), 34 | HttpProxyAgent.getAgentProxyPort() 35 | )) 36 | ); 37 | 38 | // Change all new outgoing connections to trust our certificate: 39 | if (connectionContext.isSecure()) { 40 | connectionContext = OverrideHttpSettingsAdvice.interceptedConnectionContext; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/akka/ResetOldGatewaysAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.akka; 2 | 3 | import akka.http.impl.settings.HostConnectionPoolSetup; 4 | import akka.http.scaladsl.ClientTransport; 5 | import net.bytebuddy.asm.Advice; 6 | import scala.concurrent.Await; 7 | import scala.concurrent.Future; 8 | import scala.concurrent.duration.Duration; 9 | 10 | import java.lang.reflect.Method; 11 | import java.util.Collections; 12 | import java.util.Set; 13 | import java.util.WeakHashMap; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.TimeoutException; 16 | 17 | // This is very similar to ResetOldPoolsAdvice, but applies to older Akka setups, which use many PoolGateway instances, 18 | // one per config, rather than one PoolMaster instance. Otherwise the logic should be identical. 19 | public class ResetOldGatewaysAdvice { 20 | 21 | public static Set resetPoolSetups = Collections.newSetFromMap( 22 | Collections.synchronizedMap(new WeakHashMap<>()) 23 | ); 24 | 25 | @Advice.OnMethodEnter 26 | public static void beforeDispatchRequest( 27 | @Advice.This Object thisPoolGateway, 28 | @Advice.FieldValue(value = "hcps") HostConnectionPoolSetup poolSetup 29 | ) throws Exception { 30 | // If a pool config has been changed to use our proxy already, then we're perfect 31 | ClientTransport transport = poolSetup.setup().settings().transport(); 32 | boolean alreadyIntercepted = transport == ResetPoolSetupAdvice.interceptedProxyTransport; 33 | // If not, it's still OK, as long as we've previously reset the pool to ensure the connection was 34 | // re-established (we hook connection setup too, so all new conns are intercepted, even with old config) 35 | boolean alreadyReset = resetPoolSetups.contains(poolSetup); 36 | 37 | if (alreadyIntercepted || alreadyReset) return; 38 | 39 | // Otherwise this is a request to use a pre-existing connection pool which probably has connections open that 40 | // aren't using our proxy. We shutdown the pool before the request. It'll be restarted automatically when 41 | // the request does go through, but this ensures we re-establish connections (so it definitely gets intercepted) 42 | Method shutdownMethod = thisPoolGateway.getClass().getDeclaredMethod("shutdown"); 43 | 44 | Future shutdownFuture = (Future) shutdownMethod.invoke(thisPoolGateway); 45 | 46 | // We wait a little, just to ensure the shutdown is definitely started before this request is dispatched. 47 | try { 48 | Await.result(shutdownFuture, Duration.apply(10, TimeUnit.MILLISECONDS)); 49 | } catch (TimeoutException ignored) {} 50 | 51 | // Lastly, we remember this pool setup, so that we don't unnecessarily reset it again in future: 52 | resetPoolSetups.add(poolSetup); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/akka/ResetOldPoolsAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.akka; 2 | 3 | import akka.http.impl.engine.client.PoolId; 4 | import akka.http.scaladsl.ClientTransport; 5 | import net.bytebuddy.asm.Advice; 6 | import scala.concurrent.Await; 7 | import scala.concurrent.Future; 8 | import scala.concurrent.duration.Duration; 9 | 10 | import java.lang.reflect.Method; 11 | import java.util.Collections; 12 | import java.util.Set; 13 | import java.util.WeakHashMap; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.TimeoutException; 16 | 17 | public class ResetOldPoolsAdvice { 18 | 19 | public static Set resetPoolIds = Collections.newSetFromMap( 20 | Collections.synchronizedMap(new WeakHashMap<>()) 21 | ); 22 | 23 | @Advice.OnMethodEnter 24 | public static void beforeDispatchRequest( 25 | @Advice.This Object thisPoolMaster, 26 | @Advice.Argument(value = 0) PoolId poolId 27 | ) throws Exception { 28 | // If a pool config has been changed to use our proxy already, then we're perfect 29 | ClientTransport transport = poolId.hcps().setup().settings().transport(); 30 | boolean alreadyIntercepted = transport == ResetPoolSetupAdvice.interceptedProxyTransport; 31 | // If not, it's still OK, as long as we've previously reset the pool to ensure the connection was 32 | // re-established (we hook connection setup too, so all new conns are intercepted, even with old config) 33 | boolean alreadyReset = resetPoolIds.contains(poolId); 34 | 35 | if (alreadyIntercepted || alreadyReset) return; 36 | 37 | // Otherwise this is a request to use a pre-existing connection pool which probably has connections open that 38 | // aren't using our proxy. We shutdown the pool before the request. It'll be restarted automatically when 39 | // the request does go through, but this ensures we re-establish connections (so it definitely gets intercepted) 40 | Method shutdownMethod = thisPoolMaster.getClass() 41 | .getDeclaredMethod("shutdown", PoolId.class); 42 | 43 | Future shutdownFuture = (Future) shutdownMethod.invoke(thisPoolMaster, poolId); 44 | 45 | // We wait a little, just to ensure the shutdown is definitely started before this request is dispatched. 46 | try { 47 | Await.result(shutdownFuture, Duration.apply(10, TimeUnit.MILLISECONDS)); 48 | } catch (TimeoutException ignored) {} 49 | 50 | // Lastly, we remember this pool id, so that we don't unnecessarily reset it again in future: 51 | resetPoolIds.add(poolId); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/akka/ResetPoolSetupAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.akka; 2 | 3 | import akka.http.scaladsl.ClientTransport; 4 | import akka.http.scaladsl.settings.ConnectionPoolSettings; 5 | import akka.http.javadsl.ConnectionContext; 6 | import net.bytebuddy.asm.Advice; 7 | import net.bytebuddy.implementation.bytecode.assign.Assigner; 8 | import tech.httptoolkit.javaagent.HttpProxyAgent; 9 | 10 | import java.net.InetSocketAddress; 11 | 12 | public class ResetPoolSetupAdvice { 13 | 14 | // We use this to avoid re-instantiating the proxy endlessly, but also to recognize intercepted 15 | // and pre-existing settings configurations when they're used. 16 | public static ClientTransport interceptedProxyTransport = ClientTransport.httpsProxy( 17 | new InetSocketAddress( 18 | HttpProxyAgent.getAgentProxyHost(), 19 | HttpProxyAgent.getAgentProxyPort() 20 | ) 21 | ); 22 | 23 | @Advice.OnMethodExit 24 | public static void afterConstructor( 25 | @Advice.FieldValue(value = "settings", readOnly = false, typing = Assigner.Typing.DYNAMIC) ConnectionPoolSettings settings, 26 | @Advice.FieldValue(value = "connectionContext", readOnly = false, typing = Assigner.Typing.DYNAMIC) ConnectionContext connContext 27 | ) { 28 | // Change all new outgoing connections to use the proxy: 29 | settings = settings.withTransport(interceptedProxyTransport); 30 | 31 | // Change all new outgoing connections to trust our certificate: 32 | if (connContext.isSecure()) { 33 | connContext = OverrideHttpSettingsAdvice.interceptedConnectionContext; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheCustomSslProtocolSocketFactory.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import org.apache.commons.httpclient.params.HttpConnectionParams; 4 | import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | import javax.net.SocketFactory; 8 | import javax.net.ssl.SSLSocketFactory; 9 | import java.io.IOException; 10 | import java.net.*; 11 | 12 | public class ApacheCustomSslProtocolSocketFactory implements SecureProtocolSocketFactory { 13 | 14 | private final SSLSocketFactory interceptedSocketFactory = HttpProxyAgent 15 | .getInterceptedSslContext() 16 | .getSocketFactory(); 17 | 18 | @Override 19 | public Socket createSocket(String host, int port) throws IOException { 20 | return interceptedSocketFactory.createSocket(host, port); 21 | } 22 | 23 | @Override 24 | public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException { 25 | return interceptedSocketFactory.createSocket(host, port, localAddress, localPort); 26 | } 27 | 28 | @Override 29 | public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { 30 | return interceptedSocketFactory.createSocket(socket, host, port, autoClose); 31 | } 32 | 33 | @Override 34 | public Socket createSocket(String host, int port, InetAddress localAddress, int localPort, HttpConnectionParams params) throws IOException { 35 | // Marginally more complicated logic here unfortunately, since timeout isn't natively 36 | // supported. Minimal implementation taken from the existing lib implementations: 37 | if (params == null) { 38 | throw new IllegalArgumentException("Parameters may not be null"); 39 | } 40 | int timeout = params.getConnectionTimeout(); 41 | Socket socket; 42 | 43 | SocketFactory socketfactory = SSLSocketFactory.getDefault(); 44 | if (timeout == 0) { 45 | socket = socketfactory.createSocket(host, port, localAddress, localPort); 46 | } else { 47 | socket = socketfactory.createSocket(); 48 | SocketAddress localAddr = new InetSocketAddress(localAddress, localPort); 49 | SocketAddress remoteAddr = new InetSocketAddress(host, port); 50 | socket.bind(localAddr); 51 | socket.connect(remoteAddr, timeout); 52 | } 53 | 54 | return socket; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheOverrideProxyHostFieldAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.apache.commons.httpclient.ProxyHost; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | public class ApacheOverrideProxyHostFieldAdvice { 8 | 9 | @Advice.OnMethodExit 10 | public static void resetProxyHost( 11 | @Advice.FieldValue(value = "proxyHost", readOnly = false) ProxyHost proxyHostField 12 | ) { 13 | // After creating/changing HostConfiguration we override the proxy field: 14 | proxyHostField = new ProxyHost( 15 | HttpProxyAgent.getAgentProxyHost(), 16 | HttpProxyAgent.getAgentProxyPort() 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheReturnCustomSslProtocolSocketFactoryAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; 5 | 6 | public class ApacheReturnCustomSslProtocolSocketFactoryAdvice { 7 | 8 | @Advice.OnMethodExit 9 | public static void getSocketFactory( 10 | @Advice.FieldValue(value = "secure") boolean isSecure, 11 | @Advice.Return(readOnly = false) ProtocolSocketFactory returnValue 12 | ) { 13 | if (isSecure) { 14 | returnValue = new ApacheCustomSslProtocolSocketFactory(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheSetConfigProxyHostAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.apache.commons.httpclient.HostConfiguration; 5 | 6 | public class ApacheSetConfigProxyHostAdvice { 7 | 8 | @Advice.OnMethodEnter 9 | public static void beforeMakingRequests( 10 | @Advice.FieldValue(value = "hostConfiguration") HostConfiguration hostConfiguration 11 | ) { 12 | // Elsewhere, we hook setProxyHost to reset the proxy to our configured version whenever it's called. 13 | // Then, here we hook various methods to call it before they use the config: 14 | hostConfiguration.setProxyHost(null); // null here is ignored as this method is already hooked 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheSetSslSocketFactoryAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | 5 | import javax.net.ssl.SSLContext; 6 | import java.lang.reflect.Field; 7 | import java.util.Arrays; 8 | 9 | public class ApacheSetSslSocketFactoryAdvice { 10 | 11 | @Advice.OnMethodEnter 12 | public static void beforeCreateSocket(@Advice.This Object thisFactory) throws Exception { 13 | // Before creating the socket - replace the SSL context so the new socket trusts us. 14 | 15 | boolean intercepted = false; 16 | for (String factoryFieldName : Arrays.asList("socketfactory", "socketFactory")) { 17 | try { 18 | // Detect which field(s) are present on this class 19 | Field field = getDeclaredFieldInClassTree(thisFactory.getClass(), factoryFieldName); 20 | 21 | // Allow ourselves to change the socket factory value 22 | field.setAccessible(true); 23 | 24 | // Overwrite the socket factory with our own: 25 | field.set(thisFactory, SSLContext.getDefault().getSocketFactory()); 26 | intercepted = true; 27 | } catch (NoSuchFieldException ignored) { } 28 | } 29 | 30 | if (!intercepted) { 31 | throw new IllegalStateException("Apache HttpClient interception setup failed"); 32 | } 33 | } 34 | 35 | public static Field getDeclaredFieldInClassTree(Class type, String fieldName) throws NoSuchFieldException { 36 | for (Class clazz = type; clazz != null; clazz = clazz.getSuperclass()) { 37 | try { 38 | return clazz.getDeclaredField(fieldName); 39 | } catch (NoSuchFieldException ignored) { } 40 | } 41 | throw new NoSuchFieldException(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheV4ReturnProxyRouteAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import net.bytebuddy.implementation.bytecode.assign.Assigner; 5 | import org.apache.http.HttpHost; 6 | import org.apache.http.conn.routing.HttpRoute; 7 | 8 | import java.net.*; 9 | 10 | public class ApacheV4ReturnProxyRouteAdvice { 11 | @Advice.OnMethodExit 12 | public static void determineRoute( 13 | // We type this dynamically, because in some cases (notably Gradle) we seemingly can't reach the 14 | // HttpRoute type from ByteBuddy, only at runtime. 15 | @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object returnValue 16 | ) { 17 | HttpRoute existingValue = (HttpRoute) returnValue; 18 | // We guarantee that the default proxy selector is always our own. This ensures that we can 19 | // always grab the proxy URL without needing to access our injected classes. 20 | Proxy proxy = ProxySelector.getDefault().select(URI.create("https://example.com")).get(0); 21 | InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); 22 | 23 | returnValue = new HttpRoute( 24 | existingValue.getTargetHost(), 25 | existingValue.getLocalAddress(), 26 | new HttpHost(proxyAddress.getHostString(), proxyAddress.getPort()), 27 | existingValue.isSecure() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/apacheclient/ApacheV5ReturnProxyRouteAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.apacheclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.apache.hc.core5.http.HttpHost; 5 | import org.apache.hc.client5.http.HttpRoute; 6 | import tech.httptoolkit.javaagent.HttpProxyAgent; 7 | 8 | public class ApacheV5ReturnProxyRouteAdvice { 9 | @Advice.OnMethodExit 10 | public static void determineRoute( 11 | @Advice.Return(readOnly = false) HttpRoute returnValue 12 | ) { 13 | returnValue = new HttpRoute( 14 | returnValue.getTargetHost(), 15 | returnValue.getLocalAddress(), 16 | new HttpHost( 17 | HttpProxyAgent.getAgentProxyHost(), 18 | HttpProxyAgent.getAgentProxyPort() 19 | ), 20 | returnValue.isSecure() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/asynchttpclient/AsyncHttpClientReturnProxySelectorAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.asynchttpclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.asynchttpclient.proxy.ProxyServer; 5 | import org.asynchttpclient.proxy.ProxyServerSelector; 6 | import tech.httptoolkit.javaagent.HttpProxyAgent; 7 | 8 | public class AsyncHttpClientReturnProxySelectorAdvice { 9 | 10 | public static ProxyServerSelector proxyServerSelector = uri -> new ProxyServer.Builder( 11 | HttpProxyAgent.getAgentProxyHost(), 12 | HttpProxyAgent.getAgentProxyPort() 13 | ).build(); 14 | 15 | @Advice.OnMethodExit 16 | public static void getProxyServerSelector(@Advice.Return(readOnly = false) ProxyServerSelector returnValue) { 17 | returnValue = proxyServerSelector; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/asynchttpclient/AsyncHttpClientReturnSslContextAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.asynchttpclient; 2 | 3 | import io.netty.handler.ssl.SslContext; 4 | import io.netty.handler.ssl.SslContextBuilder; 5 | import net.bytebuddy.asm.Advice; 6 | import tech.httptoolkit.javaagent.HttpProxyAgent; 7 | 8 | import javax.net.ssl.SSLException; 9 | 10 | public class AsyncHttpClientReturnSslContextAdvice { 11 | @Advice.OnMethodExit 12 | public static void getSslContext(@Advice.Return(readOnly = false) SslContext returnValue) { 13 | try { 14 | returnValue = SslContextBuilder 15 | .forClient() 16 | .trustManager(HttpProxyAgent.getInterceptedTrustManagerFactory()) 17 | .build(); 18 | } catch (SSLException e) { 19 | throw new RuntimeException(e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/asynchttpclient/AsyncHttpResetSslEngineFactoryAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.asynchttpclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.asynchttpclient.AsyncHttpClientConfig; 5 | import org.asynchttpclient.SslEngineFactory; 6 | 7 | import java.util.Collections; 8 | import java.util.Set; 9 | import java.util.WeakHashMap; 10 | 11 | public class AsyncHttpResetSslEngineFactoryAdvice { 12 | 13 | // Track each ChannelManager with a weak ref, to avoid unnecessary reflection overhead by only 14 | // initializing them once, instead of every request 15 | public static Set patchedChannelManagers = Collections.newSetFromMap( 16 | Collections.synchronizedMap(new WeakHashMap<>()) 17 | ); 18 | 19 | @Advice.OnMethodEnter 20 | public static void createSslHandler( 21 | @Advice.This Object thisChannelManager 22 | ) { 23 | if (patchedChannelManagers.contains(thisChannelManager)) return; 24 | 25 | try { 26 | Class ChannelManager = thisChannelManager.getClass(); 27 | 28 | SslEngineFactory sslEngineFactory = (SslEngineFactory) ChannelManager 29 | .getDeclaredField("sslEngineFactory") 30 | .get(thisChannelManager); 31 | 32 | AsyncHttpClientConfig config = (AsyncHttpClientConfig) ChannelManager 33 | .getDeclaredField("config") 34 | .get(thisChannelManager); 35 | 36 | // Reinitialize the SSL Engine from the config (which uses our new cert) 37 | // before building the SSL handler. 38 | sslEngineFactory.init(config); 39 | } catch (Exception e) { 40 | throw new RuntimeException(e); 41 | } 42 | 43 | patchedChannelManagers.add(thisChannelManager); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/jettyclient/JettyResetDestinationsAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.jettyclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.eclipse.jetty.client.*; 5 | 6 | import java.util.Collections; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.WeakHashMap; 10 | 11 | public class JettyResetDestinationsAdvice { 12 | 13 | // Track each client with a weak ref, to avoid unnecessary reflection overhead by only 14 | // initializing them once, instead of every request 15 | public static Set patchedHttpClients = Collections.newSetFromMap( 16 | Collections.synchronizedMap(new WeakHashMap<>()) 17 | ); 18 | 19 | @Advice.OnMethodEnter 20 | public static void beforeResolveDestination( 21 | @Advice.This Object thisHttpClient 22 | // ^ Note that we can't use the real HttpClient type here, since this class is redefining it, so it would 23 | // cause a circular reference that breaks patching completely. 24 | ) { 25 | if (patchedHttpClients.contains(thisHttpClient)) return; 26 | 27 | // If this is the first time that we've seen this client, it's possible that it existed before we attached, 28 | // and it might have some existing open connections that don't use our proxy. To fix that, just once per 29 | // client, we use reflection to get the destinations (cached connections) and reset them. 30 | try { 31 | @SuppressWarnings("unchecked") 32 | Map destinations = (Map) 33 | thisHttpClient.getClass().getDeclaredField("destinations").get(thisHttpClient); 34 | 35 | // Reset this destinations list: 36 | for (HttpDestination destination : destinations.values()) { 37 | destination.close(); 38 | } 39 | destinations.clear(); 40 | } catch (Exception e) { 41 | throw new RuntimeException(e); 42 | } 43 | 44 | patchedHttpClients.add(thisHttpClient); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/jettyclient/JettyReturnProxyConfigurationAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.jettyclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.eclipse.jetty.client.HttpProxy; 5 | import org.eclipse.jetty.client.Origin; 6 | import org.eclipse.jetty.client.ProxyConfiguration; 7 | import tech.httptoolkit.javaagent.HttpProxyAgent; 8 | 9 | public class JettyReturnProxyConfigurationAdvice { 10 | @Advice.OnMethodExit 11 | public static void getProxyConfiguration(@Advice.Return(readOnly = false) ProxyConfiguration returnValue) { 12 | Origin.Address proxyAddress = new Origin.Address(HttpProxyAgent.getAgentProxyHost(), HttpProxyAgent.getAgentProxyPort()); 13 | 14 | ProxyConfiguration proxyConfig = new ProxyConfiguration(); 15 | proxyConfig.getProxies().add(new HttpProxy(proxyAddress, false)); 16 | 17 | returnValue = proxyConfig; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/jettyclient/JettyReturnSslContextFactoryV10Advice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.jettyclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.eclipse.jetty.util.ssl.SslContextFactory; 5 | 6 | import javax.net.ssl.SSLContext; 7 | 8 | public class JettyReturnSslContextFactoryV10Advice { 9 | @Advice.OnMethodExit 10 | public static void getSslContextFactory( 11 | @Advice.Return(readOnly = false) SslContextFactory.Client returnValue 12 | ) throws Exception { 13 | SslContextFactory.Client sslFactory = new SslContextFactory.Client(); 14 | sslFactory.setSslContext(SSLContext.getDefault()); 15 | sslFactory.start(); 16 | 17 | returnValue = sslFactory; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/jettyclient/JettyReturnSslContextFactoryV9Advice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.jettyclient; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import org.eclipse.jetty.util.ssl.SslContextFactory; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | import javax.net.ssl.SSLContext; 8 | 9 | public class JettyReturnSslContextFactoryV9Advice { 10 | 11 | @Advice.OnMethodExit 12 | public static void getSslContextFactory( 13 | @Advice.Return(readOnly = false) SslContextFactory returnValue 14 | ) throws Exception { 15 | SslContextFactory sslFactory = new JettyV9StubContextFactory(); 16 | sslFactory.setSslContext(SSLContext.getDefault()); 17 | try { 18 | sslFactory.start(); 19 | } catch (Exception e) { 20 | throw new RuntimeException(e); 21 | } 22 | 23 | returnValue = sslFactory; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/jettyclient/JettyV9StubContextFactory.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.jettyclient; 2 | 3 | import org.eclipse.jetty.util.ssl.SslContextFactory; 4 | 5 | // Does absolutely nothing, except for not being abstract in the v10 types, which means we can 6 | // properly instantiate it in v9. This is part of the silly games required to support both 7 | // versions whilst we can only depend directly on one at compile time. 8 | public class JettyV9StubContextFactory extends SslContextFactory { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ktor/KtorResetProxyFieldAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.ktor; 2 | 3 | import io.ktor.network.tls.TLSConfig; 4 | import net.bytebuddy.asm.Advice; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | import javax.net.ssl.X509TrustManager; 8 | import java.net.InetSocketAddress; 9 | import java.net.Proxy; 10 | 11 | public class KtorResetProxyFieldAdvice { 12 | 13 | @Advice.OnMethodEnter 14 | public static void beforeExecute( 15 | @Advice.FieldValue(value = "proxy", readOnly = false) Proxy proxyField 16 | ) throws Exception { 17 | proxyField = new Proxy(Proxy.Type.HTTP, new InetSocketAddress( 18 | HttpProxyAgent.getAgentProxyHost(), 19 | HttpProxyAgent.getAgentProxyPort() 20 | )); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/ktor/KtorResetTlsClientTrustAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.ktor; 2 | 3 | import io.ktor.network.tls.TLSConfig; 4 | import net.bytebuddy.asm.Advice; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | import javax.net.ssl.X509TrustManager; 8 | 9 | public class KtorResetTlsClientTrustAdvice { 10 | 11 | @Advice.OnMethodEnter 12 | public static void beforeOpenTLSSession( 13 | @Advice.Argument(value = 3, readOnly = false) TLSConfig tlsClientConfig 14 | ) throws Exception { 15 | // We're rewriting Kotlin bytecode from Java now, so some things get funky. Here it seems 16 | // that coroutines result in a double call where the outer call has no args, so we need this: 17 | if (tlsClientConfig == null) return; 18 | 19 | // Clone the config, but replace the trust manager with one that trusts only our certificate: 20 | tlsClientConfig = new TLSConfig( 21 | tlsClientConfig.getRandom(), 22 | tlsClientConfig.getCertificates(), 23 | (X509TrustManager) HttpProxyAgent 24 | .getInterceptedTrustManagerFactory() 25 | .getTrustManagers()[0], 26 | tlsClientConfig.getCipherSuites(), 27 | tlsClientConfig.getServerName() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/reactornetty/ReactorNettyResetAllConfigAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.reactornetty; 2 | 3 | import io.netty.handler.ssl.SslContextBuilder; 4 | import net.bytebuddy.asm.Advice; 5 | import reactor.netty.http.client.HttpClientConfig; 6 | import reactor.netty.tcp.SslProvider; 7 | import reactor.netty.transport.ClientTransportConfig; 8 | import reactor.netty.transport.ProxyProvider; 9 | import tech.httptoolkit.javaagent.HttpProxyAgent; 10 | 11 | import java.lang.reflect.Field; 12 | import java.net.InetSocketAddress; 13 | 14 | public class ReactorNettyResetAllConfigAdvice { 15 | 16 | // Netty's HTTP Client works by creating a new client subclass, and passing the existing client's 17 | // config. We hook that here: we rewrite the config whenever it's used, affecting all clients 18 | // involved, and in practice anybody using config anywhere. 19 | 20 | @Advice.OnMethodEnter 21 | public static void beforeConstructor( 22 | @Advice.Argument(value=0) HttpClientConfig baseHttpConfig 23 | ) throws Exception { 24 | // It would be nice to do this setup statically, but due to how some classloader configs work (e.g. Spring) it 25 | // seems this can fail when run in a static block, so we just repeat the process for every interception: 26 | final SslProvider agentSslProvider = SslProvider.builder() 27 | .sslContext( 28 | SslContextBuilder 29 | .forClient() 30 | .trustManager(HttpProxyAgent.getInterceptedTrustManagerFactory()) 31 | .build() 32 | ).build(); 33 | 34 | final ProxyProvider agentProxyProvider = ProxyProvider.builder() 35 | .type(ProxyProvider.Proxy.HTTP) 36 | .address(new InetSocketAddress( 37 | HttpProxyAgent.getAgentProxyHost(), 38 | HttpProxyAgent.getAgentProxyPort() 39 | )) 40 | .build(); 41 | 42 | Field configSslField; 43 | Field proxyProviderField; 44 | 45 | try { 46 | // Rewrite the fields we want to mess with in the client config: 47 | configSslField = HttpClientConfig.class.getDeclaredField("sslProvider"); 48 | configSslField.setAccessible(true); 49 | 50 | proxyProviderField = ClientTransportConfig.class.getDeclaredField("proxyProvider"); 51 | proxyProviderField.setAccessible(true); 52 | } catch (Exception e) { 53 | throw new RuntimeException(e); 54 | } 55 | 56 | configSslField.set(baseHttpConfig, agentSslProvider); 57 | proxyProviderField.set(baseHttpConfig, agentProxyProvider); 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/reactornetty/ReactorNettyResetHttpClientSecureSslAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.reactornetty; 2 | 3 | import io.netty.handler.ssl.SslContextBuilder; 4 | import net.bytebuddy.asm.Advice; 5 | import reactor.netty.tcp.SslProvider; 6 | import tech.httptoolkit.javaagent.HttpProxyAgent; 7 | 8 | public class ReactorNettyResetHttpClientSecureSslAdvice { 9 | 10 | public static final SslProvider agentSslProvider; 11 | 12 | static { 13 | try { 14 | // Initialize our intercepted SSL provider: 15 | agentSslProvider = SslProvider.builder() 16 | .sslContext( 17 | SslContextBuilder 18 | .forClient() 19 | .trustManager(HttpProxyAgent.getInterceptedTrustManagerFactory()) 20 | .build() 21 | ).build(); 22 | } catch (Exception e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | 27 | // In v0.9 versions of Reactor Netty, the sslProvider is stored on HttpClientSecure. Here we hook that class's 28 | // constructor and replace the SSL provider as soon as it's set. 29 | 30 | @Advice.OnMethodExit 31 | public static void afterConstructor( 32 | @Advice.FieldValue(value = "sslProvider", readOnly = false) SslProvider sslProviderField 33 | ) { 34 | sslProviderField = agentSslProvider; 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/reactornetty/ReactorNettyV09ResetProxyProviderFieldAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.reactornetty; 2 | 3 | import net.bytebuddy.asm.Advice; 4 | import reactor.netty.tcp.ProxyProvider; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | import java.net.InetSocketAddress; 8 | 9 | 10 | // Reset the proxyProvider field to use our own intercepted proxy after certain constructors 11 | // complete. Note that this uses the v0.9 proxyProvider class, so it would fail if applied to 12 | // when v1 is loaded in the target app. 13 | public class ReactorNettyV09ResetProxyProviderFieldAdvice { 14 | 15 | public static final ProxyProvider agentProxyProvider = ProxyProvider.builder() 16 | .type(ProxyProvider.Proxy.HTTP) 17 | .address(new InetSocketAddress( 18 | HttpProxyAgent.getAgentProxyHost(), 19 | HttpProxyAgent.getAgentProxyPort() 20 | )) 21 | .build(); 22 | 23 | @Advice.OnMethodExit 24 | public static void afterConstructor( 25 | @Advice.FieldValue(value = "proxyProvider", readOnly = false) ProxyProvider proxyProviderField 26 | ) { 27 | proxyProviderField = agentProxyProvider; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxHttpClientReturnProxyConfigurationAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.vertxclient; 2 | 3 | import io.vertx.core.net.ProxyOptions; 4 | import net.bytebuddy.asm.Advice; 5 | import tech.httptoolkit.javaagent.HttpProxyAgent; 6 | 7 | public class VertxHttpClientReturnProxyConfigurationAdvice { 8 | 9 | @Advice.OnMethodExit 10 | public static void getProxyConfiguration(@Advice.Return(readOnly = false) ProxyOptions returnValue) { 11 | returnValue = new ProxyOptions().setHost(HttpProxyAgent.getAgentProxyHost()).setPort(HttpProxyAgent.getAgentProxyPort()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/tech/httptoolkit/javaagent/advice/vertxclient/VertxNetClientOptionsSetTrustOptionsAdvice.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent.advice.vertxclient; 2 | 3 | import io.vertx.core.http.HttpClientOptions; 4 | import io.vertx.core.net.ClientOptionsBase; 5 | import io.vertx.core.net.NetClientOptions; 6 | import io.vertx.core.net.TrustOptions; 7 | import net.bytebuddy.asm.Advice; 8 | import tech.httptoolkit.javaagent.HttpProxyAgent; 9 | 10 | public class VertxNetClientOptionsSetTrustOptionsAdvice { 11 | 12 | @Advice.OnMethodExit 13 | public static void afterConstructor( 14 | @Advice.This NetClientOptions thisNetClientOptions, 15 | @Advice.Argument(value = 0) ClientOptionsBase other 16 | ) { 17 | if (other instanceof HttpClientOptions) { 18 | thisNetClientOptions.setTrustOptions(TrustOptions.wrap(HttpProxyAgent.getInterceptedTrustManagerFactory().getTrustManagers()[0])); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/AgentConfig.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import java.lang.IllegalArgumentException 4 | import java.lang.Integer.parseInt 5 | import java.net.URI 6 | import java.net.URISyntaxException 7 | 8 | data class Config( 9 | val certPath: String, 10 | val proxyHost: String, 11 | val proxyPort: Int 12 | ) 13 | 14 | fun getConfigFromEnv(): Config { 15 | val proxyUrl: String? = System.getenv("HTTPS_PROXY") 16 | if (proxyUrl.isNullOrEmpty()) { 17 | throw IllegalArgumentException("HTTPS interception failed, proxy URL not provided.") 18 | } 19 | 20 | val parsedProxyUrl = try { 21 | URI(proxyUrl) 22 | } catch(parseError: URISyntaxException) { 23 | throw IllegalArgumentException("HTTPS interception failed, could not parse proxy URL") 24 | } 25 | 26 | val certPath: String? = System.getenv("SSL_CERT_FILE") 27 | 28 | if (certPath.isNullOrEmpty()) { 29 | throw IllegalArgumentException("HTTPS interception failed, certificate path not provided.") 30 | } 31 | 32 | val proxyHost = parsedProxyUrl.host 33 | val proxyPort = parsedProxyUrl.port 34 | 35 | return Config(certPath, proxyHost, proxyPort) 36 | } 37 | 38 | fun formatConfigArg(proxyHost: String, proxyPort: String, certPath: String): String { 39 | return "$proxyHost|$proxyPort|$certPath" 40 | } 41 | 42 | fun getConfigFromArg(arg: String): Config { 43 | val (proxyHost, proxyPort, certPath) = arg.split("|", limit = 3) 44 | // ^ Limited so that you can use | in filenames, if you *really* want to be difficult 45 | return Config(certPath, proxyHost, parseInt(proxyPort)) 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/AgentMain.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("HttpProxyAgent") 2 | 3 | package tech.httptoolkit.javaagent 4 | 5 | import net.bytebuddy.ByteBuddy 6 | import net.bytebuddy.agent.builder.AgentBuilder 7 | import net.bytebuddy.asm.Advice 8 | import net.bytebuddy.description.type.TypeDescription 9 | import net.bytebuddy.dynamic.ClassFileLocator 10 | import net.bytebuddy.dynamic.DynamicType 11 | import net.bytebuddy.dynamic.scaffold.TypeValidation 12 | import net.bytebuddy.matcher.ElementMatchers.none 13 | import net.bytebuddy.pool.TypePool 14 | import net.bytebuddy.utility.JavaModule 15 | import java.lang.instrument.Instrumentation 16 | import javax.net.ssl.SSLContext 17 | import java.net.* 18 | import java.security.ProtectionDomain 19 | import javax.net.ssl.HttpsURLConnection 20 | import javax.net.ssl.TrustManagerFactory 21 | 22 | 23 | lateinit var InterceptedSslContext: SSLContext 24 | private set 25 | 26 | lateinit var InterceptedTrustManagerFactory: TrustManagerFactory 27 | private set 28 | 29 | lateinit var AgentProxyHost: String 30 | private set 31 | 32 | var AgentProxyPort = -1 33 | private set 34 | 35 | lateinit var AgentProxySelector: ProxySelector 36 | private set 37 | 38 | // If attached at startup with a -javaagent argument, use either arguments or env 39 | fun premain(arguments: String?, instrumentation: Instrumentation) { 40 | val config = try { 41 | getConfigFromArg(arguments!!) 42 | } catch (e: Throwable) { 43 | // If that fails for any reason (any kind of parse error at all), try to 44 | // use our env variables instead 45 | getConfigFromEnv() 46 | } 47 | interceptAllHttps(config, instrumentation) 48 | } 49 | 50 | // If attached after startup, pull config from the passed arguments 51 | fun agentmain(arguments: String?, instrumentation: Instrumentation) { 52 | if (arguments.isNullOrEmpty()) { 53 | throw Error("Can't attach proxy agent without configuration arguments") 54 | } 55 | 56 | // If attached as a test, we don't intercept anything, we're just checking that it's 57 | // possible to attach in the first place with the current VM. 58 | if (arguments == "attach-test") { 59 | println("Agent attach test successful") 60 | return 61 | }; 62 | 63 | val config = getConfigFromArg(arguments) 64 | interceptAllHttps(config, instrumentation) 65 | } 66 | 67 | fun interceptAllHttps(config: Config, instrumentation: Instrumentation) { 68 | val (certPath, proxyHost, proxyPort) = config 69 | 70 | InterceptedTrustManagerFactory = buildTrustManagerFactoryForCertificate(certPath) 71 | InterceptedSslContext = buildSslContextForCertificate(InterceptedTrustManagerFactory) 72 | AgentProxyHost = proxyHost 73 | AgentProxyPort = proxyPort 74 | 75 | // Reconfigure the JVM default settings: 76 | setDefaultProxy(proxyHost, proxyPort) 77 | setDefaultSslContext(InterceptedSslContext) 78 | 79 | val debugMode = !System.getenv("DEBUG_JVM_HTTP_PROXY_AGENT").isNullOrEmpty() 80 | val logger = TransformationLogger(debugMode) 81 | 82 | // Disabling type validation allows us to intercept non-Java types, e.g. Kotlin 83 | // in OkHttp. See https://github.com/raphw/byte-buddy/issues/764 84 | var agentBuilder = AgentBuilder.Default( 85 | ByteBuddy().with(TypeValidation.DISABLED) 86 | ) 87 | .ignore(none()) 88 | .with(AgentBuilder.TypeStrategy.Default.REDEFINE) 89 | .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) 90 | .disableClassFormatChanges() 91 | .with(logger) 92 | 93 | arrayOf( 94 | OkHttpClientV3Transformer(logger), 95 | OkHttpClientV2Transformer(logger), 96 | ApacheClientRoutingV4Transformer(logger), 97 | ApacheClientRoutingV5Transformer(logger), 98 | ApacheSslSocketFactoryTransformer(logger), 99 | ApacheClientTlsStrategyTransformer(logger), 100 | ApacheHostConfigurationTransformer(logger), 101 | ApacheHttpMethodDirectorTransformer(logger), 102 | ApacheProtocolTransformer(logger), 103 | JavaClientTransformer(logger), 104 | UrlConnectionTransformer(logger), 105 | HttpsUrlConnectionTransformer(logger), 106 | ProxySelectorTransformer(logger), 107 | SslContextTransformer(logger), 108 | JettyClientTransformer(logger), 109 | AsyncHttpClientConfigTransformer(logger), 110 | AsyncHttpChannelManagerTransformer(logger), 111 | ReactorNettyClientConfigTransformer(logger), 112 | ReactorNettyProxyProviderTransformer(logger), 113 | ReactorNettyOverrideRequestAddressTransformer(logger), 114 | ReactorNettyHttpClientSecureTransformer(logger), 115 | KtorClientEngineConfigTransformer(logger), 116 | KtorCioEngineTransformer(logger), 117 | KtorClientTlsTransformer(logger), 118 | AkkaHttpTransformer(logger), 119 | AkkaPoolSettingsTransformer(logger), 120 | AkkaPoolTransformer(logger), 121 | AkkaGatewayTransformer(logger), 122 | VertxHttpClientTransformer(logger), 123 | VertxNetClientOptionsTransformer(logger), 124 | ).forEach { matchingAgentTransformer -> 125 | agentBuilder = matchingAgentTransformer.register(agentBuilder) 126 | } 127 | 128 | agentBuilder.installOn(instrumentation) 129 | 130 | System.err.println("HTTP Toolkit interception active") 131 | } 132 | 133 | abstract class MatchingAgentTransformer(private val logger: TransformationLogger) : AgentBuilder.Transformer { 134 | abstract fun register(builder: AgentBuilder): AgentBuilder 135 | abstract fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> 136 | 137 | override fun transform( 138 | builder: DynamicType.Builder<*>, 139 | typeDescription: TypeDescription, 140 | classLoader: ClassLoader?, 141 | module: JavaModule?, 142 | protectionDomain: ProtectionDomain? 143 | ): DynamicType.Builder<*> { 144 | logger.beforeTransformation(typeDescription) 145 | 146 | return transform(builder) { adviceName -> 147 | val locator = if (classLoader != null) { 148 | ClassFileLocator.Compound( 149 | ClassFileLocator.ForClassLoader.of(classLoader), 150 | ClassFileLocator.ForClassLoader.of(ByteBuddy::class.java.classLoader) 151 | ) 152 | } else { 153 | ClassFileLocator.ForClassLoader.of(ByteBuddy::class.java.classLoader) 154 | } 155 | Advice.to(TypePool.Default.of(locator).describe(adviceName).resolve(), locator) 156 | } 157 | } 158 | } 159 | 160 | private fun setDefaultProxy(proxyHost: String, proxyPort: Int) { 161 | System.setProperty("http.proxyHost", proxyHost) 162 | System.setProperty("http.proxyPort", proxyPort.toString()) 163 | System.setProperty("https.proxyHost", proxyHost) 164 | System.setProperty("https.proxyPort", proxyPort.toString()) 165 | 166 | // We back up the properties in our namespace too, in case anybody manually overrides the above: 167 | System.setProperty("tech.httptoolkit.proxyHost", proxyHost) 168 | System.setProperty("tech.httptoolkit.proxyPort", proxyPort.toString()) 169 | 170 | val proxySelector = ConstantProxySelector(proxyHost, proxyPort) 171 | AgentProxySelector = proxySelector 172 | ProxySelector.setDefault(proxySelector) 173 | } 174 | 175 | private fun setDefaultSslContext(context: SSLContext) { 176 | SSLContext.setDefault(context) 177 | HttpsURLConnection.setDefaultSSLSocketFactory(context.socketFactory) 178 | } 179 | -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/AkkaClientTransformers.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.ByteBuddy 4 | import net.bytebuddy.agent.builder.AgentBuilder 5 | import net.bytebuddy.asm.Advice 6 | import net.bytebuddy.dynamic.ClassFileLocator 7 | import net.bytebuddy.dynamic.DynamicType 8 | import net.bytebuddy.matcher.ElementMatchers.* 9 | import net.bytebuddy.pool.TypePool 10 | import tech.httptoolkit.javaagent.advice.akka.* 11 | 12 | // First, we hook outgoing connection creation, and ensure that new connections always go via the proxy & trust us: 13 | class AkkaHttpTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 14 | override fun register(builder: AgentBuilder): AgentBuilder { 15 | return builder 16 | .type(named("akka.http.scaladsl.HttpExt")) // Scala compiles Http()s methods as 'Ext' for some reason 17 | .transform(this) 18 | } 19 | 20 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 21 | return builder 22 | .visit( 23 | loadAdvice("tech.httptoolkit.javaagent.advice.akka.OverrideHttpSettingsAdvice") 24 | .on(hasMethodName("_outgoingConnection")) 25 | ) 26 | } 27 | } 28 | 29 | // Second, when a connection pool setup is created (part of creating any connection pool, but also for 30 | // sending any individual request) we change its configuration. This isn't strictly necessary given the above, 31 | // but helps generally, and makes the 3rd step possible. 32 | class AkkaPoolSettingsTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 33 | override fun register(builder: AgentBuilder): AgentBuilder { 34 | return builder 35 | .type(named("akka.http.impl.settings.ConnectionPoolSetup")) 36 | .transform(this) 37 | } 38 | 39 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 40 | return builder 41 | .visit( 42 | loadAdvice("tech.httptoolkit.javaagent.advice.akka.ResetPoolSetupAdvice") 43 | .on(isConstructor()) 44 | ) 45 | } 46 | } 47 | 48 | // Then, to ensure that any existing connections trust us too, we monitor all calls to dispatchRequest, and reset 49 | // any pools that don't have intercepted configuration (so preare -existing), just once per pool id. 50 | class AkkaPoolTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 51 | override fun register(builder: AgentBuilder): AgentBuilder { 52 | return builder 53 | .type(named("akka.http.impl.engine.client.PoolMaster")) 54 | .transform(this) 55 | } 56 | 57 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 58 | return builder 59 | .visit( 60 | loadAdvice("tech.httptoolkit.javaagent.advice.akka.ResetOldPoolsAdvice") 61 | .on(hasMethodName("dispatchRequest")) 62 | ) 63 | } 64 | } 65 | 66 | // The above works perfectly for new Akka, but as a last step we duplicate the 3rd step for slightly older versions: 67 | class AkkaGatewayTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 68 | override fun register(builder: AgentBuilder): AgentBuilder { 69 | return builder 70 | .type(named("akka.http.impl.engine.client.PoolGateway")) // Exists on <10.2.0 71 | .transform(this) 72 | } 73 | 74 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 75 | return builder 76 | .visit( 77 | loadAdvice("tech.httptoolkit.javaagent.advice.akka.ResetOldGatewaysAdvice") 78 | .on(hasMethodName("apply")) 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/ApacheAsyncClientTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | import tech.httptoolkit.javaagent.advice.OverrideSslContextFieldAdvice 8 | 9 | // Apache async client hooks depend on the non-async Apache client transformers, which successfully transform proxy 10 | // configuration, but we need to separate re-transform TLS configuration too. 11 | 12 | // To do so, we get all instances of TlsStrategy/SSLIOSessionStrategy, all of which seem to have an sslContext private 13 | // field which they wrap around the connection their implementation upgrade(). Most of the real ones inherit from 14 | // AbstractClientTlsStrategy, which does this, but there's other examples too. We hook upgrade(), so that that 15 | // field is reset to use our context before any client TLS upgrade happens. 16 | 17 | class ApacheClientTlsStrategyTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 18 | override fun register(builder: AgentBuilder): AgentBuilder { 19 | return builder 20 | // For v5 we need to hook into every client TlsStrategy (of which there are many). 21 | .type( 22 | hasSuperType(named("org.apache.hc.core5.http.nio.ssl.TlsStrategy")) 23 | ).and( 24 | // There are both Server & Client strategies with the same interface, and checking the name is the only 25 | // way to tell the difference: 26 | nameContains("Client") 27 | ).and( 28 | // All strategies either do this, or extend a class that does this (which will be intercepted 29 | // first by itself anyway) 30 | declaresField(named("sslContext")) 31 | ).and( 32 | not(isInterface()) 33 | ).transform(this) 34 | // For v4, we do exactly the same, but there's only a single implementation: 35 | .type( 36 | named("org.apache.http.nio.conn.ssl.SSLIOSessionStrategy") 37 | ).transform(this) 38 | } 39 | 40 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 41 | return builder 42 | .visit( 43 | loadAdvice("tech.httptoolkit.javaagent.advice.OverrideSslContextFieldAdvice") 44 | .on(hasMethodName("upgrade")) 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/ApacheClientTransformers.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.method.MethodDescription 6 | import net.bytebuddy.dynamic.DynamicType 7 | import net.bytebuddy.matcher.ElementMatchers.* 8 | import tech.httptoolkit.javaagent.advice.apacheclient.* 9 | 10 | // For both v4 & v5 we override all implementations of the RoutePlanner interface, and we redefine all routes 11 | // to go via our proxy instead of their existing configuration. 12 | 13 | class ApacheClientRoutingV4Transformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 14 | override fun register(builder: AgentBuilder): AgentBuilder { 15 | return builder.type( 16 | hasSuperType(named("org.apache.http.conn.routing.HttpRoutePlanner")) 17 | ).and( 18 | not(isInterface()) 19 | ).transform(this) 20 | } 21 | 22 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 23 | return builder.visit( 24 | loadAdvice("tech.httptoolkit.javaagent.advice.apacheclient.ApacheV4ReturnProxyRouteAdvice") 25 | .on(hasMethodName("determineRoute")) 26 | ) 27 | } 28 | } 29 | 30 | class ApacheClientRoutingV5Transformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 31 | override fun register(builder: AgentBuilder): AgentBuilder { 32 | return builder.type( 33 | hasSuperType(named("org.apache.hc.client5.http.routing.HttpRoutePlanner")) 34 | ).and( 35 | not(isInterface()) 36 | ).transform(this) 37 | } 38 | 39 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 40 | return builder.visit( 41 | loadAdvice("tech.httptoolkit.javaagent.advice.apacheclient.ApacheV5ReturnProxyRouteAdvice") 42 | .on(hasMethodName("determineRoute")) 43 | ) 44 | } 45 | } 46 | 47 | // For certificates, we prepend to Apache SslConnectionSocketFactory's createLayeredSocket, so that before any 48 | // socket is created, the SSL context is replaced with our configured SslSocketFactory that uses our configured 49 | // SSLContext, which trusts our certificate, straight after initialization. 50 | 51 | class ApacheSslSocketFactoryTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 52 | override fun register(builder: AgentBuilder): AgentBuilder { 53 | return builder 54 | .type( 55 | named("org.apache.http.conn.ssl.SSLConnectionSocketFactory") 56 | ).transform(this) 57 | .type( 58 | named("org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory") 59 | ).transform(this) 60 | } 61 | 62 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 63 | return builder 64 | .visit( 65 | loadAdvice("tech.httptoolkit.javaagent.advice.apacheclient.ApacheSetSslSocketFactoryAdvice") 66 | .on(hasMethodName("createLayeredSocket")) 67 | ); 68 | } 69 | } 70 | 71 | // Meanwhile, for V3 we need to do something totally different: we patch HostConfiguration to apply a proxy to 72 | // all new configurations (and ignore changes), we patch HttpMethodDirector to update existing configurations 73 | // as they're used, and we patch Protocol to change the SslSocketFactory on all secure protocols. 74 | 75 | class ApacheHostConfigurationTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 76 | override fun register(builder: AgentBuilder): AgentBuilder { 77 | return builder 78 | .type( 79 | named("org.apache.commons.httpclient.HostConfiguration") 80 | ).transform(this) 81 | } 82 | 83 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 84 | return builder 85 | // Override the proxy field value for all new configurations, and for any attempts to call 86 | // setProxy/ProxyHost. We don't no-op these, because we want to call them ourselves later on 87 | // existing configs to reset them - we don't just want to ignore this. 88 | .visit( 89 | loadAdvice("tech.httptoolkit.javaagent.advice.apacheclient.ApacheOverrideProxyHostFieldAdvice") 90 | .on(isConstructor() 91 | .or(hasMethodName("setProxy")) 92 | .or(hasMethodName("setProxyHost")) 93 | ) 94 | ) 95 | } 96 | } 97 | 98 | // Whenever an HttpMethodDirector is used, we reset the proxy in the passed configuration. This uses the above 99 | // hooks, which ensure that setProxyHost(anything) automatically loads & sets our intercepted proxy. 100 | // We *don't* want to reset all proxy hosts in all existing configurations, because that's a) quite tricky and 101 | // b) some are used as keys in existing direct connections in pools, and we don't want to match those later. 102 | class ApacheHttpMethodDirectorTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 103 | override fun register(builder: AgentBuilder): AgentBuilder { 104 | return builder 105 | .type( 106 | named("org.apache.commons.httpclient.HttpMethodDirector") 107 | ).transform(this) 108 | } 109 | 110 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 111 | return builder 112 | .visit( 113 | loadAdvice("tech.httptoolkit.javaagent.advice.apacheclient.ApacheSetConfigProxyHostAdvice") 114 | .on(hasMethodName("executeMethod")) 115 | ) 116 | } 117 | } 118 | 119 | // Every v3 configuration has a protocol, and each one can build sockets in its own unique way. Here, we patch 120 | // all of them so that all _secure_ protocols trust only our certificate, and nothing else. This would 121 | // be an issue for a generic TCP client, but for HTTPS we know we should be the only authority present. 122 | class ApacheProtocolTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 123 | override fun register(builder: AgentBuilder): AgentBuilder { 124 | return builder 125 | .type( 126 | named("org.apache.commons.httpclient.protocol.Protocol") 127 | ).transform(this) 128 | } 129 | 130 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 131 | return builder 132 | .visit( 133 | loadAdvice("tech.httptoolkit.javaagent.advice.apacheclient.ApacheReturnCustomSslProtocolSocketFactoryAdvice") 134 | .on(hasMethodName("getSocketFactory")) 135 | ) 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/AsyncHttpClientConfigTransformers.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | import tech.httptoolkit.javaagent.advice.asynchttpclient.AsyncHttpClientReturnProxySelectorAdvice 8 | import tech.httptoolkit.javaagent.advice.asynchttpclient.AsyncHttpClientReturnSslContextAdvice 9 | import tech.httptoolkit.javaagent.advice.asynchttpclient.AsyncHttpResetSslEngineFactoryAdvice 10 | 11 | // For new clients, we just need to override the properties on the convenient config 12 | // class that contains both proxy & SSL configuration. 13 | class AsyncHttpClientConfigTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 14 | override fun register(builder: AgentBuilder): AgentBuilder { 15 | return builder 16 | .type( 17 | hasSuperType(named("org.asynchttpclient.AsyncHttpClientConfig")) 18 | ).and( 19 | not(isInterface()) 20 | ).transform(this) 21 | } 22 | 23 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 24 | return builder 25 | .visit( 26 | loadAdvice("tech.httptoolkit.javaagent.advice.asynchttpclient.AsyncHttpClientReturnSslContextAdvice") 27 | .on(hasMethodName("getSslContext"))) 28 | .visit( 29 | loadAdvice("tech.httptoolkit.javaagent.advice.asynchttpclient.AsyncHttpClientReturnProxySelectorAdvice") 30 | .on(hasMethodName("getProxyServerSelector"))) 31 | } 32 | } 33 | 34 | // For existing classes, we need to hook SSL Handler creation, called for 35 | // every new connection 36 | class AsyncHttpChannelManagerTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 37 | override fun register(builder: AgentBuilder): AgentBuilder { 38 | return builder 39 | .type( 40 | named("org.asynchttpclient.netty.channel.ChannelManager") 41 | ).transform(this) 42 | } 43 | 44 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 45 | return builder 46 | .visit( 47 | loadAdvice("tech.httptoolkit.javaagent.advice.asynchttpclient.AsyncHttpResetSslEngineFactoryAdvice") 48 | .on(hasMethodName("createSslHandler"))) 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/AttachMain.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("AttachMain") 2 | 3 | package tech.httptoolkit.javaagent 4 | 5 | import com.sun.tools.attach.* 6 | import kotlin.system.exitProcess 7 | import java.lang.management.ManagementFactory 8 | import java.io.File 9 | import java.lang.IllegalArgumentException 10 | 11 | // This file is the only one that uses com.sun.tools.attach.VirtualMachine. That's important because that 12 | // requires tools.jar to be in your classpath (i.e. it requires a JDK not a JRE). If you run this without 13 | // a JDK, you'll get errors about that class being unavailable. 14 | 15 | // Check the class is available before we properly use it - the goal being that this fails clearly before 16 | // actually using VirtualMachine fails confusingly. 17 | val x: Class<*> = try { // Var declaration required for static init for some reason 18 | Class.forName("com.sun.tools.attach.VirtualMachine") 19 | } catch (e: ClassNotFoundException) { 20 | System.err.println( 21 | "Could not start. Attaching to a running virtual machine requires a JDK including com.sun.tools.attach, not a JRE." 22 | ) 23 | exitProcess(1) 24 | } 25 | 26 | // If run directly, can either list potential targets (list-targets) or attach to a target (pid, ...config) 27 | fun main(args: Array) { 28 | // Self-test ensures that the JVM we're using is capable of scanning & attachment. It *doesn't* fully 29 | // test its ability to transform classes as we'd like. 30 | if (args.size == 1 && args[0] == "self-test") { 31 | val selfAttachAllowed = System.getProperty("jdk.attach.allowAttachSelf") 32 | if (selfAttachAllowed != "true") { 33 | throw IllegalArgumentException("Cannot run self-test without -Djdk.attach.allowAttachSelf=true") 34 | } 35 | 36 | getTargets() // Check we can scan for targets 37 | attachAgent(getOwnPid(), "attach-test") // Check we can attach (against ourselves) 38 | } else if (args.size == 1 && args[0] == "list-targets") { 39 | // List-targets prints a list of pid:name target paids 40 | val pid = getOwnPid() 41 | val vms = getTargets() 42 | vms.forEach { vmd -> 43 | if (vmd.id() != pid) { 44 | println("${vmd.id()}:${vmd.displayName()}") 45 | } 46 | } 47 | 48 | exitProcess(0) 49 | } else if (args.size == 4) { 50 | // 4-args format attaches to a target pid with the given config values 51 | val (pid, proxyHost, proxyPort, certPath) = args 52 | attachAgent(pid, formatConfigArg(proxyHost, proxyPort, certPath)) 53 | } else { 54 | System.err.println("Usage: java -jar ") 55 | System.err.println("Or pass a single 'self-test' or 'list-targets' arg to check capabilities or scan for pids") 56 | exitProcess(2) 57 | } 58 | } 59 | 60 | fun getOwnPid(): String { 61 | // This should work in general, but it's implementation dependent: 62 | val pid = ManagementFactory.getRuntimeMXBean().name.split("@")[0] 63 | 64 | return if (pid.toLongOrNull() != null) { 65 | pid 66 | } else { 67 | ProcessHandle.current().pid().toString() 68 | } 69 | } 70 | 71 | fun getTargets(): List { 72 | val vms = VirtualMachine.list() 73 | if (vms.isEmpty()) { 74 | // VMs should never be empty, because at the very least _we_ should be in there! If it's empty then 75 | // scanning isn't working at all, and we should fail clearly. 76 | System.err.println("Can't scan for attachable JVMs. Are we running in a JRE instead of a JDK?") 77 | exitProcess(4) 78 | } 79 | return vms 80 | } 81 | 82 | fun attachAgent( 83 | pid: String, 84 | agentArg: String 85 | ) { 86 | val jarPath = File( 87 | ConstantProxySelector::class.java // Any arbitrary class defined inside this JAR 88 | .protectionDomain.codeSource.location.toURI() 89 | ).absolutePath 90 | 91 | // Inject the agent into the target VM 92 | try { 93 | val vm: VirtualMachine = VirtualMachine.attach(pid) 94 | vm.loadAgent(jarPath, agentArg) 95 | vm.detach() 96 | } catch (e: AgentLoadException) { 97 | if (e.message == "0") { 98 | // This is a cross-JVM-version bug, and this is actually a success result. 99 | // See https://stackoverflow.com/questions/54340438/ 100 | return 101 | } else { 102 | System.err.println("Attaching agent failed") 103 | e.printStackTrace() 104 | exitProcess(3) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/ConstantProxySelector.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import java.io.IOException 4 | import java.net.* 5 | 6 | class ConstantProxySelector(proxyHost: String, proxyPort: Int) : ProxySelector() { 7 | 8 | private val proxyAddress: InetSocketAddress = InetSocketAddress(proxyHost, proxyPort) 9 | 10 | override fun select(uri: URI): MutableList { 11 | return if (uri.scheme == "http" || uri.scheme == "https") { 12 | mutableListOf(Proxy(Proxy.Type.HTTP, proxyAddress)) 13 | } else { 14 | mutableListOf(Proxy.NO_PROXY) 15 | } 16 | } 17 | 18 | override fun connectFailed(p0: URI?, p1: SocketAddress?, p2: IOException?) { 19 | println("Proxy connection failed") 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/CustomSslContext.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import java.io.File 4 | import java.io.FileInputStream 5 | import java.security.KeyStore 6 | import java.security.cert.CertificateFactory 7 | import java.util.* 8 | import javax.net.ssl.SSLContext 9 | import javax.net.ssl.TrustManagerFactory 10 | 11 | fun buildTrustManagerFactoryForCertificate(certPath: String): TrustManagerFactory { 12 | val certFile = File(certPath) 13 | val certificates = CertificateFactory.getInstance("X.509") 14 | .generateCertificates(FileInputStream(certFile)) 15 | 16 | val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) 17 | keyStore.load(null, null) 18 | for (certificate in certificates) { 19 | keyStore.setCertificateEntry(UUID.randomUUID().toString(), certificate) 20 | } 21 | 22 | val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) 23 | trustManagerFactory.init(keyStore) 24 | return trustManagerFactory 25 | } 26 | 27 | fun buildSslContextForCertificate(trustManagerFactory: TrustManagerFactory): SSLContext { 28 | val sslContext = SSLContext.getInstance("TLS") 29 | sslContext.init(null, trustManagerFactory.trustManagers, null) 30 | return sslContext 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/HttpsUrlConnectionTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | import tech.httptoolkit.javaagent.advice.ReturnSslSocketFactoryAdvice 8 | 9 | // We override the SSLSocketFactory field for HttpsURLConnections. This is the only way to access the 10 | // configured field, so this effectively reconfigured every such connection to trust our certificate. 11 | // Without this, connections still work as our SSLContext is the default, but this ensures they work 12 | // even for connections that are explicitly configured with their own settings. 13 | class HttpsUrlConnectionTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 14 | override fun register(builder: AgentBuilder): AgentBuilder { 15 | return builder 16 | .type( 17 | named("javax.net.ssl.HttpsURLConnection") 18 | ).transform(this) 19 | } 20 | 21 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 22 | return builder 23 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnSslSocketFactoryAdvice") 24 | .on(hasMethodName("getSSLSocketFactory"))) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/JavaClientTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | import tech.httptoolkit.javaagent.advice.ReturnProxySelectorAdvice 8 | import tech.httptoolkit.javaagent.advice.ReturnSslContextAdvice 9 | 10 | 11 | class JavaClientTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 12 | override fun register(builder: AgentBuilder): AgentBuilder { 13 | return builder 14 | .type( 15 | hasSuperType(named("java.net.http.HttpClient")) 16 | ).and( 17 | not(isInterface()) 18 | ).transform(this) 19 | } 20 | 21 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 22 | return builder 23 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnProxySelectorAdvice") 24 | .on(hasMethodName("proxy"))) 25 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnSslContextAdvice") 26 | .on(hasMethodName("sslContext"))) 27 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnSslContextAdvice") 28 | .on(hasMethodName("theSSLContext"))) 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/JettyClientTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.method.MethodDescription 6 | import net.bytebuddy.dynamic.DynamicType 7 | import net.bytebuddy.matcher.ElementMatchers.* 8 | import org.eclipse.jetty.util.ssl.SslContextFactory 9 | import tech.httptoolkit.javaagent.advice.jettyclient.JettyResetDestinationsAdvice 10 | import tech.httptoolkit.javaagent.advice.jettyclient.JettyReturnProxyConfigurationAdvice 11 | import tech.httptoolkit.javaagent.advice.jettyclient.JettyReturnSslContextFactoryV10Advice 12 | import tech.httptoolkit.javaagent.advice.jettyclient.JettyReturnSslContextFactoryV9Advice 13 | 14 | /** 15 | * Transforms the JettyClient to use our proxy & trust our certificate. 16 | * 17 | * For new clients, we just need to override the proxyConfiguration and 18 | * sslContextFactory properties on the HTTP client itself. 19 | * 20 | * For existing clients, we do that, and we also reset the destinations 21 | * (internal connection pools) when resolveDestination is first called 22 | * on each client. 23 | */ 24 | class JettyClientTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 25 | 26 | override fun register(builder: AgentBuilder): AgentBuilder { 27 | return builder 28 | .type( 29 | named("org.eclipse.jetty.client.HttpClient") 30 | ).transform(this) 31 | } 32 | 33 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 34 | return builder 35 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.jettyclient.JettyReturnProxyConfigurationAdvice") 36 | .on(hasMethodName("getProxyConfiguration"))) 37 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.jettyclient.JettyReturnSslContextFactoryV10Advice") 38 | .on(hasMethodName("getSslContextFactory").and( 39 | returns(SslContextFactory.Client::class.java) 40 | ))) 41 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.jettyclient.JettyReturnSslContextFactoryV9Advice") 42 | .on(hasMethodName("getSslContextFactory").and( 43 | returns(SslContextFactory::class.java) 44 | ))) 45 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.jettyclient.JettyResetDestinationsAdvice") 46 | .on(hasMethodName("resolveDestination"))) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/KtorCioTransformers.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.method.MethodDescription 6 | import net.bytebuddy.dynamic.DynamicType 7 | import net.bytebuddy.matcher.ElementMatchers.* 8 | import tech.httptoolkit.javaagent.advice.ReturnProxyAdvice 9 | import tech.httptoolkit.javaagent.advice.ktor.KtorResetProxyFieldAdvice 10 | import tech.httptoolkit.javaagent.advice.ktor.KtorResetTlsClientTrustAdvice 11 | 12 | // To intercept HTTPS, we need to change the trust manager in TLSConfig instances. We don't want 13 | // to mess with server config though, so we clone the config argument and replace it with one that 14 | // uses our custom trust manager instead, every time a new TLS session is opened: 15 | class KtorClientTlsTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 16 | override fun register(builder: AgentBuilder): AgentBuilder { 17 | return builder 18 | .type( 19 | named("io.ktor.network.tls.TLSClientSessionJvmKt") 20 | ).transform(this) 21 | } 22 | 23 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 24 | return builder 25 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ktor.KtorResetTlsClientTrustAdvice") 26 | .on(hasMethodName("openTLSSession") 27 | .and(takesArgument(3, named("io.ktor.network.tls.TLSConfig"))))) 28 | } 29 | } 30 | 31 | // Proxy configuration for new clients is easy: we just hook getProxy() in the engine 32 | // configuration to return our proxy. 33 | class KtorClientEngineConfigTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 34 | override fun register(builder: AgentBuilder): AgentBuilder { 35 | return builder 36 | .type( 37 | named("io.ktor.client.engine.HttpClientEngineConfig") 38 | ).transform(this) 39 | } 40 | 41 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 42 | return builder 43 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnProxyAdvice") 44 | .on(hasMethodName("getProxy"))) 45 | } 46 | } 47 | 48 | // Proxy configuration for existing clients is only mildly harder: we hook individual engines 49 | // elsewhere anyway, so it shouldn't matter much, but not CIO which is ktor specific. For CIO, 50 | // we just need one more hook that resets the proxy field to the value from config (hooked above) 51 | // before any requests are executed: 52 | class KtorCioEngineTransformer(logger: TransformationLogger) : MatchingAgentTransformer(logger) { 53 | override fun register(builder: AgentBuilder): AgentBuilder { 54 | return builder 55 | .type( 56 | named("io.ktor.client.engine.cio.CIOEngine") 57 | ).transform(this) 58 | } 59 | 60 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 61 | return builder 62 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ktor.KtorResetProxyFieldAdvice") 63 | .on(hasMethodName("execute"))) 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/OkHttpClientTransformers.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | import tech.httptoolkit.javaagent.advice.ReturnProxyAdvice 8 | import tech.httptoolkit.javaagent.advice.ReturnSslSocketFactoryAdvice 9 | 10 | /** 11 | * Transforms the OkHttpClient for v3 & 4 to use our proxy & trust our certificate. 12 | * 13 | * We do that by overwriting the proxy & sslSocketFactory properties on all OkHttp 14 | * clients to always return our proxy & a socket factory that only trusts our 15 | * certificate, ignoring anything the application has configured or defaulted to. 16 | * 17 | * Without this, proxy settings work by default, but certificates do not - OkHttp 18 | * only trusts the default built-in certificates, and refuses ours. 19 | */ 20 | class OkHttpClientV3Transformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 21 | override fun register(builder: AgentBuilder): AgentBuilder { 22 | return builder 23 | .type( 24 | named("okhttp3.OkHttpClient") 25 | ).transform(this) 26 | } 27 | 28 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 29 | return builder 30 | // v3 uses proxy() functions, while v4 uses Kotlin getters that compile to the same thing 31 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnProxyAdvice") 32 | .on(hasMethodName("proxy"))) 33 | // This means we ignore client certs, but that's fine: we can't pass them through the proxy anyway. That 34 | // needs to be configured separately in the proxy's configuration. 35 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnSslSocketFactoryAdvice") 36 | .on(hasMethodName("sslSocketFactory"))) 37 | } 38 | } 39 | 40 | /** 41 | * Transforms the OkHttpClient for v2 to use our proxy & trust our certificate. 42 | * 43 | * We do that by overwriting the proxy & sslSocketFactory properties on all OkHttp 44 | * clients to always return our proxy & a socket factory that only trusts our 45 | * certificate, ignoring anything the application has configured or defaulted to. 46 | * 47 | * Without this, proxy settings work by default, but certificates do not - OkHttp 48 | * only trusts the default built-in certificates, and refuses ours. 49 | */ 50 | class OkHttpClientV2Transformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 51 | override fun register(builder: AgentBuilder): AgentBuilder { 52 | return builder 53 | .type( 54 | named("com.squareup.okhttp.OkHttpClient") 55 | ).transform(this) 56 | } 57 | 58 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 59 | return builder 60 | // v2 uses getX methods: 61 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnProxyAdvice") 62 | .on(hasMethodName("getProxy"))) 63 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnSslSocketFactoryAdvice") 64 | .on(hasMethodName("getSslSocketFactory"))) 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/ProxySelectorTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.method.MethodDescription 6 | import net.bytebuddy.dynamic.DynamicType 7 | import net.bytebuddy.matcher.ElementMatchers.* 8 | import tech.httptoolkit.javaagent.advice.OverrideAllProxySelectionAdvice 9 | import tech.httptoolkit.javaagent.advice.OverrideUrlConnectionProxyAdvice 10 | import tech.httptoolkit.javaagent.advice.SkipMethodAdvice 11 | 12 | // To ensure that target applications don't override our ProxySelector (which we configure as the default), we 13 | // also patch the ProxySelector class itself, to guarantee that our proxy is always always selected, and 14 | // to stop anybody else changing the default. 15 | class ProxySelectorTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 16 | override fun register(builder: AgentBuilder): AgentBuilder { 17 | return builder 18 | .type( 19 | hasSuperType(named("java.net.ProxySelector")) 20 | ).and( 21 | not(isInterface()) 22 | ).transform(this) 23 | } 24 | 25 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 26 | return builder 27 | // We patch *all* proxy selectors, so that even code which doesn't use the default 28 | // still returns our proxy regardless. 29 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.OverrideAllProxySelectionAdvice") 30 | .on( 31 | hasMethodName("select") 32 | .and(takesArguments(1)) 33 | .and(takesArgument(0, named("java.net.URI"))))) 34 | // We already set the default ProxySelector on startup, before we intercept anything. 35 | // Here we patch ProxySelector so nobody can overwrite that later. 36 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.SkipMethodAdvice") 37 | .on(hasMethodName("setDefault"))); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/ReactorNettyTransformers.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.field.FieldDescription 6 | import net.bytebuddy.description.method.MethodDescription 7 | import net.bytebuddy.description.type.TypeDescription 8 | import net.bytebuddy.dynamic.DynamicType 9 | import net.bytebuddy.matcher.ElementMatchers.* 10 | import tech.httptoolkit.javaagent.advice.ReturnProxyAddressAdvice 11 | import tech.httptoolkit.javaagent.advice.reactornetty.ReactorNettyResetAllConfigAdvice 12 | import tech.httptoolkit.javaagent.advice.reactornetty.ReactorNettyResetHttpClientSecureSslAdvice 13 | import tech.httptoolkit.javaagent.advice.reactornetty.ReactorNettyV09ResetProxyProviderFieldAdvice 14 | 15 | // To patch Reactor-Netty's v1 HTTP client, we hook the constructor of the client itself. It has a constructor 16 | // that receives the config as part of every single HTTP request - we hook that to reset the relevant 17 | // config props every time they're used. 18 | 19 | private val matchConfigConstructor = isConstructor() 20 | .and(takesArguments(1)) 21 | .and(takesArgument(0, 22 | named("reactor.netty.http.client.HttpClientConfig") 23 | )) 24 | 25 | class ReactorNettyClientConfigTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 26 | 27 | override fun register(builder: AgentBuilder): AgentBuilder { 28 | return builder 29 | .type( 30 | hasSuperType(named("reactor.netty.http.client.HttpClient")) 31 | ).and( 32 | not(isInterface()) 33 | ).and( 34 | // This matches v1+ only, where the config is passed into the constructor repeatedly, and can 35 | // be mutated there. v0.9 is handled separately below. 36 | declaresMethod(matchConfigConstructor) 37 | ).transform(this) 38 | } 39 | 40 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 41 | return builder 42 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.reactornetty.ReactorNettyResetAllConfigAdvice") 43 | .on(matchConfigConstructor) 44 | ) 45 | } 46 | } 47 | 48 | // In v0.9, that wasn't the case. Instead, the SSL provider and proxy provider are passed as arguments to 49 | // and stored within various client classes. Here, we patch all their constructors to reset those fields 50 | // immediately after instantiation, ensuring our values replace the given arguments. 51 | 52 | // First, the sslProvider field: 53 | class ReactorNettyHttpClientSecureTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 54 | override fun register(builder: AgentBuilder): AgentBuilder { 55 | return builder 56 | .type( 57 | named("reactor.netty.http.client.HttpClientSecure") 58 | ).and( 59 | declaresField(named("sslProvider")) 60 | ).transform(this) 61 | } 62 | 63 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 64 | return builder 65 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.reactornetty.ReactorNettyResetHttpClientSecureSslAdvice") 66 | .on(isConstructor())) 67 | } 68 | } 69 | 70 | // Then each of the important cases where a proxy provider is stored: 71 | class ReactorNettyProxyProviderTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 72 | override fun register(builder: AgentBuilder): AgentBuilder { 73 | return builder 74 | .type( 75 | declaresField(named("proxyProvider").and( 76 | // This only applies to v0.9+ which uses this package name, not v1+ where ProxyProvider 77 | // lives in reactor.netty.transport (handled by the other transformer above) 78 | fieldType(named("reactor.netty.tcp.ProxyProvider"))) 79 | ) 80 | ).and( 81 | named( 82 | "reactor.netty.http.client.HttpClientConnect\$MonoHttpConnect" 83 | ).or( 84 | named( 85 | "reactor.netty.http.client.HttpClientConnect\$HttpClientHandler" 86 | ) 87 | ) 88 | ).transform(this) 89 | } 90 | 91 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 92 | return builder 93 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.reactornetty.ReactorNettyV09ResetProxyProviderFieldAdvice") 94 | .on(isConstructor())) 95 | } 96 | } 97 | 98 | // Then, on top of all that, we also forcibly set the socket address for all outgoing HTTP connections, because that's 99 | // that's the goal, and the above proxyProvider logic doesn't properly cover everything as proxy logic is spread across 100 | // a few places including the generic TCP clients (which we shouldn't touch). This is a bit messy/risky, but only 101 | // applies to v0.9, since v1+ stores the config in a properly structured way. 102 | class ReactorNettyOverrideRequestAddressTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 103 | override fun register(builder: AgentBuilder): AgentBuilder { 104 | return builder 105 | .type( 106 | declaresField(named("proxyProvider").and( 107 | // This ensures this only applies to v0.9+ which uses this package name, not v1+ where 108 | // ProxyProvider lives in reactor.netty.transport (handled separately above). 109 | fieldType(named("reactor.netty.tcp.ProxyProvider"))) 110 | ) 111 | ).and( 112 | named( 113 | "reactor.netty.http.client.HttpClientConnect\$HttpClientHandler" 114 | ) 115 | ).transform(this) 116 | } 117 | 118 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 119 | return builder 120 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.ReturnProxyAddressAdvice") 121 | .on(hasMethodName("get"))) 122 | } 123 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/SslContextTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | import tech.httptoolkit.javaagent.advice.SkipMethodAdvice 8 | 9 | // We patch SSL context purely to ensure that the default context that we set isn't changed later 10 | // by anybody else. The default context is already set in AgentMain before we begin patching. 11 | class SslContextTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 12 | override fun register(builder: AgentBuilder): AgentBuilder { 13 | return builder 14 | .type( 15 | named("javax.net.ssl.SSLContext") 16 | ).transform(this) 17 | } 18 | 19 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 20 | return builder 21 | // We set the default SSLContext on startup, before we intercepted anything. 22 | // Here we patch SSLContext itself so nobody can overwrite that later. 23 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.SkipMethodAdvice") 24 | .on(hasMethodName("setDefault"))); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/TransformationLogger.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.description.type.TypeDescription 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.utility.JavaModule 7 | 8 | class TransformationLogger(private val debugMode: Boolean) : AgentBuilder.Listener.Adapter() { 9 | 10 | private val transformingTypes: ArrayList = ArrayList() 11 | 12 | fun beforeTransformation(type: TypeDescription) { 13 | transformingTypes.add(type.canonicalName ?: "Unknown") 14 | } 15 | 16 | override fun onError( 17 | typeName: String, 18 | classLoader: ClassLoader?, 19 | module: JavaModule?, 20 | loaded: Boolean, 21 | throwable: Throwable 22 | ) { 23 | when { 24 | transformingTypes.contains(typeName) -> { 25 | System.err.println("Error configuring proxy hooks for $typeName:") 26 | throwable.printStackTrace(System.err) 27 | } 28 | debugMode -> { 29 | System.err.println("Unexpected agent configuration error $typeName:") 30 | throwable.printStackTrace(System.err) 31 | } 32 | } 33 | } 34 | 35 | override fun onTransformation( 36 | typeDescription: TypeDescription?, 37 | classLoader: ClassLoader?, 38 | module: JavaModule?, 39 | loaded: Boolean, 40 | dynamicType: DynamicType? 41 | ) { 42 | if (debugMode) { 43 | println("Proxy hooks configured for $typeDescription") 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/UrlConnectionTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.method.MethodDescription 6 | import net.bytebuddy.dynamic.DynamicType 7 | import net.bytebuddy.matcher.ElementMatchers.* 8 | import tech.httptoolkit.javaagent.advice.OverrideUrlConnectionProxyAdvice 9 | 10 | // We override URL.openConnection() so that even if a proxy setting is passed explicitly, it's 11 | // overridden and ignored (for HTTP(S) traffic only) so all such traffic goes to our proxy. 12 | class UrlConnectionTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 13 | override fun register(builder: AgentBuilder): AgentBuilder { 14 | return builder 15 | .type( 16 | named("java.net.URL") 17 | ).transform(this) 18 | } 19 | 20 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 21 | return builder 22 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.OverrideUrlConnectionProxyAdvice") 23 | .on( 24 | hasMethodName("openConnection") 25 | .and(takesArguments(1)) 26 | .and(takesArgument(0, named("java.net.Proxy"))))) 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/VertxHttpClientTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.dynamic.DynamicType 6 | import net.bytebuddy.matcher.ElementMatchers.* 7 | 8 | // Ensures that the proxy is used by overriding the getProxyOptions method of HttpClientImpl 9 | // to always return our proxy information 10 | class VertxHttpClientTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 11 | override fun register(builder: AgentBuilder): AgentBuilder { 12 | return builder 13 | .type( 14 | named("io.vertx.core.http.impl.HttpClientImpl") 15 | ).transform(this) 16 | } 17 | 18 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 19 | return builder 20 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.vertxclient.VertxHttpClientReturnProxyConfigurationAdvice") 21 | .on(hasMethodName("getProxyOptions"))) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/tech/httptoolkit/javaagent/VertxNetClientOptionsTransformer.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.javaagent 2 | 3 | import net.bytebuddy.agent.builder.AgentBuilder 4 | import net.bytebuddy.asm.Advice 5 | import net.bytebuddy.description.method.MethodDescription 6 | import net.bytebuddy.dynamic.DynamicType 7 | import net.bytebuddy.matcher.ElementMatchers.* 8 | 9 | // Ensures that the proxy is trusted by setting the Vert'x TrustOptions based on the TrustManager 10 | // created by the agent 11 | class VertxNetClientOptionsTransformer(logger: TransformationLogger): MatchingAgentTransformer(logger) { 12 | override fun register(builder: AgentBuilder): AgentBuilder { 13 | return builder 14 | .type( 15 | named("io.vertx.core.net.NetClientOptions") 16 | ).transform(this) 17 | } 18 | 19 | override fun transform(builder: DynamicType.Builder<*>, loadAdvice: (String) -> Advice): DynamicType.Builder<*> { 20 | return builder 21 | .visit(loadAdvice("tech.httptoolkit.javaagent.advice.vertxclient.VertxNetClientOptionsSetTrustOptionsAdvice") 22 | .on( 23 | isConstructor() 24 | .and(takesArguments(1)) 25 | .and(takesArgument(0, named("io.vertx.core.net.ClientOptionsBase"))))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/kotlin/IntegrationTests.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("BlockingMethodInNonBlockingContext") 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer 4 | import com.github.tomakehurst.wiremock.client.WireMock.* 5 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.core.test.TestCase 8 | import io.kotest.core.test.TestResult 9 | import io.kotest.matchers.shouldBe 10 | import io.kotest.matchers.string.shouldNotBeEmpty 11 | import java.io.BufferedReader 12 | import java.lang.Thread.sleep 13 | import java.nio.file.Path 14 | import java.nio.file.Paths 15 | import java.util.concurrent.TimeUnit 16 | 17 | // We require TEST_JAR to always be set when running the tests, giving us the appropriate path 18 | // to the specific agent jar that we're testing. 19 | val AGENT_JAR_PATH = System.getenv("TEST_JAR")!! 20 | val x = run { 21 | println("Testing $AGENT_JAR_PATH") 22 | } 23 | 24 | // For the test-app JAR we just use a constant string 25 | val TEST_APP_JAR = Paths.get("test-app", "build", "libs", "test-app-1.0-SNAPSHOT-all.jar").toString() 26 | 27 | val resourcesPath: Path = Paths.get("src", "test", "resources") 28 | 29 | const val proxyHost = "127.0.0.1" 30 | val certPath = resourcesPath.resolve("cert.pem").toAbsolutePath().toString() 31 | 32 | // We always launch subprocesses with the same Java that we're using ourselves 33 | val javaPath = Paths.get(System.getProperty("java.home"), "bin", "java").toString() 34 | 35 | val wireMockServer = WireMockServer(options() 36 | .dynamicPort() 37 | .enableBrowserProxying(true) 38 | .caKeystorePath(resourcesPath.resolve("cert.jks").toAbsolutePath().toString()) 39 | .caKeystorePassword("password") 40 | ) 41 | 42 | val runningProcs = arrayListOf() 43 | 44 | class IntegrationTests : StringSpec({ 45 | "Launching a self test should return successfully" { 46 | val proc = ProcessBuilder( 47 | javaPath, 48 | "-Djdk.attach.allowAttachSelf=true", 49 | "-jar", AGENT_JAR_PATH, 50 | "self-test" 51 | ).start() 52 | runningProcs.add(proc) 53 | 54 | proc.waitFor(10, TimeUnit.SECONDS) 55 | 56 | proc.isAlive.shouldBe(false) 57 | proc.exitValue().shouldBe(0) 58 | } 59 | 60 | "Launching with list-targets should return successfully" { 61 | val proc = ProcessBuilder( 62 | javaPath, 63 | "-jar", AGENT_JAR_PATH, 64 | "list-targets" 65 | ).start() 66 | runningProcs.add(proc) 67 | val outputReader = proc.inputStream.bufferedReader() 68 | 69 | proc.waitFor(10, TimeUnit.SECONDS) 70 | 71 | val output = outputReader.use(BufferedReader::readText) 72 | output.shouldNotBeEmpty() 73 | 74 | proc.isAlive.shouldBe(false) 75 | proc.exitValue().shouldBe(0) 76 | } 77 | 78 | "Launching with -javaagent should intercept all clients" { 79 | 80 | val agentArgs = "$proxyHost|${wireMockServer.port()}|$certPath" 81 | 82 | val proc = ProcessBuilder( 83 | javaPath, 84 | "-javaagent:$AGENT_JAR_PATH=$agentArgs", 85 | "-jar", TEST_APP_JAR 86 | ).inheritIO().start() 87 | runningProcs.add(proc) 88 | 89 | proc.waitFor(30, TimeUnit.SECONDS) 90 | 91 | proc.isAlive.shouldBe(false) 92 | proc.exitValue().shouldBe(0) 93 | } 94 | 95 | "Launching directly and attaching later should eventually intercept all clients" { 96 | // Start up the target: 97 | val targetProc = ProcessBuilder( 98 | javaPath, 99 | "-jar", TEST_APP_JAR 100 | ).inheritIO().start() 101 | runningProcs.add(targetProc) 102 | 103 | targetProc.inputStream.read() // Wait until some output appears 104 | sleep(2000) // Sleep a little extra, to check everything's fully running 105 | 106 | // It shouldn't quit yet - it should block until we intercept 107 | targetProc.isAlive.shouldBe(true) 108 | 109 | // Attach the agent: 110 | val agentAttachProc = ProcessBuilder( 111 | javaPath, 112 | "-jar", AGENT_JAR_PATH, 113 | targetProc.pid().toString(), proxyHost, wireMockServer.port().toString(), certPath 114 | ).inheritIO().start() 115 | runningProcs.add(agentAttachProc) 116 | 117 | // Agent attacher should quit happily 118 | agentAttachProc.waitFor(30, TimeUnit.SECONDS) 119 | agentAttachProc.isAlive.shouldBe(false) 120 | agentAttachProc.exitValue().shouldBe(0) 121 | 122 | // Target should pick up proxy details & quit happily, eventually 123 | targetProc.waitFor(30, TimeUnit.SECONDS) 124 | targetProc.isAlive.shouldBe(false) 125 | targetProc.exitValue().shouldBe(0) 126 | } 127 | }) { 128 | override fun beforeTest(testCase: TestCase) { 129 | super.beforeTest(testCase) 130 | 131 | runningProcs.clear() 132 | wireMockServer.start() 133 | 134 | // Send a 200 response for all requests 135 | wireMockServer.stubFor(any(anyUrl()).willReturn(ok())) 136 | } 137 | 138 | override fun afterTest(testCase: TestCase, result: TestResult) { 139 | super.afterTest(testCase, result) 140 | wireMockServer.stop() 141 | runningProcs.forEach { proc -> proc.destroyForcibly() } 142 | } 143 | } -------------------------------------------------------------------------------- /src/test/resources/cert.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/httptoolkit/jvm-http-proxy-agent/d43d27373aba210c9c4b564d9e22960d64a0766d/src/test/resources/cert.jks -------------------------------------------------------------------------------- /src/test/resources/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDvzCCAqegAwIBAgIUTZKcZHgoQQEobPlmVzDPh0Ae/o0wDQYJKoZIhvcNAQEL 3 | BQAwZzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjERMA8GA1UECgwIV2ly 4 | ZU1vY2sxNDAyBgNVBAMMK1dpcmVNb2NrIExvY2FsIFNlbGYgU2lnbmVkIFJvb3Qg 5 | Q2VydGlmaWNhdGUwHhcNMjEwMjIzMTgzNjA2WhcNMzEwMjIxMTgzNjA2WjBnMQsw 6 | CQYDVQQGEwJHQjEPMA0GA1UECAwGTG9uZG9uMREwDwYDVQQKDAhXaXJlTW9jazE0 7 | MDIGA1UEAwwrV2lyZU1vY2sgTG9jYWwgU2VsZiBTaWduZWQgUm9vdCBDZXJ0aWZp 8 | Y2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOrMcv5JKYTUJhP+ 9 | gdDaRitZRaFSMwuUn68as4p4pRFfHG4fE+EmP6dFFFt/hgC1smDqaiIZ0erg5GFp 10 | AtBNTUr8cpADCGF4IKPT2JeIthUE2uzGEfEpzS/hflbKyXVLYlTj4PkRu5NEYElj 11 | 5Js6BuiFn3kCcveMMJPMX7CjSMl1K+uelVEkd+N1ZqTqL9kGpxci90jE1ZLh/rY8 12 | vYyYD7bsCpvf7mkpqIqkmAyuwN0gImpWLaZ7DaqdCxhpc4qaGbxeZHBePaEtvxky 13 | YU/M58H6APT6w9KbvG3c3HtVrXciEO+BvBkCfzzWQx3JCayLAMlDoiKEpNfMU5F2 14 | sjtssccCAwEAAaNjMGEwHQYDVR0OBBYEFLTwBxvA4QDn9qKqU4tshRIJCUOmMB8G 15 | A1UdIwQYMBaAFLTwBxvA4QDn9qKqU4tshRIJCUOmMA8GA1UdEwEB/wQFMAMBAf8w 16 | DgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQCncPb3Fyq2pZvPlzNZ 17 | VCvDgYj6DTgzTYvhx41P/OruxlMZryNrpbNF0xd9y480QLIRiYz6/7EH4q1RAdFO 18 | z1RaJ8//XA5V4koFggSGwW8jLAMqvMry724pQlhtAet2mQS2P8V725uChlcP2594 19 | gc+a6efiXjVpJjdX3NI5uvpVvMlKA7CNhZX1NRl/DvpVApuM/8m9b3ZrSU0KQzb8 20 | pw1BzToAzlqtK4gwZwJWEzy3HexsPhljNLqM6G9inW0GoUgAr/gvzkLc6HElzKOR 21 | 4PU4mHaF2UBQJgvwdySOJxZhbAFxbTJCwm8nGkKh3Mp7CxNG2pBDSbPj1k3rOnHS 22 | e+1U 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /test-app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'com.github.johnrengelman.shadow' 4 | id 'org.jetbrains.kotlin.jvm' 5 | } 6 | 7 | group 'tech.httptoolkit' 8 | version '1.0-SNAPSHOT' 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation group: 'commons-httpclient', name: 'commons-httpclient', version: '3.1' 16 | implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5' 17 | implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3' 18 | implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.4' 19 | 20 | implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0' 21 | implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.7.5' 22 | implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.9.0' 23 | 24 | implementation group: 'org.eclipse.jetty', name: 'jetty-client', version: '10.0.0' 25 | implementation group: 'org.asynchttpclient', name: 'async-http-client', version: '2.12.2' 26 | implementation group: 'org.springframework', name: 'spring-webflux', version: '5.3.4' 27 | implementation group: 'io.projectreactor.netty', name: 'reactor-netty', version: '1.0.4' 28 | 29 | implementation group: 'io.ktor', name: 'ktor-client-core', version: '1.5.2' 30 | implementation group: 'io.ktor', name: 'ktor-client-cio', version: '1.5.2' 31 | 32 | implementation group: 'com.typesafe.akka', name: 'akka-actor_2.13', version: '2.6.13' 33 | implementation group: 'com.typesafe.akka', name: 'akka-http_2.13', version: '10.2.4' 34 | implementation group: 'com.typesafe.akka', name: 'akka-stream_2.13', version: '2.6.13' 35 | 36 | implementation group: 'io.vertx', name: 'vertx-core', version: '4.2.2' 37 | implementation group: 'io.vertx', name: 'vertx-web-client', version: '4.2.2' 38 | 39 | implementation group: 'org.jboss.resteasy', name: 'resteasy-client', version: '4.7.4.Final' 40 | } 41 | 42 | test { 43 | useJUnitPlatform() 44 | } 45 | 46 | jar { 47 | manifest { 48 | attributes 'Main-Class': 'tech.httptoolkit.testapp.Main' 49 | } 50 | } 51 | 52 | compileJava { 53 | sourceCompatibility = '11' 54 | targetCompatibility = '11' 55 | } 56 | 57 | import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer 58 | 59 | shadowJar { 60 | transform(AppendingTransformer) { 61 | resource = 'reference.conf' // Required for akka 62 | } 63 | with jar 64 | } 65 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/Main.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp; 2 | 3 | import tech.httptoolkit.testapp.cases.*; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.*; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | import java.util.stream.Collectors; 11 | 12 | import static java.lang.Thread.sleep; 13 | import static java.util.Map.entry; 14 | 15 | public class Main { 16 | 17 | private static final Map> cases = Map.ofEntries( 18 | entry("apache-v3", new ApacheHttpClientV3Case()), 19 | entry("apache-v4", new ApacheHttpClientV4Case()), 20 | entry("rest-easy-with-apache-v4", new RestEasyWithApacheHttpClientV4Case()), 21 | entry("apache-v5", new ApacheHttpClientV5Case()), 22 | entry("apache-async-v4", new ApacheHttpAsyncClientV4Case()), 23 | entry("apache-async-v5", new ApacheHttpAsyncClientV5Case()), 24 | entry("http-url-conn", new HttpUrlConnCase()), 25 | entry("java-http-client", new JavaHttpClientCase()), 26 | entry("okhttp-v2", new OkHttpV2Case()), 27 | entry("okhttp-v4", new OkHttpV4Case()), 28 | entry("retrofit", new RetrofitCase()), 29 | entry("jetty-client", new JettyClientCase()), 30 | entry("async-http-client", new AsyncHttpClientCase()), 31 | entry("spring-web", new SpringWebClientCase()), 32 | entry("ktor-cio", new KtorCioCase()), 33 | entry("akka-req-http", new AkkaRequestClientCase()), 34 | entry("akka-host-http", new AkkaHostClientCase()), 35 | entry("vertx-httpclient", new VertxHttpClientCase()), 36 | entry("vertx-webclient", new VertxWebClientCase()) 37 | ); 38 | 39 | public static void main(String[] args) throws Exception { 40 | String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); 41 | String pid = runtimeName.split("@")[0]; 42 | System.out.println("PID: " + pid); // Purely for convenient manual attachment to this process 43 | 44 | String url = "https://example.com/404/"; // Always returns a 404 45 | ExecutorService executor = Executors.newCachedThreadPool(); 46 | 47 | while (true) { 48 | AtomicBoolean allSuccessful = new AtomicBoolean(true); 49 | 50 | List> tests = executor.invokeAll(cases.entrySet().stream().map((entry) -> ((Callable) () -> { 51 | String name = entry.getKey(); 52 | ClientCase clientCase = entry.getValue(); 53 | 54 | try { 55 | int result = clientCase.testNew(url); 56 | if (result != 200) { 57 | System.out.println("Unexpected result for new " + name + ": " + result); 58 | allSuccessful.set(false); 59 | } 60 | } catch (Throwable e) { 61 | System.out.println("Unexpected failure for new " + name + ": " + e.toString()); 62 | System.out.println(e.toString()); 63 | allSuccessful.set(false); 64 | } 65 | 66 | try { 67 | int result = clientCase.testExisting(url); 68 | if (result != 200) { 69 | System.out.println("Unexpected result for existing " + name + ": " + result); 70 | allSuccessful.set(false); 71 | } 72 | } catch (Throwable e) { 73 | System.out.println("Unexpected failure for existing " + name + ": " + e.toString()); 74 | allSuccessful.set(false); 75 | } 76 | 77 | return null; 78 | })).collect(Collectors.toList())); 79 | 80 | // Wait for all tests to complete 81 | for (Future f: tests) { f.get(); } 82 | 83 | if (allSuccessful.get()) { 84 | System.out.println("All cases intercepted successfully"); 85 | System.exit(0); 86 | } 87 | 88 | //noinspection BusyWait 89 | sleep(1000); 90 | } 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/AkkaHostClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import akka.NotUsed; 4 | import akka.actor.ActorSystem; 5 | import akka.actor.ExtendedActorSystem; 6 | import akka.http.javadsl.ConnectHttp; 7 | import akka.http.javadsl.HostConnectionPool; 8 | import akka.http.javadsl.Http; 9 | import akka.http.javadsl.model.HttpRequest; 10 | import akka.http.javadsl.model.HttpResponse; 11 | import akka.http.javadsl.model.Uri; 12 | import akka.japi.Pair; 13 | import akka.stream.javadsl.Flow; 14 | import akka.stream.javadsl.Sink; 15 | import akka.stream.javadsl.Source; 16 | import scala.util.Try; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | // Here we use a host-based persistent Akka connection pool, just to make sure that that case is covered too, in 24 | // addition to the request client case. 25 | public class AkkaHostClientCase extends ClientCase< 26 | Flow, Pair, NotUsed>, HostConnectionPool> 27 | > { 28 | 29 | private static final ActorSystem system = ExtendedActorSystem.create(); 30 | 31 | @Override 32 | public Flow, Pair, NotUsed>, HostConnectionPool> newClient(String url) throws Exception { 33 | Uri uri = Uri.create(url); 34 | return new Http((ExtendedActorSystem) system) 35 | .cachedHostConnectionPoolHttps(ConnectHttp.toHost(uri)); // HTTPS required here, or HTTP is *always* used 36 | } 37 | 38 | @Override 39 | public int test( 40 | String url, 41 | Flow, Pair, NotUsed>, HostConnectionPool> clientFlow 42 | ) throws URISyntaxException, ExecutionException, InterruptedException { 43 | Source, NotUsed> requestSource = Source.single( 44 | new Pair<>(HttpRequest.create(url), null) 45 | ); 46 | 47 | CompletableFuture, NotUsed>> responseFuture = requestSource 48 | .via(clientFlow) 49 | .runWith(Sink.head(), system) 50 | .toCompletableFuture(); 51 | 52 | HttpResponse response = responseFuture.get().first().get(); 53 | response.discardEntityBytes(system); 54 | return response.status().intValue(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/AkkaRequestClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import akka.actor.ActorSystem; 4 | import akka.http.javadsl.Http; 5 | import akka.http.javadsl.model.HttpRequest; 6 | import akka.http.javadsl.model.HttpResponse; 7 | 8 | import java.net.URISyntaxException; 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.ExecutionException; 11 | 12 | public class AkkaRequestClientCase extends ClientCase { 13 | 14 | private static final ActorSystem system = ActorSystem.create(); 15 | 16 | @Override 17 | public Http newClient(String url) throws Exception { 18 | return Http.get(system); 19 | } 20 | 21 | @Override 22 | public int test(String url, Http client) throws URISyntaxException, ExecutionException, InterruptedException { 23 | CompletableFuture responseFuture = client 24 | .singleRequest(HttpRequest.create(url)) 25 | .toCompletableFuture(); 26 | HttpResponse response = responseFuture.get(); 27 | response.discardEntityBytes(system); 28 | return response.status().intValue(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/ApacheHttpAsyncClientV4Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.apache.http.HttpResponse; 4 | import org.apache.http.client.methods.HttpGet; 5 | import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; 6 | import org.apache.http.impl.nio.client.HttpAsyncClients; 7 | 8 | import java.util.concurrent.Future; 9 | 10 | public class ApacheHttpAsyncClientV4Case extends ClientCase { 11 | 12 | @Override 13 | public CloseableHttpAsyncClient newClient(String url) { 14 | CloseableHttpAsyncClient client = HttpAsyncClients.createDefault(); 15 | client.start(); 16 | return client; 17 | } 18 | 19 | @Override 20 | public void stopClient(CloseableHttpAsyncClient client) throws Exception { 21 | client.close(); 22 | } 23 | 24 | @Override 25 | public int test(String url, CloseableHttpAsyncClient client) throws Exception { 26 | HttpGet request = new HttpGet(url); 27 | Future future = client.execute(request, null); 28 | HttpResponse response = future.get(); 29 | return response.getStatusLine().getStatusCode(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/ApacheHttpAsyncClientV5Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; 4 | import org.apache.hc.client5.http.async.methods.SimpleHttpRequests; 5 | import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; 6 | import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; 7 | import org.apache.hc.client5.http.impl.async.HttpAsyncClients; 8 | 9 | import java.util.concurrent.Future; 10 | 11 | public class ApacheHttpAsyncClientV5Case extends ClientCase { 12 | 13 | @Override 14 | public CloseableHttpAsyncClient newClient(String url) { 15 | CloseableHttpAsyncClient client = HttpAsyncClients.createDefault(); 16 | client.start(); 17 | return client; 18 | } 19 | 20 | @Override 21 | public void stopClient(CloseableHttpAsyncClient client) throws Exception { 22 | client.close(); 23 | } 24 | 25 | @Override 26 | public int test(String url, CloseableHttpAsyncClient client) throws Exception { 27 | SimpleHttpRequest request = SimpleHttpRequests.get(url); 28 | Future future = client.execute(request, null); 29 | SimpleHttpResponse response = future.get(); 30 | return response.getCode(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/ApacheHttpClientV3Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.apache.commons.httpclient.HttpClient; 4 | import org.apache.commons.httpclient.HttpMethod; 5 | import org.apache.commons.httpclient.methods.GetMethod; 6 | 7 | import java.io.IOException; 8 | 9 | public class ApacheHttpClientV3Case extends ClientCase { 10 | 11 | @Override 12 | public HttpClient newClient(String url) { 13 | return new HttpClient(); 14 | } 15 | 16 | @Override 17 | public int test(String url, HttpClient client) throws IOException { 18 | HttpMethod method = new GetMethod(url); 19 | client.executeMethod(method); 20 | method.releaseConnection(); 21 | return method.getStatusCode(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/ApacheHttpClientV4Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.client.methods.HttpGet; 5 | import org.apache.http.impl.client.HttpClients; 6 | 7 | import java.io.IOException; 8 | 9 | public class ApacheHttpClientV4Case extends ClientCase { 10 | 11 | @Override 12 | public HttpClient newClient(String url) { 13 | return HttpClients.createDefault(); 14 | } 15 | 16 | @Override 17 | public int test(String url, HttpClient client) throws IOException { 18 | HttpGet request = new HttpGet(url); 19 | return client.execute(request, response -> response.getStatusLine().getStatusCode()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/ApacheHttpClientV5Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.apache.hc.client5.http.classic.HttpClient; 4 | import org.apache.hc.client5.http.classic.methods.HttpGet; 5 | import org.apache.hc.client5.http.impl.classic.HttpClients; 6 | 7 | import java.io.IOException; 8 | 9 | public class ApacheHttpClientV5Case extends ClientCase { 10 | 11 | @Override 12 | public HttpClient newClient(String url) { 13 | return HttpClients.createDefault(); 14 | } 15 | 16 | @Override 17 | public int test(String url, HttpClient client) throws IOException { 18 | HttpGet request = new HttpGet(url); 19 | return client.execute(request, response -> response.getCode()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/AsyncHttpClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.asynchttpclient.AsyncHttpClient; 4 | import org.asynchttpclient.Request; 5 | import org.asynchttpclient.Response; 6 | 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.Future; 9 | 10 | import static org.asynchttpclient.Dsl.*; 11 | 12 | public class AsyncHttpClientCase extends ClientCase { 13 | 14 | @Override 15 | public AsyncHttpClient newClient(String url) { 16 | return asyncHttpClient(); 17 | } 18 | 19 | @Override 20 | public int test(String url, AsyncHttpClient client) throws ExecutionException, InterruptedException { 21 | Request request = get(url).build(); 22 | Future whenResponse = client.executeRequest(request); 23 | return whenResponse.get().getStatusCode(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/ClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | public abstract class ClientCase { 4 | 5 | public abstract T newClient(String url) throws Exception; 6 | public abstract int test(String url, T client) throws Exception; 7 | 8 | private String existingClientUrl; 9 | private T client; 10 | 11 | public T existingClient(String url) throws Exception { 12 | if (client == null) { 13 | client = newClient(url); 14 | existingClientUrl = url; 15 | } else if (!existingClientUrl.equals(url)) { 16 | throw new RuntimeException("Existing client must get the same URL every time"); 17 | } 18 | 19 | return client; 20 | } 21 | 22 | public void stopClient(T client) throws Exception { 23 | // Do nothing by default, but subclasses can override this 24 | } 25 | 26 | public int testNew(String url) throws Exception { 27 | T client = newClient(url); 28 | int result = test(url, client); 29 | stopClient(client); 30 | return result; 31 | } 32 | 33 | public int testExisting(String url) throws Exception { 34 | return test(url, existingClient(url)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/HttpUrlConnCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | import java.net.MalformedURLException; 6 | import java.net.Proxy; 7 | import java.net.URL; 8 | 9 | // We take over default HttpUrlConnections with no problem by setting config vars, but that doesn't apply in all 10 | // cases, e.g. if the target code manages its own proxy config, or disables it. We intercept here to forcibly 11 | // ensure that our proxy is _always_ used, regardless of the passed proxy configuration. 12 | public class HttpUrlConnCase extends ClientCase { 13 | 14 | @Override 15 | public URL newClient(String url) throws MalformedURLException { 16 | return new URL(url); 17 | } 18 | 19 | @Override 20 | public int test(String url, URL urlInstance) throws IOException { 21 | HttpURLConnection connection = (HttpURLConnection) urlInstance.openConnection(Proxy.NO_PROXY); 22 | return connection.getResponseCode(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/JavaHttpClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import java.io.IOException; 4 | import java.net.URI; 5 | import java.net.http.HttpClient; 6 | import java.net.http.HttpRequest; 7 | import java.net.http.HttpResponse; 8 | 9 | public class JavaHttpClientCase extends ClientCase { 10 | 11 | @Override 12 | public HttpClient newClient(String url) { 13 | return HttpClient.newHttpClient(); 14 | } 15 | 16 | @Override 17 | public int test(String url, HttpClient client) throws IOException, InterruptedException { 18 | var request = HttpRequest.newBuilder(URI.create(url)) 19 | .build(); 20 | 21 | var response = client.send(request, HttpResponse.BodyHandlers.ofString()); 22 | return response.statusCode(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/JettyClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | 4 | import org.eclipse.jetty.client.HttpClient; 5 | import org.eclipse.jetty.client.api.ContentResponse; 6 | 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.TimeoutException; 9 | 10 | public class JettyClientCase extends ClientCase { 11 | 12 | @Override 13 | public HttpClient newClient(String url) throws Exception { 14 | HttpClient client = new HttpClient(); 15 | client.start(); 16 | return client; 17 | } 18 | 19 | @Override 20 | public void stopClient(HttpClient client) throws Exception { 21 | client.stop(); 22 | } 23 | 24 | @Override 25 | public int test(String url, HttpClient client) throws InterruptedException, ExecutionException, TimeoutException { 26 | ContentResponse res = client.GET(url); 27 | return res.getStatus(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/KtorCioCase.kt: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.cio.* 5 | import io.ktor.client.request.* 6 | import io.ktor.client.statement.* 7 | import kotlinx.coroutines.runBlocking 8 | 9 | class KtorCioCase : ClientCase() { 10 | 11 | override fun newClient(url: String): HttpClient { 12 | return HttpClient(CIO) 13 | } 14 | 15 | override fun stopClient(client: HttpClient) { 16 | client.close() 17 | } 18 | 19 | override fun test(url: String, client: HttpClient): Int = runBlocking { 20 | val response = client.get(url) 21 | response.status.value 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/OkHttpV2Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import com.squareup.okhttp.OkHttpClient; 4 | import com.squareup.okhttp.Request; 5 | import com.squareup.okhttp.Response; 6 | 7 | import java.io.IOException; 8 | 9 | public class OkHttpV2Case extends ClientCase { 10 | 11 | @Override 12 | public OkHttpClient newClient(String url) { 13 | return new OkHttpClient(); 14 | } 15 | 16 | @Override 17 | public int test(String url, OkHttpClient client) throws IOException { 18 | Request request = new Request.Builder() 19 | .url(url) 20 | .build(); 21 | 22 | Response response = client.newCall(request).execute(); 23 | return response.code(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/OkHttpV4Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import okhttp3.OkHttpClient; 4 | import okhttp3.Request; 5 | import okhttp3.Response; 6 | 7 | import java.io.IOException; 8 | 9 | public class OkHttpV4Case extends ClientCase { 10 | 11 | @Override 12 | public OkHttpClient newClient(String url) { 13 | return new OkHttpClient(); 14 | } 15 | 16 | @Override 17 | public int test(String url, OkHttpClient client) throws IOException { 18 | Request request = new Request.Builder() 19 | .url(url) 20 | .build(); 21 | 22 | Response response = client.newCall(request).execute(); 23 | response.body().close(); 24 | return response.code(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/RestEasyWithApacheHttpClientV4Case.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.jboss.resteasy.client.jaxrs.ResteasyClient; 4 | import org.jboss.resteasy.client.jaxrs.engines.ClientHttpEngineBuilder43; 5 | import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl; 6 | 7 | import javax.net.ssl.SSLContext; 8 | import java.io.IOException; 9 | import java.net.MalformedURLException; 10 | import java.net.URI; 11 | import java.security.NoSuchAlgorithmException; 12 | 13 | public class RestEasyWithApacheHttpClientV4Case extends ClientCase { 14 | 15 | @Override 16 | public ResteasyClient newClient(String url) throws MalformedURLException { 17 | ResteasyClientBuilderImpl resteasyClientBuilder = new ResteasyClientBuilderImpl(); 18 | resteasyClientBuilder.sslContext(getSslContext()); 19 | resteasyClientBuilder.httpEngine(new ClientHttpEngineBuilder43() 20 | .resteasyClientBuilder(resteasyClientBuilder).build()); 21 | return resteasyClientBuilder.build(); 22 | } 23 | 24 | @Override 25 | public int test(String url, ResteasyClient resteasyClient) throws IOException { 26 | return resteasyClient.target(URI.create(url)).request().get().getStatus(); 27 | } 28 | 29 | private SSLContext getSslContext() { 30 | try { 31 | return SSLContext.getDefault(); 32 | } catch (NoSuchAlgorithmException e) { 33 | throw new IllegalStateException(e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/RetrofitCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import retrofit2.Call; 4 | import retrofit2.Response; 5 | import retrofit2.Retrofit; 6 | import retrofit2.http.GET; 7 | 8 | import java.io.IOException; 9 | 10 | public class RetrofitCase extends ClientCase { 11 | 12 | @Override 13 | public RetrofitCase.ExampleRetrofitClient newClient(String url) { 14 | Retrofit retrofit = new Retrofit.Builder() 15 | .baseUrl(url) 16 | .build(); 17 | return retrofit.create(RetrofitCase.ExampleRetrofitClient.class); 18 | } 19 | 20 | @Override 21 | public int test(String url, RetrofitCase.ExampleRetrofitClient client) throws IOException { 22 | Response response = client.exampleRequest().execute(); 23 | return response.code(); 24 | } 25 | 26 | public interface ExampleRetrofitClient { 27 | @GET("/") 28 | Call exampleRequest(); 29 | } 30 | } -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/SpringWebClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.web.reactive.function.client.WebClient; 5 | import reactor.core.publisher.Mono; 6 | 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | 10 | public class SpringWebClientCase extends ClientCase { 11 | @Override 12 | public WebClient newClient(String url) throws Exception { 13 | return WebClient.create(); 14 | } 15 | 16 | @Override 17 | public int test(String url, WebClient client) throws URISyntaxException { 18 | Mono result = client.get() 19 | .uri(new URI(url)) 20 | .accept(MediaType.ALL) 21 | .exchangeToMono(response -> Mono.just(response.rawStatusCode())); 22 | 23 | //noinspection ConstantConditions 24 | return result.block(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxHttpClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.http.HttpClient; 5 | import io.vertx.core.http.HttpClientRequest; 6 | import io.vertx.core.http.HttpClientResponse; 7 | import io.vertx.core.http.HttpMethod; 8 | 9 | import java.net.URISyntaxException; 10 | import java.util.concurrent.ExecutionException; 11 | 12 | public class VertxHttpClientCase extends ClientCase { 13 | @Override 14 | public HttpClient newClient(String url) throws Exception { 15 | return Vertx.vertx().createHttpClient(); 16 | } 17 | 18 | @Override 19 | public int test(String url, HttpClient client) throws URISyntaxException, InterruptedException, ExecutionException { 20 | HttpClientResponse response = client 21 | .request(HttpMethod.GET, url) 22 | .compose(HttpClientRequest::send) 23 | .toCompletionStage().toCompletableFuture().get(); 24 | return response.statusCode(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test-app/src/main/java/tech/httptoolkit/testapp/cases/VertxWebClientCase.java: -------------------------------------------------------------------------------- 1 | package tech.httptoolkit.testapp.cases; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.http.HttpMethod; 6 | import io.vertx.ext.web.client.HttpResponse; 7 | import io.vertx.ext.web.client.WebClient; 8 | import java.net.URISyntaxException; 9 | import java.util.concurrent.ExecutionException; 10 | 11 | public class VertxWebClientCase extends ClientCase { 12 | @Override 13 | public WebClient newClient(String url) throws Exception { 14 | return WebClient.create(Vertx.vertx()); 15 | } 16 | 17 | @Override 18 | public int test(String url, WebClient client) throws URISyntaxException, InterruptedException, ExecutionException { 19 | HttpResponse response = client 20 | .request(HttpMethod.GET, url) 21 | .send() 22 | .toCompletionStage().toCompletableFuture().get(); 23 | return response.statusCode(); 24 | } 25 | } 26 | --------------------------------------------------------------------------------