├── .github └── workflows │ └── build-and-deploy.yml ├── .gitignore ├── .travis.yml ├── KEYS ├── LICENSE ├── README.rst ├── SECURITY.md ├── checkstyle.xml ├── pom.xml ├── src ├── main │ └── java │ │ └── io │ │ └── cdap │ │ └── http │ │ ├── AbstractHandlerHook.java │ │ ├── AbstractHttpHandler.java │ │ ├── AbstractHttpResponder.java │ │ ├── AuthHandler.java │ │ ├── BodyConsumer.java │ │ ├── BodyProducer.java │ │ ├── ChannelPipelineModifier.java │ │ ├── ChunkResponder.java │ │ ├── ExceptionHandler.java │ │ ├── HandlerContext.java │ │ ├── HandlerHook.java │ │ ├── HttpHandler.java │ │ ├── HttpResponder.java │ │ ├── NettyHttpService.java │ │ ├── RequiredRoles.java │ │ ├── SSLConfig.java │ │ ├── SSLHandlerFactory.java │ │ ├── Secured.java │ │ ├── URLRewriter.java │ │ ├── internal │ │ ├── AuthenticationException.java │ │ ├── AuthorizationException.java │ │ ├── BasicHandlerContext.java │ │ ├── BasicHttpResponder.java │ │ ├── ChannelChunkResponder.java │ │ ├── Converter.java │ │ ├── ForwardingEventExecutorGroup.java │ │ ├── ForwardingOrderedEventExecutor.java │ │ ├── HandlerException.java │ │ ├── HandlerInfo.java │ │ ├── HttpDispatcher.java │ │ ├── HttpMethodInfo.java │ │ ├── HttpResourceHandler.java │ │ ├── HttpResourceModel.java │ │ ├── ImmutablePair.java │ │ ├── InternalHttpResponder.java │ │ ├── InternalHttpResponse.java │ │ ├── NonStickyEventExecutorGroup.java │ │ ├── ParamConvertUtils.java │ │ ├── PatternPathRouterWithGroups.java │ │ ├── RequestRouter.java │ │ ├── WrappedHttpResponder.java │ │ └── package-info.java │ │ └── package-info.java └── test │ ├── java │ └── io │ │ └── cdap │ │ └── http │ │ ├── ExecutorThreadPoolTest.java │ │ ├── HandlerHookTest.java │ │ ├── HttpServerTest.java │ │ ├── HttpsServerTest.java │ │ ├── InternalHttpResponderTest.java │ │ ├── MutualAuthServerTest.java │ │ ├── PathRouterTest.java │ │ ├── SSLClientContext.java │ │ ├── SSLKeyStoreTest.java │ │ ├── TestChannelHandler.java │ │ ├── TestHandler.java │ │ └── URLRewriterTest.java │ └── resources │ ├── cert.jks │ └── client.jks └── suppressions.xml /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | # Copyright © 2022 Cask Data, Inc. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 3 | # use this file except in compliance with the License. You may obtain a copy of 4 | # the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 9 | # License for the specific language governing permissions and limitations under 10 | # the License. 11 | 12 | name: Build and Deploy 13 | on: 14 | schedule: 15 | - cron: '0 8 * * *' 16 | workflow_dispatch: 17 | 18 | jobs: 19 | build: 20 | runs-on: k8s-runner-build 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | branch: [ develop, release/1.7 ] 26 | 27 | steps: 28 | - name: Get Secrets from GCP Secret Manager 29 | id: 'secrets' 30 | uses: 'google-github-actions/get-secretmanager-secrets@v0' 31 | with: 32 | secrets: |- 33 | CDAP_OSSRH_USERNAME:cdapio-github-builds/CDAP_OSSRH_USERNAME 34 | CDAP_OSSRH_PASSWORD:cdapio-github-builds/CDAP_OSSRH_PASSWORD 35 | CDAP_GPG_PASSPHRASE:cdapio-github-builds/CDAP_GPG_PASSPHRASE 36 | CDAP_GPG_PRIVATE_KEY:cdapio-github-builds/CDAP_GPG_PRIVATE_KEY 37 | 38 | - name: Recursively Checkout Repository 39 | uses: actions/checkout@v3 40 | with: 41 | fetch-depth: 0 42 | path: netty 43 | ref: ${{ matrix.branch }} 44 | 45 | - name: Cache 46 | uses: actions/cache@v3 47 | with: 48 | path: ~/.m2/repository 49 | key: ${{ runner.os }}-maven-${{ github.workflow }}-${{ hashFiles('**/pom.xml') }} 50 | restore-keys: | 51 | ${{ runner.os }}-maven-${{ github.workflow }} 52 | 53 | - name: Set up GPG conf 54 | run: | 55 | echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf 56 | echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf 57 | 58 | - name: Import GPG key 59 | run: | 60 | echo "$GPG_PRIVATE_KEY" > private.key 61 | gpg --import --batch private.key 62 | env: 63 | GPG_PRIVATE_KEY: ${{ steps.secrets.outputs.CDAP_GPG_PRIVATE_KEY }} 64 | 65 | - name: Deploy Maven 66 | working-directory: netty 67 | run: mvn -U clean deploy -P release -Dgpg.passphrase=$CDAP_GPG_PASSPHRASE 68 | env: 69 | CDAP_OSSRH_USERNAME: ${{ steps.secrets.outputs.CDAP_OSSRH_USERNAME }} 70 | CDAP_OSSRH_PASSWORD: ${{ steps.secrets.outputs.CDAP_OSSRH_PASSWORD }} 71 | CDAP_GPG_PASSPHRASE: ${{ steps.secrets.outputs.CDAP_GPG_PASSPHRASE }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This is the main .gitignore file. Patterns in here will apply in sub directories too. 2 | 3 | # Ignore basic Java files 4 | *.jar 5 | *.class 6 | 7 | # Ignore Intellij files 8 | *.iml 9 | *.ipr 10 | *.iws 11 | atlassian-ide-plugin.xml 12 | .idea/ 13 | dependency-reduced-pom.xml 14 | 15 | # Ignore Eclipse files 16 | .classpath 17 | .project 18 | .settings/ 19 | 20 | # Ignore Maven files 21 | target/ 22 | 23 | # Ignore JOE files 24 | *~ 25 | 26 | # Ignore INTLJ output 27 | out/ 28 | build/ 29 | 30 | # Ignore Gradle temp dir 31 | .gradle/ 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright © 2014 Cask Data, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations under 14 | # the License. 15 | # 16 | 17 | language: java 18 | 19 | jdk: 20 | - openjdk8 21 | 22 | branches: 23 | only: 24 | - develop 25 | 26 | script: mvn test -Dsurefire.redirectTestOutputToFile=false 27 | 28 | sudo: false 29 | 30 | cache: 31 | directories: 32 | - $HOME/.m2 33 | -------------------------------------------------------------------------------- /KEYS: -------------------------------------------------------------------------------- 1 | This file contains the PGP keys of various developers. 2 | 3 | Users: pgp < KEYS 4 | gpg --import KEYS 5 | Developers: 6 | pgp -kxa and append it to this file. 7 | (pgpk -ll && pgpk -xa ) >> this file. 8 | (gpg --list-sigs 9 | && gpg --armor --export ) >> this file. 10 | 11 | pub 4096R/819EFE61 2014-09-24 12 | uid Cask Data (CODE SIGNING KEY) 13 | sig 3 819EFE61 2014-09-24 Cask Data (CODE SIGNING KEY) 14 | 15 | -----BEGIN PGP PUBLIC KEY BLOCK----- 16 | Version: GnuPG v1.4.11 (GNU/Linux) 17 | 18 | mQINBFQjTj8BEADMhynxJjJXna7koGbLAXYPoK0kLidYS/xb8Oy+V2rA6P3Cyhto 19 | SBLMctDpaCy2LmRRiW6hwkbbmKtOeWvh51cPbWROCTxDWdhjQjpPuPDU8yO9hk// 20 | z+ocwajmNdamYpfOWL34p20KjAXp32klKSJ8lL4NhNadpEaxOuOHooDYtwWGsP2Z 21 | 6Aa2uaVF1HhFnN/MKaZw32pR8e8ChNqkcN99F9xvSYMcgczDvZJ0enVTTFVKuEb7 22 | 9eV9kkua6btvmQBHrSxaD76RXO8xjb/Cm2/O8YRhrZGgNEJtJ/iyJb33MZgbtgNz 23 | YPS2B2dWpGCChNrvMax6NBvgxQ9doLI96Y9bj1qrzjuXNKHSym/jMnA4CaCw0t3y 24 | YO02Orpv6WK4PLAD7YLlhPzMwLPhOVhDDHI9lPOBaF9pt/0g53FDZU7gS7vv3Hup 25 | Jg8iSyMETdiMScK1GNXeeG2IzDoZ33U4BC4ChYWCFfBdeKi7OkATipWMgGmQI6Nv 26 | k+KAh/BOJA2C3yRjIvb0sJKPIy12ljZcCltK3cBhRNvP7yGgFFfH8qQp9HUqD+2q 27 | EsnpRqWBnhKQksJObWUwgAq2HaR4c2JBv1RR2v5lLCdVz7teDAmzxwsdoNqlXRR6 28 | N8uABr0nXYKcKV8WGNjihmpXclzYLPfAt8tMISQg/BsI3LDdo50QQi5aeQARAQAB 29 | tCpDYXNrIERhdGEgKENPREUgU0lHTklORyBLRVkpIDxkZXZAY2Fzay5jbz6JAjgE 30 | EwECACIFAlQjTj8CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEKP2ofaB 31 | nv5hCLwP/iyCokEUI89IQIAX+pfyAPSOeT03ifHEZARnyOt9483Bap4o0u6PFzMw 32 | HiSkYni2kUMHhmfgXiAbt9LZGsyfdYbX4+O3CR7+1EmSpX/McJlmgY6Y3QXWLH2p 33 | 4t3sfc6F6K5Y+XKdKQqBUz1gEK3hCXCrybIRcPGmxOEv35Wc/KExGiTFa/YwGOJM 34 | n++dwRZMn7x6uQ+oEiUlTy9D8dnh2Cg59BxItoaZWJu9Ml0btR8YYgBld4Y7ekMQ 35 | AIpyzXXeuKQ2L6IsJfxb36EQsbr8V8L44sR5t5vaYv5u3QBqNb3jTZNkL+J8YtgP 36 | /2ZViSDKLEBSZ3kcJNd6OfPGAA85u/JHugvNi9sixQxfKa7nekgxY62NB929u/xL 37 | QvFe6R2KoryDZAt40UWLtw7QHsGJknmD6KJqnsOjkqj/LKVBawhWjJM6mru7X/TE 38 | u+jiEj6qJoK3b56dfmhBN+sBfc1UUNbU4jCHNyF7T6a9s1KwSIQRZS9PJ7aEc1Go 39 | Q2mujaYy+Ar6udZGFXRySQUMsSseBON8e+Dtw/5GIcsypv5GkHQIZgEEOpEGnLOi 40 | rjt845Dt4cjOnZ81KqwfCcgTlBVNX/J1nVqnl2jUhVc2+bSa/D5jLiYEzaLVDyuy 41 | nhfm8nN+Jmluk5vpVQPa7Jeh+7n2PKTzypZdTZtZeuDu+euid/CZuQINBFQjTj8B 42 | EADEjR7YfDnKmLmz08CQkwmrWtmn/ib4Tsa69fh8KxScY1vNXA/+XdBi/ZT0BHRp 43 | FpaXu3DZT3bNj7uuJtUWMkA3H4biDnl6H4CBQ4tUw1IZe8drYdfmjtxNfLI5w/Io 44 | QTJroeyCIWhT5Jey2bHkV2qT3tkB/nh6TwUKK0B7jLXUpvaoHxfTac0SxIFQkT/D 45 | IGqsgvmG5hFSSnWpatxwQ1OmvBM/Co8CclEQn45HaBDocCScmfYvKO89OdTOTyD0 46 | FMbsKHuhX0/IE9ELy7HNSbayF8clAaEV7wZvh/RzSxzKIvYkeH2O7O2SfWppEjEq 47 | xJVrutBfQOwmFfVsEmHcdK//yRHJdru/pNGgKljgB7gKngAmT5eM5M9JmHENuUQx 48 | VMPjHKHGujHzV5ydNShpa75JJgAesQXjTBKwWwHdeo2CQ8EJx7oHN+BkkDGzQLSB 49 | 3w3Ya3SSkyZqulse1nAohzLyIXR+LtKnEKPK7N745wMJ9IqN9bU+c2/vVQTFkBvL 50 | wgwL0/5V0scW48Y6dMKFtiMHXknCftAf9HMLxEih82Ysulk3cO9AtWvISNiL/fLB 51 | tWQ3Yt+RCXUras/PIBc/UZxdkZx4HhCStTK9YMAbOvZR+iTtXRsYicxEc0e4Mn/B 52 | KPEVf8XwPDnuSkXToduOKETdXU3f/FQHxQhCwqaPqB7CGwARAQABiQIfBBgBAgAJ 53 | BQJUI04/AhsMAAoJEKP2ofaBnv5hLAgQAJJLkzsheFXQse+6sBsWLbvWDGdN4jxw 54 | cffb2x5laTp7s28G8rveAFtYXDYlH3iq64vclhK+b4uTAyluJWsrV4G8SaDG3Hdj 55 | tfmmbdJ75khkfCjVRvYYA0kGj7AXNt9QpJuOqr+tyVclvZUZtA+2sEIhM442Zgar 56 | NosoS25hbjsPlaRIU+CVF5FE9Jf8Gay1cWPB7ru6xr16rvX5IxLRA0l0oqQTUfFq 57 | W6cmE6lh8bJcXwZWhzhhy2PToIS/1cNkDhMnBCCsUesbK7udMujLHB9eSGqN964z 58 | 0GQdE0MZ2a+5hJbIjRm2mYKs8dr+VPxsdmiek4mw/RIdBIp3WSbJggYEyqPNVfx8 59 | 5WKcowT+UyG65ZrFDlCE2RJegEG7TCnCA34H/LyrJOTOQn1aAeWziEE/X8jU/LXk 60 | TP4F3eRO53ogJJbTI2zOvlxlxLgm+wl9fnnXDtmhHKgChLuCpFVh/rgGwaERymri 61 | Ly7kG7TXrIS73e+A4XHLPj6NX41oNLNx+eWtSR46qql9Qy3E8BpnjO8kqgchRWKL 62 | JHTA82FLaVIsypQA5Lr73cGIxEQ2IZk2APccdzBAK3pUPCg+Vo79v1H2sFsgm7MB 63 | wooUHFv0+jcUtG+XDe5vBOt4BJX89Vae0/tv1LvdyCKN8ab/afm3thN2WW8hQxGZ 64 | 8tO0rTskVL8P 65 | =Q6FN 66 | -----END PGP PUBLIC KEY BLOCK----- 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | netty-http 2 | ========== 3 | A library to develop HTTP services with `Netty `__. Supports the capability to route end-points based on `JAX-RS `__-style annotations. Implements Guava's Service interface to manage the runtime-state of the HTTP service. 4 | 5 | Need for this library 6 | --------------------- 7 | `Netty `__ is a powerful framework to write asynchronous event-driven high-performance applications. While it is relatively easy to write a RESTful HTTP service using netty, the mapping between HTTP routes to handlers is 8 | not a straight-forward task. 9 | 10 | Mapping the routes to method handlers requires writing custom channel handlers and a lot of boilerplate code 11 | as well as knowledge of Netty's internals in order to correctly chain different handlers. The mapping could be 12 | error-prone and tedious when a service handles many end-points. 13 | 14 | This library solves these problems by using `JAX-RS `__ annotations to build a path routing layer on top of Netty. 15 | 16 | Build the HTTP Library 17 | ====================== 18 | 19 | :: 20 | 21 | $ git clone https://github.com/cdapio/netty-http.git 22 | $ cd netty-http 23 | $ mvn clean package 24 | 25 | 26 | Setting up an HTTP Service using the Library 27 | ============================================ 28 | Setting up an HTTP service is very simple using this library: 29 | 30 | * Implement handler methods for different HTTP requests 31 | * Annotate the routes for each handler 32 | * Use a builder to setup the HTTP service 33 | 34 | Example: A simple HTTP service that responds to the ``/v1/ping`` endpoint can be setup as: 35 | 36 | .. code:: java 37 | 38 | // Set up Handlers for Ping 39 | public class PingHandler extends AbstractHttpHandler { 40 | @Path("/v1/ping") 41 | @GET 42 | public void testGet(HttpRequest request, HttpResponder responder){ 43 | responder.sendString(HttpResponseStatus.OK, "OK"); 44 | } 45 | } 46 | 47 | // Setup HTTP service and add Handlers 48 | 49 | // You can either add varargs of HttpHandler or as a list of HttpHanlders as below to the NettyService Builder 50 | 51 | List handlers = new ArrayList<>(); 52 | handlers.add(new PingHandler()); 53 | handlers.add(...otherHandler...) 54 | 55 | NettyHttpService service = NettyHttpService.builder("Name_of_app") 56 | .setPort(7777) // Optionally set the port. If unset, it will bind to an ephemeral port 57 | .setHttpHandlers(handlers) 58 | .build(); 59 | 60 | // Start the HTTP service 61 | service.start(); 62 | 63 | 64 | Example: Sample HTTP service that manages an application lifecycle: 65 | 66 | .. code:: java 67 | 68 | // Set up handlers 69 | // Setting up Path annotation on a class level will be pre-pended with 70 | @Path("/v1/apps") 71 | public class ApplicationHandler extends AbstractHandler { 72 | 73 | // The HTTP endpoint v1/apps/deploy will be handled by the deploy method given below 74 | @Path("deploy") 75 | @POST 76 | public void deploy(HttpRequest request, HttpResponder responder) { 77 | // .. 78 | // Deploy application and send status 79 | // .. 80 | responder.sendStatus(HttpResponseStatus.OK); 81 | } 82 | 83 | // For deploying larger-size applications we can use the BodyConsumer abstract-class, 84 | // we can handle the chunks as we receive it 85 | // and handle clean up when we are done in the finished method, this approach is memory efficient 86 | @Path("deploybig") 87 | @POST 88 | public BodyConsumer deployBig(HttpRequest request, HttpResponder responder) { 89 | return new BodyConsumer() { 90 | @Override 91 | public void chunk(ChannelBuffer request, HttpResponder responder) { 92 | // write the incoming data to a file 93 | } 94 | @Override 95 | public void finished(HttpResponder responder) { 96 | //deploy the app and send response 97 | responder.sendStatus(HttpResponseStatus.OK); 98 | } 99 | @Override 100 | public void handleError(Throwable cause) { 101 | // if there were any error during this process, this will be called. 102 | // do clean-up here. 103 | } 104 | } 105 | } 106 | 107 | // The HTTP endpoint v1/apps/{id}/start will be handled by the start method given below 108 | @Path("{id}/start") 109 | @POST 110 | public void start(HttpRequest request, HttpResponder responder, @PathParam("id") String id) { 111 | // The id that is passed in HTTP request will be mapped to a String via the PathParam annotation 112 | // .. 113 | // Start the application 114 | // .. 115 | responder.sendStatus(HttpResponseStatus.OK); 116 | } 117 | 118 | // The HTTP endpoint v1/apps/{id}/stop will be handled by the stop method given below 119 | @Path("{id}/stop") 120 | @POST 121 | public void stop(HttpRequest request, HttpResponder responder, @PathParam("id") String id) { 122 | // The id that is passed in HTTP request will be mapped to a String via the PathParam annotation 123 | // .. 124 | // Stop the application 125 | // .. 126 | responder.sendStatus(HttpResponseStatus.OK); 127 | } 128 | 129 | // The HTTP endpoint v1/apps/{id}/status will be handled by the status method given below 130 | @Path("{id}/status") 131 | @GET 132 | public void status(HttpRequest request, HttpResponder responder, @PathParam("id") String id) { 133 | // The id that is passed in HTTP request will be mapped to a String via the PathParam annotation 134 | // .. 135 | // Retrieve status the application 136 | // .. 137 | JsonObject status = new JsonObject(); 138 | status.addProperty("status", "RUNNING"); 139 | responder.sendJson(HttpResponseStatus.OK, status.toString()); 140 | } 141 | } 142 | 143 | // Setup HTTP service and add Handlers 144 | 145 | // You can either add varargs of HttpHandler or as a list of HttpHanlders as below to the NettyService Builder 146 | 147 | List handlers = new ArrayList<>(); 148 | handlers.add(new PingHandler()); 149 | handlers.add(...otherHandler...) 150 | 151 | NettyHttpService service = NettyHttpService.builder("Name_of_app") 152 | .setPort(7777) 153 | .setHttpHandlers(handlers) 154 | .build(); 155 | 156 | // Start the HTTP service 157 | service.start(); 158 | 159 | 160 | Setting up an HTTPS Service 161 | --------------------------- 162 | To run an HTTPS Service, add an additional function call to the builder:: 163 | 164 | enableSSL(, , ) 165 | 166 | Code Sample: 167 | 168 | .. code:: java 169 | 170 | // Setup HTTPS service and add Handlers 171 | NettyHttpService service = NettyHttpService.builder() 172 | .setPort(7777) 173 | .addHttpHandlers(new ApplicationHandler()) 174 | .enableSSL(SSLConfig.builder(new File("/path/to/keyStore.jks", "keyStorePassword") 175 | .setCertificatePassword("certificatePassword").build()) 176 | .build(); 177 | 178 | * Set ``String:certificatePassword`` as "null" when not applicable 179 | * ``File:keyStore`` points to the key store that holds your SSL certificate 180 | 181 | References 182 | ---------- 183 | * `Guava `__ 184 | * `Jersey `__ 185 | * `Netty `__ 186 | 187 | Contributing to netty-http 188 | ========================== 189 | Are you interested in making netty-http better? Our development model is a simple pull-based model with a consensus building phase, similar to the Apache's voting process. If you want to help make netty-http better, by adding new features, fixing bugs, or even suggesting improvements to something that's already there, here's how you can contribute: 190 | 191 | * Fork netty-http into your own GitHub repository 192 | * Create a topic branch with an appropriate name 193 | * Work on your favorite feature to your content 194 | * Once you are satisfied, create a pull request by going to the cdapio/netty-http project. 195 | * Address all the review comments 196 | * Once addressed, the changes will be committed to the cdapio/netty-http repo. 197 | 198 | License 199 | ======= 200 | 201 | Copyright © 2014-2019 Cask Data, Inc. All Rights Reserved. 202 | 203 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 204 | 205 | http://www.apache.org/licenses/LICENSE-2.0 206 | 207 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 208 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). 6 | We use g.co/vulnz for our intake, and do coordination and disclosure here on 7 | GitHub (including using GitHub Security Advisory). The Google Security Team will 8 | respond within 5 working days of your report on g.co/vulnz. 9 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/AbstractHandlerHook.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.cdap.http.internal.HandlerInfo; 20 | import io.netty.handler.codec.http.HttpRequest; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | 23 | /** 24 | * A base implementation of {@link HandlerHook} that provides no-op for both 25 | * {@link HandlerHook#preCall(HttpRequest, HttpResponder, HandlerInfo)} 26 | * and {@link HandlerHook#postCall(HttpRequest, HttpResponseStatus, HandlerInfo)} methods. 27 | */ 28 | public abstract class AbstractHandlerHook implements HandlerHook { 29 | @Override 30 | public boolean preCall(HttpRequest request, HttpResponder responder, HandlerInfo handlerInfo) { 31 | return true; 32 | } 33 | 34 | @Override 35 | public void postCall(HttpRequest request, HttpResponseStatus status, HandlerInfo handlerInfo) { 36 | // no-op 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/AbstractHttpHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.cdap.http.internal.HttpResourceHandler; 20 | import io.cdap.http.internal.InternalHttpResponder; 21 | import io.cdap.http.internal.InternalHttpResponse; 22 | import io.netty.handler.codec.http.HttpRequest; 23 | 24 | /** 25 | * A base implementation of {@link HttpHandler} that provides a method for sending a request to other 26 | * handlers that exist in the same server. 27 | */ 28 | public abstract class AbstractHttpHandler implements HttpHandler { 29 | private HttpResourceHandler httpResourceHandler; 30 | 31 | @Override 32 | public void init(HandlerContext context) { 33 | this.httpResourceHandler = context.getHttpResourceHandler(); 34 | } 35 | 36 | @Override 37 | public void destroy(HandlerContext context) { 38 | // No-op 39 | } 40 | 41 | /** 42 | * Send a request to another handler internal to the server, getting back the response body and response code. 43 | * 44 | * @param request request to send to another handler. 45 | * @return {@link InternalHttpResponse} containing the response code and body. 46 | */ 47 | protected InternalHttpResponse sendInternalRequest(HttpRequest request) { 48 | InternalHttpResponder responder = new InternalHttpResponder(); 49 | httpResourceHandler.handle(request, responder); 50 | return responder.getResponse(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/AbstractHttpResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.buffer.Unpooled; 21 | import io.netty.handler.codec.http.DefaultHttpHeaders; 22 | import io.netty.handler.codec.http.EmptyHttpHeaders; 23 | import io.netty.handler.codec.http.HttpHeaderNames; 24 | import io.netty.handler.codec.http.HttpHeaders; 25 | import io.netty.handler.codec.http.HttpResponseStatus; 26 | 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.nio.ByteBuffer; 30 | import java.nio.charset.StandardCharsets; 31 | 32 | /** 33 | * Base implementation of {@link HttpResponder} to simplify child implementations. 34 | */ 35 | public abstract class AbstractHttpResponder implements HttpResponder { 36 | 37 | protected static final String OCTET_STREAM_TYPE = "application/octet-stream"; 38 | 39 | @Override 40 | public void sendJson(HttpResponseStatus status, String jsonString) { 41 | sendString(status, jsonString, new DefaultHttpHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), 42 | "application/json")); 43 | } 44 | 45 | @Override 46 | public void sendString(HttpResponseStatus status, String data) { 47 | sendString(status, data, EmptyHttpHeaders.INSTANCE); 48 | } 49 | 50 | @Override 51 | public void sendString(HttpResponseStatus status, String data, HttpHeaders headers) { 52 | if (data == null) { 53 | sendStatus(status, headers); 54 | return; 55 | } 56 | ByteBuf buffer = Unpooled.wrappedBuffer(StandardCharsets.UTF_8.encode(data)); 57 | sendContent(status, buffer, addContentTypeIfMissing(new DefaultHttpHeaders().add(headers), 58 | "text/plain; charset=utf-8")); 59 | } 60 | 61 | @Override 62 | public void sendStatus(HttpResponseStatus status) { 63 | sendContent(status, Unpooled.EMPTY_BUFFER, EmptyHttpHeaders.INSTANCE); 64 | } 65 | 66 | @Override 67 | public void sendStatus(HttpResponseStatus status, HttpHeaders headers) { 68 | sendContent(status, Unpooled.EMPTY_BUFFER, headers); 69 | } 70 | 71 | @Override 72 | public void sendByteArray(HttpResponseStatus status, byte[] bytes, HttpHeaders headers) { 73 | ByteBuf buffer = Unpooled.wrappedBuffer(bytes); 74 | sendContent(status, buffer, headers); 75 | } 76 | 77 | @Override 78 | public void sendBytes(HttpResponseStatus status, ByteBuffer buffer, HttpHeaders headers) { 79 | sendContent(status, Unpooled.wrappedBuffer(buffer), headers); 80 | } 81 | 82 | @Override 83 | public void sendFile(File file) throws IOException { 84 | sendFile(file, EmptyHttpHeaders.INSTANCE); 85 | } 86 | 87 | @Override 88 | public ChunkResponder sendChunkStart(HttpResponseStatus status) { 89 | return sendChunkStart(status, EmptyHttpHeaders.INSTANCE); 90 | } 91 | 92 | protected final HttpHeaders addContentTypeIfMissing(HttpHeaders headers, String contentType) { 93 | if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) { 94 | headers.set(HttpHeaderNames.CONTENT_TYPE, contentType); 95 | } 96 | 97 | return headers; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/AuthHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package io.cdap.http; 17 | 18 | import io.netty.handler.codec.http.HttpRequest; 19 | 20 | /** 21 | * Basic interface for implementing an AuthHandler. 22 | */ 23 | public interface AuthHandler { 24 | /** 25 | * This method is responsible for deciding whether a request is authenticated. 26 | * 27 | * @param request the HttpRequest in question. 28 | * @return true if this request should be handled. 29 | * @see Secured 30 | */ 31 | public boolean isAuthenticated(HttpRequest request); 32 | 33 | /** 34 | * This method is responsible for deciding whether a request meets the role requirement. 35 | * 36 | * @param request the HttpRequest in question. 37 | * @param roles the roles that are required for this request to be handled. 38 | * @return true if this request should be handled. 39 | * @see RequiredRoles 40 | */ 41 | public boolean hasRoles(HttpRequest request, String[] roles); 42 | 43 | /** 44 | * Returns the value for the WWW-Authenticate header field that will be 45 | * set for requests which were rejected by {@link #isAuthenticated(HttpRequest)} 46 | * or {@link #hasRoles(HttpRequest, String[])}. 47 | * @return value of the WWW-Authenticate header field 48 | */ 49 | public String getWWWAuthenticateHeader(); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/BodyConsumer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | 21 | /** 22 | * HttpHandler would extend this abstract class and implement methods to stream the body directly. 23 | * chunk method would receive the http-chunks of the body and finished would be called 24 | * on receipt of the last chunk. 25 | */ 26 | public abstract class BodyConsumer { 27 | /** 28 | * Http request content will be streamed directly to this method. 29 | * 30 | * @param request the next chunk to be consumed 31 | * @param responder a {@link HttpResponder} for sending response back to client. 32 | */ 33 | public abstract void chunk(ByteBuf request, HttpResponder responder); 34 | 35 | /** 36 | * This is called on the receipt of the last HttpChunk. 37 | * 38 | * @param responder a {@link HttpResponder} for sending response back to client. 39 | */ 40 | public abstract void finished(HttpResponder responder); 41 | 42 | /** 43 | * When there is exception on netty while streaming, it will be propagated to handler 44 | * so the handler can do the cleanup. Implementations should not write to an HttpResponder. 45 | * Instead, use a {@link ExceptionHandler}. 46 | * 47 | * @param cause the reaons of the failure. 48 | */ 49 | public abstract void handleError(Throwable cause); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/BodyProducer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2015-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | 21 | import javax.annotation.Nullable; 22 | 23 | /** 24 | * Class for producing response body in streaming fashion. 25 | */ 26 | public abstract class BodyProducer { 27 | 28 | /** 29 | * Returns the size of the content in bytes that this producer is going to produce. 30 | *

31 | * If a negative number is returned, the size is unknown and the {@code Content-Length} header 32 | * won't be set and {@code Transfer-Encoding: chunked} will be used. 33 | *

34 | * By default, {@code -1L} is returned. 35 | * 36 | * @return the size of the content in bytes 37 | */ 38 | public long getContentLength() { 39 | return -1L; 40 | } 41 | 42 | /** 43 | * Returns a {@link ByteBuf} representing the next chunk of bytes to send. If the returned 44 | * {@link ByteBuf} is an empty buffer, it signals the end of the streaming. 45 | * 46 | * @return the next chunk of bytes to send 47 | * @throws Exception if there is any error 48 | */ 49 | public abstract ByteBuf nextChunk() throws Exception; 50 | 51 | /** 52 | * This method will get called after the last chunk of the body get sent successfully. 53 | * 54 | * @throws Exception if there is any error 55 | */ 56 | public abstract void finished() throws Exception; 57 | 58 | /** 59 | * This method will get called if there is any error raised when sending body chunks. This method will 60 | * also get called if there is exception raised from the {@link #nextChunk()} or {@link #finished()} method. 61 | * 62 | * @param cause the reason of the failure or {@code null} if the reason of failure is unknown. 63 | */ 64 | public abstract void handleError(@Nullable Throwable cause); 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/ChannelPipelineModifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.channel.ChannelPipeline; 20 | 21 | /** 22 | * This class allows user modify a {@link ChannelPipeline} when it gets initialized, which happens on every 23 | * new channel 24 | */ 25 | public abstract class ChannelPipelineModifier { 26 | 27 | /** 28 | * Modifies the given {@link ChannelPipeline}. 29 | * 30 | * @param pipeline the pipeline to be modified 31 | */ 32 | public abstract void modify(ChannelPipeline pipeline); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/ChunkResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | 21 | import java.io.Closeable; 22 | import java.io.Flushable; 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | 26 | /** 27 | * A responder for sending chunk-encoded response 28 | */ 29 | public interface ChunkResponder extends Closeable, Flushable { 30 | 31 | /** 32 | * Adds a chunk of data to the response. The content will be sent to the client asynchronously. 33 | * 34 | * @param chunk content to send 35 | * @throws IOException if the connection is already closed 36 | */ 37 | void sendChunk(ByteBuffer chunk) throws IOException; 38 | 39 | /** 40 | * Adds a chunk of data to the response. The content will be sent to the client asynchronously. 41 | * 42 | * @param chunk content to send 43 | * @throws IOException if this {@link ChunkResponder} already closed or the connection is closed 44 | */ 45 | void sendChunk(ByteBuf chunk) throws IOException; 46 | 47 | /** 48 | * Flushes all the chunks writen so far to the client asynchronously. 49 | */ 50 | @Override 51 | default void flush() { 52 | // no-op 53 | } 54 | 55 | /** 56 | * Closes this responder which signals the end of the chunk response. 57 | */ 58 | @Override 59 | void close() throws IOException; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2015-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.handler.codec.http.HttpRequest; 20 | import io.netty.handler.codec.http.HttpResponseStatus; 21 | 22 | /** 23 | * Handles exceptions and provides a response via the {@link HttpResponder}. 24 | */ 25 | public class ExceptionHandler { 26 | public void handle(Throwable t, HttpRequest request, HttpResponder responder) { 27 | String message = String.format("Exception encountered while processing request : %s", t.getMessage()); 28 | responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/HandlerContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.cdap.http.internal.HttpResourceHandler; 20 | 21 | /** 22 | * Place holder for information about the environment. Will be passed in during lifecycle management calls of 23 | * HttpHandlers. Currently has methods to get RunTimeArguments. 24 | */ 25 | public interface HandlerContext { 26 | 27 | /** 28 | * @return the {@link HttpResourceHandler} associated with this context, 29 | * used to let one handler call another internally. 30 | */ 31 | HttpResourceHandler getHttpResourceHandler(); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/HandlerHook.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.cdap.http.internal.HandlerInfo; 20 | import io.netty.handler.codec.http.HttpRequest; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | 23 | /** 24 | * Interface that needs to be implemented to intercept handler method calls. 25 | */ 26 | public interface HandlerHook { 27 | 28 | /** 29 | * preCall is run before a handler method call is made. If any of the preCalls throw exception or return false then 30 | * no other subsequent preCalls will be called and the request processing will be terminated, 31 | * also no postCall hooks will be called. 32 | * 33 | * @param request HttpRequest being processed. 34 | * @param responder HttpResponder to send response. 35 | * @param handlerInfo Info on handler method that will be called. 36 | * @return true if the request processing can continue, otherwise the hook should send response and return false to 37 | * stop further processing. 38 | */ 39 | boolean preCall(HttpRequest request, HttpResponder responder, HandlerInfo handlerInfo); 40 | 41 | /** 42 | * postCall is run after a handler method call is made. If any of the postCalls throw and exception then the 43 | * remaining postCalls will still be called. If the handler method was not called then postCall hooks will not be 44 | * called. 45 | * 46 | * @param request HttpRequest being processed. 47 | * @param status Http status returned to the client. 48 | * @param handlerInfo Info on handler method that was called. 49 | */ 50 | void postCall(HttpRequest request, HttpResponseStatus status, HandlerInfo handlerInfo); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/HttpHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | /** 20 | * Interface that needs to be implemented for handling HTTP methods. init and destroy methods can be used to manage 21 | * the lifecycle of the object. The framework will call init and destroy during startup and shutdown respectively. 22 | * No handles will be called before init method of the class is called or after the destroy method is called. 23 | * The handlers should be annotated with Jax-RS annotations to handle appropriate path and HTTP Methods. 24 | * Note: Only the annotations in the given handler object will be inspected and be available for routing. The 25 | * annotations from the base class (if extended) will not be applied to the given handler object. 26 | * Note: The framework that calls the handler assumes that the implementation is threadsafe. 27 | * Note: If the HttpHandler implementation is extended, the annotations are not inherited from the base class. 28 | * 29 | * Example: 30 | * public class ApiHandler implements HttpHandler{ 31 | * {@literal @}Override 32 | * public void init(HandlerContext context){ 33 | * //Perform bootstrap operations before any of the handlers in this class gets called. 34 | * } 35 | * {@literal @}Override 36 | * public void destroy(HandlerContext context){ 37 | * //Perform teardown operations before the server shuts down. 38 | * } 39 | * 40 | * {@literal @}Path("/common/v1/widgets") 41 | * {@literal @}GET 42 | * public void handleGet(HttpRequest request, HttpResponder responder){ 43 | * //Handle Http request 44 | * } 45 | * } 46 | * 47 | */ 48 | public interface HttpHandler { 49 | 50 | /** 51 | * init method will be called before the netty pipeline is setup. Any initialization operation can be performed 52 | * in this method. 53 | * 54 | * @param context instance of HandlerContext. 55 | */ 56 | void init(HandlerContext context); 57 | 58 | /** 59 | * destroy method will be called before shutdown. Any teardown task can be performed in this method. 60 | * At this point the server will not accept any more new requests. 61 | * 62 | * @param context instance of HandlerContext. 63 | */ 64 | void destroy(HandlerContext context); 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/HttpResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.handler.codec.http.HttpHeaders; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.nio.ByteBuffer; 26 | 27 | /** 28 | * HttpResponder is used to send response back to clients. 29 | */ 30 | public interface HttpResponder { 31 | 32 | /** 33 | * Sends json response back to the client. This is a convenient method to send json encoded string with 34 | * content type automatically set to {@code application/json}. 35 | * 36 | * @param status Status of the response. 37 | * @param jsonString The json string to send back. 38 | */ 39 | void sendJson(HttpResponseStatus status, String jsonString); 40 | 41 | /** 42 | * Send a string response in UTF-8 encoding back to the http client. 43 | * 44 | * @param status status of the Http response. 45 | * @param data string data to be sent back. 46 | */ 47 | void sendString(HttpResponseStatus status, String data); 48 | 49 | /** 50 | * Send a string response in UTF-8 encoding back to the http client. 51 | * 52 | * @param status status of the Http response. 53 | * @param data string data to be sent back. 54 | * @param headers additional headers to send with the response. 55 | */ 56 | void sendString(HttpResponseStatus status, String data, HttpHeaders headers); 57 | 58 | /** 59 | * Send only a status code back to client without any content. 60 | * 61 | * @param status status of the Http response. 62 | */ 63 | void sendStatus(HttpResponseStatus status); 64 | 65 | /** 66 | * Send only a status code back to client without any content. 67 | * 68 | * @param status status of the Http response. 69 | * @param headers additional headers to send with the response. 70 | */ 71 | void sendStatus(HttpResponseStatus status, HttpHeaders headers); 72 | 73 | /** 74 | * Send a response containing raw bytes. Default content type is "application/octet-stream", but can be 75 | * overridden by the headers parameter. 76 | * 77 | * @param status status of the Http response. 78 | * @param bytes bytes to be sent back. 79 | * @param headers additional headers to send with the response. 80 | */ 81 | void sendByteArray(HttpResponseStatus status, byte[] bytes, HttpHeaders headers); 82 | 83 | /** 84 | * Sends a response containing raw bytes. Default content type is "application/octet-stream", but can be 85 | * overridden by the headers parameter. 86 | * 87 | * @param status status of the Http response 88 | * @param buffer bytes to send 89 | * @param headers additional headers to send with the response. 90 | */ 91 | void sendBytes(HttpResponseStatus status, ByteBuffer buffer, HttpHeaders headers); 92 | 93 | /** 94 | * Respond to the client saying the response will be in chunks. The response body can be sent in chunks 95 | * using the {@link ChunkResponder} returned. 96 | * 97 | * @param status the status code to respond with 98 | * @return chunk responder for sending the response 99 | */ 100 | ChunkResponder sendChunkStart(HttpResponseStatus status); 101 | 102 | /** 103 | * Respond to the client saying the response will be in chunks. The response body can be sent in chunks 104 | * using the {@link ChunkResponder} returned. 105 | * 106 | * @param status the status code to respond with 107 | * @param headers additional headers to send with the response. 108 | * @return chunk responder for sending the response 109 | */ 110 | ChunkResponder sendChunkStart(HttpResponseStatus status, HttpHeaders headers); 111 | 112 | /** 113 | * Send response back to client. Default content type is "application/octet-stream", but can be 114 | * overridden by the headers parameter. 115 | * 116 | * @param status Status of the response. 117 | * @param content Content to be sent back. 118 | * @param headers additional headers to send with the response. 119 | */ 120 | void sendContent(HttpResponseStatus status, ByteBuf content, HttpHeaders headers); 121 | 122 | /** 123 | * Sends a file content back to client with response status 200 with content type as "application/octet-stream". 124 | * 125 | * @param file The file to send 126 | * @throws IOException if failed to open and read the file 127 | */ 128 | void sendFile(File file) throws IOException; 129 | 130 | /** 131 | * Sends a file content back to client with response status 200. Default content type is "application/octet-stream", 132 | * but can be overridden by the headers parameter. 133 | * 134 | * @param file The file to send 135 | * @param headers additional headers to send with the response. 136 | * @throws IOException if failed to open and read the file 137 | */ 138 | void sendFile(File file, HttpHeaders headers) throws IOException; 139 | 140 | /** 141 | * Sends response back to client. The response body is produced by the given {@link BodyProducer}. This method 142 | * will return immediate after it is called. Invocation of methods on the given {@link BodyProducer} will be 143 | * triggered from another thread. Default content type is "application/octet-stream", but can be 144 | * overridden by the headers parameter. 145 | * 146 | * @param status Status of the response. 147 | * @param bodyProducer a {@link BodyProducer} to produce response body. 148 | * @param headers additional headers to send with the response. 149 | */ 150 | void sendContent(HttpResponseStatus status, BodyProducer bodyProducer, HttpHeaders headers); 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/RequiredRoles.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import java.lang.annotation.Documented; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.Target; 22 | import static java.lang.annotation.ElementType.METHOD; 23 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 24 | 25 | /** 26 | * Indicates that the annotated method should only be called if the requesting 27 | * {@link io.netty.handler.codec.http.HttpRequest} was deemed authorized by the registered's 28 | * {@link AuthHandler#hasRoles(io.netty.handler.codec.http.HttpRequest, String[])} method. 29 | */ 30 | @Documented 31 | @Retention(RUNTIME) 32 | @Target(METHOD) 33 | public @interface RequiredRoles { 34 | String[] value(); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/SSLConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import java.io.File; 20 | 21 | /** 22 | * A class that encapsulates SSLContext configuration. 23 | */ 24 | public class SSLConfig { 25 | private final File keyStore; 26 | private final String keyStorePassword; 27 | private final String certificatePassword; 28 | private final File trustKeyStore; 29 | private final String trustKeyStorePassword; 30 | 31 | private SSLConfig(File keyStore, String keyStorePassword, 32 | String certificatePassword, File trustKeyStore, String trustKeyStorePassword) { 33 | this.keyStore = keyStore; 34 | this.keyStorePassword = keyStorePassword; 35 | this.certificatePassword = certificatePassword; 36 | this.trustKeyStore = trustKeyStore; 37 | this.trustKeyStorePassword = trustKeyStorePassword; 38 | } 39 | 40 | /** 41 | * @return KeyStore file 42 | */ 43 | public File getKeyStore() { 44 | return keyStore; 45 | } 46 | 47 | /** 48 | * @return KeyStore password. 49 | */ 50 | public String getKeyStorePassword() { 51 | return keyStorePassword; 52 | } 53 | 54 | /** 55 | * @return certificate password 56 | */ 57 | public String getCertificatePassword() { 58 | return certificatePassword; 59 | } 60 | 61 | /** 62 | * @return trust KeyStore file 63 | */ 64 | public File getTrustKeyStore() { 65 | return trustKeyStore; 66 | } 67 | 68 | /** 69 | * @return trust KeyStore password. 70 | */ 71 | public String getTrustKeyStorePassword() { 72 | return trustKeyStorePassword; 73 | } 74 | 75 | /** 76 | * Creates a builder for the SSLConfig. 77 | * 78 | * @param keyStore the keystore 79 | * @param keyStorePassword the password for the keystore 80 | * @return instance of {@code Builder} 81 | */ 82 | public static Builder builder(File keyStore, String keyStorePassword) { 83 | return new Builder(keyStore, keyStorePassword); 84 | } 85 | 86 | /** 87 | * Builder to help create the SSLConfig. 88 | */ 89 | public static class Builder { 90 | private final File keyStore; 91 | private final String keyStorePassword; 92 | private String certificatePassword; 93 | private File trustKeyStore; 94 | private String trustKeyStorePassword; 95 | 96 | private Builder(File keyStore, String keyStorePassword) { 97 | this.keyStore = keyStore; 98 | this.keyStorePassword = keyStorePassword; 99 | } 100 | 101 | /** 102 | * Set the certificate password for KeyStore. 103 | * 104 | * @param certificatePassword certificate password 105 | * @return instance of {@code Builder}. 106 | */ 107 | public Builder setCertificatePassword(String certificatePassword) { 108 | this.certificatePassword = certificatePassword; 109 | return this; 110 | } 111 | 112 | /** 113 | * Set trust KeyStore file. 114 | * 115 | * @param trustKeyStore trust KeyStore file. 116 | * @return an instance of {@code Builder}. 117 | */ 118 | public Builder setTrustKeyStore(File trustKeyStore) { 119 | this.trustKeyStore = trustKeyStore; 120 | return this; 121 | } 122 | 123 | /** 124 | * Set trust KeyStore password. 125 | * 126 | * @param trustKeyStorePassword trust KeyStore password. 127 | * @return an instance of {@code Builder}. 128 | */ 129 | public Builder setTrustKeyStorePassword(String trustKeyStorePassword) { 130 | if (trustKeyStorePassword == null) { 131 | throw new IllegalArgumentException("KeyStore Password Not Configured"); 132 | } 133 | this.trustKeyStorePassword = trustKeyStorePassword; 134 | return this; 135 | } 136 | 137 | /** 138 | * @return instance of {@code SSLConfig} 139 | */ 140 | public SSLConfig build() { 141 | if (keyStore == null) { 142 | throw new IllegalArgumentException("Certificate File Not Configured"); 143 | } 144 | if (keyStorePassword == null) { 145 | throw new IllegalArgumentException("KeyStore Password Not Configured"); 146 | } 147 | return new SSLConfig(keyStore, keyStorePassword, certificatePassword, trustKeyStore, trustKeyStorePassword); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/SSLHandlerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBufAllocator; 20 | import io.netty.handler.ssl.SslContext; 21 | import io.netty.handler.ssl.SslContextBuilder; 22 | import io.netty.handler.ssl.SslHandler; 23 | 24 | import java.io.File; 25 | import java.io.FileInputStream; 26 | import java.io.InputStream; 27 | import java.security.KeyStore; 28 | import java.security.Security; 29 | import javax.net.ssl.KeyManagerFactory; 30 | import javax.net.ssl.SSLEngine; 31 | import javax.net.ssl.TrustManagerFactory; 32 | 33 | /** 34 | * A class that encapsulates SSL Certificate Information. 35 | */ 36 | public class SSLHandlerFactory { 37 | 38 | private final SslContext sslContext; 39 | private boolean needClientAuth; 40 | 41 | public SSLHandlerFactory(SSLConfig sslConfig) { 42 | String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); 43 | if (algorithm == null) { 44 | algorithm = "SunX509"; 45 | } 46 | try { 47 | KeyStore ks = getKeyStore(sslConfig.getKeyStore(), sslConfig.getKeyStorePassword()); 48 | // Set up key manager factory to use our key store 49 | KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); 50 | kmf.init(ks, sslConfig.getCertificatePassword() != null ? sslConfig.getCertificatePassword().toCharArray() 51 | : sslConfig.getKeyStorePassword().toCharArray()); 52 | 53 | SslContextBuilder builder = SslContextBuilder.forServer(kmf); 54 | if (sslConfig.getTrustKeyStore() != null) { 55 | this.needClientAuth = true; 56 | KeyStore tks = getKeyStore(sslConfig.getTrustKeyStore(), sslConfig.getTrustKeyStorePassword()); 57 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); 58 | tmf.init(tks); 59 | builder.trustManager(tmf); 60 | } 61 | 62 | this.sslContext = builder.build(); 63 | } catch (Exception e) { 64 | throw new IllegalArgumentException("Failed to initialize the server-side SSLContext", e); 65 | } 66 | } 67 | 68 | public SSLHandlerFactory(SslContext sslContext) { 69 | this.sslContext = sslContext; 70 | } 71 | 72 | private static KeyStore getKeyStore(File keyStore, String keyStorePassword) throws Exception { 73 | try (InputStream is = new FileInputStream(keyStore)) { 74 | KeyStore ks = KeyStore.getInstance("JKS"); 75 | ks.load(is, keyStorePassword.toCharArray()); 76 | return ks; 77 | } 78 | } 79 | 80 | /** 81 | * Creates an SslHandler 82 | * 83 | * @param bufferAllocator the buffer allocator 84 | * @return instance of {@code SslHandler} 85 | */ 86 | public SslHandler create(ByteBufAllocator bufferAllocator) { 87 | SSLEngine engine = sslContext.newEngine(bufferAllocator); 88 | engine.setNeedClientAuth(needClientAuth); 89 | engine.setUseClientMode(false); 90 | return new SslHandler(engine); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/Secured.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import java.lang.annotation.Documented; 20 | import java.lang.annotation.ElementType; 21 | import java.lang.annotation.Retention; 22 | import java.lang.annotation.RetentionPolicy; 23 | import java.lang.annotation.Target; 24 | 25 | /** 26 | * Indicates that the annotated method should only be called if the requesting 27 | * {@link io.netty.handler.codec.http.HttpRequest} was deemed authenticated by the 28 | * registered's {@link AuthHandler#isAuthenticated(io.netty.handler.codec.http.HttpRequest)} method. 29 | */ 30 | @Target({ElementType.METHOD}) 31 | @Retention(RetentionPolicy.RUNTIME) 32 | @Documented 33 | public @interface Secured { 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/URLRewriter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.handler.codec.http.HttpRequest; 20 | 21 | /** 22 | * Re-writes URL of an incoming request before any handlers or their hooks are called. 23 | * This can be used to map an incoming URL to an URL that a handler understands. The re-writer overwrites the incoming 24 | * URL with the new value. 25 | * The re-writer can also send response to the clients, eg. redirect header, 26 | * and then stop further request processing. 27 | */ 28 | public interface URLRewriter { 29 | /** 30 | * Implement this to rewrite URL of an incoming request. The re-written URL needs to be updated back in 31 | * {@code request} using {@link HttpRequest#setUri(String)}. 32 | * 33 | * @param request Incoming HTTP request. 34 | * @param responder Used to send response to clients. 35 | * @return true if request processing should continue, false otherwise. 36 | */ 37 | boolean rewrite(HttpRequest request, HttpResponder responder); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 21 | import io.netty.handler.codec.http.FullHttpResponse; 22 | import io.netty.handler.codec.http.HttpRequest; 23 | import io.netty.handler.codec.http.HttpResponse; 24 | import io.netty.handler.codec.http.HttpResponseStatus; 25 | import io.netty.handler.codec.http.HttpUtil; 26 | import io.netty.handler.codec.http.HttpVersion; 27 | 28 | import java.nio.charset.StandardCharsets; 29 | 30 | /** 31 | * Exception that gets thrown when a request tries to access a secured resource 32 | * and {@link io.cdap.http.AuthHandler#isAuthenticated(HttpRequest)} doesn't accept it. 33 | */ 34 | public class AuthenticationException extends HandlerException { 35 | 36 | public AuthenticationException(String message) { 37 | super(HttpResponseStatus.UNAUTHORIZED, message); 38 | } 39 | 40 | @Override 41 | public HttpResponse createFailureResponse() { 42 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, 43 | Unpooled.copiedBuffer("UNAUTHORIZED", StandardCharsets.UTF_8)); 44 | response.headers().add("WWW-Authenticate", getMessage()); 45 | HttpUtil.setContentLength(response, response.content().readableBytes()); 46 | return response; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/AuthorizationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 21 | import io.netty.handler.codec.http.FullHttpResponse; 22 | import io.netty.handler.codec.http.HttpRequest; 23 | import io.netty.handler.codec.http.HttpResponse; 24 | import io.netty.handler.codec.http.HttpResponseStatus; 25 | import io.netty.handler.codec.http.HttpUtil; 26 | import io.netty.handler.codec.http.HttpVersion; 27 | 28 | import java.nio.charset.StandardCharsets; 29 | 30 | /** 31 | * Exception that gets thrown when a request tries to access a resource that requires roles 32 | * and {@link io.cdap.http.AuthHandler#hasRoles(HttpRequest, String[])} doesn't accept it. 33 | */ 34 | public class AuthorizationException extends HandlerException { 35 | 36 | public AuthorizationException() { 37 | super(HttpResponseStatus.UNAUTHORIZED, "Request doesn't satisfy role requirement."); 38 | } 39 | 40 | @Override 41 | public HttpResponse createFailureResponse() { 42 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, 43 | Unpooled.copiedBuffer("FORBIDDEN", StandardCharsets.UTF_8)); 44 | HttpUtil.setContentLength(response, response.content().readableBytes()); 45 | return response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/BasicHandlerContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.HandlerContext; 20 | 21 | /** 22 | * BasicHandlerContext returns an empty runtime arguments. 23 | */ 24 | public final class BasicHandlerContext implements HandlerContext { 25 | private final HttpResourceHandler httpResourceHandler; 26 | 27 | public BasicHandlerContext(HttpResourceHandler httpResourceHandler) { 28 | this.httpResourceHandler = httpResourceHandler; 29 | } 30 | 31 | @Override 32 | public HttpResourceHandler getHttpResourceHandler() { 33 | return httpResourceHandler; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/BasicHttpResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2020 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.AbstractHttpResponder; 20 | import io.cdap.http.BodyProducer; 21 | import io.cdap.http.ChunkResponder; 22 | import io.cdap.http.HttpResponder; 23 | import io.netty.buffer.ByteBuf; 24 | import io.netty.buffer.ByteBufAllocator; 25 | import io.netty.buffer.Unpooled; 26 | import io.netty.channel.Channel; 27 | import io.netty.channel.ChannelFutureListener; 28 | import io.netty.channel.ChannelHandlerContext; 29 | import io.netty.channel.ChannelPipeline; 30 | import io.netty.channel.DefaultFileRegion; 31 | import io.netty.channel.FileRegion; 32 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 33 | import io.netty.handler.codec.http.DefaultHttpHeaders; 34 | import io.netty.handler.codec.http.DefaultHttpResponse; 35 | import io.netty.handler.codec.http.FullHttpResponse; 36 | import io.netty.handler.codec.http.HttpChunkedInput; 37 | import io.netty.handler.codec.http.HttpContentCompressor; 38 | import io.netty.handler.codec.http.HttpHeaderNames; 39 | import io.netty.handler.codec.http.HttpHeaderValues; 40 | import io.netty.handler.codec.http.HttpHeaders; 41 | import io.netty.handler.codec.http.HttpResponse; 42 | import io.netty.handler.codec.http.HttpResponseStatus; 43 | import io.netty.handler.codec.http.HttpUtil; 44 | import io.netty.handler.codec.http.HttpVersion; 45 | import io.netty.handler.codec.http.LastHttpContent; 46 | import io.netty.handler.stream.ChunkedFile; 47 | import io.netty.handler.stream.ChunkedInput; 48 | import org.slf4j.Logger; 49 | import org.slf4j.LoggerFactory; 50 | 51 | import java.io.File; 52 | import java.io.IOException; 53 | import java.io.RandomAccessFile; 54 | import java.nio.charset.StandardCharsets; 55 | import java.util.NoSuchElementException; 56 | import java.util.concurrent.atomic.AtomicBoolean; 57 | import javax.annotation.Nullable; 58 | 59 | /** 60 | * Basic implementation of {@link HttpResponder} that uses {@link Channel} to write back to client. 61 | */ 62 | final class BasicHttpResponder extends AbstractHttpResponder { 63 | 64 | private static final Logger LOG = LoggerFactory.getLogger(BasicHttpResponder.class); 65 | 66 | private final Channel channel; 67 | private final AtomicBoolean responded; 68 | private final boolean sslEnabled; 69 | private final int chunkMemoryLimit; 70 | 71 | BasicHttpResponder(Channel channel, boolean sslEnabled, int chunkMemoryLimit) { 72 | this.channel = channel; 73 | this.responded = new AtomicBoolean(false); 74 | this.sslEnabled = sslEnabled; 75 | this.chunkMemoryLimit = chunkMemoryLimit; 76 | } 77 | 78 | @Override 79 | public ChunkResponder sendChunkStart(HttpResponseStatus status, HttpHeaders headers) { 80 | if (status.code() < 200 || status.code() >= 210) { 81 | throw new IllegalArgumentException("Status code must be between 200 and 210. Status code provided is " 82 | + status.code()); 83 | } 84 | 85 | HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); 86 | addContentTypeIfMissing(response.headers().add(headers), OCTET_STREAM_TYPE); 87 | 88 | if (HttpUtil.getContentLength(response, -1L) < 0) { 89 | HttpUtil.setTransferEncodingChunked(response, true); 90 | } 91 | 92 | checkNotResponded(); 93 | channel.write(response); 94 | return new ChannelChunkResponder(channel, chunkMemoryLimit); 95 | } 96 | 97 | @Override 98 | public void sendContent(HttpResponseStatus status, ByteBuf content, HttpHeaders headers) { 99 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content); 100 | response.headers().add(headers); 101 | HttpUtil.setContentLength(response, content.readableBytes()); 102 | 103 | if (content.isReadable()) { 104 | addContentTypeIfMissing(response.headers(), OCTET_STREAM_TYPE); 105 | } 106 | 107 | checkNotResponded(); 108 | channel.writeAndFlush(response); 109 | } 110 | 111 | @Override 112 | public void sendFile(File file, HttpHeaders headers) throws IOException { 113 | HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 114 | addContentTypeIfMissing(response.headers().add(headers), OCTET_STREAM_TYPE); 115 | 116 | HttpUtil.setTransferEncodingChunked(response, false); 117 | HttpUtil.setContentLength(response, file.length()); 118 | 119 | // Open the file first to make sure it is readable before sending out the response 120 | RandomAccessFile raf = new RandomAccessFile(file, "r"); 121 | try { 122 | checkNotResponded(); 123 | 124 | // Write the initial line and the header. 125 | channel.write(response); 126 | 127 | // Write the content. 128 | // FileRegion only works in non-SSL case. For SSL case, use ChunkedFile instead. 129 | // See https://github.com/netty/netty/issues/2075 130 | if (sslEnabled) { 131 | // The HttpChunkedInput will write out the last content 132 | channel.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 8192))); 133 | } else { 134 | final Runnable completion = prepareSendFile(channel); 135 | try { 136 | // The FileRegion will close the file channel when it is done sending. 137 | FileRegion region = new DefaultFileRegion(raf.getChannel(), 0, file.length()); 138 | channel.write(region); 139 | channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).addListener(future -> completion.run()); 140 | } catch (Throwable t) { 141 | completion.run(); 142 | throw t; 143 | } 144 | } 145 | } catch (Throwable t) { 146 | try { 147 | raf.close(); 148 | } catch (IOException ex) { 149 | t.addSuppressed(ex); 150 | } 151 | throw t; 152 | } 153 | } 154 | 155 | @Override 156 | public void sendContent(HttpResponseStatus status, final BodyProducer bodyProducer, HttpHeaders headers) { 157 | final long contentLength; 158 | try { 159 | contentLength = bodyProducer.getContentLength(); 160 | } catch (Throwable t) { 161 | bodyProducer.handleError(t); 162 | // Response with error and close the connection 163 | sendContent( 164 | HttpResponseStatus.INTERNAL_SERVER_ERROR, 165 | Unpooled.copiedBuffer("Failed to determined content length. Cause: " + t.getMessage(), StandardCharsets.UTF_8), 166 | new DefaultHttpHeaders() 167 | .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE) 168 | .set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8")); 169 | return; 170 | } 171 | 172 | HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); 173 | addContentTypeIfMissing(response.headers().add(headers), OCTET_STREAM_TYPE); 174 | 175 | if (contentLength < 0L) { 176 | HttpUtil.setTransferEncodingChunked(response, true); 177 | } else { 178 | HttpUtil.setTransferEncodingChunked(response, false); 179 | HttpUtil.setContentLength(response, contentLength); 180 | } 181 | 182 | checkNotResponded(); 183 | 184 | // Streams the data produced by the given BodyProducer 185 | channel.writeAndFlush(response).addListener(future -> { 186 | if (!future.isSuccess()) { 187 | callBodyProducerHandleError(bodyProducer, future.cause()); 188 | channel.close(); 189 | return; 190 | } 191 | channel.writeAndFlush(new HttpChunkedInput(new BodyProducerChunkedInput(bodyProducer, contentLength))) 192 | .addListener(createBodyProducerCompletionListener(bodyProducer)); 193 | }); 194 | } 195 | 196 | /** 197 | * Returns {@code true} if response was sent. 198 | */ 199 | boolean isResponded() { 200 | return responded.get(); 201 | } 202 | 203 | private ChannelFutureListener createBodyProducerCompletionListener(final BodyProducer bodyProducer) { 204 | return future -> { 205 | if (!future.isSuccess()) { 206 | callBodyProducerHandleError(bodyProducer, future.cause()); 207 | channel.close(); 208 | return; 209 | } 210 | 211 | try { 212 | bodyProducer.finished(); 213 | } catch (Throwable t) { 214 | callBodyProducerHandleError(bodyProducer, t); 215 | channel.close(); 216 | } 217 | }; 218 | } 219 | 220 | private void callBodyProducerHandleError(BodyProducer bodyProducer, @Nullable Throwable failureCause) { 221 | try { 222 | bodyProducer.handleError(failureCause); 223 | } catch (Throwable t) { 224 | LOG.warn("Exception raised from BodyProducer.handleError() for {}", bodyProducer, t); 225 | } 226 | } 227 | 228 | private void checkNotResponded() { 229 | if (!responded.compareAndSet(false, true)) { 230 | throw new IllegalStateException("Response has already been sent"); 231 | } 232 | } 233 | 234 | /** 235 | * Prepares the given {@link Channel} for the sending file. 236 | * 237 | * @param channel the channel to prepare 238 | * @return a {@link Runnable} that should be called when the send file is completed to revert the action 239 | */ 240 | private Runnable prepareSendFile(Channel channel) { 241 | // Remove the "compressor" from the pipeline to skip content encoding since FileRegion do zero-copy write, 242 | // hence bypassing any user space data operation. 243 | try { 244 | final ChannelPipeline pipeline = channel.pipeline(); 245 | pipeline.remove("compressor"); 246 | return () -> pipeline.addAfter("codec", "compressor", new HttpContentCompressor()); 247 | } catch (NoSuchElementException e) { 248 | // Ignore if there is no compressor 249 | return () -> { 250 | // no-op 251 | }; 252 | } 253 | } 254 | 255 | /** 256 | * A {@link ChunkedInput} implementation that produce chunks using {@link BodyProducer}. 257 | */ 258 | private static final class BodyProducerChunkedInput implements ChunkedInput { 259 | 260 | private final BodyProducer bodyProducer; 261 | private final long length; 262 | private long bytesProduced; 263 | private ByteBuf nextChunk; 264 | private boolean completed; 265 | 266 | private BodyProducerChunkedInput(BodyProducer bodyProducer, long length) { 267 | this.bodyProducer = bodyProducer; 268 | this.length = length; 269 | } 270 | 271 | @Override 272 | public boolean isEndOfInput() throws Exception { 273 | if (completed) { 274 | return true; 275 | } 276 | if (nextChunk == null) { 277 | nextChunk = bodyProducer.nextChunk(); 278 | } 279 | 280 | completed = !nextChunk.isReadable(); 281 | if (completed) { 282 | try { 283 | if (length >= 0 && bytesProduced != length) { 284 | throw new IllegalStateException("Body size doesn't match with content length. " + 285 | "Content-Length: " + length + ", bytes produced: " + bytesProduced); 286 | } 287 | } finally { 288 | // We should release the buffer if it is completed since this will be the last place that uses the buffer, 289 | // as the buffer won't be returned by the readChunk method. 290 | // Also, the buffer won't get double released since this method entrance is protected by the `completed` 291 | // field. 292 | nextChunk.release(); 293 | } 294 | } 295 | 296 | return completed; 297 | } 298 | 299 | @Override 300 | public void close() throws Exception { 301 | // No-op. Calling of the BodyProducer.finish() is done via the channel future completion. 302 | } 303 | 304 | @Override 305 | public ByteBuf readChunk(ChannelHandlerContext ctx) throws Exception { 306 | return readChunk(ctx.alloc()); 307 | } 308 | 309 | @Override 310 | public ByteBuf readChunk(ByteBufAllocator allocator) throws Exception { 311 | if (isEndOfInput()) { 312 | // This shouldn't happen, but just to guard 313 | throw new IllegalStateException("No more data to produce from body producer"); 314 | } 315 | ByteBuf chunk = nextChunk; 316 | bytesProduced += chunk.readableBytes(); 317 | nextChunk = null; 318 | return chunk; 319 | } 320 | 321 | @Override 322 | public long length() { 323 | return length; 324 | } 325 | 326 | @Override 327 | public long progress() { 328 | return length >= 0 ? bytesProduced : 0; 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/ChannelChunkResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | 20 | import io.cdap.http.ChunkResponder; 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.buffer.Unpooled; 23 | import io.netty.channel.Channel; 24 | import io.netty.handler.codec.http.DefaultHttpContent; 25 | import io.netty.handler.codec.http.LastHttpContent; 26 | 27 | import java.io.IOException; 28 | import java.nio.ByteBuffer; 29 | import java.util.concurrent.atomic.AtomicBoolean; 30 | import java.util.concurrent.atomic.AtomicLong; 31 | 32 | /** 33 | * A {@link ChunkResponder} that writes chunks to a {@link Channel}. 34 | */ 35 | final class ChannelChunkResponder implements ChunkResponder { 36 | 37 | private final Channel channel; 38 | private final AtomicBoolean closed; 39 | private final AtomicLong bufferedSize; 40 | private final int chunkMemoryLimit; 41 | 42 | ChannelChunkResponder(Channel channel, int chunkMemoryLimit) { 43 | this.channel = channel; 44 | this.closed = new AtomicBoolean(); 45 | this.bufferedSize = new AtomicLong(); 46 | this.chunkMemoryLimit = chunkMemoryLimit; 47 | } 48 | 49 | @Override 50 | public void sendChunk(ByteBuffer chunk) throws IOException { 51 | sendChunk(Unpooled.wrappedBuffer(chunk)); 52 | } 53 | 54 | @Override 55 | public void sendChunk(ByteBuf chunk) throws IOException { 56 | if (closed.get()) { 57 | throw new IOException("ChunkResponder already closed."); 58 | } 59 | if (!channel.isActive()) { 60 | throw new IOException("Connection already closed."); 61 | } 62 | int chunkSize = chunk.readableBytes(); 63 | channel.write(new DefaultHttpContent(chunk)); 64 | tryFlush(chunkSize); 65 | } 66 | 67 | @Override 68 | public void flush() { 69 | // Use the limit as the size to force a flush 70 | tryFlush(chunkMemoryLimit); 71 | } 72 | 73 | @Override 74 | public void close() { 75 | if (!closed.compareAndSet(false, true)) { 76 | return; 77 | } 78 | channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 79 | } 80 | 81 | private void tryFlush(int size) { 82 | long newSize = bufferedSize.addAndGet(size); 83 | if (newSize >= chunkMemoryLimit) { 84 | channel.flush(); 85 | // Subtract what were flushed. 86 | // This is correct for single thread. 87 | // For concurrent calls, this provides a lower bound, 88 | // meaning more data might get flushed then being subtracted. 89 | // This make sure we won't go over the memory limit, but might flush more often than needed. 90 | bufferedSize.addAndGet(-1 * newSize); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/Converter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import javax.annotation.Nullable; 20 | 21 | /** 22 | * Converts an object of one type to another. 23 | * 24 | * @param the source object type 25 | * @param the target object type 26 | */ 27 | public interface Converter { 28 | 29 | /** 30 | * Converts an object. 31 | * 32 | * @throws Exception if the conversion failed 33 | */ 34 | @Nullable 35 | T convert(F from) throws Exception; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/ForwardingEventExecutorGroup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.netty.util.concurrent.EventExecutor; 20 | import io.netty.util.concurrent.EventExecutorGroup; 21 | import io.netty.util.concurrent.Future; 22 | import io.netty.util.concurrent.ScheduledFuture; 23 | 24 | import java.util.Collection; 25 | import java.util.Iterator; 26 | import java.util.List; 27 | import java.util.concurrent.Callable; 28 | import java.util.concurrent.ExecutionException; 29 | import java.util.concurrent.TimeUnit; 30 | import java.util.concurrent.TimeoutException; 31 | 32 | /** 33 | * A {@link EventExecutorGroup} that forwards all methods to another {@link EventExecutorGroup}. 34 | */ 35 | public class ForwardingEventExecutorGroup implements EventExecutorGroup { 36 | 37 | private final EventExecutorGroup delegate; 38 | 39 | public ForwardingEventExecutorGroup(EventExecutorGroup delegate) { 40 | this.delegate = delegate; 41 | } 42 | 43 | @Override 44 | public boolean isShuttingDown() { 45 | return delegate.isShuttingDown(); 46 | } 47 | 48 | @Override 49 | public Future shutdownGracefully() { 50 | return delegate.shutdownGracefully(); 51 | } 52 | 53 | @Override 54 | public Future shutdownGracefully(long l, long l1, TimeUnit timeUnit) { 55 | return delegate.shutdownGracefully(l, l1, timeUnit); 56 | } 57 | 58 | @Override 59 | public Future terminationFuture() { 60 | return delegate.terminationFuture(); 61 | } 62 | 63 | @Override 64 | @Deprecated 65 | public void shutdown() { 66 | delegate.shutdown(); 67 | } 68 | 69 | @Override 70 | @Deprecated 71 | public List shutdownNow() { 72 | return delegate.shutdownNow(); 73 | } 74 | 75 | @Override 76 | public EventExecutor next() { 77 | return delegate.next(); 78 | } 79 | 80 | @Override 81 | public Iterator iterator() { 82 | return delegate.iterator(); 83 | } 84 | 85 | @Override 86 | public Future submit(Runnable runnable) { 87 | return delegate.submit(runnable); 88 | } 89 | 90 | @Override 91 | public Future submit(Runnable runnable, T t) { 92 | return delegate.submit(runnable, t); 93 | } 94 | 95 | @Override 96 | public Future submit(Callable callable) { 97 | return delegate.submit(callable); 98 | } 99 | 100 | @Override 101 | public ScheduledFuture schedule(Runnable runnable, long l, TimeUnit timeUnit) { 102 | return delegate.schedule(runnable, l, timeUnit); 103 | } 104 | 105 | @Override 106 | public ScheduledFuture schedule(Callable callable, long l, TimeUnit timeUnit) { 107 | return delegate.schedule(callable, l, timeUnit); 108 | } 109 | 110 | @Override 111 | public ScheduledFuture scheduleAtFixedRate(Runnable runnable, long l, long l1, TimeUnit timeUnit) { 112 | return delegate.scheduleAtFixedRate(runnable, l, l1, timeUnit); 113 | } 114 | 115 | @Override 116 | public ScheduledFuture scheduleWithFixedDelay(Runnable runnable, long l, long l1, TimeUnit timeUnit) { 117 | return delegate.scheduleWithFixedDelay(runnable, l, l1, timeUnit); 118 | } 119 | 120 | @Override 121 | public boolean isShutdown() { 122 | return delegate.isShutdown(); 123 | } 124 | 125 | @Override 126 | public boolean isTerminated() { 127 | return delegate.isTerminated(); 128 | } 129 | 130 | @Override 131 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 132 | return delegate.awaitTermination(timeout, unit); 133 | } 134 | 135 | @Override 136 | public List> invokeAll(Collection> tasks) 137 | throws InterruptedException { 138 | return delegate.invokeAll(tasks); 139 | } 140 | 141 | @Override 142 | public List> invokeAll(Collection> tasks, 143 | long timeout, TimeUnit unit) throws InterruptedException { 144 | return delegate.invokeAll(tasks, timeout, unit); 145 | } 146 | 147 | @Override 148 | public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { 149 | return delegate.invokeAny(tasks); 150 | } 151 | 152 | @Override 153 | public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) 154 | throws InterruptedException, ExecutionException, TimeoutException { 155 | return delegate.invokeAny(tasks, timeout, unit); 156 | } 157 | 158 | @Override 159 | public void execute(Runnable command) { 160 | delegate.execute(command); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/ForwardingOrderedEventExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.netty.util.concurrent.EventExecutor; 20 | import io.netty.util.concurrent.EventExecutorGroup; 21 | import io.netty.util.concurrent.Future; 22 | import io.netty.util.concurrent.OrderedEventExecutor; 23 | import io.netty.util.concurrent.ProgressivePromise; 24 | import io.netty.util.concurrent.Promise; 25 | import io.netty.util.concurrent.ScheduledFuture; 26 | 27 | import java.util.Collection; 28 | import java.util.Iterator; 29 | import java.util.List; 30 | import java.util.concurrent.Callable; 31 | import java.util.concurrent.ExecutionException; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.concurrent.TimeoutException; 34 | 35 | /** 36 | * An {@link OrderedEventExecutor} that forwards all methods to another {@link OrderedEventExecutor}. 37 | */ 38 | public class ForwardingOrderedEventExecutor implements OrderedEventExecutor { 39 | 40 | private final OrderedEventExecutor delegate; 41 | 42 | public ForwardingOrderedEventExecutor(OrderedEventExecutor delegate) { 43 | this.delegate = delegate; 44 | } 45 | 46 | @Override 47 | public EventExecutor next() { 48 | return delegate.next(); 49 | } 50 | 51 | @Override 52 | public EventExecutorGroup parent() { 53 | return delegate.parent(); 54 | } 55 | 56 | @Override 57 | public boolean inEventLoop() { 58 | return delegate.inEventLoop(); 59 | } 60 | 61 | @Override 62 | public boolean inEventLoop(Thread thread) { 63 | return delegate.inEventLoop(thread); 64 | } 65 | 66 | @Override 67 | public Promise newPromise() { 68 | return delegate.newPromise(); 69 | } 70 | 71 | @Override 72 | public ProgressivePromise newProgressivePromise() { 73 | return delegate.newProgressivePromise(); 74 | } 75 | 76 | @Override 77 | public Future newSucceededFuture(V v) { 78 | return delegate.newSucceededFuture(v); 79 | } 80 | 81 | @Override 82 | public Future newFailedFuture(Throwable throwable) { 83 | return delegate.newFailedFuture(throwable); 84 | } 85 | 86 | @Override 87 | public boolean isShuttingDown() { 88 | return delegate.isShuttingDown(); 89 | } 90 | 91 | @Override 92 | public Future shutdownGracefully() { 93 | return delegate.shutdownGracefully(); 94 | } 95 | 96 | @Override 97 | public Future shutdownGracefully(long l, long l1, TimeUnit timeUnit) { 98 | return delegate.shutdownGracefully(l, l1, timeUnit); 99 | } 100 | 101 | @Override 102 | public Future terminationFuture() { 103 | return delegate.terminationFuture(); 104 | } 105 | 106 | @Override 107 | @Deprecated 108 | public void shutdown() { 109 | delegate.shutdown(); 110 | } 111 | 112 | @Override 113 | @Deprecated 114 | public List shutdownNow() { 115 | return delegate.shutdownNow(); 116 | } 117 | 118 | @Override 119 | public Iterator iterator() { 120 | return delegate.iterator(); 121 | } 122 | 123 | @Override 124 | public Future submit(Runnable runnable) { 125 | return delegate.submit(runnable); 126 | } 127 | 128 | @Override 129 | public Future submit(Runnable runnable, T t) { 130 | return delegate.submit(runnable, t); 131 | } 132 | 133 | @Override 134 | public Future submit(Callable callable) { 135 | return delegate.submit(callable); 136 | } 137 | 138 | @Override 139 | public ScheduledFuture schedule(Runnable runnable, long l, TimeUnit timeUnit) { 140 | return delegate.schedule(runnable, l, timeUnit); 141 | } 142 | 143 | @Override 144 | public ScheduledFuture schedule(Callable callable, long l, TimeUnit timeUnit) { 145 | return delegate.schedule(callable, l, timeUnit); 146 | } 147 | 148 | @Override 149 | public ScheduledFuture scheduleAtFixedRate(Runnable runnable, long l, long l1, TimeUnit timeUnit) { 150 | return delegate.scheduleAtFixedRate(runnable, l, l1, timeUnit); 151 | } 152 | 153 | @Override 154 | public ScheduledFuture scheduleWithFixedDelay(Runnable runnable, long l, long l1, TimeUnit timeUnit) { 155 | return delegate.scheduleWithFixedDelay(runnable, l, l1, timeUnit); 156 | } 157 | 158 | @Override 159 | public boolean isShutdown() { 160 | return delegate.isShutdown(); 161 | } 162 | 163 | @Override 164 | public boolean isTerminated() { 165 | return delegate.isTerminated(); 166 | } 167 | 168 | @Override 169 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 170 | return delegate.awaitTermination(timeout, unit); 171 | } 172 | 173 | @Override 174 | public List> invokeAll(Collection> tasks) 175 | throws InterruptedException { 176 | return delegate.invokeAll(tasks); 177 | } 178 | 179 | @Override 180 | public List> invokeAll(Collection> tasks, 181 | long timeout, TimeUnit unit) throws InterruptedException { 182 | return delegate.invokeAll(tasks, timeout, unit); 183 | } 184 | 185 | @Override 186 | public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { 187 | return delegate.invokeAny(tasks); 188 | } 189 | 190 | @Override 191 | public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) 192 | throws InterruptedException, ExecutionException, TimeoutException { 193 | return delegate.invokeAny(tasks, timeout, unit); 194 | } 195 | 196 | @Override 197 | public void execute(Runnable command) { 198 | delegate.execute(command); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/HandlerException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 21 | import io.netty.handler.codec.http.FullHttpResponse; 22 | import io.netty.handler.codec.http.HttpResponse; 23 | import io.netty.handler.codec.http.HttpResponseStatus; 24 | import io.netty.handler.codec.http.HttpUtil; 25 | import io.netty.handler.codec.http.HttpVersion; 26 | 27 | import java.nio.charset.StandardCharsets; 28 | 29 | /** 30 | *Creating Http Response for Exception messages. 31 | */ 32 | public class HandlerException extends Exception { 33 | 34 | private final HttpResponseStatus failureStatus; 35 | private final String message; 36 | 37 | public HandlerException(HttpResponseStatus failureStatus, String message) { 38 | super(message); 39 | this.failureStatus = failureStatus; 40 | this.message = message; 41 | } 42 | 43 | HandlerException(HttpResponseStatus failureStatus, String message, Throwable cause) { 44 | super(message, cause); 45 | this.failureStatus = failureStatus; 46 | this.message = message; 47 | } 48 | 49 | public HttpResponse createFailureResponse() { 50 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, failureStatus, 51 | Unpooled.copiedBuffer(message, StandardCharsets.UTF_8)); 52 | HttpUtil.setContentLength(response, response.content().readableBytes()); 53 | return response; 54 | } 55 | 56 | public String getMessage() { 57 | return message; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/HandlerInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.HttpHandler; 20 | 21 | /** 22 | * Contains information about {@link HttpHandler} method. 23 | */ 24 | public class HandlerInfo { 25 | private final String handlerName; 26 | private final String methodName; 27 | 28 | public HandlerInfo(String handlerName, String methodName) { 29 | this.handlerName = handlerName; 30 | this.methodName = methodName; 31 | } 32 | 33 | public String getHandlerName() { 34 | return handlerName; 35 | } 36 | 37 | public String getMethodName() { 38 | return methodName; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/HttpDispatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.netty.channel.ChannelHandlerContext; 20 | import io.netty.channel.ChannelInboundHandlerAdapter; 21 | import io.netty.handler.codec.http.HttpContent; 22 | import io.netty.handler.codec.http.HttpRequest; 23 | import io.netty.util.AttributeKey; 24 | import io.netty.util.ReferenceCountUtil; 25 | 26 | /** 27 | * HttpDispatcher that invokes the appropriate http-handler method. The handler and the arguments are read 28 | * from the {@code RequestRouter} context. 29 | */ 30 | 31 | public class HttpDispatcher extends ChannelInboundHandlerAdapter { 32 | 33 | public static final AttributeKey METHOD_INFO_KEY = AttributeKey.newInstance("methodInfo"); 34 | 35 | @Override 36 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 37 | HttpMethodInfo methodInfo = ctx.channel().attr(METHOD_INFO_KEY).get(); 38 | 39 | try { 40 | if (methodInfo == null) { 41 | // This shouldn't happen 42 | throw new IllegalStateException("No handler dispatch information available"); 43 | } 44 | if (msg instanceof HttpRequest) { 45 | methodInfo.invoke((HttpRequest) msg); 46 | } else if (msg instanceof HttpContent) { 47 | methodInfo.chunk((HttpContent) msg); 48 | } else { 49 | // Since the release will be called in finally, we retain the count before delegating to downstream 50 | ReferenceCountUtil.retain(msg); 51 | ctx.fireChannelRead(msg); 52 | } 53 | } finally { 54 | ReferenceCountUtil.release(msg); 55 | } 56 | } 57 | 58 | @Override 59 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 60 | HttpMethodInfo methodInfo = ctx.channel().attr(METHOD_INFO_KEY).getAndSet(null); 61 | if (methodInfo != null) { 62 | methodInfo.disconnected(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/HttpMethodInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.BodyConsumer; 20 | import io.cdap.http.ExceptionHandler; 21 | import io.cdap.http.HttpHandler; 22 | import io.cdap.http.HttpResponder; 23 | import io.netty.buffer.ByteBuf; 24 | import io.netty.handler.codec.http.DefaultHttpHeaders; 25 | import io.netty.handler.codec.http.HttpContent; 26 | import io.netty.handler.codec.http.HttpHeaderNames; 27 | import io.netty.handler.codec.http.HttpHeaderValues; 28 | import io.netty.handler.codec.http.HttpRequest; 29 | import io.netty.handler.codec.http.HttpResponseStatus; 30 | import io.netty.handler.codec.http.LastHttpContent; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | import java.lang.reflect.InvocationTargetException; 35 | import java.lang.reflect.Method; 36 | import java.nio.channels.ClosedChannelException; 37 | 38 | /** 39 | * HttpMethodInfo is a helper class having state information about the http handler method to be invoked, the handler 40 | * and arguments required for invocation by the Dispatcher. RequestRouter populates this class and stores in its 41 | * context as attachment. 42 | */ 43 | class HttpMethodInfo { 44 | 45 | private static final Logger LOG = LoggerFactory.getLogger(HttpMethodInfo.class); 46 | 47 | private final Method method; 48 | private final HttpHandler handler; 49 | private final HttpResponder responder; 50 | private final Object[] args; 51 | private final boolean isStreaming; 52 | private final ExceptionHandler exceptionHandler; 53 | private final boolean isSecured; 54 | private final String[] requiredRoles; 55 | 56 | private HttpRequest request; 57 | private BodyConsumer bodyConsumer; 58 | 59 | HttpMethodInfo(Method method, HttpHandler handler, 60 | HttpResponder responder, Object[] args, ExceptionHandler exceptionHandler, 61 | boolean isSecured, String[] requiredRoles) { 62 | this.method = method; 63 | this.handler = handler; 64 | this.isStreaming = BodyConsumer.class.isAssignableFrom(method.getReturnType()); 65 | this.responder = responder; 66 | this.exceptionHandler = exceptionHandler; 67 | this.isSecured = isSecured; 68 | this.requiredRoles = requiredRoles; 69 | 70 | // The actual arguments list to invoke handler method 71 | this.args = new Object[args.length + 2]; 72 | // The actual HttpRequest object will be provided to the invoke method, since 73 | // the HttpObjectAggregator may create a different instance 74 | this.args[0] = null; 75 | this.args[1] = responder; 76 | System.arraycopy(args, 0, this.args, 2, args.length); 77 | } 78 | 79 | /** 80 | * Calls the httpHandler method. 81 | */ 82 | void invoke(HttpRequest request) throws Exception { 83 | bodyConsumer = null; 84 | Object invokeResult; 85 | try { 86 | args[0] = this.request = request; 87 | invokeResult = method.invoke(handler, args); 88 | } catch (InvocationTargetException e) { 89 | exceptionHandler.handle(e.getTargetException(), request, responder); 90 | return; 91 | } catch (Throwable t) { 92 | exceptionHandler.handle(t, request, responder); 93 | return; 94 | } 95 | 96 | if (isStreaming) { 97 | // Casting guarantee to be succeeded. 98 | bodyConsumer = (BodyConsumer) invokeResult; 99 | } 100 | } 101 | 102 | void chunk(HttpContent chunk) throws Exception { 103 | if (bodyConsumer == null) { 104 | // If the handler method doesn't want to handle chunk request, the bodyConsumer will be null. 105 | // It applies to case when the handler method inspects the request and decides to decline it. 106 | // Usually the handler also closes the connection after declining the request. 107 | // However, depending on the closing time and the request, 108 | // there may be some chunk of data already sent by the client. 109 | return; 110 | } 111 | 112 | if (chunk.content().isReadable()) { 113 | bodyConsumerChunk(chunk.content()); 114 | } 115 | 116 | if (chunk instanceof LastHttpContent) { 117 | bodyConsumerFinish(); 118 | } 119 | } 120 | 121 | void disconnected() { 122 | if (bodyConsumer != null) { 123 | bodyConsumerError(new ClosedChannelException()); 124 | } 125 | } 126 | 127 | /** 128 | * Calls the {@link BodyConsumer#chunk(ByteBuf, HttpResponder)} method. If the chunk method call 129 | * throws exception, the {@link BodyConsumer#handleError(Throwable)} will be called and this method will 130 | * throw {@link HandlerException}. 131 | */ 132 | private void bodyConsumerChunk(ByteBuf buffer) throws HandlerException { 133 | try { 134 | bodyConsumer.chunk(buffer, responder); 135 | } catch (Throwable t) { 136 | try { 137 | bodyConsumerError(t); 138 | } catch (Throwable t2) { 139 | exceptionHandler.handle(t2, request, responder); 140 | // log original throwable since we'll lose it otherwise 141 | LOG.debug("Handled exception thrown from handleError. original exception from chunk call was:", t); 142 | return; 143 | } 144 | exceptionHandler.handle(t, request, responder); 145 | } 146 | } 147 | 148 | /** 149 | * Calls {@link BodyConsumer#finished(HttpResponder)} method. The current bodyConsumer will be set to {@code null} 150 | * after the call. 151 | */ 152 | private void bodyConsumerFinish() { 153 | BodyConsumer consumer = bodyConsumer; 154 | bodyConsumer = null; 155 | try { 156 | consumer.finished(responder); 157 | } catch (Throwable t) { 158 | exceptionHandler.handle(t, request, responder); 159 | } 160 | } 161 | 162 | /** 163 | * Calls {@link BodyConsumer#handleError(Throwable)}. The current 164 | * bodyConsumer will be set to {@code null} after the call. 165 | */ 166 | private void bodyConsumerError(Throwable cause) { 167 | BodyConsumer consumer = bodyConsumer; 168 | bodyConsumer = null; 169 | consumer.handleError(cause); 170 | } 171 | 172 | /** 173 | * Sends the error to responder. 174 | */ 175 | void sendError(HttpResponseStatus status, Throwable ex) { 176 | String msg; 177 | 178 | if (ex instanceof InvocationTargetException) { 179 | msg = String.format("Exception Encountered while processing request : %s", ex.getCause().getMessage()); 180 | } else { 181 | msg = String.format("Exception Encountered while processing request: %s", ex.getMessage()); 182 | } 183 | 184 | // Send the status and message, followed by closing of the connection. 185 | responder.sendString(status, msg, new DefaultHttpHeaders().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)); 186 | if (bodyConsumer != null) { 187 | bodyConsumerError(ex); 188 | } 189 | } 190 | 191 | /** 192 | * Returns true if the handler method's return type is BodyConsumer. 193 | */ 194 | boolean isStreaming() { 195 | return isStreaming; 196 | } 197 | 198 | /** 199 | * Returns true if the method was annotated by {@link Secured}. 200 | */ 201 | public boolean isSecured() { 202 | return isSecured; 203 | } 204 | 205 | /** 206 | * Returns the roles that required to execute this method as specified by {@link RequiredRoles} 207 | * or null if none were specified. 208 | */ 209 | public String[] getRequiredRoles() { 210 | return requiredRoles; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/HttpResourceModel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.ExceptionHandler; 20 | import io.cdap.http.HttpHandler; 21 | import io.cdap.http.HttpResponder; 22 | import io.netty.handler.codec.http.HttpMethod; 23 | import io.netty.handler.codec.http.HttpRequest; 24 | import io.netty.handler.codec.http.HttpResponseStatus; 25 | import io.netty.handler.codec.http.QueryStringDecoder; 26 | 27 | import java.lang.annotation.Annotation; 28 | import java.lang.reflect.Method; 29 | import java.lang.reflect.Type; 30 | import java.util.ArrayList; 31 | import java.util.Arrays; 32 | import java.util.Collections; 33 | import java.util.HashSet; 34 | import java.util.IdentityHashMap; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.Set; 38 | import javax.annotation.Nullable; 39 | import javax.ws.rs.DefaultValue; 40 | import javax.ws.rs.HeaderParam; 41 | import javax.ws.rs.PathParam; 42 | import javax.ws.rs.QueryParam; 43 | 44 | /** 45 | * HttpResourceModel contains information needed to handle Http call for a given path. Used as a destination in 46 | * {@code PatternPathRouterWithGroups} to route URI paths to right Http end points. 47 | */ 48 | public final class HttpResourceModel { 49 | 50 | private static final Set> SUPPORTED_PARAM_ANNOTATIONS = 51 | Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PathParam.class, QueryParam.class, HeaderParam.class))); 52 | 53 | private final Set httpMethods; 54 | private final String path; 55 | private final Method method; 56 | private final HttpHandler handler; 57 | private final List, ParameterInfo>> paramsInfo; 58 | private final ExceptionHandler exceptionHandler; 59 | private final boolean isSecured; 60 | private final String[] requiredRoles; 61 | 62 | /** 63 | * Construct a resource model with HttpMethod, method that handles httprequest, Object that contains the method. 64 | * 65 | * @param httpMethods Set of http methods that is handled by the resource. 66 | * @param path path associated with this model. 67 | * @param method handler that handles the http request. 68 | * @param handler instance {@code HttpHandler}. 69 | * @param exceptionHandler the ExceptionHandler. 70 | * @param isSecured whether the resource is secured. 71 | * @param requiredRoles the roles required for this resource. 72 | */ 73 | public HttpResourceModel(Set httpMethods, String path, Method method, HttpHandler handler, 74 | ExceptionHandler exceptionHandler, boolean isSecured, String[] requiredRoles) { 75 | this.httpMethods = httpMethods; 76 | this.path = path; 77 | this.method = method; 78 | this.handler = handler; 79 | this.paramsInfo = createParametersInfos(method); 80 | this.exceptionHandler = exceptionHandler; 81 | this.isSecured = isSecured; 82 | this.requiredRoles = requiredRoles; 83 | } 84 | 85 | /** 86 | * @return httpMethods. 87 | */ 88 | public Set getHttpMethod() { 89 | return httpMethods; 90 | } 91 | 92 | /** 93 | * @return path associated with this model. 94 | */ 95 | public String getPath() { 96 | return path; 97 | } 98 | 99 | /** 100 | * @return handler method that handles an http end-point. 101 | */ 102 | public Method getMethod() { 103 | return method; 104 | } 105 | 106 | /** 107 | * @return instance of {@code HttpHandler}. 108 | */ 109 | public HttpHandler getHttpHandler() { 110 | return handler; 111 | } 112 | 113 | /** 114 | * Handle http Request. 115 | * 116 | * @param request HttpRequest to be handled. 117 | * @param responder HttpResponder to write the response. 118 | * @param groupValues Values needed for the invocation. 119 | */ 120 | @SuppressWarnings("unchecked") 121 | public HttpMethodInfo handle(HttpRequest request, 122 | HttpResponder responder, Map groupValues) throws Exception { 123 | //TODO: Refactor group values. 124 | try { 125 | if (httpMethods.contains(request.method())) { 126 | //Setup args for reflection call 127 | Object [] args = new Object[paramsInfo.size()]; 128 | 129 | int idx = 0; 130 | for (Map, ParameterInfo> info : paramsInfo) { 131 | if (info.containsKey(PathParam.class)) { 132 | args[idx] = getPathParamValue(info, groupValues); 133 | } 134 | if (info.containsKey(QueryParam.class)) { 135 | args[idx] = getQueryParamValue(info, request.uri()); 136 | } 137 | if (info.containsKey(HeaderParam.class)) { 138 | args[idx] = getHeaderParamValue(info, request); 139 | } 140 | idx++; 141 | } 142 | 143 | return new HttpMethodInfo(method, handler, responder, args, exceptionHandler, isSecured, requiredRoles); 144 | } else { 145 | //Found a matching resource but could not find the right HttpMethod so return 405 146 | throw new HandlerException(HttpResponseStatus.METHOD_NOT_ALLOWED, String.format 147 | ("Problem accessing: %s. Reason: Method Not Allowed", request.uri())); 148 | } 149 | } catch (Throwable e) { 150 | throw new HandlerException(HttpResponseStatus.INTERNAL_SERVER_ERROR, 151 | String.format("Error in executing request: %s %s", request.method(), 152 | request.uri()), e); 153 | } 154 | } 155 | 156 | @Override 157 | public String toString() { 158 | return "HttpResourceModel{" + 159 | "httpMethods=" + httpMethods + 160 | ", path='" + path + '\'' + 161 | ", method=" + method + 162 | ", handler=" + handler + 163 | '}'; 164 | } 165 | 166 | @SuppressWarnings("unchecked") 167 | private Object getPathParamValue(Map, ParameterInfo> annotations, 168 | Map groupValues) throws Exception { 169 | ParameterInfo info = (ParameterInfo) annotations.get(PathParam.class); 170 | PathParam pathParam = info.getAnnotation(); 171 | String value = groupValues.get(pathParam.value()); 172 | if (value == null) { 173 | throw new IllegalArgumentException("Could not resolve value for path parameter " + pathParam.value()); 174 | } 175 | return info.convert(value); 176 | } 177 | 178 | @SuppressWarnings("unchecked") 179 | private Object getQueryParamValue(Map, ParameterInfo> annotations, 180 | String uri) throws Exception { 181 | ParameterInfo> info = (ParameterInfo>) annotations.get(QueryParam.class); 182 | QueryParam queryParam = info.getAnnotation(); 183 | List values = new QueryStringDecoder(uri).parameters().get(queryParam.value()); 184 | 185 | return (values == null) ? info.convert(defaultValue(annotations)) : info.convert(values); 186 | } 187 | 188 | @SuppressWarnings("unchecked") 189 | private Object getHeaderParamValue(Map, ParameterInfo> annotations, 190 | HttpRequest request) throws Exception { 191 | ParameterInfo> info = (ParameterInfo>) annotations.get(HeaderParam.class); 192 | HeaderParam headerParam = info.getAnnotation(); 193 | String headerName = headerParam.value(); 194 | boolean hasHeader = request.headers().contains(headerName); 195 | return hasHeader ? info.convert(request.headers().getAll(headerName)) : info.convert(defaultValue(annotations)); 196 | } 197 | 198 | /** 199 | * Returns a List of String created based on the {@link DefaultValue} if it is presented in the annotations Map. 200 | * 201 | * @return a List of String or an empty List if {@link DefaultValue} is not presented 202 | */ 203 | private List defaultValue(Map, ParameterInfo> annotations) { 204 | ParameterInfo defaultInfo = annotations.get(DefaultValue.class); 205 | if (defaultInfo == null) { 206 | return Collections.emptyList(); 207 | } 208 | 209 | DefaultValue defaultValue = defaultInfo.getAnnotation(); 210 | return Collections.singletonList(defaultValue.value()); 211 | } 212 | 213 | /** 214 | * Gathers all parameters' annotations for the given method, starting from the third parameter. 215 | */ 216 | private List, ParameterInfo>> createParametersInfos(Method method) { 217 | if (method.getParameterTypes().length <= 2) { 218 | return Collections.emptyList(); 219 | } 220 | 221 | List, ParameterInfo>> result = new ArrayList<>(); 222 | Type[] parameterTypes = method.getGenericParameterTypes(); 223 | Annotation[][] parameterAnnotations = method.getParameterAnnotations(); 224 | 225 | for (int i = 2; i < parameterAnnotations.length; i++) { 226 | Annotation[] annotations = parameterAnnotations[i]; 227 | Map, ParameterInfo> paramAnnotations = new IdentityHashMap<>(); 228 | 229 | for (Annotation annotation : annotations) { 230 | Class annotationType = annotation.annotationType(); 231 | ParameterInfo parameterInfo; 232 | 233 | if (PathParam.class.isAssignableFrom(annotationType)) { 234 | parameterInfo = ParameterInfo.create(annotation, 235 | ParamConvertUtils.createPathParamConverter(parameterTypes[i])); 236 | } else if (QueryParam.class.isAssignableFrom(annotationType)) { 237 | parameterInfo = ParameterInfo.create(annotation, 238 | ParamConvertUtils.createQueryParamConverter(parameterTypes[i])); 239 | } else if (HeaderParam.class.isAssignableFrom(annotationType)) { 240 | parameterInfo = ParameterInfo.create(annotation, 241 | ParamConvertUtils.createHeaderParamConverter(parameterTypes[i])); 242 | } else { 243 | parameterInfo = ParameterInfo.create(annotation, null); 244 | } 245 | 246 | paramAnnotations.put(annotationType, parameterInfo); 247 | } 248 | 249 | // Must have either @PathParam, @QueryParam or @HeaderParam, but not two or more. 250 | int presence = 0; 251 | for (Class annotationClass : paramAnnotations.keySet()) { 252 | if (SUPPORTED_PARAM_ANNOTATIONS.contains(annotationClass)) { 253 | presence++; 254 | } 255 | } 256 | if (presence != 1) { 257 | throw new IllegalArgumentException( 258 | String.format("Must have exactly one annotation from %s for parameter %d in method %s", 259 | SUPPORTED_PARAM_ANNOTATIONS, i, method)); 260 | } 261 | 262 | result.add(Collections.unmodifiableMap(paramAnnotations)); 263 | } 264 | 265 | return Collections.unmodifiableList(result); 266 | } 267 | 268 | /** 269 | * A container class to hold information about a handler method parameters. 270 | */ 271 | private static final class ParameterInfo { 272 | private final Annotation annotation; 273 | private final Converter converter; 274 | 275 | static ParameterInfo create(Annotation annotation, @Nullable Converter converter) { 276 | return new ParameterInfo<>(annotation, converter); 277 | } 278 | 279 | private ParameterInfo(Annotation annotation, @Nullable Converter converter) { 280 | this.annotation = annotation; 281 | this.converter = converter; 282 | } 283 | 284 | @SuppressWarnings("unchecked") 285 | V getAnnotation() { 286 | return (V) annotation; 287 | } 288 | 289 | Object convert(T input) throws Exception { 290 | return (converter == null) ? null : converter.convert(input); 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/ImmutablePair.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import java.util.Objects; 20 | 21 | /** 22 | * An {@link ImmutablePair} consists of two elements within. The elements once set 23 | * in the ImmutablePair cannot be modified. The class itself is final, so that it 24 | * cannot be subclassed. This is general norm for creating Immutable classes. 25 | * Please note that the {@link ImmutablePair} cannot be modified once set, but the 26 | * objects within them can be, so in general it means that if there are mutable objects 27 | * within the pair then the pair itself is effectively mutable. 28 | * 29 | *
 30 |  *   ImmutablePair tupleStreamPair= new
 31 |  *    ImmutablePair (tuple, identifier);
 32 |  *   ...
 33 |  *   ...
 34 |  *   Tuple t = tupleStreamPair.getFirst();
 35 |  *   TupleInputStreamIdentifier identifier = tupleStreamPair.getSecond();
 36 |  *   ...
 37 |  * 
38 | * 39 | * @param type A 40 | * @param type B 41 | */ 42 | final class ImmutablePair { 43 | private final A first; 44 | private final B second; 45 | 46 | public static ImmutablePair of(A first, B second) { 47 | return new ImmutablePair<>(first, second); 48 | } 49 | 50 | /** 51 | * Constructs a Immutable Pair. 52 | * @param first object in pair 53 | * @param second object in pair 54 | */ 55 | private ImmutablePair(A first, B second) { 56 | this.first = first; 57 | this.second = second; 58 | } 59 | 60 | /** 61 | * Returns first object from pair. 62 | * @return first object from pair. 63 | */ 64 | public A getFirst() { 65 | return first; 66 | } 67 | 68 | /** 69 | * Return second object from pair. 70 | * @return second object from pair. 71 | */ 72 | public B getSecond() { 73 | return second; 74 | } 75 | 76 | /** 77 | * Returns a string representation of {@link ImmutablePair} object. 78 | * @return string representation of this object. 79 | */ 80 | @Override 81 | public String toString() { 82 | return "ImmutablePair{" + 83 | "first=" + first + 84 | ", second=" + second + 85 | '}'; 86 | } 87 | 88 | /** 89 | * Returns a hash code value for this object. 90 | * @return hash code value of this object. 91 | */ 92 | @Override 93 | public int hashCode() { 94 | return Objects.hash(first, second); 95 | } 96 | 97 | /** 98 | * Returns whether some other object "is equal" to this object. 99 | * @param o reference object with which to compare 100 | * @return true if object is the same as the obj argument; false otherwise. 101 | */ 102 | @Override 103 | public boolean equals(Object o) { 104 | if (o == null) { 105 | return false; 106 | } 107 | if (!(o instanceof ImmutablePair)) { 108 | return false; 109 | } 110 | ImmutablePair other = (ImmutablePair) o; 111 | return Objects.equals(first, other.first) && Objects.equals(second, other.second); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/InternalHttpResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.AbstractHttpResponder; 20 | import io.cdap.http.BodyProducer; 21 | import io.cdap.http.ChunkResponder; 22 | import io.netty.buffer.ByteBuf; 23 | import io.netty.buffer.ByteBufInputStream; 24 | import io.netty.buffer.Unpooled; 25 | import io.netty.handler.codec.http.HttpHeaders; 26 | import io.netty.handler.codec.http.HttpResponseStatus; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.io.File; 31 | import java.io.FileInputStream; 32 | import java.io.IOException; 33 | import java.io.InputStream; 34 | import java.nio.ByteBuffer; 35 | 36 | /** 37 | * InternalHttpResponder is used when a handler is being called internally by some other handler, and thus there 38 | * is no need to go through the network. It stores the status code and content in memory and returns them when asked. 39 | */ 40 | public class InternalHttpResponder extends AbstractHttpResponder { 41 | 42 | private static final Logger LOG = LoggerFactory.getLogger(InternalHttpResponder.class); 43 | 44 | private InternalHttpResponse response; 45 | 46 | @Override 47 | public ChunkResponder sendChunkStart(final HttpResponseStatus status, HttpHeaders headers) { 48 | return new ChunkResponder() { 49 | 50 | private ByteBuf contentChunks = Unpooled.EMPTY_BUFFER; 51 | private boolean closed; 52 | 53 | @Override 54 | public void sendChunk(ByteBuffer chunk) throws IOException { 55 | sendChunk(Unpooled.wrappedBuffer(chunk)); 56 | } 57 | 58 | @Override 59 | public synchronized void sendChunk(ByteBuf chunk) throws IOException { 60 | if (closed) { 61 | throw new IOException("ChunkResponder already closed."); 62 | } 63 | contentChunks = Unpooled.wrappedBuffer(contentChunks, chunk); 64 | } 65 | 66 | @Override 67 | public synchronized void close() throws IOException { 68 | if (closed) { 69 | return; 70 | } 71 | closed = true; 72 | response = new AbstractInternalResponse(status.code()) { 73 | @Override 74 | public InputStream openInputStream() throws IOException { 75 | return new ByteBufInputStream(contentChunks.duplicate()); 76 | } 77 | }; 78 | } 79 | }; 80 | } 81 | 82 | @Override 83 | public void sendContent(HttpResponseStatus status, final ByteBuf content, HttpHeaders headers) { 84 | response = new AbstractInternalResponse(status.code()) { 85 | @Override 86 | public InputStream openInputStream() throws IOException { 87 | return new ByteBufInputStream(content.duplicate()); 88 | } 89 | }; 90 | } 91 | 92 | @Override 93 | public void sendFile(final File file, HttpHeaders headers) { 94 | response = new AbstractInternalResponse(HttpResponseStatus.OK.code()) { 95 | @Override 96 | public InputStream openInputStream() throws IOException { 97 | return new FileInputStream(file); 98 | } 99 | }; 100 | } 101 | 102 | @Override 103 | public void sendContent(HttpResponseStatus status, BodyProducer bodyProducer, HttpHeaders headers) { 104 | // Buffer all contents produced by the body producer 105 | ByteBuf contentChunks = Unpooled.EMPTY_BUFFER; 106 | try { 107 | ByteBuf chunk = bodyProducer.nextChunk(); 108 | while (chunk.isReadable()) { 109 | contentChunks = Unpooled.wrappedBuffer(contentChunks, chunk); 110 | chunk = bodyProducer.nextChunk(); 111 | } 112 | 113 | bodyProducer.finished(); 114 | final ByteBuf finalContentChunks = contentChunks; 115 | response = new AbstractInternalResponse(status.code()) { 116 | @Override 117 | public InputStream openInputStream() throws IOException { 118 | return new ByteBufInputStream(finalContentChunks.duplicate()); 119 | } 120 | }; 121 | } catch (Throwable t) { 122 | try { 123 | bodyProducer.handleError(t); 124 | } catch (Throwable et) { 125 | LOG.warn("Exception raised from BodyProducer.handleError() for {}", bodyProducer, et); 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Returns the the last {@link InternalHttpResponse} created via one of the send methods. 132 | * 133 | * @throws IllegalStateException if there is no {@link InternalHttpResponse} available 134 | */ 135 | public InternalHttpResponse getResponse() { 136 | if (response == null) { 137 | throw new IllegalStateException("No InternalHttpResponse available"); 138 | } 139 | return response; 140 | } 141 | 142 | /** 143 | * Abstract implementation of {@link InternalHttpResponse}. 144 | */ 145 | private abstract static class AbstractInternalResponse implements InternalHttpResponse { 146 | private final int statusCode; 147 | 148 | AbstractInternalResponse(int statusCode) { 149 | this.statusCode = statusCode; 150 | } 151 | 152 | @Override 153 | public int getStatusCode() { 154 | return statusCode; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/InternalHttpResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | 22 | /** 23 | * Interface used to get the status code and content from calling another handler internally. 24 | */ 25 | public interface InternalHttpResponse { 26 | 27 | int getStatusCode(); 28 | 29 | /** 30 | * Opens an {@link InputStream} that contains the response content. The caller is responsible of closing the 31 | * returned stream. 32 | * 33 | * @return an {@link InputStream} for reading response content 34 | * @throws IOException if failed to open the stream 35 | */ 36 | InputStream openInputStream() throws IOException; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/PatternPathRouterWithGroups.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.regex.Matcher; 24 | import java.util.regex.Pattern; 25 | 26 | /** 27 | * Matches incoming un-matched paths to destinations. Designed to be used for routing URI paths to http resources. 28 | * Parameters within braces "{}" are treated as template parameter (a named wild-card pattern). 29 | * 30 | * @param represents the destination of the routes. 31 | */ 32 | public final class PatternPathRouterWithGroups { 33 | 34 | //GROUP_PATTERN is used for named wild card pattern in paths which is specified within braces. 35 | //Example: {id} 36 | public static final Pattern GROUP_PATTERN = Pattern.compile("\\{(.*?)\\}"); 37 | 38 | // non-greedy wild card match. 39 | private static final Pattern WILD_CARD_PATTERN = Pattern.compile("\\*\\*"); 40 | 41 | private final int maxPathParts; 42 | private final List> patternRouteList; 43 | 44 | public static PatternPathRouterWithGroups create(int maxPathParts) { 45 | return new PatternPathRouterWithGroups<>(maxPathParts); 46 | } 47 | 48 | /** 49 | * Initialize PatternPathRouterWithGroups. 50 | */ 51 | public PatternPathRouterWithGroups(int maxPathParts) { 52 | this.maxPathParts = maxPathParts; 53 | this.patternRouteList = new ArrayList<>(); 54 | } 55 | 56 | /** 57 | * Add a source and destination. 58 | * 59 | * @param source Source path to be routed. Routed path can have named wild-card pattern with braces "{}". 60 | * @param destination Destination of the path. 61 | */ 62 | public void add(final String source, final T destination) { 63 | 64 | // replace multiple slashes with a single slash. 65 | String path = source.replaceAll("/+", "/"); 66 | 67 | path = (path.endsWith("/") && path.length() > 1) 68 | ? path.substring(0, path.length() - 1) : path; 69 | 70 | 71 | String[] parts = path.split("/", maxPathParts + 2); 72 | if (parts.length - 1 > maxPathParts) { 73 | throw new IllegalArgumentException(String.format("Number of parts of path %s exceeds allowed limit %s", 74 | source, maxPathParts)); 75 | } 76 | StringBuilder sb = new StringBuilder(); 77 | List groupNames = new ArrayList<>(); 78 | 79 | for (String part : parts) { 80 | Matcher groupMatcher = GROUP_PATTERN.matcher(part); 81 | if (groupMatcher.matches()) { 82 | groupNames.add(groupMatcher.group(1)); 83 | sb.append("([^/]+?)"); 84 | } else if (WILD_CARD_PATTERN.matcher(part).matches()) { 85 | sb.append(".*?"); 86 | } else { 87 | sb.append(part); 88 | } 89 | sb.append("/"); 90 | } 91 | 92 | //Ignore the last "/" 93 | sb.setLength(sb.length() - 1); 94 | 95 | Pattern pattern = Pattern.compile(sb.toString()); 96 | patternRouteList.add(ImmutablePair.of(pattern, new RouteDestinationWithGroups(destination, groupNames))); 97 | } 98 | 99 | /** 100 | * Get a list of destinations and the values matching templated parameter for the given path. 101 | * Returns an empty list when there are no destinations that are matched. 102 | * 103 | * @param path path to be routed. 104 | * @return List of Destinations matching the given route. 105 | */ 106 | public List> getDestinations(String path) { 107 | 108 | String cleanPath = (path.endsWith("/") && path.length() > 1) 109 | ? path.substring(0, path.length() - 1) : path; 110 | 111 | List> result = new ArrayList<>(); 112 | 113 | for (ImmutablePair patternRoute : patternRouteList) { 114 | Map groupNameValuesBuilder = new HashMap<>(); 115 | Matcher matcher = patternRoute.getFirst().matcher(cleanPath); 116 | if (matcher.matches()) { 117 | int matchIndex = 1; 118 | for (String name : patternRoute.getSecond().getGroupNames()) { 119 | String value = matcher.group(matchIndex); 120 | groupNameValuesBuilder.put(name, value); 121 | matchIndex++; 122 | } 123 | result.add(new RoutableDestination<>(patternRoute.getSecond().getDestination(), groupNameValuesBuilder)); 124 | } 125 | } 126 | return result; 127 | } 128 | 129 | /** 130 | * Helper class to store the groupNames and Destination. 131 | */ 132 | private final class RouteDestinationWithGroups { 133 | 134 | private final T destination; 135 | private final List groupNames; 136 | 137 | public RouteDestinationWithGroups (T destination, List groupNames) { 138 | this.destination = destination; 139 | this.groupNames = groupNames; 140 | } 141 | 142 | public T getDestination() { 143 | return destination; 144 | } 145 | 146 | public List getGroupNames() { 147 | return groupNames; 148 | } 149 | } 150 | 151 | /** 152 | * Represents a matched destination. 153 | * @param Type of destination. 154 | */ 155 | public static final class RoutableDestination { 156 | private final T destination; 157 | private final Map groupNameValues; 158 | 159 | /** 160 | * Construct the RouteableDestination with the given parameters. 161 | * 162 | * @param destination destination of the route. 163 | * @param groupNameValues parameters 164 | */ 165 | public RoutableDestination(T destination, Map groupNameValues) { 166 | this.destination = destination; 167 | this.groupNameValues = groupNameValues; 168 | } 169 | 170 | /** 171 | * @return destination of the route. 172 | */ 173 | public T getDestination() { 174 | return destination; 175 | } 176 | 177 | /** 178 | * @return Map of templated parameter and string representation group value matching the templated parameter as 179 | * the value. 180 | */ 181 | public Map getGroupNameValues() { 182 | return groupNameValues; 183 | } 184 | 185 | @Override 186 | public String toString() { 187 | return "RoutableDestination{" + 188 | "destination=" + destination + 189 | ", groupNameValues=" + groupNameValues + 190 | '}'; 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/RequestRouter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.HttpResponder; 20 | import io.netty.channel.ChannelFutureListener; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.channel.ChannelInboundHandlerAdapter; 23 | import io.netty.channel.ChannelPipeline; 24 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 25 | import io.netty.handler.codec.http.HttpObjectAggregator; 26 | import io.netty.handler.codec.http.HttpRequest; 27 | import io.netty.handler.codec.http.HttpResponse; 28 | import io.netty.handler.codec.http.HttpResponseStatus; 29 | import io.netty.handler.codec.http.HttpServerExpectContinueHandler; 30 | import io.netty.handler.codec.http.HttpUtil; 31 | import io.netty.handler.codec.http.HttpVersion; 32 | import io.netty.util.ReferenceCountUtil; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | import java.util.concurrent.atomic.AtomicBoolean; 37 | import javax.annotation.Nullable; 38 | 39 | /** 40 | * RequestRouter that uses {@code HttpMethodHandler} to determine the http-handler method Signature of http request. It 41 | * uses this signature to dynamically configure the Netty Pipeline. Http Handler methods with return-type BodyConsumer 42 | * will be streamed , while other methods will use HttpChunkAggregator 43 | */ 44 | 45 | public class RequestRouter extends ChannelInboundHandlerAdapter { 46 | 47 | private static final Logger LOG = LoggerFactory.getLogger(HttpDispatcher.class); 48 | 49 | private final int chunkMemoryLimit; 50 | private final HttpResourceHandler httpMethodHandler; 51 | private final AtomicBoolean exceptionRaised; 52 | private final boolean sslEnabled; 53 | 54 | private HttpMethodInfo methodInfo; 55 | 56 | public RequestRouter(HttpResourceHandler methodHandler, int chunkMemoryLimit, boolean sslEnabled) { 57 | this.httpMethodHandler = methodHandler; 58 | this.chunkMemoryLimit = chunkMemoryLimit; 59 | this.exceptionRaised = new AtomicBoolean(false); 60 | this.sslEnabled = sslEnabled; 61 | } 62 | 63 | /** 64 | * If the HttpRequest is valid and handled it will be sent upstream, if it cannot be invoked 65 | * the response will be written back immediately. 66 | */ 67 | @Override 68 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 69 | try { 70 | if (exceptionRaised.get()) { 71 | return; 72 | } 73 | 74 | if (!(msg instanceof HttpRequest)) { 75 | // If there is no methodInfo, it means the request was already rejected. 76 | // What we received here is just residue of the request, which can be ignored. 77 | if (methodInfo != null) { 78 | ReferenceCountUtil.retain(msg); 79 | ctx.fireChannelRead(msg); 80 | } 81 | return; 82 | } 83 | HttpRequest request = (HttpRequest) msg; 84 | BasicHttpResponder responder = new BasicHttpResponder(ctx.channel(), sslEnabled, chunkMemoryLimit); 85 | 86 | // Reset the methodInfo for the incoming request error handling 87 | methodInfo = null; 88 | methodInfo = prepareHandleMethod(request, responder, ctx); 89 | 90 | if (methodInfo != null) { 91 | ReferenceCountUtil.retain(msg); 92 | ctx.fireChannelRead(msg); 93 | } else { 94 | if (!responder.isResponded()) { 95 | // If not yet responded, just respond with a not found and close the connection 96 | HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); 97 | HttpUtil.setContentLength(response, 0); 98 | HttpUtil.setKeepAlive(response, false); 99 | ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 100 | } else { 101 | // If already responded, just close the connection 102 | ctx.channel().close(); 103 | } 104 | } 105 | } finally { 106 | ReferenceCountUtil.release(msg); 107 | } 108 | } 109 | 110 | @Override 111 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 112 | String exceptionMessage = "Exception caught in channel processing."; 113 | if (exceptionRaised.compareAndSet(false, true)) { 114 | if (methodInfo != null) { 115 | methodInfo.sendError(HttpResponseStatus.INTERNAL_SERVER_ERROR, cause); 116 | methodInfo = null; 117 | } else { 118 | if (cause instanceof HandlerException) { 119 | HttpResponse response = ((HandlerException) cause).createFailureResponse(); 120 | HttpUtil.setKeepAlive(response, false); 121 | // trace logs for user errors, error logs for internal server errors 122 | if (isUserError(response)) { 123 | LOG.trace(exceptionMessage, cause); 124 | } else { 125 | LOG.error(exceptionMessage, cause); 126 | } 127 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 128 | } else { 129 | LOG.error(exceptionMessage, cause); 130 | HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, 131 | HttpResponseStatus.INTERNAL_SERVER_ERROR); 132 | HttpUtil.setContentLength(response, 0); 133 | HttpUtil.setKeepAlive(response, false); 134 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 135 | } 136 | } 137 | } else { 138 | LOG.trace(exceptionMessage, cause); 139 | } 140 | } 141 | 142 | /** 143 | * If return type of the handler http method is BodyConsumer, remove {@link HttpObjectAggregator} class if it exists; 144 | * otherwise adds the {@link HttpObjectAggregator} to the pipeline if its not present already. 145 | */ 146 | @Nullable 147 | private HttpMethodInfo prepareHandleMethod(HttpRequest httpRequest, 148 | HttpResponder responder, ChannelHandlerContext ctx) throws Exception { 149 | HttpMethodInfo methodInfo = httpMethodHandler.getDestinationMethod(httpRequest, responder); 150 | 151 | if (methodInfo == null) { 152 | return null; 153 | } 154 | 155 | ctx.channel().attr(HttpDispatcher.METHOD_INFO_KEY).set(methodInfo); 156 | 157 | ChannelPipeline pipeline = ctx.channel().pipeline(); 158 | if (methodInfo.isStreaming()) { 159 | if (pipeline.get("aggregator") != null) { 160 | pipeline.remove("aggregator"); 161 | } 162 | if (pipeline.get("continue") == null) { 163 | pipeline.addAfter("router", "continue", new HttpServerExpectContinueHandler()); 164 | } 165 | } else { 166 | if (pipeline.get("continue") != null) { 167 | pipeline.remove("continue"); 168 | } 169 | if (pipeline.get("aggregator") == null) { 170 | pipeline.addAfter("router", "aggregator", new HttpObjectAggregator(chunkMemoryLimit)); 171 | } 172 | } 173 | return methodInfo; 174 | } 175 | 176 | private boolean isUserError(HttpResponse response) { 177 | int code = response.status().code(); 178 | return code == HttpResponseStatus.BAD_REQUEST.code() || code == HttpResponseStatus.NOT_FOUND.code() || 179 | code == HttpResponseStatus.METHOD_NOT_ALLOWED.code() || code == HttpResponseStatus.UNAUTHORIZED.code() 180 | || code == HttpResponseStatus.FORBIDDEN.code(); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/WrappedHttpResponder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http.internal; 18 | 19 | import io.cdap.http.AbstractHttpResponder; 20 | import io.cdap.http.BodyProducer; 21 | import io.cdap.http.ChunkResponder; 22 | import io.cdap.http.HandlerHook; 23 | import io.cdap.http.HttpResponder; 24 | import io.netty.buffer.ByteBuf; 25 | import io.netty.handler.codec.http.HttpHeaders; 26 | import io.netty.handler.codec.http.HttpRequest; 27 | import io.netty.handler.codec.http.HttpResponseStatus; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import java.io.File; 32 | import java.io.IOException; 33 | import java.nio.ByteBuffer; 34 | 35 | /** 36 | * Wrap HttpResponder to call post handler hook. 37 | */ 38 | final class WrappedHttpResponder extends AbstractHttpResponder { 39 | private static final Logger LOG = LoggerFactory.getLogger(WrappedHttpResponder.class); 40 | 41 | private final HttpResponder delegate; 42 | private final Iterable handlerHooks; 43 | private final HttpRequest httpRequest; 44 | private final HandlerInfo handlerInfo; 45 | 46 | WrappedHttpResponder(HttpResponder delegate, Iterable handlerHooks, 47 | HttpRequest httpRequest, HandlerInfo handlerInfo) { 48 | this.delegate = delegate; 49 | this.handlerHooks = handlerHooks; 50 | this.httpRequest = httpRequest; 51 | this.handlerInfo = handlerInfo; 52 | } 53 | 54 | @Override 55 | public ChunkResponder sendChunkStart(final HttpResponseStatus status, HttpHeaders headers) { 56 | final ChunkResponder chunkResponder = delegate.sendChunkStart(status, headers); 57 | return new ChunkResponder() { 58 | @Override 59 | public void sendChunk(ByteBuffer chunk) throws IOException { 60 | chunkResponder.sendChunk(chunk); 61 | } 62 | 63 | @Override 64 | public void sendChunk(ByteBuf chunk) throws IOException { 65 | chunkResponder.sendChunk(chunk); 66 | } 67 | 68 | @Override 69 | public void flush() { 70 | chunkResponder.flush(); 71 | } 72 | 73 | @Override 74 | public void close() throws IOException { 75 | chunkResponder.close(); 76 | runHook(status); 77 | } 78 | }; 79 | } 80 | 81 | @Override 82 | public void sendContent(HttpResponseStatus status, ByteBuf content, HttpHeaders headers) { 83 | delegate.sendContent(status, content, headers); 84 | runHook(status); 85 | } 86 | 87 | @Override 88 | public void sendFile(File file, HttpHeaders headers) throws IOException { 89 | delegate.sendFile(file, headers); 90 | runHook(HttpResponseStatus.OK); 91 | } 92 | 93 | @Override 94 | public void sendContent(HttpResponseStatus status, BodyProducer bodyProducer, HttpHeaders headers) { 95 | delegate.sendContent(status, bodyProducer, headers); 96 | runHook(status); 97 | } 98 | 99 | private void runHook(HttpResponseStatus status) { 100 | for (HandlerHook hook : handlerHooks) { 101 | try { 102 | hook.postCall(httpRequest, status, handlerInfo); 103 | } catch (Throwable t) { 104 | LOG.error("Post handler hook threw exception: ", t); 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/internal/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2017-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | /** 18 | * This is an internal package that contains the internal implementations of the netty-http library. 19 | * User shouldn't use classes inside this package directly as the API can change at any time. 20 | */ 21 | package io.cdap.http.internal; 22 | -------------------------------------------------------------------------------- /src/main/java/io/cdap/http/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | /** 18 | * Service and components to build Netty based Http web service. 19 | * {@code NettyHttpService} sets up the necessary pipeline and manages starting, stopping, 20 | * state-management of the web service. 21 | * 22 | *

23 | * In-order to handle http requests, {@code HttpHandler} must be implemented. The methods 24 | * in the classes implemented from {@code HttpHandler} must be annotated with Jersey annotations to 25 | * specify http uri paths and http methods. 26 | * Note: Only supports the following annotations: 27 | * {@link javax.ws.rs.Path Path}, 28 | * {@link javax.ws.rs.PathParam PathParam}, 29 | * {@link javax.ws.rs.GET GET}, 30 | * {@link javax.ws.rs.PUT PUT}, 31 | * {@link javax.ws.rs.POST POST}, 32 | * {@link javax.ws.rs.DELETE DELETE}, 33 | * 34 | * {@link io.cdap.http.RequiredRoles RequiredRoles}, 35 | * {@link io.cdap.http.Secured Secured}. 36 | * 37 | * Note: Doesn't support getting Annotations from base class if the HttpHandler implements also extends 38 | * a class with annotation. 39 | * 40 | * Sample usage Handlers and Netty service setup: 41 | * 42 | *

43 |  * //Setup Handlers
44 |  *
45 |  * {@literal @}Path("/common/v1/")
46 |  * public class ApiHandler implements HttpHandler {
47 |  *
48 |  *   {@literal @}Path("widgets")
49 |  *   {@literal @}GET
50 |  *   public void widgetHandler(HttpRequest request, HttpResponder responder) {
51 |  *     responder.sendJson(HttpResponseStatus.OK, "{\"key\": \"value\"}");
52 |  *   }
53 |  *
54 |  *   {@literal @}Override
55 |  *   public void init(HandlerContext context) {
56 |  *     //Perform bootstrap operations before any of the handlers in this class gets called.
57 |  *   }
58 |  *
59 |  *   {@literal @}Override
60 |  *   public void destroy(HandlerContext context) {
61 |  *    //Perform teardown operations the server shuts down.
62 |  *   }
63 |  * }
64 |  *
65 |  * //Set up and start the http service
66 |  * NettyHttpService service = NettyHttpService.builder()
67 |  *                                            .addHttpHandlers(ImmutableList.of(new Handler())
68 |  *                                            .setPort(8989)
69 |  *                                            .build();
70 |  * service.start();
71 |  *
72 |  * // ....
73 |  *
74 |  * //Stop the web-service
75 |  * service.shutdown();
76 |  *
77 |  * 
78 | */ 79 | package io.cdap.http; 80 | 81 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/ExecutorThreadPoolTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2021 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.channel.ChannelHandler; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.channel.ChannelInboundHandlerAdapter; 23 | import io.netty.channel.ChannelPipeline; 24 | import io.netty.handler.codec.http.HttpRequest; 25 | import io.netty.handler.codec.http.HttpResponseStatus; 26 | import io.netty.util.concurrent.EventExecutor; 27 | import org.junit.Assert; 28 | import org.junit.Test; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | import java.io.OutputStream; 33 | import java.net.HttpURLConnection; 34 | import java.net.InetSocketAddress; 35 | import java.net.URL; 36 | import java.nio.charset.StandardCharsets; 37 | import java.util.Collections; 38 | import java.util.concurrent.atomic.AtomicReference; 39 | import javax.ws.rs.POST; 40 | import javax.ws.rs.Path; 41 | 42 | /** 43 | * Unit test for testing the threading behavior when {@link NettyHttpService.Builder#setExecThreadPoolSize(int)} is 44 | * set to non-zero. 45 | */ 46 | public class ExecutorThreadPoolTest { 47 | 48 | @Test 49 | public void testSameEventThread() throws Exception { 50 | AtomicReference handlerThread = new AtomicReference<>(); 51 | 52 | NettyHttpService httpService = NettyHttpService.builder("test") 53 | .setExecThreadPoolSize(5) 54 | .setHttpHandlers(new TestHandler(handlerThread)) 55 | .setChannelPipelineModifier(new ChannelPipelineModifier() { 56 | @Override 57 | public void modify(ChannelPipeline pipeline) { 58 | // Modify the pipeline to insert a handler before the dispatcher with the same EventExecutor 59 | // This is to test the invocation of the dispatcher would always be in the same thread 60 | // as the handler inserted before it. 61 | EventExecutor executor = pipeline.context("dispatcher").executor(); 62 | pipeline.addBefore(executor, "dispatcher", "authenticator", new TestChannelHandler(handlerThread)); 63 | } 64 | }) 65 | .build(); 66 | 67 | httpService.start(); 68 | try { 69 | InetSocketAddress addr = httpService.getBindAddress(); 70 | URL url = new URL(String.format("http://%s:%d/upload", addr.getHostName(), addr.getPort())); 71 | HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); 72 | try { 73 | urlConn.setRequestMethod("POST"); 74 | urlConn.setDoOutput(true); 75 | urlConn.setChunkedStreamingMode(32); 76 | 77 | // The NonStickyEventExecutorGroup has max run of 1024. So we create a data of size > 32K. 78 | String data = String.join("", Collections.nCopies(32768, "0123456789")); 79 | try (OutputStream os = urlConn.getOutputStream()) { 80 | os.write(data.getBytes(StandardCharsets.UTF_8)); 81 | } 82 | 83 | Assert.assertEquals(HttpURLConnection.HTTP_OK, urlConn.getResponseCode()); 84 | } finally { 85 | urlConn.disconnect(); 86 | } 87 | } finally { 88 | httpService.stop(); 89 | } 90 | } 91 | 92 | /** 93 | * A {@link ChannelHandler} for testing executor thread behavior. 94 | */ 95 | private static final class TestChannelHandler extends ChannelInboundHandlerAdapter { 96 | 97 | private final AtomicReference handlerThread; 98 | 99 | private TestChannelHandler(AtomicReference handlerThread) { 100 | this.handlerThread = handlerThread; 101 | } 102 | 103 | @Override 104 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 105 | handlerThread.set(Thread.currentThread()); 106 | super.channelRead(ctx, msg); 107 | } 108 | } 109 | 110 | /** 111 | * Test handler for testing executor thread behavior. 112 | */ 113 | public static final class TestHandler extends AbstractHttpHandler { 114 | 115 | private static final Logger LOG = LoggerFactory.getLogger(TestHandler.class); 116 | 117 | private final AtomicReference handlerThread; 118 | 119 | public TestHandler(AtomicReference handlerThread) { 120 | this.handlerThread = handlerThread; 121 | } 122 | 123 | @POST 124 | @Path("/upload") 125 | public BodyConsumer upload(HttpRequest request, HttpResponder responder) { 126 | if (!Thread.currentThread().equals(handlerThread.get())) { 127 | responder.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR); 128 | return null; 129 | } 130 | 131 | return new BodyConsumer() { 132 | @Override 133 | public void chunk(ByteBuf request, HttpResponder responder) { 134 | if (!Thread.currentThread().equals(handlerThread.get())) { 135 | responder.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR); 136 | throw new IllegalStateException("Wrong thread " + Thread.currentThread() + " " + handlerThread.get()); 137 | } 138 | } 139 | 140 | @Override 141 | public void finished(HttpResponder responder) { 142 | if (Thread.currentThread().equals(handlerThread.get())) { 143 | responder.sendStatus(HttpResponseStatus.OK); 144 | } else { 145 | responder.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR); 146 | } 147 | } 148 | 149 | @Override 150 | public void handleError(Throwable cause) { 151 | LOG.error("Exception raised", cause); 152 | } 153 | }; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/HandlerHookTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.cdap.http.internal.HandlerInfo; 20 | import io.netty.handler.codec.http.HttpRequest; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | import org.junit.AfterClass; 23 | import org.junit.Assert; 24 | import org.junit.Before; 25 | import org.junit.BeforeClass; 26 | import org.junit.Test; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.net.HttpURLConnection; 31 | import java.net.URI; 32 | import java.net.URL; 33 | import java.util.Arrays; 34 | import java.util.Collections; 35 | import java.util.HashMap; 36 | import java.util.Map; 37 | import java.util.concurrent.CyclicBarrier; 38 | import java.util.concurrent.TimeUnit; 39 | 40 | /** 41 | * Tests handler hooks. 42 | */ 43 | public class HandlerHookTest { 44 | private static final Logger LOG = LoggerFactory.getLogger(HandlerHookTest.class); 45 | 46 | private static String hostname = "127.0.0.1"; 47 | private static URI baseURI; 48 | private static NettyHttpService service; 49 | private static final TestHandlerHook handlerHook1 = new TestHandlerHook(); 50 | private static final TestHandlerHook handlerHook2 = new TestHandlerHook(); 51 | 52 | @BeforeClass 53 | public static void setup() throws Exception { 54 | 55 | NettyHttpService.Builder builder = NettyHttpService.builder("test-hook"); 56 | builder.setHttpHandlers(new TestHandler()); 57 | builder.setHandlerHooks(Arrays.asList(handlerHook1, handlerHook2)); 58 | builder.setHost(hostname); 59 | 60 | service = builder.build(); 61 | service.start(); 62 | int port = service.getBindAddress().getPort(); 63 | baseURI = URI.create(String.format("http://%s:%d", hostname, port)); 64 | } 65 | 66 | @Before 67 | public void reset() { 68 | handlerHook1.reset(); 69 | handlerHook2.reset(); 70 | } 71 | 72 | @Test 73 | public void testHandlerHookCall() throws Exception { 74 | int status = doGet("/test/v1/resource"); 75 | Assert.assertEquals(HttpResponseStatus.OK.code(), status); 76 | 77 | awaitPostHook(); 78 | Assert.assertEquals(1, handlerHook1.getNumPreCalls()); 79 | Assert.assertEquals(1, handlerHook1.getNumPostCalls()); 80 | 81 | Assert.assertEquals(1, handlerHook2.getNumPreCalls()); 82 | Assert.assertEquals(1, handlerHook2.getNumPostCalls()); 83 | } 84 | 85 | @Test 86 | public void testPreHookReject() throws Exception { 87 | int status = doGet("/test/v1/resource", "X-Request-Type", "Reject"); 88 | Assert.assertEquals(HttpResponseStatus.NOT_ACCEPTABLE.code(), status); 89 | 90 | // Wait for any post handlers to be called 91 | TimeUnit.MILLISECONDS.sleep(100); 92 | Assert.assertEquals(1, handlerHook1.getNumPreCalls()); 93 | 94 | // The second pre-call should not have happened due to rejection by the first pre-call 95 | // None of the post calls should have happened. 96 | Assert.assertEquals(0, handlerHook1.getNumPostCalls()); 97 | Assert.assertEquals(0, handlerHook2.getNumPreCalls()); 98 | Assert.assertEquals(0, handlerHook2.getNumPostCalls()); 99 | } 100 | 101 | @Test 102 | public void testHandlerException() throws Exception { 103 | int status = doGet("/test/v1/exception"); 104 | Assert.assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), status); 105 | 106 | awaitPostHook(); 107 | Assert.assertEquals(1, handlerHook1.getNumPreCalls()); 108 | Assert.assertEquals(1, handlerHook1.getNumPostCalls()); 109 | 110 | Assert.assertEquals(1, handlerHook2.getNumPreCalls()); 111 | Assert.assertEquals(1, handlerHook2.getNumPostCalls()); 112 | } 113 | 114 | @Test 115 | public void testPreException() throws Exception { 116 | int status = doGet("/test/v1/resource", "X-Request-Type", "PreException"); 117 | Assert.assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR.code(), status); 118 | 119 | // Wait for any post handlers to be called 120 | TimeUnit.MILLISECONDS.sleep(100); 121 | Assert.assertEquals(1, handlerHook1.getNumPreCalls()); 122 | 123 | // The second pre-call should not have happened due to exception in the first pre-call 124 | // None of the post calls should have happened. 125 | Assert.assertEquals(0, handlerHook1.getNumPostCalls()); 126 | Assert.assertEquals(0, handlerHook2.getNumPreCalls()); 127 | Assert.assertEquals(0, handlerHook2.getNumPostCalls()); 128 | } 129 | 130 | @Test 131 | public void testPostException() throws Exception { 132 | int status = doGet("/test/v1/resource", "X-Request-Type", "PostException"); 133 | Assert.assertEquals(HttpResponseStatus.OK.code(), status); 134 | 135 | awaitPostHook(); 136 | Assert.assertEquals(1, handlerHook1.getNumPreCalls()); 137 | Assert.assertEquals(1, handlerHook1.getNumPostCalls()); 138 | 139 | Assert.assertEquals(1, handlerHook2.getNumPreCalls()); 140 | Assert.assertEquals(1, handlerHook2.getNumPostCalls()); 141 | } 142 | 143 | @Test 144 | public void testUnknownPath() throws Exception { 145 | int status = doGet("/unknown/path/test/v1/resource"); 146 | Assert.assertEquals(HttpResponseStatus.NOT_FOUND.code(), status); 147 | 148 | // Wait for any post handlers to be called 149 | TimeUnit.MILLISECONDS.sleep(100); 150 | Assert.assertEquals(0, handlerHook1.getNumPreCalls()); 151 | Assert.assertEquals(0, handlerHook1.getNumPostCalls()); 152 | 153 | Assert.assertEquals(0, handlerHook2.getNumPreCalls()); 154 | Assert.assertEquals(0, handlerHook2.getNumPostCalls()); 155 | } 156 | 157 | @AfterClass 158 | public static void teardown() throws Exception { 159 | service.stop(); 160 | } 161 | 162 | private void awaitPostHook() throws Exception { 163 | handlerHook1.awaitPost(); 164 | handlerHook2.awaitPost(); 165 | } 166 | 167 | private static class TestHandlerHook extends AbstractHandlerHook { 168 | private volatile int numPreCalls = 0; 169 | private volatile int numPostCalls = 0; 170 | private final CyclicBarrier postBarrier = new CyclicBarrier(2); 171 | 172 | public int getNumPreCalls() { 173 | return numPreCalls; 174 | } 175 | 176 | public int getNumPostCalls() { 177 | return numPostCalls; 178 | } 179 | 180 | public void reset() { 181 | numPreCalls = 0; 182 | numPostCalls = 0; 183 | } 184 | 185 | public void awaitPost() throws Exception { 186 | postBarrier.await(); 187 | } 188 | 189 | @Override 190 | public boolean preCall(HttpRequest request, HttpResponder responder, HandlerInfo handlerInfo) { 191 | ++numPreCalls; 192 | 193 | String header = request.headers().get("X-Request-Type"); 194 | if (header != null && header.equals("Reject")) { 195 | responder.sendStatus(HttpResponseStatus.NOT_ACCEPTABLE); 196 | return false; 197 | } 198 | 199 | if (header != null && header.equals("PreException")) { 200 | throw new IllegalArgumentException("PreException"); 201 | } 202 | 203 | return true; 204 | } 205 | 206 | @Override 207 | public void postCall(HttpRequest request, HttpResponseStatus status, HandlerInfo handlerInfo) { 208 | try { 209 | ++numPostCalls; 210 | 211 | String header = request.headers().get("X-Request-Type"); 212 | if (header != null && header.equals("PostException")) { 213 | throw new IllegalArgumentException("PostException"); 214 | } 215 | } finally { 216 | try { 217 | postBarrier.await(); 218 | } catch (Exception e) { 219 | LOG.error("Got exception: ", e); 220 | } 221 | } 222 | } 223 | } 224 | 225 | private static int doGet(String resource) throws Exception { 226 | return doGet(resource, Collections.emptyMap()); 227 | } 228 | 229 | private static int doGet(String resource, String key, String value, String...keyValues) throws Exception { 230 | Map headerMap = new HashMap<>(); 231 | headerMap.put(key, value); 232 | 233 | for (int i = 0; i < keyValues.length; i += 2) { 234 | headerMap.put(keyValues[i], keyValues[i + 1]); 235 | } 236 | return doGet(resource, headerMap); 237 | } 238 | 239 | private static int doGet(String resource, Map headers) throws Exception { 240 | URL url = baseURI.resolve(resource).toURL(); 241 | HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); 242 | try { 243 | if (headers != null) { 244 | for (Map.Entry entry : headers.entrySet()) { 245 | urlConn.setRequestProperty(entry.getKey(), entry.getValue()); 246 | } 247 | } 248 | return urlConn.getResponseCode(); 249 | } finally { 250 | urlConn.disconnect(); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/HttpsServerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.buffer.ByteBufAllocator; 20 | import io.netty.handler.codec.http.HttpHeaderNames; 21 | import io.netty.handler.codec.http.HttpHeaderValues; 22 | import io.netty.handler.codec.http.HttpMethod; 23 | import io.netty.handler.ssl.SslHandler; 24 | import org.junit.BeforeClass; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.net.HttpURLConnection; 30 | import java.net.Socket; 31 | import java.net.URI; 32 | import java.net.URL; 33 | import java.nio.file.Files; 34 | import java.nio.file.StandardCopyOption; 35 | import javax.annotation.Nullable; 36 | import javax.net.ssl.HostnameVerifier; 37 | import javax.net.ssl.HttpsURLConnection; 38 | import javax.net.ssl.SSLEngine; 39 | import javax.net.ssl.SSLSession; 40 | 41 | /** 42 | * Test the HttpsServer. 43 | */ 44 | public class HttpsServerTest extends HttpServerTest { 45 | 46 | private static SSLClientContext sslClientContext; 47 | 48 | @BeforeClass 49 | public static void setup() throws Exception { 50 | File keyStore = tmpFolder.newFile(); 51 | 52 | try (InputStream is = SSLKeyStoreTest.class.getClassLoader().getResourceAsStream("cert.jks")) { 53 | Files.copy(is, keyStore.toPath(), StandardCopyOption.REPLACE_EXISTING); 54 | } 55 | 56 | /* IMPORTANT 57 | * Provide Certificate Configuration Here * * 58 | * enableSSL() 59 | * KeyStore : SSL certificate 60 | * KeyStorePassword : Key Store Password 61 | * CertificatePassword : Certificate password if different from Key Store password or null 62 | */ 63 | 64 | NettyHttpService.Builder builder = createBaseNettyHttpServiceBuilder(); 65 | builder.enableSSL(SSLConfig.builder(keyStore, "secret").setCertificatePassword("secret") 66 | .build()); 67 | 68 | sslClientContext = new SSLClientContext(); 69 | service = builder.build(); 70 | service.start(); 71 | } 72 | 73 | @Override 74 | protected HttpURLConnection request(String path, HttpMethod method, boolean keepAlive) throws IOException { 75 | URL url = getBaseURI().resolve(path).toURL(); 76 | HttpsURLConnection.setDefaultSSLSocketFactory(sslClientContext.getClientContext().getSocketFactory()); 77 | HostnameVerifier allHostsValid = new HostnameVerifier() { 78 | @Override 79 | public boolean verify(String hostname, SSLSession session) { 80 | return true; 81 | } 82 | }; 83 | 84 | // Install the all-trusting host verifier 85 | HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid); 86 | HttpsURLConnection urlConn = (HttpsURLConnection) url.openConnection(); 87 | if (method == HttpMethod.POST || method == HttpMethod.PUT) { 88 | urlConn.setDoOutput(true); 89 | } 90 | urlConn.setRequestMethod(method.name()); 91 | if (!keepAlive) { 92 | urlConn.setRequestProperty(HttpHeaderNames.CONNECTION.toString(), HttpHeaderValues.CLOSE.toString()); 93 | } 94 | return urlConn; 95 | } 96 | 97 | @Override 98 | protected Socket createRawSocket(URL url) throws IOException { 99 | return sslClientContext.getClientContext().getSocketFactory().createSocket(url.getHost(), url.getPort()); 100 | } 101 | 102 | @Nullable 103 | @Override 104 | protected SslHandler createSslHandler(ByteBufAllocator bufAllocator) throws Exception { 105 | SSLEngine sslEngine = sslClientContext.getClientContext().createSSLEngine(); 106 | sslEngine.setUseClientMode(true); 107 | return new SslHandler(sslEngine); 108 | } 109 | 110 | static void setSslClientContext(SSLClientContext sslClientContext) { 111 | HttpsServerTest.sslClientContext = sslClientContext; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/InternalHttpResponderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import com.google.gson.Gson; 20 | import com.google.gson.JsonObject; 21 | import io.cdap.http.internal.InternalHttpResponder; 22 | import io.cdap.http.internal.InternalHttpResponse; 23 | import io.netty.buffer.Unpooled; 24 | import io.netty.handler.codec.http.DefaultHttpHeaders; 25 | import io.netty.handler.codec.http.EmptyHttpHeaders; 26 | import io.netty.handler.codec.http.HttpHeaderNames; 27 | import io.netty.handler.codec.http.HttpResponseStatus; 28 | import org.junit.Assert; 29 | import org.junit.Test; 30 | 31 | import java.io.BufferedReader; 32 | import java.io.IOException; 33 | import java.io.InputStreamReader; 34 | import java.io.Reader; 35 | import java.nio.charset.StandardCharsets; 36 | import javax.annotation.Nullable; 37 | 38 | /** 39 | * 40 | */ 41 | public class InternalHttpResponderTest { 42 | 43 | @Test 44 | public void testSendJson() throws IOException { 45 | InternalHttpResponder responder = new InternalHttpResponder(); 46 | JsonObject output = new JsonObject(); 47 | output.addProperty("data", "this is some data"); 48 | responder.sendJson(HttpResponseStatus.OK, output.toString()); 49 | 50 | InternalHttpResponse response = responder.getResponse(); 51 | Assert.assertEquals(HttpResponseStatus.OK.code(), response.getStatusCode()); 52 | try (Reader reader = new InputStreamReader(response.openInputStream(), "UTF-8")) { 53 | JsonObject responseData = new Gson().fromJson(reader, JsonObject.class); 54 | Assert.assertEquals(output, responseData); 55 | } 56 | } 57 | 58 | @Test 59 | public void testSendString() throws IOException { 60 | InternalHttpResponder responder = new InternalHttpResponder(); 61 | responder.sendString(HttpResponseStatus.BAD_REQUEST, "bad request"); 62 | 63 | validateResponse(responder.getResponse(), HttpResponseStatus.BAD_REQUEST, "bad request"); 64 | } 65 | 66 | @Test 67 | public void testSendStatus() throws IOException { 68 | InternalHttpResponder responder = new InternalHttpResponder(); 69 | responder.sendStatus(HttpResponseStatus.NOT_FOUND); 70 | 71 | validateResponse(responder.getResponse(), HttpResponseStatus.NOT_FOUND, null); 72 | } 73 | 74 | @Test 75 | public void testSendByteArray() throws IOException { 76 | InternalHttpResponder responder = new InternalHttpResponder(); 77 | responder.sendByteArray(HttpResponseStatus.OK, "abc".getBytes(StandardCharsets.UTF_8), EmptyHttpHeaders.INSTANCE); 78 | 79 | validateResponse(responder.getResponse(), HttpResponseStatus.OK, "abc"); 80 | } 81 | 82 | @Test 83 | public void testSendError() throws IOException { 84 | InternalHttpResponder responder = new InternalHttpResponder(); 85 | responder.sendString(HttpResponseStatus.NOT_FOUND, "not found"); 86 | 87 | validateResponse(responder.getResponse(), HttpResponseStatus.NOT_FOUND, "not found"); 88 | } 89 | 90 | @Test 91 | public void testChunks() throws IOException { 92 | InternalHttpResponder responder = new InternalHttpResponder(); 93 | ChunkResponder chunkResponder = responder.sendChunkStart(HttpResponseStatus.OK, null); 94 | chunkResponder.sendChunk(Unpooled.wrappedBuffer("a".getBytes(StandardCharsets.UTF_8))); 95 | chunkResponder.sendChunk(Unpooled.wrappedBuffer("b".getBytes(StandardCharsets.UTF_8))); 96 | chunkResponder.sendChunk(Unpooled.wrappedBuffer("c".getBytes(StandardCharsets.UTF_8))); 97 | chunkResponder.close(); 98 | 99 | validateResponse(responder.getResponse(), HttpResponseStatus.OK, "abc"); 100 | } 101 | 102 | @Test 103 | public void testSendContent() throws IOException { 104 | InternalHttpResponder responder = new InternalHttpResponder(); 105 | responder.sendContent(HttpResponseStatus.OK, Unpooled.wrappedBuffer("abc".getBytes(StandardCharsets.UTF_8)), 106 | new DefaultHttpHeaders().set(HttpHeaderNames.CONTENT_TYPE, "contentType")); 107 | 108 | validateResponse(responder.getResponse(), HttpResponseStatus.OK, "abc"); 109 | } 110 | 111 | private void validateResponse(InternalHttpResponse response, HttpResponseStatus expectedStatus, 112 | @Nullable String expectedData) 113 | throws IOException { 114 | int code = response.getStatusCode(); 115 | Assert.assertEquals(expectedStatus.code(), code); 116 | if (expectedData != null) { 117 | // read it twice to make sure the input supplier gives the full stream more than once. 118 | for (int i = 0; i < 2; i++) { 119 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.openInputStream(), "UTF-8"))) { 120 | String data = reader.readLine(); 121 | Assert.assertEquals(expectedData, data); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/MutualAuthServerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import org.junit.BeforeClass; 20 | 21 | import java.io.File; 22 | import java.io.InputStream; 23 | import java.nio.file.Files; 24 | import java.nio.file.StandardCopyOption; 25 | 26 | /** 27 | * Test the HttpsServer with mutual authentication. 28 | */ 29 | public class MutualAuthServerTest extends HttpsServerTest { 30 | 31 | @BeforeClass 32 | public static void setup() throws Exception { 33 | NettyHttpService.Builder builder = createBaseNettyHttpServiceBuilder(); 34 | 35 | File keyStore = tmpFolder.newFile(); 36 | try (InputStream is = SSLKeyStoreTest.class.getClassLoader().getResourceAsStream("cert.jks")) { 37 | Files.copy(is, keyStore.toPath(), StandardCopyOption.REPLACE_EXISTING); 38 | } 39 | 40 | File trustKeyStore = tmpFolder.newFile(); 41 | try (InputStream is = SSLKeyStoreTest.class.getClassLoader().getResourceAsStream("client.jks")) { 42 | Files.copy(is, trustKeyStore.toPath(), StandardCopyOption.REPLACE_EXISTING); 43 | } 44 | 45 | String keyStorePassword = "secret"; 46 | String trustKeyStorePassword = "password"; 47 | builder.enableSSL(SSLConfig.builder(keyStore, keyStorePassword).setTrustKeyStore(trustKeyStore) 48 | .setTrustKeyStorePassword(trustKeyStorePassword) 49 | .build()); 50 | 51 | setSslClientContext(new SSLClientContext(trustKeyStore, trustKeyStorePassword)); 52 | service = builder.build(); 53 | service.start(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/PathRouterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.cdap.http.internal.PatternPathRouterWithGroups; 20 | import org.junit.Assert; 21 | import org.junit.Test; 22 | 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | 28 | /** 29 | * Test the routing logic using String as the destination. 30 | */ 31 | public class PathRouterTest { 32 | 33 | @Test 34 | public void testPathRoutings() { 35 | 36 | PatternPathRouterWithGroups pathRouter = PatternPathRouterWithGroups.create(25); 37 | pathRouter.add("/", "empty"); 38 | pathRouter.add("/foo/{baz}/b", "foobarb"); 39 | pathRouter.add("/foo/bar/baz", "foobarbaz"); 40 | pathRouter.add("/baz/bar", "bazbar"); 41 | pathRouter.add("/bar", "bar"); 42 | pathRouter.add("/foo/bar", "foobar"); 43 | pathRouter.add("//multiple/slash//route", "multipleslashroute"); 44 | 45 | pathRouter.add("/abc/bar", "abc-bar"); 46 | pathRouter.add("/abc/{type}/{id}", "abc-type-id"); 47 | 48 | pathRouter.add("/multi/match/**", "multi-match-*"); 49 | pathRouter.add("/multi/match/def", "multi-match-def"); 50 | 51 | pathRouter.add("/multi/maxmatch/**", "multi-max-match-*"); 52 | pathRouter.add("/multi/maxmatch/{id}", "multi-max-match-id"); 53 | pathRouter.add("/multi/maxmatch/foo", "multi-max-match-foo"); 54 | 55 | pathRouter.add("**/wildcard/{id}", "wildcard-id"); 56 | pathRouter.add("/**/wildcard/{id}", "slash-wildcard-id"); 57 | 58 | pathRouter.add("**/wildcard/**/foo/{id}", "wildcard-foo-id"); 59 | pathRouter.add("/**/wildcard/**/foo/{id}", "slash-wildcard-foo-id"); 60 | 61 | pathRouter.add("**/wildcard/**/foo/{id}/**", "wildcard-foo-id-2"); 62 | pathRouter.add("/**/wildcard/**/foo/{id}/**", "slash-wildcard-foo-id-2"); 63 | 64 | List> routes; 65 | 66 | routes = pathRouter.getDestinations("/"); 67 | Assert.assertEquals(1, routes.size()); 68 | Assert.assertEquals("empty", routes.get(0).getDestination()); 69 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 70 | 71 | routes = pathRouter.getDestinations("/foo/bar/baz"); 72 | Assert.assertEquals(1, routes.size()); 73 | Assert.assertEquals("foobarbaz", routes.get(0).getDestination()); 74 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 75 | 76 | routes = pathRouter.getDestinations("/baz/bar"); 77 | Assert.assertEquals(1, routes.size()); 78 | Assert.assertEquals("bazbar", routes.get(0).getDestination()); 79 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 80 | 81 | routes = pathRouter.getDestinations("/foo/bar/baz/moo"); 82 | Assert.assertTrue(routes.isEmpty()); 83 | 84 | routes = pathRouter.getDestinations("/bar/121"); 85 | Assert.assertTrue(routes.isEmpty()); 86 | 87 | routes = pathRouter.getDestinations("/foo/bar/b"); 88 | Assert.assertEquals(1, routes.size()); 89 | Assert.assertEquals("foobarb", routes.get(0).getDestination()); 90 | Assert.assertEquals(1, routes.get(0).getGroupNameValues().size()); 91 | Assert.assertEquals("bar", routes.get(0).getGroupNameValues().get("baz")); 92 | 93 | routes = pathRouter.getDestinations("/foo/bar"); 94 | Assert.assertEquals(1, routes.size()); 95 | Assert.assertEquals("foobar", routes.get(0).getDestination()); 96 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 97 | 98 | routes = pathRouter.getDestinations("/abc/bar/id"); 99 | Assert.assertEquals(1, routes.size()); 100 | Assert.assertEquals("abc-type-id", routes.get(0).getDestination()); 101 | 102 | routes = pathRouter.getDestinations("/multiple/slash/route"); 103 | Assert.assertEquals(1, routes.size()); 104 | Assert.assertEquals("multipleslashroute", routes.get(0).getDestination()); 105 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 106 | 107 | routes = pathRouter.getDestinations("/foo/bar/bazooka"); 108 | Assert.assertTrue(routes.isEmpty()); 109 | 110 | routes = pathRouter.getDestinations("/multi/match/def"); 111 | Assert.assertEquals(2, routes.size()); 112 | Assert.assertEquals(new HashSet<>(Arrays.asList("multi-match-def", "multi-match-*")), 113 | new HashSet<>(Arrays.asList(routes.get(0).getDestination(), routes.get(1).getDestination()))); 114 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 115 | Assert.assertTrue(routes.get(1).getGroupNameValues().isEmpty()); 116 | 117 | routes = pathRouter.getDestinations("/multi/match/ghi"); 118 | Assert.assertEquals(1, routes.size()); 119 | Assert.assertEquals("multi-match-*", routes.get(0).getDestination()); 120 | Assert.assertTrue(routes.get(0).getGroupNameValues().isEmpty()); 121 | 122 | routes = pathRouter.getDestinations("/multi/maxmatch/id1"); 123 | Assert.assertEquals(2, routes.size()); 124 | Assert.assertEquals(new HashSet<>(Arrays.asList("multi-max-match-id", "multi-max-match-*")), 125 | new HashSet<>(Arrays.asList(routes.get(0).getDestination(), routes.get(1).getDestination()))); 126 | 127 | Assert.assertEquals(new HashSet<>(Arrays.asList(Collections.singletonMap("id", "id1"), 128 | Collections.emptyMap())), 129 | new HashSet<>(Arrays.asList(routes.get(0).getGroupNameValues(), 130 | routes.get(1).getGroupNameValues())) 131 | ); 132 | 133 | routes = pathRouter.getDestinations("/multi/maxmatch/foo"); 134 | Assert.assertEquals(3, routes.size()); 135 | Assert.assertEquals(new HashSet<>(Arrays.asList("multi-max-match-id", "multi-max-match-*", "multi-max-match-foo")), 136 | new HashSet<>(Arrays.asList(routes.get(0).getDestination(), routes.get(1).getDestination(), 137 | routes.get(2).getDestination()))); 138 | 139 | Assert.assertEquals(new HashSet<>(Arrays.asList(Collections.singletonMap("id", "foo"), 140 | Collections.emptyMap())), 141 | new HashSet<>(Arrays.asList(routes.get(0).getGroupNameValues(), 142 | routes.get(1).getGroupNameValues())) 143 | ); 144 | 145 | routes = pathRouter.getDestinations("/foo/bar/wildcard/id1"); 146 | Assert.assertEquals(2, routes.size()); 147 | Assert.assertEquals(new HashSet<>(Arrays.asList("wildcard-id", "slash-wildcard-id")), 148 | new HashSet<>(Arrays.asList(routes.get(0).getDestination(), routes.get(1).getDestination()))); 149 | 150 | Assert.assertEquals(new HashSet<>(Arrays.asList(Collections.singletonMap("id", "id1"), 151 | Collections.singletonMap("id", "id1"))), 152 | new HashSet<>(Arrays.asList(routes.get(0).getGroupNameValues(), 153 | routes.get(1).getGroupNameValues())) 154 | ); 155 | 156 | routes = pathRouter.getDestinations("/wildcard/id1"); 157 | Assert.assertEquals(1, routes.size()); 158 | Assert.assertEquals("wildcard-id", routes.get(0).getDestination()); 159 | Assert.assertEquals(Collections.singletonMap("id", "id1"), routes.get(0).getGroupNameValues()); 160 | 161 | routes = pathRouter.getDestinations("/foo/bar/wildcard/bar/foo/id1"); 162 | Assert.assertEquals(2, routes.size()); 163 | Assert.assertEquals(new HashSet<>(Arrays.asList("wildcard-foo-id", "slash-wildcard-foo-id")), 164 | new HashSet<>(Arrays.asList(routes.get(0).getDestination(), routes.get(1).getDestination()))); 165 | 166 | Assert.assertEquals(new HashSet<>(Arrays.asList(Collections.singletonMap("id", "id1"), 167 | Collections.singletonMap("id", "id1"))), 168 | new HashSet<>(Arrays.asList(routes.get(0).getGroupNameValues(), 169 | routes.get(1).getGroupNameValues())) 170 | ); 171 | 172 | routes = pathRouter.getDestinations("/foo/bar/wildcard/bar/foo/id1/baz/bar"); 173 | Assert.assertEquals(2, routes.size()); 174 | Assert.assertEquals(new HashSet<>(Arrays.asList("wildcard-foo-id-2", "slash-wildcard-foo-id-2")), 175 | new HashSet<>(Arrays.asList(routes.get(0).getDestination(), routes.get(1).getDestination()))); 176 | 177 | Assert.assertEquals(new HashSet<>(Arrays.asList(Collections.singletonMap("id", "id1"), 178 | Collections.singletonMap("id", "id1"))), 179 | new HashSet<>(Arrays.asList(routes.get(0).getGroupNameValues(), 180 | routes.get(1).getGroupNameValues())) 181 | ); 182 | 183 | routes = pathRouter.getDestinations("/wildcard/bar/foo/id1/baz/bar"); 184 | Assert.assertEquals(1, routes.size()); 185 | Assert.assertEquals("wildcard-foo-id-2", routes.get(0).getDestination()); 186 | Assert.assertEquals(Collections.singletonMap("id", "id1"), routes.get(0).getGroupNameValues()); 187 | } 188 | 189 | @Test(expected = IllegalArgumentException.class) 190 | public void testMaxPathParts() throws Exception { 191 | PatternPathRouterWithGroups pathRouter = PatternPathRouterWithGroups.create(5); 192 | pathRouter.add("/1/2/3/4/5/6", "max-path-parts"); 193 | } 194 | 195 | @Test 196 | public void testMaxPathParts1() throws Exception { 197 | PatternPathRouterWithGroups pathRouter = PatternPathRouterWithGroups.create(6); 198 | pathRouter.add("/1/2/3/4/5/6", "max-path-parts"); 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/SSLClientContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.security.KeyStore; 26 | import javax.net.ssl.KeyManagerFactory; 27 | import javax.net.ssl.SSLContext; 28 | 29 | /** 30 | * Provides Client Context 31 | * Used by HttpsServerTest 32 | */ 33 | public class SSLClientContext { 34 | 35 | private SSLContext clientContext; 36 | private String protocol = "TLS"; 37 | 38 | public SSLClientContext() { 39 | this(null, null); 40 | } 41 | 42 | public SSLClientContext(File keyStore, String keyStorePassword) { 43 | 44 | try { 45 | KeyManagerFactory kmf = null; 46 | if (keyStore != null && keyStorePassword != null) { 47 | KeyStore ks = getKeyStore(keyStore, keyStorePassword); 48 | kmf = KeyManagerFactory.getInstance("SunX509"); 49 | kmf.init(ks, keyStorePassword.toCharArray()); 50 | } 51 | clientContext = SSLContext.getInstance(protocol); 52 | clientContext.init(kmf == null ? null : kmf.getKeyManagers(), 53 | InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null); 54 | } catch (Exception e) { 55 | throw new RuntimeException("Failed to initialize the client-side SSLContext", e); 56 | } 57 | } 58 | 59 | private static KeyStore getKeyStore(File keyStore, String keyStorePassword) throws IOException { 60 | try (InputStream is = new FileInputStream(keyStore)) { 61 | KeyStore ks = KeyStore.getInstance("JKS"); 62 | ks.load(is, keyStorePassword.toCharArray()); 63 | return ks; 64 | } catch (Exception ex) { 65 | if (ex instanceof RuntimeException) { 66 | throw ((RuntimeException) ex); 67 | } 68 | throw new IOException(ex); 69 | } 70 | } 71 | 72 | public SSLContext getClientContext() { 73 | return clientContext; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/SSLKeyStoreTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import org.junit.BeforeClass; 20 | import org.junit.ClassRule; 21 | import org.junit.Test; 22 | import org.junit.rules.TemporaryFolder; 23 | 24 | import java.io.File; 25 | import java.io.InputStream; 26 | import java.nio.file.Files; 27 | import java.nio.file.StandardCopyOption; 28 | 29 | /** 30 | * Tests SSL KeyStore behaviour 31 | */ 32 | public class SSLKeyStoreTest { 33 | private static File keyStore; 34 | 35 | @ClassRule 36 | public static TemporaryFolder tmpFolder = new TemporaryFolder(); 37 | 38 | @BeforeClass 39 | public static void setup() throws Exception { 40 | keyStore = tmpFolder.newFile(); 41 | try (InputStream is = SSLKeyStoreTest.class.getClassLoader().getResourceAsStream("cert.jks")) { 42 | Files.copy(is, keyStore.toPath(), StandardCopyOption.REPLACE_EXISTING); 43 | } 44 | } 45 | 46 | @Test (expected = IllegalArgumentException.class) 47 | public void testSslCertPathConfiguration1() throws IllegalArgumentException { 48 | //Bad Certificate Path 49 | new SSLHandlerFactory(SSLConfig.builder(new File("badCertificate"), "secret").setCertificatePassword("secret") 50 | .build()); 51 | } 52 | 53 | @Test (expected = IllegalArgumentException.class) 54 | public void testSslCertPathConfiguration2() throws IllegalArgumentException { 55 | //Null Certificate Path 56 | new SSLHandlerFactory(SSLConfig.builder(null, "secret").setCertificatePassword("secret").build()); 57 | } 58 | 59 | @Test (expected = IllegalArgumentException.class) 60 | public void testSslKeyStorePassConfiguration2() throws IllegalArgumentException { 61 | //Missing Key Pass 62 | new SSLHandlerFactory(SSLConfig.builder(keyStore, null).setCertificatePassword("secret").build()); 63 | } 64 | 65 | @Test 66 | public void testSslCertPassConfiguration() throws IllegalArgumentException { 67 | //Bad Cert Pass 68 | new SSLHandlerFactory(SSLConfig.builder(keyStore, "secret").build()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/TestChannelHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import io.netty.channel.ChannelHandlerContext; 20 | import io.netty.channel.ChannelOutboundHandlerAdapter; 21 | import io.netty.channel.ChannelPromise; 22 | import io.netty.handler.codec.http.HttpResponse; 23 | 24 | /** 25 | * Test ChannelHandler that adds a default header to every response. 26 | */ 27 | public class TestChannelHandler extends ChannelOutboundHandlerAdapter { 28 | 29 | static final String HEADER_FIELD = "testHeaderField"; 30 | static final String HEADER_VALUE = "testHeaderValue"; 31 | 32 | @Override 33 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 34 | if (!(msg instanceof HttpResponse)) { 35 | super.write(ctx, msg, promise); 36 | return; 37 | } 38 | HttpResponse response = (HttpResponse) msg; 39 | response.headers().add(HEADER_FIELD, HEADER_VALUE); 40 | super.write(ctx, response, promise); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/cdap/http/URLRewriterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2014-2019 Cask Data, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package io.cdap.http; 18 | 19 | import com.google.gson.Gson; 20 | import com.google.gson.reflect.TypeToken; 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.buffer.Unpooled; 23 | import io.netty.handler.codec.http.DefaultHttpHeaders; 24 | import io.netty.handler.codec.http.HttpHeaderNames; 25 | import io.netty.handler.codec.http.HttpMethod; 26 | import io.netty.handler.codec.http.HttpRequest; 27 | import io.netty.handler.codec.http.HttpResponseStatus; 28 | import org.junit.AfterClass; 29 | import org.junit.Assert; 30 | import org.junit.BeforeClass; 31 | import org.junit.Test; 32 | 33 | import java.io.IOException; 34 | import java.io.InputStream; 35 | import java.net.HttpURLConnection; 36 | import java.net.URI; 37 | import java.net.URL; 38 | import java.nio.charset.StandardCharsets; 39 | import java.util.Collections; 40 | import java.util.Map; 41 | import javax.annotation.Nullable; 42 | 43 | /** 44 | * Test URLRewriter. 45 | */ 46 | public class URLRewriterTest { 47 | private static final Gson GSON = new Gson(); 48 | 49 | private static String hostname = "127.0.0.1"; 50 | private static NettyHttpService service; 51 | private static URI baseURI; 52 | 53 | @BeforeClass 54 | public static void setup() throws Exception { 55 | 56 | NettyHttpService.Builder builder = NettyHttpService.builder("test-url-rewrite"); 57 | builder.setHttpHandlers(new TestHandler()); 58 | builder.setUrlRewriter(new TestURLRewriter()); 59 | builder.setHost(hostname); 60 | 61 | service = builder.build(); 62 | service.start(); 63 | int port = service.getBindAddress().getPort(); 64 | 65 | baseURI = URI.create(String.format("http://%s:%d", hostname, port)); 66 | } 67 | 68 | @AfterClass 69 | public static void teardown() throws Exception { 70 | service.stop(); 71 | } 72 | 73 | @Test 74 | public void testUrlRewrite() throws Exception { 75 | int status = doGet("/rewrite/test/v1/resource"); 76 | Assert.assertEquals(HttpResponseStatus.OK.code(), status); 77 | 78 | HttpURLConnection urlConn = request("/rewrite/test/v1/tweets/7648", HttpMethod.PUT); 79 | Assert.assertEquals(HttpResponseStatus.OK.code(), urlConn.getResponseCode()); 80 | Map stringMap = GSON.fromJson(getContent(urlConn), 81 | new TypeToken>() { }.getType()); 82 | Assert.assertEquals(Collections.singletonMap("status", "Handled put in tweets end-point, id: 7648"), stringMap); 83 | 84 | urlConn.disconnect(); 85 | } 86 | 87 | @Test 88 | public void testUrlRewriteNormalize() throws Exception { 89 | int status = doGet("/rewrite//test/v1//resource"); 90 | Assert.assertEquals(HttpResponseStatus.OK.code(), status); 91 | } 92 | 93 | @Test 94 | public void testRegularCall() throws Exception { 95 | int status = doGet("/test/v1/resource"); 96 | Assert.assertEquals(HttpResponseStatus.OK.code(), status); 97 | } 98 | 99 | @Test 100 | public void testUrlRewriteUnknownPath() throws Exception { 101 | int status = doGet("/rewrite/unknown/test/v1/resource"); 102 | Assert.assertEquals(HttpResponseStatus.NOT_FOUND.code(), status); 103 | } 104 | 105 | @Test 106 | public void testUrlRewriteRedirect() throws Exception { 107 | int status = doGet("/redirect/test/v1/resource"); 108 | Assert.assertEquals(HttpResponseStatus.OK.code(), status); 109 | } 110 | 111 | private static class TestURLRewriter implements URLRewriter { 112 | @Override 113 | public boolean rewrite(HttpRequest request, HttpResponder responder) { 114 | if (request.uri().startsWith("/rewrite/")) { 115 | request.setUri(request.uri().replace("/rewrite/", "/")); 116 | } 117 | 118 | if (request.uri().startsWith("/redirect/")) { 119 | responder.sendStatus(HttpResponseStatus.MOVED_PERMANENTLY, 120 | new DefaultHttpHeaders().set(HttpHeaderNames.LOCATION, 121 | request.uri().replace("/redirect/", "/rewrite/"))); 122 | return false; 123 | } 124 | return true; 125 | } 126 | } 127 | 128 | private static int doGet(String resource) throws Exception { 129 | return doGet(resource, Collections.emptyMap()); 130 | } 131 | 132 | private static int doGet(String resource, @Nullable Map headers) throws Exception { 133 | URL url = baseURI.resolve(resource).toURL(); 134 | HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); 135 | try { 136 | if (headers != null) { 137 | for (Map.Entry entry : headers.entrySet()) { 138 | urlConn.setRequestProperty(entry.getKey(), entry.getValue()); 139 | } 140 | } 141 | return urlConn.getResponseCode(); 142 | } finally { 143 | urlConn.disconnect(); 144 | } 145 | } 146 | 147 | 148 | private HttpURLConnection request(String path, HttpMethod method) throws IOException { 149 | URL url = baseURI.resolve(path).toURL(); 150 | HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); 151 | if (method == HttpMethod.POST || method == HttpMethod.PUT) { 152 | urlConn.setDoOutput(true); 153 | } 154 | urlConn.setRequestMethod(method.name()); 155 | 156 | return urlConn; 157 | } 158 | 159 | private String getContent(HttpURLConnection urlConn) throws IOException { 160 | ByteBuf buffer = Unpooled.buffer(); 161 | InputStream is = urlConn.getInputStream(); 162 | while (buffer.writeBytes(is, 1024) > 0) { 163 | // no-op 164 | } 165 | return buffer.toString(StandardCharsets.UTF_8); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/test/resources/cert.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdapio/netty-http/a65d63835e6684c9434e98da3c1b95a2f4b2fe1a/src/test/resources/cert.jks -------------------------------------------------------------------------------- /src/test/resources/client.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdapio/netty-http/a65d63835e6684c9434e98da3c1b95a2f4b2fe1a/src/test/resources/client.jks -------------------------------------------------------------------------------- /suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | --------------------------------------------------------------------------------