├── .circleci └── config.yml ├── .dockerignore ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── README.md ├── build ├── ci │ └── circleci └── package │ └── docker │ ├── Dockerfile │ ├── Dockerfile.client │ ├── configs │ ├── squid-ssl.conf │ └── squid.conf │ └── scripts │ └── squid-icap-init.sh ├── certs └── .gitkeep ├── cmd └── mock-proxy │ └── main.go ├── deployments ├── deploy.hcl └── docker-compose.yml ├── docs ├── README.md ├── examples │ ├── 001-api-mocking.md │ ├── 002-dynamic-url.md │ ├── 003-substitution-variables.md │ └── 004-git-clone.md └── images │ └── mock-proxy-diagram.png ├── go.mod ├── go.sum ├── hack ├── gen-certs.sh ├── local-dev-down.sh ├── local-dev-up.sh ├── prep-git-mocks.sh └── unstage-git-mocks.sh ├── mocks ├── api.github.com │ └── orgs │ │ └── :org │ │ └── repos.mock ├── example.com │ └── index.mock ├── git │ └── github.com │ │ └── example-repo │ │ └── README.md └── routes.hcl └── pkg └── mock ├── mock.go ├── mock_test.go ├── route.go ├── route_test.go ├── testdata ├── example.com │ ├── simple.mock │ ├── substitutions.mock │ └── users │ │ └── :name.mock └── routes.hcl ├── transform.go └── transform_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | references: 5 | images: 6 | go: &GO_IMAGE circleci/golang:1.14.2 7 | lint: &LINT_IMAGE golangci/golangci-lint:v1.25-alpine 8 | aws: &AWS_CLI_IMAGE quay.io/hashicorp/hc-awscli:1.16.313 9 | 10 | workflows: 11 | version: 2 12 | validate: 13 | jobs: 14 | - prepare-environment 15 | - lint 16 | - test 17 | - fetch-citool: 18 | filters: 19 | branches: 20 | only: 21 | - master 22 | - deploy-artifact: 23 | requires: 24 | - prepare-environment 25 | - fetch-citool 26 | - lint 27 | - test 28 | filters: 29 | branches: 30 | only: 31 | - master 32 | 33 | jobs: 34 | prepare-environment: 35 | docker: 36 | - image: *GO_IMAGE 37 | steps: 38 | - checkout 39 | - run: 40 | name: Export DEPLOY_BUILD_ID 41 | command: | 42 | short_git_sha=$( git rev-parse --short HEAD ) 43 | 44 | # the always-increasing counter, based on CIRCLE_BUILD_NUM 45 | build_counter="${CIRCLE_BUILD_NUM}" 46 | 47 | # the build identifier, which includes the short git sha 48 | DEPLOY_BUILD_ID="CIRC-${build_counter}-${short_git_sha}" 49 | echo "export DEPLOY_BUILD_ID=${DEPLOY_BUILD_ID}" >> "${BASH_ENV}" 50 | 51 | # save the ${BASH_ENV} value into a file in the workspace 52 | cp "${BASH_ENV}" bash-env 53 | - persist_to_workspace: 54 | root: "." 55 | paths: 56 | - "bash-env" 57 | 58 | fetch-citool: 59 | docker: 60 | - image: *AWS_CLI_IMAGE 61 | auth: 62 | username: $DEPLOY_QUAY_USER 63 | password: $DEPLOY_QUAY_TOKEN 64 | steps: 65 | - run: 66 | name: Download citool from S3 67 | command: | 68 | aws s3 cp s3://hc-citool-bucket/citool citool 69 | chmod +x citool 70 | - persist_to_workspace: 71 | root: "." 72 | paths: 73 | - "citool" 74 | 75 | lint: 76 | docker: 77 | - image: *LINT_IMAGE 78 | working_directory: /app 79 | steps: 80 | - checkout 81 | - run: 82 | name: Lint Code 83 | command: | 84 | golangci-lint run 85 | 86 | test: 87 | docker: 88 | - image: *GO_IMAGE 89 | steps: 90 | - checkout 91 | - run: 92 | name: Test 93 | command: | 94 | go test -v -race ./... 95 | 96 | deploy-artifact: 97 | docker: 98 | - image: *GO_IMAGE 99 | steps: 100 | - checkout 101 | - setup_remote_docker: 102 | docker_layer_caching: true 103 | - attach_workspace: 104 | at: "." 105 | - run: 106 | name: Source Environment 107 | command: | 108 | cat bash-env >> "${BASH_ENV}" 109 | - run: 110 | name: Deploy Artifact 111 | command: | 112 | ./citool deploy artifact --file="deployments/deploy.hcl" 113 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore directories / files that don't get used in Docker. 2 | deployments 3 | hack 4 | .github 5 | .circleci 6 | README.md 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp/team-tf-developer-productivity 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code. 4 | 5 | Please read the full text at https://www.hashicorp.com/community-guidelines 6 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to mock-proxy 2 | 3 | mock-proxy is an open source project and we appreciate contributions of various 4 | kinds, including bug reports and fixes, enhancement proposals, documentation 5 | updates, and user experience feedback. However, this is not an officially 6 | supported HashiCorp product. We _are_ grateful for all contributions, but this 7 | repository is primarily maintained by a small team at HashiCorp outside of 8 | their main job responsibilities and is developed on a volunteer basis. 9 | 10 | To record a bug report, enhancement proposal, or give any other product 11 | feedback, please [open a GitHub issue](https://github.com/hashicorp/mock-proxy/issues/new). 12 | 13 | **All communication on GitHub, the community forum, and other HashiCorp-provided 14 | communication channels is subject to 15 | [the HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines).** 16 | 17 | ## Local Development Environment 18 | 19 | mock-proxy has a simple proxying setup configured in docker-compose for use 20 | while developing changes locally. To start up this local development 21 | environment run: 22 | 23 | ``` 24 | ./hack/local-dev-up.sh 25 | ``` 26 | 27 | This starts the Squid proxy, an ICAP server implemented by mock-proxy, and a 28 | client that is using that proxy. You can see a simple mock implemented by 29 | running: 30 | 31 | ``` 32 | curl https://example.com 33 | ``` 34 | 35 | ## Running Tests 36 | 37 | mock-proxy has a test suite implemented for its Golang components. To run the 38 | tests, run: 39 | 40 | ``` 41 | go test ./... 42 | ``` 43 | 44 | For additional details run: 45 | 46 | ``` 47 | go test -v -race ./... 48 | ``` 49 | 50 | ## External Dependencies 51 | 52 | mock-proxy uses Go Modules for dependency management. 53 | 54 | Our dependency licensing policy for mock-proxy excludes proprietary licenses 55 | and "copyleft"-style licenses. We accept the common Mozilla Public License v2, 56 | MIT License, and BSD licenses. We will consider other open source licenses 57 | in similar spirit to those three, but if you plan to include such a dependency 58 | in a contribution we'd recommend opening a GitHub issue first to discuss what 59 | you intend to implement and what dependencies it will require so that the 60 | mock-proxy team can review the relevant licenses for whether they meet our 61 | licensing needs. 62 | 63 | If you need to add a new dependency to mock-proxy or update the selected version 64 | for an existing one, use `go get` from the root of the mock-proxy repository 65 | as follows: 66 | 67 | ``` 68 | go get github.com/hashicorp/hcl/v2@2.0.0 69 | ``` 70 | 71 | This command will download the requested version (2.0.0 in the above example) 72 | and record that version selection in the `go.mod` file. It will also record 73 | checksums for the module in the `go.sum`. 74 | 75 | To complete the dependency change and clean up any redundancy in the module 76 | metadata files by running the following commands: 77 | 78 | ``` 79 | go mod tidy 80 | ``` 81 | 82 | Because dependency changes affect a shared, top-level file, they are more likely 83 | than some other change types to become conflicted with other proposed changes 84 | during the code review process. For that reason, and to make dependency changes 85 | more visible in the change history, we prefer to record dependency changes as 86 | separate commits that include only the results of the above commands and the 87 | minimal set of changes to mock-proxy's own code for compatibility with the 88 | new version: 89 | 90 | ``` 91 | git add go.mod go.sum 92 | git commit -m "modules: go get github.com/hashicorp/hcl/v2@2.0.0" 93 | ``` 94 | 95 | You can then make use of the new or updated dependency in new code added in 96 | subsequent commits. 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't commit the binary 2 | cmd/mock-proxy/mock-proxy 3 | 4 | # Don't commit your self signed certs. 5 | certs/ca.pem 6 | certs/ca.crt 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. “Contributor” 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. “Contributor Version” 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor’s Contribution. 14 | 15 | 1.3. “Contribution” 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. “Covered Software” 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. “Incompatible With Secondary Licenses” 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of version 33 | 1.1 or earlier of the License, but not also under the terms of a 34 | Secondary License. 35 | 36 | 1.6. “Executable Form” 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. “Larger Work” 41 | 42 | means a work that combines Covered Software with other material, in a separate 43 | file or files, that is not Covered Software. 44 | 45 | 1.8. “License” 46 | 47 | means this document. 48 | 49 | 1.9. “Licensable” 50 | 51 | means having the right to grant, to the maximum extent possible, whether at the 52 | time of the initial grant or subsequently, any and all of the rights conveyed by 53 | this License. 54 | 55 | 1.10. “Modifications” 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, deletion 60 | from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. “Patent Claims” of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, process, 67 | and apparatus claims, in any patent Licensable by such Contributor that 68 | would be infringed, but for the grant of the License, by the making, 69 | using, selling, offering for sale, having made, import, or transfer of 70 | either its Contributions or its Contributor Version. 71 | 72 | 1.12. “Secondary License” 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. “Source Code Form” 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. “You” (or “Your”) 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, “You” includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, “control” means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or as 104 | part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its Contributions 108 | or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution become 113 | effective for each Contribution on the date the Contributor first distributes 114 | such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under this 119 | License. No additional rights or licenses will be implied from the distribution 120 | or licensing of Covered Software under this License. Notwithstanding Section 121 | 2.1(b) above, no patent license is granted by a Contributor: 122 | 123 | a. for any code that a Contributor has removed from Covered Software; or 124 | 125 | b. for infringements caused by: (i) Your and any other third party’s 126 | modifications of Covered Software, or (ii) the combination of its 127 | Contributions with other software (except as part of its Contributor 128 | Version); or 129 | 130 | c. under Patent Claims infringed by Covered Software in the absence of its 131 | Contributions. 132 | 133 | This License does not grant any rights in the trademarks, service marks, or 134 | logos of any Contributor (except as may be necessary to comply with the 135 | notice requirements in Section 3.4). 136 | 137 | 2.4. Subsequent Licenses 138 | 139 | No Contributor makes additional grants as a result of Your choice to 140 | distribute the Covered Software under a subsequent version of this License 141 | (see Section 10.2) or under the terms of a Secondary License (if permitted 142 | under the terms of Section 3.3). 143 | 144 | 2.5. Representation 145 | 146 | Each Contributor represents that the Contributor believes its Contributions 147 | are its original creation(s) or it has sufficient rights to grant the 148 | rights to its Contributions conveyed by this License. 149 | 150 | 2.6. Fair Use 151 | 152 | This License is not intended to limit any rights You have under applicable 153 | copyright doctrines of fair use, fair dealing, or other equivalents. 154 | 155 | 2.7. Conditions 156 | 157 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 158 | Section 2.1. 159 | 160 | 161 | 3. Responsibilities 162 | 163 | 3.1. Distribution of Source Form 164 | 165 | All distribution of Covered Software in Source Code Form, including any 166 | Modifications that You create or to which You contribute, must be under the 167 | terms of this License. You must inform recipients that the Source Code Form 168 | of the Covered Software is governed by the terms of this License, and how 169 | they can obtain a copy of this License. You may not attempt to alter or 170 | restrict the recipients’ rights in the Source Code Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | a. such Covered Software must also be made available in Source Code Form, 177 | as described in Section 3.1, and You must inform recipients of the 178 | Executable Form how they can obtain a copy of such Source Code Form by 179 | reasonable means in a timely manner, at a charge no more than the cost 180 | of distribution to the recipient; and 181 | 182 | b. You may distribute such Executable Form under the terms of this License, 183 | or sublicense it under different terms, provided that the license for 184 | the Executable Form does not attempt to limit or alter the recipients’ 185 | rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for the 191 | Covered Software. If the Larger Work is a combination of Covered Software 192 | with a work governed by one or more Secondary Licenses, and the Covered 193 | Software is not Incompatible With Secondary Licenses, this License permits 194 | You to additionally distribute such Covered Software under the terms of 195 | such Secondary License(s), so that the recipient of the Larger Work may, at 196 | their option, further distribute the Covered Software under the terms of 197 | either this License or such Secondary License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices (including 202 | copyright notices, patent notices, disclaimers of warranty, or limitations 203 | of liability) contained within the Source Code Form of the Covered 204 | Software, except that You may alter any license notices to the extent 205 | required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on behalf 212 | of any Contributor. You must make it absolutely clear that any such 213 | warranty, support, indemnity, or liability obligation is offered by You 214 | alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | 222 | If it is impossible for You to comply with any of the terms of this License 223 | with respect to some or all of the Covered Software due to statute, judicial 224 | order, or regulation then You must: (a) comply with the terms of this License 225 | to the maximum extent possible; and (b) describe the limitations and the code 226 | they affect. Such description must be placed in a text file included with all 227 | distributions of the Covered Software under this License. Except to the 228 | extent prohibited by statute or regulation, such description must be 229 | sufficiently detailed for a recipient of ordinary skill to be able to 230 | understand it. 231 | 232 | 5. Termination 233 | 234 | 5.1. The rights granted under this License will terminate automatically if You 235 | fail to comply with any of its terms. However, if You become compliant, 236 | then the rights granted under this License from a particular Contributor 237 | are reinstated (a) provisionally, unless and until such Contributor 238 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 239 | if such Contributor fails to notify You of the non-compliance by some 240 | reasonable means prior to 60 days after You have come back into compliance. 241 | Moreover, Your grants from a particular Contributor are reinstated on an 242 | ongoing basis if such Contributor notifies You of the non-compliance by 243 | some reasonable means, this is the first time You have received notice of 244 | non-compliance with this License from such Contributor, and You become 245 | compliant prior to 30 days after Your receipt of the notice. 246 | 247 | 5.2. If You initiate litigation against any entity by asserting a patent 248 | infringement claim (excluding declaratory judgment actions, counter-claims, 249 | and cross-claims) alleging that a Contributor Version directly or 250 | indirectly infringes any patent, then the rights granted to You by any and 251 | all Contributors for the Covered Software under Section 2.1 of this License 252 | shall terminate. 253 | 254 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 255 | license agreements (excluding distributors and resellers) which have been 256 | validly granted by You or Your distributors under this License prior to 257 | termination shall survive termination. 258 | 259 | 6. Disclaimer of Warranty 260 | 261 | Covered Software is provided under this License on an “as is” basis, without 262 | warranty of any kind, either expressed, implied, or statutory, including, 263 | without limitation, warranties that the Covered Software is free of defects, 264 | merchantable, fit for a particular purpose or non-infringing. The entire 265 | risk as to the quality and performance of the Covered Software is with You. 266 | Should any Covered Software prove defective in any respect, You (not any 267 | Contributor) assume the cost of any necessary servicing, repair, or 268 | correction. This disclaimer of warranty constitutes an essential part of this 269 | License. No use of any Covered Software is authorized under this License 270 | except under this disclaimer. 271 | 272 | 7. Limitation of Liability 273 | 274 | Under no circumstances and under no legal theory, whether tort (including 275 | negligence), contract, or otherwise, shall any Contributor, or anyone who 276 | distributes Covered Software as permitted above, be liable to You for any 277 | direct, indirect, special, incidental, or consequential damages of any 278 | character including, without limitation, damages for lost profits, loss of 279 | goodwill, work stoppage, computer failure or malfunction, or any and all 280 | other commercial damages or losses, even if such party shall have been 281 | informed of the possibility of such damages. This limitation of liability 282 | shall not apply to liability for death or personal injury resulting from such 283 | party’s negligence to the extent applicable law prohibits such limitation. 284 | Some jurisdictions do not allow the exclusion or limitation of incidental or 285 | consequential damages, so this exclusion and limitation may not apply to You. 286 | 287 | 8. Litigation 288 | 289 | Any litigation relating to this License may be brought only in the courts of 290 | a jurisdiction where the defendant maintains its principal place of business 291 | and such litigation shall be governed by laws of that jurisdiction, without 292 | reference to its conflict-of-law provisions. Nothing in this Section shall 293 | prevent a party’s ability to bring cross-claims or counter-claims. 294 | 295 | 9. Miscellaneous 296 | 297 | This License represents the complete agreement concerning the subject matter 298 | hereof. If any provision of this License is held to be unenforceable, such 299 | provision shall be reformed only to the extent necessary to make it 300 | enforceable. Any law or regulation which provides that the language of a 301 | contract shall be construed against the drafter shall not be used to construe 302 | this License against a Contributor. 303 | 304 | 305 | 10. Versions of the License 306 | 307 | 10.1. New Versions 308 | 309 | Mozilla Foundation is the license steward. Except as provided in Section 310 | 10.3, no one other than the license steward has the right to modify or 311 | publish new versions of this License. Each version will be given a 312 | distinguishing version number. 313 | 314 | 10.2. Effect of New Versions 315 | 316 | You may distribute the Covered Software under the terms of the version of 317 | the License under which You originally received the Covered Software, or 318 | under the terms of any subsequent version published by the license 319 | steward. 320 | 321 | 10.3. Modified Versions 322 | 323 | If you create software not governed by this License, and you want to 324 | create a new license for such software, you may create and use a modified 325 | version of this License if you rename the license and remove any 326 | references to the name of the license steward (except to note that such 327 | modified license differs from this License). 328 | 329 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 330 | If You choose to distribute Source Code Form that is Incompatible With 331 | Secondary Licenses under the terms of this version of the License, the 332 | notice described in Exhibit B of this License must be attached. 333 | 334 | Exhibit A - Source Code Form License Notice 335 | 336 | This Source Code Form is subject to the 337 | terms of the Mozilla Public License, v. 338 | 2.0. If a copy of the MPL was not 339 | distributed with this file, You can 340 | obtain one at 341 | http://mozilla.org/MPL/2.0/. 342 | 343 | If it is not possible or desirable to put the notice in a particular file, then 344 | You may include the notice in a location (such as a LICENSE file in a relevant 345 | directory) where a recipient would be likely to look for such a notice. 346 | 347 | You may add additional accurate notices of copyright ownership. 348 | 349 | Exhibit B - “Incompatible With Secondary Licenses” Notice 350 | 351 | This Source Code Form is “Incompatible 352 | With Secondary Licenses”, as defined by 353 | the Mozilla Public License, v. 2.0. 354 | 355 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mock-proxy 2 | _a.k.a. Moxie_ 3 | 4 | **Notification: this repository is archived since May 18, 2022. Please find an alternative solution for your mock-proxy needs** 5 | 6 | mock-proxy (a.k.a Moxie—short for mo(ck)(pro)xy) is a replacement proxy relying 7 | on the HTTP intercept capabilities of [ICAP](https://tools.ietf.org/html/rfc3507), 8 | as implemented in [go-icap/icap](https://github.com/go-icap/icap). This 9 | replacement proxy is intended for writing integration tests that mock responses 10 | from external services at the network level. In order to use mock-proxy, a test 11 | environment specifies it as an HTTP proxy using `http_proxy` type environment 12 | variables (or however proxies are configured in your scenario). Because it 13 | works at the network level and can be used by many services at once using proxy 14 | config, it works well in microservices environments where many services need to 15 | mock the same 3rd party dependencies. 16 | 17 | In short, ICAP allows us to specify a set of criteria to match all requests 18 | against and then route them accordingly. When a request hits the proxy, if it 19 | matches this criteria then a semi-hardcoded response is automatically short 20 | circuited in. If it does not match the criteria, the request proceeds as 21 | normal. 22 | 23 | ![moxie flow diagram](/docs/images/mock-proxy-diagram.png) 24 | 25 | ## Features 26 | 27 | * Selectively mock endpoints, allowing some requests to hit the internet, and 28 | others to be faked locally. 29 | * Configure mocked routes using an HCL2 based Routes file. 30 | * Dynamic URL support, allowing mocking traditional RESTful APIs easily. 31 | * Templated responses using "Transformer" interface, including the built in 32 | substitution-variables endpoint. 33 | * Mock `git clone` operations using the `git` route type. 34 | * Mock HTTPS endpoints using Squid's SSLBump feature. 35 | 36 | See documentation and examples for more information. 37 | 38 | ## Disclaimer 39 | 40 | This is not an officially supported HashiCorp product. 41 | 42 | ## Documentation 43 | 44 | There is documentation of how to use mock-proxy features such as the Routes 45 | file in the [docs](/docs) directory. 46 | 47 | ## Examples 48 | 49 | See [examples of different use cases for mock-proxy](/docs/examples) 50 | 51 | ## Layout 52 | 53 | In general, this project attempts to stick to the layout prescribed in [golang-standards/project-layout](https://github.com/golang-standards/project-layout) 54 | 55 | `build`: CircleCI and Docker build scripts. Also includes relevant squid proxy 56 | [config](build/package/docker/configs/squid.conf) and 57 | [setup script](build/package/docker/scripts/squid-icap-init.sh). 58 | 59 | `certs`: Self signed certificates used to configure SSL Bump for local 60 | development. 61 | 62 | `cmd`: Contains main.go file for building mock-proxy. 63 | 64 | `deployments`: Configs for publishing builds of mock-proxy. 65 | 66 | `hack`: Bash scripts for assorted tasks such as running mock-proxy locally 67 | 68 | `mocks`: Faux endpoints for testing the proxy redirect within this project. 69 | 70 | `pkg`: The main Go code directory which houses the implementation of the custom 71 | ICAP server. 72 | 73 | ## Getting started 74 | 75 | The local development and demonstration environment for mock-proxy relies on 76 | Docker to orchestrate the various services (client, ICAP server, Squid server) 77 | required to successfully mock requests. If you do not have a functional Docker 78 | environment, see the 79 | [getting started docs on Docker's website](https://docs.docker.com/get-started/#set-up-your-docker-environment). 80 | 81 | To develop locally, run `./hack/local-dev-up.sh` to start the proxy and client 82 | containers. The result of this script will leave you in a bash shell inside the 83 | client container. From here, you can create substitution variables and test API 84 | endpoints you've hardcoded via a GET curl. 85 | 86 | To keep an eye on the proxy logs, tail the proxy container logs via 87 | `docker logs -f deployments_proxy_1` (or whatever name your proxy's container 88 | happens to have). 89 | 90 | ## Dependency Management 91 | 92 | This project uses Go [modules](https://github.com/golang/go/wiki/Modules) 93 | without a vendor dir. 94 | 95 | This Modules Wiki will probably have better advice about adding / upgrading 96 | dependencies than can be stated here. 97 | -------------------------------------------------------------------------------- /build/ci/circleci: -------------------------------------------------------------------------------- 1 | ../../.circleci -------------------------------------------------------------------------------- /build/package/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14.2 as builder 2 | 3 | ENV GOBIN=/go/bin 4 | ENV CGO_ENABLED=0 5 | ENV GOOS=linux 6 | ENV GO111MODULE=on 7 | 8 | WORKDIR /go/src/github.com/hashicorp/mock-proxy 9 | 10 | # Improve build hit rate for slow `go mod download` by copying over .mod and 11 | # .sum files first. 12 | COPY go.mod go.mod 13 | COPY go.sum go.sum 14 | RUN go mod download 15 | 16 | # Currently, all Go files are in these directories, it may be necessary to add 17 | # more directories to this list over time. 18 | COPY ./cmd cmd 19 | COPY ./pkg pkg 20 | RUN go build -o mock-proxy ./cmd/mock-proxy 21 | 22 | FROM alpine:3.11 23 | RUN apk add --no-cache ca-certificates squid dumb-init bash git 24 | WORKDIR / 25 | 26 | # Initialize Squid SSL db. 27 | RUN /usr/lib/squid/security_file_certgen -c -s /var/lib/ssl_db -M 4MB 28 | 29 | COPY --from=builder /go/src/github.com/hashicorp/mock-proxy/mock-proxy . 30 | 31 | COPY ./build/package/docker/configs/squid.conf /etc/squid/squid.conf 32 | COPY ./build/package/docker/configs/squid-ssl.conf /etc/squid/squid-ssl.conf 33 | COPY ./build/package/docker/scripts/squid-icap-init.sh . 34 | 35 | ENTRYPOINT ["/usr/bin/dumb-init", "--"] 36 | CMD ["/squid-icap-init.sh"] 37 | -------------------------------------------------------------------------------- /build/package/docker/Dockerfile.client: -------------------------------------------------------------------------------- 1 | FROM alpine:3.11 2 | 3 | ARG DOCKERIZE_VERSION="v0.6.1" 4 | ARG DOCKERIZE_SHA="dddbf178ecfd55fa6670b01ac08fef63ef9490212426b9fab8a602345409da8f" 5 | 6 | RUN apk add --no-cache \ 7 | ca-certificates curl bash git 8 | 9 | # Install Dockerize: https://github.com/jwilder/dockerize 10 | RUN echo "${DOCKERIZE_SHA} -" > sumfile \ 11 | && curl -s -L "https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz" | \ 12 | tee dockerize.tar.gz | \ 13 | sha256sum -c sumfile \ 14 | && tar -xzvf dockerize.tar.gz \ 15 | && mv "dockerize" "/usr/local/bin/dockerize" \ 16 | && rm -rf "dockerize.tar.gz" "sumfile" "dockerize" 17 | 18 | # Set a bash prompt. 19 | ENV PS1="client:-$ " 20 | -------------------------------------------------------------------------------- /build/package/docker/configs/squid-ssl.conf: -------------------------------------------------------------------------------- 1 | http_port 8888 ssl-bump \ 2 | cert=/etc/squid/ssl_cert/ca.pem \ 3 | generate-host-certificates=on dynamic_cert_mem_cache_size=4MB 4 | 5 | sslcrtd_program /usr/lib/squid/security_file_certgen -s /var/lib/ssl_db -M 4MB 6 | 7 | acl step1 at_step SslBump1 8 | 9 | ssl_bump peek step1 10 | ssl_bump bump all 11 | 12 | icap_enable on 13 | icap_service service_req reqmod_precache icap://127.0.0.1:11344/icap 14 | adaptation_access service_req allow all 15 | http_access allow all 16 | 17 | cache_log /var/log/squid/cache.log 18 | -------------------------------------------------------------------------------- /build/package/docker/configs/squid.conf: -------------------------------------------------------------------------------- 1 | http_port 8888 2 | 3 | icap_enable on 4 | icap_service service_req reqmod_precache icap://127.0.0.1:11344/icap 5 | adaptation_access service_req allow all 6 | http_access allow all 7 | 8 | cache_log /var/log/squid/cache.log 9 | -------------------------------------------------------------------------------- /build/package/docker/scripts/squid-icap-init.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Create a trap that will kill all processes if either exits. 5 | PIDS=() 6 | got_sig_chld=false 7 | trap ' 8 | if ! "$got_sig_chld"; then 9 | got_sig_chld=true 10 | ((${#PIDS[@]})) && kill "${PIDS[@]}" 2> /dev/null 11 | fi 12 | ' CHLD 13 | 14 | # Background the mock-proxy ICAP protocol server. 15 | /mock-proxy & PIDS+=("$!") 16 | 17 | # Start squid in non-daemon mode, but bash backgrounded. Only use SSL bump if 18 | # an SSL cert has been mounted at /etc/squid/ssl_cert. 19 | if [ -f /etc/squid/ssl_cert/ca.pem ]; then 20 | squid -f /etc/squid/squid-ssl.conf -N & PIDS+=($!) 21 | else 22 | squid -f /etc/squid/squid.conf -N & PIDS+=($!) 23 | fi 24 | 25 | # Enable "Job Control" mode, then wait: 26 | # https://www.gnu.org/software/bash/manual/html_node/Job-Control.html#Job-Control 27 | set -m 28 | wait 29 | set +m 30 | -------------------------------------------------------------------------------- /certs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/mock-proxy/598d3817ad0abe7c94f54956c8a27fadae4083b1/certs/.gitkeep -------------------------------------------------------------------------------- /cmd/mock-proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/hashicorp/go-hclog" 9 | 10 | "github.com/hashicorp/mock-proxy/pkg/mock" 11 | ) 12 | 13 | func main() { 14 | if err := inner(); err != nil { 15 | hclog.Default().Error("mock-proxy error: %s\n", err) 16 | os.Exit(1) 17 | } 18 | } 19 | 20 | func inner() error { 21 | options := []mock.Option{} 22 | 23 | if portString := os.Getenv("API_PORT"); portString != "" { 24 | port, err := strconv.Atoi(portString) 25 | if err != nil { 26 | return fmt.Errorf("invalid API_PORT: %w", err) 27 | } 28 | options = append(options, mock.WithAPIPort(port)) 29 | } 30 | 31 | logLevel := "INFO" 32 | if envLog := os.Getenv("LOG_LEVEL"); envLog != "" { 33 | logLevel = envLog 34 | } 35 | 36 | options = append(options, mock.WithLogger(hclog.New(&hclog.LoggerOptions{ 37 | Name: "mock-proxy", 38 | Level: hclog.LevelFromString(logLevel), 39 | }))) 40 | 41 | m, err := mock.NewMockServer(options...) 42 | if err != nil { 43 | return err 44 | } 45 | return m.Serve() 46 | } 47 | -------------------------------------------------------------------------------- /deployments/deploy.hcl: -------------------------------------------------------------------------------- 1 | # The "vcs-mock-proxy" project name refers to HashiCorp's internal use of this 2 | # tool (mocking version control software providers). You may see scattered use 3 | # of this name throughout the project, but should ignore it in favor of just 4 | # mock-proxy. 5 | project = "vcs-mock-proxy" 6 | deploy_id = env.BUILD_ID 7 | 8 | artifact { 9 | artifact_type = "quay_repo" 10 | 11 | config { 12 | docker_file = "build/package/docker/Dockerfile" 13 | repo = project.name 14 | tag = project.deploy_id 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /deployments/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.3' 3 | 4 | services: 5 | proxy: 6 | build: 7 | context: ../ 8 | dockerfile: build/package/docker/Dockerfile 9 | networks: 10 | default: 11 | aliases: [squid.proxy] 12 | volumes: 13 | - "../certs:/etc/squid/ssl_cert" 14 | - "../mocks:/mocks" 15 | 16 | client: 17 | build: 18 | context: ../ 19 | dockerfile: build/package/docker/Dockerfile.client 20 | environment: 21 | http_proxy: http://squid.proxy:8888 22 | https_proxy: http://squid.proxy:8888 23 | depends_on: 24 | - proxy 25 | networks: 26 | default: 27 | aliases: [my.client] 28 | volumes: 29 | - "../certs:/usr/local/share/ca-certificates" 30 | 31 | networks: 32 | default: 33 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Local Development && Demonstration 2 | 3 | To make it easy to test mock-proxy locally and to simplify developing changes 4 | to the mock-proxy codebase, a `docker-compose` based local development 5 | environment has been created. 6 | 7 | To start this local development environment run: 8 | 9 | ``` 10 | ./hack/local-dev-up.sh 11 | ``` 12 | 13 | This will start two Docker containers, `mock_proxy_proxy_1` and 14 | `mock_proxy_client_run_${UNIQ_ID}` and automatically drop you into a shell 15 | session running bash in the client. 16 | 17 | `mock_proxy_proxy_1` runs the custom ICAP server and Squid proxy. 18 | 19 | `mock_proxy_client_run_${UNIQ_ID}` runs a shell session with appropriate 20 | `http_proxy` and `https_proxy` environment variables set to allow the user to 21 | automatically interact with the mock-proxy setup on `mock_proxy_proxy_1`. 22 | 23 | When you are done with a session, or having issues with the local development 24 | environment, you should run the script: 25 | 26 | ``` 27 | ./hack/local-dev-down.sh 28 | ``` 29 | 30 | To clean up the local development environment. 31 | 32 | ## Routes File 33 | 34 | The Routes file defines which endpoints should be mocked. It is defined in HCL, 35 | and should be stored in your mocks directory at `routes.hcl`. 36 | 37 | Create each route as a block. The host should match the hostname of the site 38 | you want to mock. The path matches the path, but you can use rails route style 39 | `:foo` substitutions to make dynamic URLs. The type can either be `git` or 40 | `http`, depending on whether you want to mock a git clone operation or not. 41 | 42 | ```hcl 43 | # You can reach this at any /orgs/$VALUE/repos request, and a substitution will 44 | # be added that replaces {key=org, value=$VALUE} 45 | route { 46 | host = "api.github.com" 47 | path = "/orgs/:org/repos" 48 | type = "http" 49 | } 50 | ``` 51 | 52 | Do not create overlapping routes. This will cause an error, as the mock routing 53 | logic cannot determine which route to apply to a given request. 54 | 55 | ## Mocking Different Response Codes 56 | 57 | By default, all mocks return a 200 when they succeed. That's not the only 58 | possible thing you might need to mock though. In order to request a different 59 | response code along for a mock, send along the header 60 | `X-Desired-Response-Code`. 61 | 62 | ``` 63 | ./hack/local-dev-up.sh 64 | curl --head --header "X-Desired-Response-Code: 204" example.com 65 | ``` 66 | 67 | ## SSL Certificates and Mocking HTTPS requests 68 | 69 | Using Squid's SSL Bump configuration, mock-proxy can also act as an 70 | `https_proxy` and successfully mock upstream requests to HTTPS endpoints. 71 | 72 | It does so in this local configuration using a self-signed certificate in 73 | `/certs`. This self-signed cert is automatically trusted for local dev. 74 | 75 | To generate these certificates, use the script: `/hack/gen-certs.sh`. This may 76 | also be useful example code if you need to incorporate self-signed certs into 77 | another system using mock-proxy. 78 | 79 | If configuring mock-proxy in another environment, you will need to volume mount 80 | a self-signed certificate to `/etc/squid/ssl_cert/ca.pem`, and trust that 81 | certificate on any system attempting to use mock-proxy as an `https_proxy`. 82 | 83 | ## Mocking Git Clones 84 | 85 | mock-proxy also supports mocking Git Clones made via HTTP. To do so, add a 86 | route to your routes.hcl file: 87 | 88 | ```hcl 89 | route { 90 | host = "github.com" 91 | path = "/example-repo" 92 | type = "git" 93 | } 94 | ``` 95 | 96 | You'll next need to add a directory (for this example) at 97 | `/mocks/git/github.com/example-repo`. In this repo, you can then run a script 98 | `/hack/prep-git-mocks.sh` to automatically initialize a git repo. Don't commit 99 | this repo, as we don't want to manage git submodules here. You can unstage that 100 | initialization with another script `/hack/unstage-git-mocks.sh`. 101 | 102 | Once you've added a route, a directory exists at the correct path, and you've 103 | initialized git in it, you can run a clone by starting up the local dev 104 | environment and making an HTTP clone request: 105 | 106 | ``` 107 | ./hack/local-dev-up.sh 108 | git clone http://github.com/example-repo 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/examples/001-api-mocking.md: -------------------------------------------------------------------------------- 1 | ## API Mocking 2 | 3 | Aaron is working on a Python script that will hit an API endpoint his coworker 4 | is still iterating on. His coworker sends over some mocks with the API response 5 | structure. 6 | 7 | Using this he generates a Routes file: 8 | 9 | ```hcl 10 | route { 11 | host = "example.com" 12 | path = "/" 13 | type = "http" 14 | } 15 | ``` 16 | 17 | And a mock file, at `/mocks/example.com/index.mock`. 18 | 19 | ```json 20 | { 21 | "field1": 0, 22 | "field2": "string" 23 | } 24 | ``` 25 | 26 | Now he can work on his script before the site is up, while still using the full 27 | network stack of his program. 28 | 29 | ```python 30 | import requests 31 | 32 | proxies = { 33 | 'http': 'http://squid.proxy:8888', 34 | 'https': 'http://squid.proxy:8888', 35 | } 36 | 37 | resp = requests.get( 38 | 'https://example.com', 39 | proxies=proxies, 40 | verify='/usr/local/share/ca-certificates/ca.pem' 41 | ) 42 | print(resp.json()) 43 | ``` 44 | 45 | ```bash 46 | python test.py 47 | ### {'field1': 0, 'field2': 'string'} 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/examples/002-dynamic-url.md: -------------------------------------------------------------------------------- 1 | ## Dynamic URL 2 | 3 | Kori is working on a jq script she's going to use when hacking around with 4 | some bash. She doesn't want to deal with authentication or risk getting rate 5 | limited while she's working on it. So she uses mock-proxy to substitute for 6 | the real thing. 7 | 8 | Using the [CircleCI documentation](https://circleci.com/docs/api/v2/#get-recent-runs-of-a-workflow) 9 | she generates a Routes file: 10 | 11 | ```hcl 12 | route { 13 | host = "circleci.com" 14 | path = "/api/v2/insights/:project/workflows/:workflow" 15 | type = "http" 16 | } 17 | ``` 18 | 19 | And a mock file, at `/mocks/cirleci.com/api/v2/insights/:project/workflows/:workflow.mock`. 20 | 21 | The use of `:project` and `:workflow` in the route and mock file definitions 22 | will automatically create substitutions for the `{{.project}}` and 23 | `{{.workflow}}` values in the mock file when a request is made that matches the 24 | pattern. 25 | 26 | ```json 27 | { 28 | "items": [ 29 | { 30 | "id": "{{.project}}/{{.workflow}}-1", 31 | "duration": 0, 32 | "created_at": "2020-04-29T16:35:21Z", 33 | "stopped_at": "2020-04-29T16:35:21Z", 34 | "credits-used": 0, 35 | "status": "success" 36 | }, 37 | { 38 | "id": "{{.project}}/{{.workflow}}-2", 39 | "duration": 0, 40 | "created_at": "2020-04-29T16:35:21Z", 41 | "stopped_at": "2020-04-29T16:35:21Z", 42 | "credits-used": 0, 43 | "status": "running" 44 | } 45 | ], 46 | "next_page_token": "string" 47 | } 48 | ``` 49 | 50 | Now once she starts mock-proxy, she can iterate on her jq script without 51 | needing to hit the real CircleCI endpoints. 52 | 53 | ```bash 54 | export http_proxy=http://squid.proxy:8888 55 | export https_proxy=http://squid.proxy:8888 56 | 57 | curl --silent https://circleci.com/api/v2/insights/myproject/workflows/myworkflow | \ 58 | jq -r '.items[] | "\(.id) \(.status)"' 59 | 60 | ### myproject/myworkflow-1 success 61 | ### myproject/myworkflow-2 running 62 | 63 | curl --silent https://circleci.com/api/v2/insights/otherproject/workflows/otherworkflow | \ 64 | jq -r '.items[] | "\(.id) \(.status)"' 65 | 66 | ### otherproject/otherworkflow-1 success 67 | ### otherproject/otherworkflow-2 running 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/examples/003-substitution-variables.md: -------------------------------------------------------------------------------- 1 | ## Substitution Variables 2 | 3 | Perry is working on a system that parses HTML, and passes it through a series 4 | of filter programs that may do additional work. He wants to test passing 5 | special characters all the way through the network stack. 6 | 7 | To this end Perry creates a Routes file: 8 | 9 | ```hcl 10 | route { 11 | host = "example.com" 12 | path = "/" 13 | type = "http" 14 | } 15 | ``` 16 | 17 | And a mock file, at `/mocks/example.com/index.mock`. 18 | 19 | ``` 20 | Hello, {{ .Name }} 21 | ``` 22 | 23 | Now he can iterate different possible characters by passing them as 24 | "Substitution Variables". 25 | 26 | ```bash 27 | curl -X POST -F "key=Name" -F "value=World" squid.proxy/substitution-variables 28 | parse 29 | ### Hello, World 30 | curl -X POST -F "key=Name" -F "value=>&2" squid.proxy/substitution-variables 31 | parse 32 | ### Hello, >&2 33 | curl -X POST -F "key=Name" -F 'value=!!_@3%2F' squid.proxy/substitution-variables 34 | parse 35 | ### Hello, !!_@3%2F 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/examples/004-git-clone.md: -------------------------------------------------------------------------------- 1 | ## Git Clone 2 | 3 | London is working on a service that needs to mock out cloning git repositories. 4 | 5 | To that end she generates a Routes file: 6 | 7 | ```hcl 8 | route { 9 | host = "github.com" 10 | path = "/example-repo" 11 | type = "git" 12 | } 13 | ``` 14 | 15 | And initializes a git repository at `/mocks/git/github.com/example-repo`. 16 | 17 | Now she can clone the fake git repository with http clones: 18 | 19 | ```bash 20 | export http_proxy=http://squid.proxy:8888 21 | export https_proxy=http://squid.proxy:8888 22 | 23 | git clone https://github.com/example-repo --depth=1 24 | ### Cloning into 'example-repo'... 25 | ### Unpacking objects: 100% (3/3), done. 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/images/mock-proxy-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/mock-proxy/598d3817ad0abe7c94f54956c8a27fadae4083b1/docs/images/mock-proxy-diagram.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/mock-proxy 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-icap/icap v0.0.0-20151011115316-ca4fad4ebb28 7 | github.com/hashicorp/go-hclog v0.12.2 8 | github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 9 | github.com/stretchr/testify v1.4.0 10 | gopkg.in/src-d/go-billy.v4 v4.3.2 11 | gopkg.in/src-d/go-git.v4 v4.13.1 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 2 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 3 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 4 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 5 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 6 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 7 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 8 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 9 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= 13 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 14 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 19 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 20 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 21 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 22 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 23 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 26 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 27 | github.com/go-icap/icap v0.0.0-20151011115316-ca4fad4ebb28 h1:A5Ks5k2Y1Y3/ibkMrBwcn2+qzgWUCxHZsGatPyCVExc= 28 | github.com/go-icap/icap v0.0.0-20151011115316-ca4fad4ebb28/go.mod h1:CtySfVdYEiotF1PcVuY0qvLnk02XbSatTR1WJfnIjSk= 29 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 30 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 31 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 32 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 33 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 34 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 35 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 37 | github.com/hashicorp/go-hclog v0.12.2 h1:F1fdYblUEsxKiailtkhCCG2g4bipEgaHiDc8vffNpD4= 38 | github.com/hashicorp/go-hclog v0.12.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 39 | github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= 40 | github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 h1:PFfGModn55JA0oBsvFghhj0v93me+Ctr3uHC/UmFAls= 41 | github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0= 42 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 43 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 44 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 45 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 46 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 47 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 48 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 49 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 52 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 53 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 54 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 55 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 56 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 57 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 58 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 59 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 60 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 61 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 62 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 63 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 64 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 65 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 66 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 67 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 68 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 69 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 70 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 71 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 74 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 75 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 76 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 77 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 78 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 81 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 82 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 83 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 84 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 85 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 86 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 87 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 88 | github.com/zclconf/go-cty v1.0.0 h1:EWtv3gKe2wPLIB9hQRQJa7k/059oIfAqcEkCNnaVckk= 89 | github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= 90 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 92 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 93 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 94 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 95 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 96 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 100 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= 101 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 102 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 106 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= 111 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= 113 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 115 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 116 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 117 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 118 | golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 119 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 120 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 121 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 123 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 125 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 126 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 127 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= 128 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 129 | gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= 130 | gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 131 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 132 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 133 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 134 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 135 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 136 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 137 | howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 138 | -------------------------------------------------------------------------------- /hack/gen-certs.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Always work from the root of the repo. 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | ROOT_DIR="$(cd "$SCRIPT_DIR"/.. && pwd)" 7 | 8 | # Operate in the certs directory. 9 | cd "$ROOT_DIR/certs" 10 | 11 | if ! command -v openssl > /dev/null 2>&1; then 12 | echo "This script requires openssl to run correctly" 13 | exit 1 14 | fi 15 | 16 | if ! grep -q "v3_ca" /etc/ssl/openssl.cnf; then 17 | echo "You are missing a section in /etc/ssl/openssl.cnf" 18 | echo "To correct this error, add the following section to your openssl.cnf config" 19 | echo "" 20 | echo "[ v3_ca ]" 21 | echo "basicConstraints = critical,CA:TRUE" 22 | echo "subjectKeyIdentifier = hash" 23 | echo "authorityKeyIdentifier = keyid:always,issuer:always" 24 | echo "" 25 | echo "Then rerun this command." 26 | exit 1 27 | fi 28 | 29 | echo "Okay, here we go, let's generate a certificate" 30 | echo "" 31 | openssl req -new \ 32 | -newkey rsa:2048 \ 33 | -sha256 \ 34 | -days 365 \ 35 | -nodes \ 36 | -x509 \ 37 | -extensions v3_ca \ 38 | -keyout ca.pem \ 39 | -out ca.pem \ 40 | -subj "/C=US/ST=California/L=San Francisco/O=HashiCorp/OU=Engineering Services/CN=example.com" 41 | echo "" 42 | 43 | echo "And the public key thereof" 44 | echo "" 45 | openssl x509 -in ca.pem -outform DER -out ca.crt 46 | echo "" 47 | 48 | echo "Okay! That looks good, good certificating." 49 | -------------------------------------------------------------------------------- /hack/local-dev-down.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Always work from the root of the repo. 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | ROOT_DIR="$(cd "$SCRIPT_DIR"/.. && pwd)" 7 | cd "$ROOT_DIR" 8 | 9 | export COMPOSE_PROJECT_NAME="mock_proxy" 10 | docker-compose --file deployments/docker-compose.yml down 11 | -------------------------------------------------------------------------------- /hack/local-dev-up.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Always work from the root of the repo. 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | ROOT_DIR="$(cd "$SCRIPT_DIR"/.. && pwd)" 7 | cd "$ROOT_DIR" 8 | 9 | export COMPOSE_PROJECT_NAME="mock_proxy" 10 | docker-compose --file deployments/docker-compose.yml build 11 | docker-compose --file deployments/docker-compose.yml run client \ 12 | /bin/sh -c ' 13 | update-ca-certificates && \ 14 | dockerize \ 15 | -wait tcp://squid.proxy:8888 \ 16 | -timeout 60s \ 17 | -wait-retry-interval 5s \ 18 | && /bin/bash 19 | ' 20 | -------------------------------------------------------------------------------- /hack/prep-git-mocks.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Always work from the root of the repo. 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | ROOT_DIR="$(cd "$SCRIPT_DIR"/.. && pwd)" 7 | cd "$ROOT_DIR" 8 | 9 | cd "mocks/git/github.com" 10 | for d in * 11 | do 12 | ( cd "$d" && git init && git add . && git commit -m 'Initial Commit' ) 13 | done 14 | -------------------------------------------------------------------------------- /hack/unstage-git-mocks.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Always work from the root of the repo. 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 6 | ROOT_DIR="$(cd "$SCRIPT_DIR"/.. && pwd)" 7 | cd "$ROOT_DIR" 8 | 9 | cd "mocks/git/github.com" 10 | for d in * 11 | do 12 | ( cd "$d" && rm -rf .git ) 13 | done 14 | -------------------------------------------------------------------------------- /mocks/api.github.com/orgs/:org/repos.mock: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1296269, 4 | "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", 5 | "name": "Hello-World", 6 | "full_name": "{{.org}}/Hello-World", 7 | "owner": { 8 | "login": "{{.org}}", 9 | "id": 1, 10 | "node_id": "MDQ6VXNlcjE=", 11 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/{{.org}}", 14 | "html_url": "https://github.com/{{.org}}", 15 | "followers_url": "https://api.github.com/users/{{.org}}/followers", 16 | "following_url": "https://api.github.com/users/{{.org}}/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/{{.org}}/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/{{.org}}/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/{{.org}}/subscriptions", 20 | "organizations_url": "https://api.github.com/users/{{.org}}/orgs", 21 | "repos_url": "https://api.github.com/users/{{.org}}/repos", 22 | "events_url": "https://api.github.com/users/{{.org}}/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/{{.org}}/received_events", 24 | "type": "User", 25 | "site_admin": false 26 | }, 27 | "private": false, 28 | "html_url": "https://github.com/{{.org}}/Hello-World", 29 | "description": "This your first repo!", 30 | "fork": false, 31 | "url": "https://api.github.com/repos/{{.org}}/Hello-World", 32 | "archive_url": "http://api.github.com/repos/{{.org}}/Hello-World/{archive_format}{/ref}", 33 | "assignees_url": "http://api.github.com/repos/{{.org}}/Hello-World/assignees{/user}", 34 | "blobs_url": "http://api.github.com/repos/{{.org}}/Hello-World/git/blobs{/sha}", 35 | "branches_url": "http://api.github.com/repos/{{.org}}/Hello-World/branches{/branch}", 36 | "collaborators_url": "http://api.github.com/repos/{{.org}}/Hello-World/collaborators{/collaborator}", 37 | "comments_url": "http://api.github.com/repos/{{.org}}/Hello-World/comments{/number}", 38 | "commits_url": "http://api.github.com/repos/{{.org}}/Hello-World/commits{/sha}", 39 | "compare_url": "http://api.github.com/repos/{{.org}}/Hello-World/compare/{base}...{head}", 40 | "contents_url": "http://api.github.com/repos/{{.org}}/Hello-World/contents/{+path}", 41 | "contributors_url": "http://api.github.com/repos/{{.org}}/Hello-World/contributors", 42 | "deployments_url": "http://api.github.com/repos/{{.org}}/Hello-World/deployments", 43 | "downloads_url": "http://api.github.com/repos/{{.org}}/Hello-World/downloads", 44 | "events_url": "http://api.github.com/repos/{{.org}}/Hello-World/events", 45 | "forks_url": "http://api.github.com/repos/{{.org}}/Hello-World/forks", 46 | "git_commits_url": "http://api.github.com/repos/{{.org}}/Hello-World/git/commits{/sha}", 47 | "git_refs_url": "http://api.github.com/repos/{{.org}}/Hello-World/git/refs{/sha}", 48 | "git_tags_url": "http://api.github.com/repos/{{.org}}/Hello-World/git/tags{/sha}", 49 | "git_url": "git:github.com/{{.org}}/Hello-World.git", 50 | "issue_comment_url": "http://api.github.com/repos/{{.org}}/Hello-World/issues/comments{/number}", 51 | "issue_events_url": "http://api.github.com/repos/{{.org}}/Hello-World/issues/events{/number}", 52 | "issues_url": "http://api.github.com/repos/{{.org}}/Hello-World/issues{/number}", 53 | "keys_url": "http://api.github.com/repos/{{.org}}/Hello-World/keys{/key_id}", 54 | "labels_url": "http://api.github.com/repos/{{.org}}/Hello-World/labels{/name}", 55 | "languages_url": "http://api.github.com/repos/{{.org}}/Hello-World/languages", 56 | "merges_url": "http://api.github.com/repos/{{.org}}/Hello-World/merges", 57 | "milestones_url": "http://api.github.com/repos/{{.org}}/Hello-World/milestones{/number}", 58 | "notifications_url": "http://api.github.com/repos/{{.org}}/Hello-World/notifications{?since,all,participating}", 59 | "pulls_url": "http://api.github.com/repos/{{.org}}/Hello-World/pulls{/number}", 60 | "releases_url": "http://api.github.com/repos/{{.org}}/Hello-World/releases{/id}", 61 | "ssh_url": "git@github.com:{{.org}}/Hello-World.git", 62 | "stargazers_url": "http://api.github.com/repos/{{.org}}/Hello-World/stargazers", 63 | "statuses_url": "http://api.github.com/repos/{{.org}}/Hello-World/statuses/{sha}", 64 | "subscribers_url": "http://api.github.com/repos/{{.org}}/Hello-World/subscribers", 65 | "subscription_url": "http://api.github.com/repos/{{.org}}/Hello-World/subscription", 66 | "tags_url": "http://api.github.com/repos/{{.org}}/Hello-World/tags", 67 | "teams_url": "http://api.github.com/repos/{{.org}}/Hello-World/teams", 68 | "trees_url": "http://api.github.com/repos/{{.org}}/Hello-World/git/trees{/sha}", 69 | "clone_url": "https://github.com/{{.org}}/Hello-World.git", 70 | "mirror_url": "git:git.example.com/{{.org}}/Hello-World", 71 | "hooks_url": "http://api.github.com/repos/{{.org}}/Hello-World/hooks", 72 | "svn_url": "https://svn.github.com/{{.org}}/Hello-World", 73 | "homepage": "https://github.com", 74 | "language": null, 75 | "forks_count": 9, 76 | "stargazers_count": 80, 77 | "watchers_count": 80, 78 | "size": 108, 79 | "default_branch": "master", 80 | "open_issues_count": 0, 81 | "is_template": true, 82 | "topics": [ 83 | "octocat", 84 | "atom", 85 | "electron", 86 | "api" 87 | ], 88 | "has_issues": true, 89 | "has_projects": true, 90 | "has_wiki": true, 91 | "has_pages": false, 92 | "has_downloads": true, 93 | "archived": false, 94 | "disabled": false, 95 | "visibility": "public", 96 | "pushed_at": "2011-01-26T19:06:43Z", 97 | "created_at": "2011-01-26T19:01:12Z", 98 | "updated_at": "2011-01-26T19:14:43Z", 99 | "permissions": { 100 | "admin": false, 101 | "push": false, 102 | "pull": true 103 | }, 104 | "template_repository": null, 105 | "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", 106 | "subscribers_count": 42, 107 | "network_count": 0, 108 | "license": { 109 | "key": "mit", 110 | "name": "MIT License", 111 | "spdx_id": "MIT", 112 | "url": "https://api.github.com/licenses/mit", 113 | "node_id": "MDc6TGljZW5zZW1pdA==" 114 | } 115 | } 116 | ] -------------------------------------------------------------------------------- /mocks/example.com/index.mock: -------------------------------------------------------------------------------- 1 | Hello {{ .Name }}! 2 | -------------------------------------------------------------------------------- /mocks/git/github.com/example-repo/README.md: -------------------------------------------------------------------------------- 1 | Hello, World! 2 | -------------------------------------------------------------------------------- /mocks/routes.hcl: -------------------------------------------------------------------------------- 1 | route { 2 | host = "example.com" 3 | path = "/" 4 | type = "http" 5 | } 6 | 7 | # You can reach this at any /orgs/$VALUE/repos request, and a substitution will 8 | # be added that replaces {key=org, value=$VALUE} 9 | route { 10 | host = "api.github.com" 11 | path = "/orgs/:org/repos" 12 | type = "http" 13 | } 14 | 15 | route { 16 | host = "github.com" 17 | path = "/example-repo" 18 | type = "git" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "os/signal" 13 | "path/filepath" 14 | "strconv" 15 | "strings" 16 | "syscall" 17 | "time" 18 | 19 | "github.com/go-icap/icap" 20 | "github.com/hashicorp/go-hclog" 21 | "gopkg.in/src-d/go-billy.v4/osfs" 22 | 23 | gitpktline "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" 24 | gitcapability "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" 25 | gittransport "gopkg.in/src-d/go-git.v4/plumbing/transport" 26 | gitserver "gopkg.in/src-d/go-git.v4/plumbing/transport/server" 27 | ) 28 | 29 | const ( 30 | DesiredStatusCodeHeader = "X-Desired-Response-Code" 31 | ) 32 | 33 | // Transformer is an interface that applies some mutation to a mock response. 34 | // To properly implement the Transformer interface, it must be possible to 35 | // "chain" transformations together. They should not make changes that would 36 | // invalidate other transformations. 37 | type Transformer interface { 38 | Transform(r io.Reader) (t io.Reader, err error) 39 | } 40 | 41 | // Option is a configuration option for passing to the MockServer constructor. 42 | // This is used to implement the "Functional Options" pattern: 43 | // https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis 44 | type Option func(*MockServer) error 45 | 46 | // MockServer starts the HTTP and ICAP servers that are required to run the 47 | // mocking system. 48 | type MockServer struct { 49 | mockFilesRoot string 50 | 51 | icapPort int 52 | apiPort int 53 | 54 | RouteConfig RouteConfig 55 | transformers []Transformer 56 | 57 | logger hclog.Logger 58 | } 59 | 60 | // NewMockServer is a creator for a new MockServer. It makes use of functional 61 | // options to provide additional configuration on top of the defaults. 62 | func NewMockServer(options ...Option) (*MockServer, error) { 63 | ms := &MockServer{ 64 | mockFilesRoot: "/mocks", 65 | 66 | icapPort: 11344, 67 | apiPort: 80, 68 | 69 | logger: hclog.NewNullLogger(), 70 | } 71 | 72 | for _, o := range options { 73 | if err := o(ms); err != nil { 74 | return nil, err 75 | } 76 | } 77 | 78 | _, err := os.Open(ms.mockFilesRoot) 79 | if err != nil { 80 | return nil, fmt.Errorf( 81 | "invalid mock file directory %v: %w", ms.mockFilesRoot, err, 82 | ) 83 | } 84 | 85 | rc, err := ParseRoutes(filepath.Join(ms.mockFilesRoot, "routes.hcl")) 86 | if err != nil { 87 | return nil, fmt.Errorf( 88 | "invalid mock routes file %s: %w", 89 | filepath.Join(ms.mockFilesRoot, "routes.hcl"), err, 90 | ) 91 | } 92 | ms.RouteConfig = rc 93 | 94 | return ms, nil 95 | } 96 | 97 | // WithMockRoot is a functional option that changes where MockServer looks for 98 | // mock files. 99 | func WithMockRoot(root string) Option { 100 | return func(m *MockServer) error { 101 | m.mockFilesRoot = root 102 | return nil 103 | } 104 | } 105 | 106 | // WithDefaultVariables is a functional option that sets some default 107 | // transformers. These are used in testing, but can also be used to supply 108 | // "global" values. 109 | func WithDefaultVariables(vars ...*VariableSubstitution) Option { 110 | return func(m *MockServer) error { 111 | for _, v := range vars { 112 | m.addVariableSubstitution(v) 113 | } 114 | return nil 115 | } 116 | } 117 | 118 | // WithAPIPort is a functional option that changes the port the Mock server 119 | // runs its API on. 120 | func WithAPIPort(port int) Option { 121 | return func(m *MockServer) error { 122 | m.apiPort = port 123 | return nil 124 | } 125 | } 126 | 127 | // WithLogger is a functional option that configures the Mock server with a 128 | // given go-hclog Logger. 129 | func WithLogger(logger hclog.Logger) Option { 130 | return func(m *MockServer) error { 131 | if logger == nil { 132 | return fmt.Errorf("cannot call WithLogger with nil Logger, use NewNullLogger instead") 133 | } 134 | m.logger = logger 135 | return nil 136 | } 137 | } 138 | 139 | // Serve starts the actual servers and handlers, then waits for them to exit 140 | // or for an Interrupt signal. 141 | func (ms *MockServer) Serve() error { 142 | // ICAP makes use of these handlers on the DefaultServeMux 143 | http.HandleFunc("/", ms.mockHandler) 144 | icap.HandleFunc("/icap", ms.interception) 145 | 146 | // We also create a custom ServeMux mock-proxy for API endpoints 147 | apiMux := http.NewServeMux() 148 | apiMux.HandleFunc("/substitution-variables", ms.substitutionVariableHandler) 149 | 150 | icapErrC := make(chan error) 151 | apiErrC := make(chan error) 152 | 153 | // We also want to gracefully stop when the OS asks us to 154 | killSignal := make(chan os.Signal, 1) 155 | signal.Notify(killSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 156 | 157 | go func() { 158 | ms.logger.Info("starting icap server on", "port", ms.icapPort) 159 | icapErrC <- icap.ListenAndServe(fmt.Sprintf(":%d", ms.icapPort), nil) 160 | }() 161 | go func() { 162 | ms.logger.Info("starting api server on", "port", ms.apiPort) 163 | apiErrC <- http.ListenAndServe(fmt.Sprintf(":%d", ms.apiPort), apiMux) 164 | }() 165 | 166 | for { 167 | select { 168 | case err := <-icapErrC: 169 | if err != nil { 170 | ms.logger.Error("exiting due to icap error", "error", err.Error()) 171 | } 172 | return err 173 | case err := <-apiErrC: 174 | if err != nil { 175 | ms.logger.Error("exiting due to api error", "error", err.Error()) 176 | } 177 | return err 178 | case sig := <-killSignal: 179 | ms.logger.Info("exiting due to os signal", "signal", sig) 180 | return nil 181 | } 182 | } 183 | } 184 | 185 | // interception runs the ICAP handler. When a request is input, we either: 186 | // 1. If it matches a known "mocked" host, injects a response. 187 | // 2. If it does not, returns a 204 which allows the request unmodifed. 188 | func (ms *MockServer) interception(w icap.ResponseWriter, req *icap.Request) { 189 | switch req.Method { 190 | case "OPTIONS": 191 | h := w.Header() 192 | 193 | h.Set("Methods", "REQMOD") 194 | h.Set("Allow", "204") 195 | h.Set("Preview", "0") 196 | h.Set("Transfer-Preview", "*") 197 | w.WriteHeader(http.StatusOK, nil, false) 198 | case "REQMOD": 199 | ms.logger.Info("REQMOD request for", "host", req.Request.Host) 200 | ms.logger.Info("REQMOD request URL", "url", fmt.Sprintf("%+v", req.Request.URL)) 201 | 202 | route, _ := ms.RouteConfig.MatchRoute(req.Request.URL) 203 | if route != nil { 204 | icap.ServeLocally(w, req) 205 | } else { 206 | // Return the request unmodified. 207 | w.WriteHeader(http.StatusNoContent, nil, false) 208 | } 209 | default: 210 | // This ICAP server is only able to handle REQMOD, will not be using 211 | // RESMOD mode. 212 | w.WriteHeader(http.StatusMethodNotAllowed, nil, false) 213 | ms.logger.Error("invalid request method to ICAP server", "method", req.Method) 214 | } 215 | } 216 | 217 | // mockHandler receives requests and based on them, returns one of the known 218 | // .mock files, after running it through the configured Transformers. 219 | func (ms *MockServer) mockHandler(w http.ResponseWriter, r *http.Request) { 220 | ms.logger.Info("MOCK request", "url", r.URL.String()) 221 | 222 | successCode := http.StatusOK 223 | successCodeString := r.Header.Get(DesiredStatusCodeHeader) 224 | if successCodeString != "" { 225 | var err error 226 | successCode, err = strconv.Atoi(successCodeString) 227 | if err != nil { 228 | ms.logger.Error(fmt.Sprintf("failed to parse %s", DesiredStatusCodeHeader), 229 | "error", err.Error()) 230 | http.Error(w, fmt.Sprintf("failed to parse %s: %s", 231 | DesiredStatusCodeHeader, err.Error()), http.StatusInternalServerError) 232 | } 233 | } 234 | 235 | route, err := ms.RouteConfig.MatchRoute(r.URL) 236 | if err != nil || route == nil { 237 | if err == nil { 238 | err = fmt.Errorf("found no matching route for %s", r.URL.String()) 239 | } 240 | ms.logger.Error("failed to find a matching route", "error", err.Error()) 241 | http.Error(w, fmt.Sprintf("failed to find a matching route: %s", 242 | err.Error()), http.StatusInternalServerError) 243 | return 244 | } 245 | 246 | ms.logger.Info("parsing URL", "route", fmt.Sprintf("%+v", route), "url", r.URL) 247 | path, localTransformers, err := route.ParseURL(r.URL) 248 | if err != nil { 249 | ms.logger.Error("failed to parse mock URL for route", "error", err.Error()) 250 | http.Error(w, fmt.Sprintf("failed to parse mock URL for route: %s", 251 | err.Error()), http.StatusInternalServerError) 252 | } 253 | ms.logger.Info("parsed URL produced path", "path", path) 254 | 255 | switch route.Type { 256 | case "http": 257 | ms.logger.Info("detected an http mock attempt") 258 | fileName := filepath.Join(ms.mockFilesRoot, path) 259 | mock, err := os.Open(fileName) 260 | if err != nil { 261 | ms.logger.Error("failed opening mock file", "error", err.Error()) 262 | http.Error(w, fmt.Sprintf("failed opening mock file: %s", err.Error()), http.StatusNotFound) 263 | return 264 | } 265 | 266 | // Apply the configured transformations to the mock file 267 | transformers := append(ms.transformers, localTransformers...) 268 | var res io.Reader = mock 269 | for _, t := range transformers { 270 | res, err = t.Transform(res) 271 | if err != nil { 272 | ms.logger.Error("error applying transformations", "error", err.Error()) 273 | http.Error( 274 | w, 275 | fmt.Sprintf("error applying transformations: %s", err.Error()), 276 | http.StatusInternalServerError, 277 | ) 278 | return 279 | } 280 | } 281 | 282 | w.WriteHeader(successCode) 283 | _, err = io.Copy(w, res) 284 | if err != nil { 285 | ms.logger.Error("failed copying to response", "error", err.Error()) 286 | http.Error( 287 | w, 288 | "failed copying to response", 289 | http.StatusInternalServerError, 290 | ) 291 | return 292 | } 293 | case "git": 294 | ms.logger.Info("detected a git clone attempt") 295 | 296 | mockFS := osfs.New(filepath.Join(ms.mockFilesRoot)) 297 | loader := gitserver.NewFilesystemLoader( 298 | mockFS, 299 | ) 300 | gitServer := gitserver.NewServer(loader) 301 | 302 | ep, err := gittransport.NewEndpoint(path) 303 | if err != nil { 304 | ms.logger.Error("failed creating transport", "error", err.Error()) 305 | http.Error(w, fmt.Sprintf("failed creating transport: %s", 306 | err.Error()), http.StatusInternalServerError) 307 | return 308 | } 309 | 310 | fs, _ := mockFS.Chroot(ep.Path) 311 | ms.logger.Info("attempting to load local git repo", "filepath", fmt.Sprintf("%+v", fs.Root())) 312 | 313 | sess, err := gitServer.NewUploadPackSession(ep, nil) 314 | if err != nil { 315 | ms.logger.Error("failed creating git-upload-pack session", "error", err.Error()) 316 | http.Error(w, fmt.Sprintf("failed creating git-upload-pack session: %s", 317 | err.Error()), http.StatusInternalServerError) 318 | return 319 | } 320 | defer sess.Close() 321 | 322 | if strings.HasSuffix(r.URL.String(), "info/refs?service=git-upload-pack") { 323 | ms.logger.Info("detected a reference advertisement request") 324 | 325 | refs, err := sess.AdvertisedReferences() 326 | if err != nil { 327 | ms.logger.Error("failed to load reference advertisement", "error", err.Error()) 328 | http.Error(w, fmt.Sprintf("failed to load reference advertisement: %s", 329 | err.Error()), http.StatusInternalServerError) 330 | return 331 | } 332 | 333 | // Add the Shallow capability 334 | if err := refs.Capabilities.Add(gitcapability.Shallow); err != nil { 335 | ms.logger.Error("failed to add shallow capability", "error", err.Error()) 336 | http.Error(w, fmt.Sprintf("failed to add shallow capability: %s", 337 | err.Error()), http.StatusInternalServerError) 338 | return 339 | } 340 | 341 | // To successfully interact with smart git clone, we must set a 342 | // prefix saying which service this is. 343 | refs.Prefix = [][]byte{ 344 | []byte( 345 | fmt.Sprintf("# service=%s", gittransport.UploadPackServiceName), 346 | ), 347 | // Note: This is a semantically significant flush, and I don't 348 | // really know why, but do not touch. 349 | gitpktline.Flush, 350 | } 351 | w.Header().Add("Content-Type", "application/x-git-upload-pack-advertisement") 352 | w.Header().Add("Cache-Control", "no-cache") 353 | 354 | w.WriteHeader(successCode) 355 | if err := refs.Encode(w); err != nil { 356 | ms.logger.Error("failed writing response", "error", err.Error()) 357 | http.Error(w, fmt.Sprintf("failed writing response: %s", 358 | err.Error()), http.StatusInternalServerError) 359 | return 360 | } 361 | return 362 | } else if strings.HasSuffix(r.URL.String(), "git-upload-pack") { 363 | ms.logger.Info("detected a git-upload-pack request") 364 | 365 | ctx, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second) 366 | defer cancelFunc() 367 | 368 | w.Header().Add("Content-Type", "application/x-git-upload-pack-result") 369 | w.Header().Add("Cache-Control", "no-cache") 370 | 371 | // Originally tried implementing `upload-pack` in `go-git`, but 372 | // this got complicated, and git --stateless-rpc serves this purpose 373 | // well. 374 | cmd := exec.CommandContext(ctx, "git", "upload-pack", "--stateless-rpc", fs.Root()) 375 | 376 | // Set the stdin to read from the request, and the stdout to write 377 | // to the response. 378 | cmd.Stdin = r.Body 379 | cmd.Stdout = w 380 | 381 | if err := cmd.Start(); err != nil { 382 | ms.logger.Error("error starting git upload-pack", "error", err.Error()) 383 | http.Error(w, fmt.Sprintf("error starting git upload-pack: %s", 384 | err.Error()), 500) 385 | return 386 | } 387 | 388 | if err := cmd.Wait(); err != nil { 389 | if err != nil { 390 | // For shallow clones, the exit code will be 128, if this is 391 | // the case, don't error. 392 | var exerr *exec.ExitError 393 | if errors.As(err, &exerr) { 394 | if exerr.ExitCode() != 128 { 395 | ms.logger.Error("error running git upload-pack", "error", err.Error()) 396 | http.Error(w, fmt.Sprintf("error running git upload-pack: %s", 397 | err.Error()), 500) 398 | return 399 | } 400 | } else { 401 | ms.logger.Error("error running git upload-pack", "error", err.Error()) 402 | http.Error(w, fmt.Sprintf("error running git upload-pack: %s", 403 | err.Error()), 500) 404 | return 405 | } 406 | } 407 | } 408 | 409 | return 410 | } else { 411 | ms.logger.Error("detected an unknown git request type", "url", r.URL.String()) 412 | http.Error(w, fmt.Sprintf("detected an unknown git request type: %s", 413 | r.URL.String()), http.StatusNotFound) 414 | return 415 | } 416 | default: 417 | ms.logger.Error("detected an unknown route type", "url", r.URL.String()) 418 | http.Error(w, fmt.Sprintf("detected an unknown route type: %s", 419 | r.URL.String()), http.StatusNotFound) 420 | return 421 | } 422 | } 423 | 424 | // substitutionVariableHandler can receive a GET or POST request. 425 | // GET) Returns a JSON representation of the current variable substitutions. 426 | // POST) Adds a new variable substitution based on multi-part form values. 427 | // curl -X POST -F "key=A" -F "value=B" squid.proxy/substitution-variables 428 | func (ms *MockServer) substitutionVariableHandler( 429 | w http.ResponseWriter, 430 | r *http.Request, 431 | ) { 432 | switch r.Method { 433 | case http.MethodGet: 434 | resp := []struct { 435 | Key string `json:"key"` 436 | Value string `json:"value"` 437 | }{} 438 | 439 | for _, transform := range ms.transformers { 440 | switch tr := transform.(type) { 441 | case *VariableSubstitution: 442 | resp = append(resp, struct { 443 | Key string `json:"key"` 444 | Value string `json:"value"` 445 | }{ 446 | Key: tr.key, 447 | Value: tr.value, 448 | }) 449 | } 450 | } 451 | 452 | js, err := json.Marshal(resp) 453 | if err != nil { 454 | http.Error(w, err.Error(), http.StatusInternalServerError) 455 | return 456 | } 457 | 458 | w.Header().Set("Content-Type", "application/json") 459 | _, _ = w.Write(js) 460 | case http.MethodPost: 461 | err := r.ParseMultipartForm(4096) 462 | if err != nil { 463 | http.Error( 464 | w, 465 | fmt.Sprintf("error parsing input form: %s", err.Error()), 466 | http.StatusInternalServerError, 467 | ) 468 | return 469 | } 470 | 471 | key := r.PostForm.Get("key") 472 | value := r.PostForm.Get("value") 473 | 474 | if key == "" || value == "" { 475 | http.Error( 476 | w, 477 | "both key and value must be supplied", 478 | http.StatusBadRequest, 479 | ) 480 | return 481 | } 482 | 483 | vs, err := NewVariableSubstitution(key, value) 484 | if err != nil { 485 | http.Error(w, err.Error(), http.StatusInternalServerError) 486 | return 487 | } 488 | 489 | ms.addVariableSubstitution(vs) 490 | w.WriteHeader(http.StatusOK) 491 | } 492 | } 493 | 494 | // addVariableSubstitution adds a new variable substitution. It iterates the 495 | // currently configured Transformers, and if an existing substitution for a 496 | // variable with the new key already exists, replaces it instead of having two. 497 | func (ms *MockServer) addVariableSubstitution( 498 | new *VariableSubstitution, 499 | ) { 500 | var replaced bool 501 | for idx, transform := range ms.transformers { 502 | switch tr := transform.(type) { 503 | case *VariableSubstitution: 504 | if tr.key == new.key { 505 | ms.transformers[idx] = new 506 | replaced = true 507 | } 508 | } 509 | } 510 | if !replaced { 511 | ms.transformers = append(ms.transformers, new) 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /pkg/mock/mock_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "mime/multipart" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestNewMockServer(t *testing.T) { 16 | tcs := []struct { 17 | name string 18 | options []Option 19 | want *MockServer 20 | }{ 21 | { 22 | name: "simple", 23 | options: []Option{ 24 | WithMockRoot("testdata/"), 25 | }, 26 | want: &MockServer{ 27 | apiPort: 80, 28 | icapPort: 11344, 29 | 30 | mockFilesRoot: "testdata/", 31 | }, 32 | }, 33 | { 34 | name: "alternate API port", 35 | options: []Option{ 36 | WithMockRoot("testdata/"), 37 | WithAPIPort(39980), 38 | }, 39 | want: &MockServer{ 40 | apiPort: 39980, 41 | icapPort: 11344, 42 | 43 | mockFilesRoot: "testdata/", 44 | }, 45 | }, 46 | } 47 | 48 | for _, tc := range tcs { 49 | tc := tc // capture range variable 50 | t.Run(tc.name, func(t *testing.T) { 51 | t.Parallel() 52 | 53 | got, err := NewMockServer(tc.options...) 54 | require.Nil(t, err) 55 | 56 | assert.Equal(t, tc.want.apiPort, got.apiPort) 57 | assert.Equal(t, tc.want.icapPort, got.icapPort) 58 | assert.Equal(t, tc.want.mockFilesRoot, got.mockFilesRoot) 59 | }) 60 | } 61 | } 62 | 63 | func TestMockServerMockHandler(t *testing.T) { 64 | tcs := []struct { 65 | name string 66 | options []Option 67 | url string 68 | headers map[string]string 69 | want string 70 | wantCode int 71 | }{ 72 | { 73 | name: "simple", 74 | url: "http://example.com/simple", 75 | options: []Option{ 76 | WithMockRoot("testdata/"), 77 | }, 78 | want: "Hello, World!\n", 79 | }, 80 | { 81 | name: "substitutions", 82 | url: "http://example.com/substitutions", 83 | options: []Option{ 84 | WithMockRoot("testdata/"), 85 | WithDefaultVariables( 86 | &VariableSubstitution{key: "name", value: "Davenport"}, 87 | ), 88 | }, 89 | want: "Hello, Davenport!\n", 90 | }, 91 | { 92 | name: "dynamic url", 93 | url: "http://example.com/users/russell", 94 | options: []Option{ 95 | WithMockRoot("testdata/"), 96 | }, 97 | want: "russell\n", 98 | }, 99 | { 100 | name: "url encoded substitution variable", 101 | url: "http://example.com/users/url%2Fencoded", 102 | options: []Option{ 103 | WithMockRoot("testdata/"), 104 | }, 105 | want: "url/encoded\n", 106 | }, 107 | { 108 | name: "url encoded alternative characters", 109 | url: "http://example.com/users/url%2Fencoded%2Dvalue", 110 | options: []Option{ 111 | WithMockRoot("testdata/"), 112 | }, 113 | want: "url/encoded-value\n", 114 | }, 115 | { 116 | name: "with X-Desired-Response-Code", 117 | url: "http://example.com/users/notexists", 118 | options: []Option{ 119 | WithMockRoot("testdata/"), 120 | }, 121 | headers: map[string]string{ 122 | "X-Desired-Response-Code": "404", 123 | }, 124 | want: "notexists\n", 125 | wantCode: 404, 126 | }, 127 | } 128 | 129 | for _, tc := range tcs { 130 | tc := tc // capture range variable 131 | t.Run(tc.name, func(t *testing.T) { 132 | t.Parallel() 133 | 134 | ms, err := NewMockServer(tc.options...) 135 | require.Nil(t, err) 136 | 137 | req, err := http.NewRequest(http.MethodGet, tc.url, nil) 138 | require.Nil(t, err) 139 | 140 | for k, v := range tc.headers { 141 | req.Header.Add(k, v) 142 | } 143 | 144 | recorder := httptest.NewRecorder() 145 | 146 | ms.mockHandler(recorder, req) 147 | 148 | wantCode := http.StatusOK 149 | if tc.wantCode != 0 { 150 | wantCode = tc.wantCode 151 | } 152 | assert.Equal(t, wantCode, recorder.Result().StatusCode) 153 | 154 | gotBytes, err := ioutil.ReadAll(recorder.Result().Body) 155 | require.Nil(t, err) 156 | got := string(gotBytes) 157 | 158 | assert.Equal(t, tc.want, got) 159 | }) 160 | } 161 | } 162 | 163 | func TestMockServerSubstitutionVariableHandler_GET(t *testing.T) { 164 | tcs := []struct { 165 | name string 166 | options []Option 167 | want string 168 | }{ 169 | { 170 | name: "simple", 171 | options: []Option{ 172 | WithMockRoot("testdata/"), 173 | WithDefaultVariables( 174 | &VariableSubstitution{key: "name", value: "Davenport"}, 175 | ), 176 | }, 177 | want: `[{"key":"name","value":"Davenport"}]`, 178 | }, 179 | { 180 | name: "multi", 181 | options: []Option{ 182 | WithMockRoot("testdata/"), 183 | WithDefaultVariables( 184 | &VariableSubstitution{key: "name", value: "Davenport"}, 185 | &VariableSubstitution{key: "name", value: "Barry"}, 186 | &VariableSubstitution{key: "foo", value: "bar"}, 187 | ), 188 | }, 189 | want: `[{"key":"name","value":"Barry"},{"key":"foo","value":"bar"}]`, 190 | }, 191 | } 192 | 193 | for _, tc := range tcs { 194 | tc := tc // capture range variable 195 | t.Run(tc.name, func(t *testing.T) { 196 | t.Parallel() 197 | 198 | ms, err := NewMockServer(tc.options...) 199 | require.Nil(t, err) 200 | 201 | req, err := http.NewRequest(http.MethodGet, "", nil) 202 | require.Nil(t, err) 203 | 204 | recorder := httptest.NewRecorder() 205 | 206 | ms.substitutionVariableHandler(recorder, req) 207 | 208 | assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) 209 | 210 | gotBytes, err := ioutil.ReadAll(recorder.Result().Body) 211 | require.Nil(t, err) 212 | got := string(gotBytes) 213 | 214 | assert.Equal(t, tc.want, got) 215 | }) 216 | } 217 | } 218 | 219 | func TestMockServerSubstitutionVariableHandler_POST(t *testing.T) { 220 | tcs := []struct { 221 | name string 222 | options []Option 223 | key string 224 | value string 225 | want []Transformer 226 | }{ 227 | { 228 | name: "simple", 229 | key: "name", 230 | value: "Davenport", 231 | options: []Option{ 232 | WithMockRoot("testdata/"), 233 | }, 234 | want: []Transformer{ 235 | &VariableSubstitution{key: "name", value: "Davenport"}, 236 | }, 237 | }, 238 | { 239 | name: "replace", 240 | key: "name", 241 | value: "Barry", 242 | options: []Option{ 243 | WithMockRoot("testdata/"), 244 | WithDefaultVariables( 245 | &VariableSubstitution{key: "name", value: "Davenport"}, 246 | ), 247 | }, 248 | want: []Transformer{ 249 | &VariableSubstitution{key: "name", value: "Barry"}, 250 | }, 251 | }, 252 | { 253 | name: "add", 254 | key: "foo", 255 | value: "bar", 256 | options: []Option{ 257 | WithMockRoot("testdata/"), 258 | WithDefaultVariables( 259 | &VariableSubstitution{key: "name", value: "Davenport"}, 260 | ), 261 | }, 262 | want: []Transformer{ 263 | &VariableSubstitution{key: "name", value: "Davenport"}, 264 | &VariableSubstitution{key: "foo", value: "bar"}, 265 | }, 266 | }, 267 | } 268 | 269 | for _, tc := range tcs { 270 | tc := tc // capture range variable 271 | t.Run(tc.name, func(t *testing.T) { 272 | t.Parallel() 273 | 274 | ms, err := NewMockServer(tc.options...) 275 | require.Nil(t, err) 276 | 277 | var formBody bytes.Buffer 278 | formWriter := multipart.NewWriter(&formBody) 279 | _ = formWriter.WriteField("key", tc.key) 280 | _ = formWriter.WriteField("value", tc.value) 281 | formWriter.Close() 282 | 283 | req, err := http.NewRequest(http.MethodPost, "", &formBody) 284 | require.Nil(t, err) 285 | req.Header.Set("Content-Type", formWriter.FormDataContentType()) 286 | 287 | recorder := httptest.NewRecorder() 288 | 289 | ms.substitutionVariableHandler(recorder, req) 290 | 291 | if !assert.Equal(t, http.StatusOK, recorder.Result().StatusCode) { 292 | resBytes, err := ioutil.ReadAll(recorder.Result().Body) 293 | require.Nil(t, err) 294 | res := string(resBytes) 295 | require.Fail(t, res) 296 | } 297 | 298 | got := ms.transformers 299 | assert.Equal(t, tc.want, got) 300 | }) 301 | } 302 | } 303 | 304 | func TestMockServerAddVariableSubstitution(t *testing.T) { 305 | tcs := []struct { 306 | name string 307 | options []Option 308 | substitutions []*VariableSubstitution 309 | want []Transformer 310 | }{ 311 | { 312 | name: "simple", 313 | options: []Option{ 314 | WithMockRoot("testdata/"), 315 | }, 316 | substitutions: []*VariableSubstitution{ 317 | {key: "foo", value: "bar"}, 318 | }, 319 | want: []Transformer{ 320 | &VariableSubstitution{key: "foo", value: "bar"}, 321 | }, 322 | }, 323 | { 324 | name: "adding with different key adds", 325 | options: []Option{ 326 | WithMockRoot("testdata/"), 327 | }, 328 | substitutions: []*VariableSubstitution{ 329 | {key: "foo", value: "bar"}, 330 | {key: "bing", value: "baz"}, 331 | }, 332 | want: []Transformer{ 333 | &VariableSubstitution{key: "foo", value: "bar"}, 334 | &VariableSubstitution{key: "bing", value: "baz"}, 335 | }, 336 | }, 337 | { 338 | name: "adding with same key overrides", 339 | options: []Option{ 340 | WithMockRoot("testdata/"), 341 | }, 342 | substitutions: []*VariableSubstitution{ 343 | {key: "foo", value: "bar"}, 344 | {key: "foo", value: "baz"}, 345 | }, 346 | want: []Transformer{ 347 | &VariableSubstitution{key: "foo", value: "baz"}, 348 | }, 349 | }, 350 | } 351 | 352 | for _, tc := range tcs { 353 | tc := tc // capture range variable 354 | t.Run(tc.name, func(t *testing.T) { 355 | t.Parallel() 356 | 357 | ms, err := NewMockServer(tc.options...) 358 | require.Nil(t, err) 359 | 360 | for _, s := range tc.substitutions { 361 | ms.addVariableSubstitution(s) 362 | } 363 | 364 | got := ms.transformers 365 | assert.Equal(t, tc.want, got) 366 | }) 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /pkg/mock/route.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/hashicorp/hcl2/gohcl" 13 | "github.com/hashicorp/hcl2/hclparse" 14 | ) 15 | 16 | // The Route struct represents a single mocked route. 17 | type Route struct { 18 | Host string `hcl:"host"` 19 | Path string `hcl:"path"` 20 | Type string `hcl:"type"` 21 | } 22 | 23 | // RouteConfig is a type alias for many Routes. 24 | type RouteConfig []*Route 25 | 26 | // RouteConfigHCL is used for converting HCL Blocks to RouteConfig. 27 | type RouteConfigHCL struct { 28 | RouteConfig RouteConfig `hcl:"route,block"` 29 | } 30 | 31 | // ParseRoutes parses an input Routes file, using HCL2, into RouteConfig. 32 | func ParseRoutes(inFile string) (RouteConfig, error) { 33 | input, err := os.Open(inFile) 34 | if err != nil { 35 | return []*Route{}, fmt.Errorf( 36 | "error in ParseRoutes opening config file: %w", err, 37 | ) 38 | } 39 | defer input.Close() 40 | 41 | src, err := ioutil.ReadAll(input) 42 | if err != nil { 43 | return []*Route{}, fmt.Errorf( 44 | "error in ParseRoutes reading input `%s`: %w", inFile, err, 45 | ) 46 | } 47 | 48 | parser := hclparse.NewParser() 49 | srcHCL, diag := parser.ParseHCL(src, inFile) 50 | if diag.HasErrors() { 51 | return []*Route{}, fmt.Errorf( 52 | "error in ParseRoutes parsing HCL: %w", diag, 53 | ) 54 | } 55 | 56 | rc := &RouteConfigHCL{} 57 | if diag := gohcl.DecodeBody(srcHCL.Body, nil, rc); diag.HasErrors() { 58 | return []*Route{}, fmt.Errorf( 59 | "error in ParseRoutes decoding HCL configuration: %w", diag, 60 | ) 61 | } 62 | 63 | // Return an instantiated RouteConfig instead of a nil pointer. 64 | if rc.RouteConfig == nil { 65 | rc.RouteConfig = RouteConfig{} 66 | } 67 | return rc.RouteConfig, nil 68 | } 69 | 70 | // ParseURL is used by a single Route to convert that route to a filepath, a 71 | // list of transforms created by dynamic URLs, and an error. This should only 72 | // by used on a URL that the Route Matches, as determined below. 73 | func (r *Route) ParseURL(in *url.URL) (string, []Transformer, error) { 74 | // Parse r.Host as an HTTP URL, then take its Hostname to strip ports, the 75 | // custom port breaks paths down the road. 76 | u, err := url.Parse("http://" + r.Host) 77 | if err != nil { 78 | return "", []Transformer{}, fmt.Errorf("error parsing route Host: %w", err) 79 | } 80 | routeHostname := u.Hostname() 81 | 82 | switch r.Type { 83 | case "http": 84 | // An early escape for empty paths 85 | if r.Path == "" || r.Path == "/" { 86 | return fmt.Sprintf("%s/index.mock", routeHostname), []Transformer{}, nil 87 | } 88 | 89 | subs, err := findSubstitutions(r.Path, in.EscapedPath()) 90 | if err != nil { 91 | return "", []Transformer{}, 92 | fmt.Errorf("error performing substitutions: %w", err) 93 | } 94 | 95 | return fmt.Sprintf("%s%s.mock", routeHostname, r.Path), subs, nil 96 | case "git": 97 | // At this time, you can't template anything about git repos, because 98 | // of how references work. 99 | return filepath.Join("/git", routeHostname, r.Path, ".git"), []Transformer{}, nil 100 | default: 101 | return "", []Transformer{}, fmt.Errorf("unknown route type %s", r.Type) 102 | } 103 | } 104 | 105 | // findSubstitutions is a helper function that abstracts out some pretty nasty 106 | // regexp logic. In short, take a dynamic URL, and a templating Path from the 107 | // Route, and convert the dynamic URL to a set of transformations with the 108 | // :foo values turned into keys and the actual values as values. 109 | // Template: /mypath/:foo/bar/:baz 110 | // Input: /mypath/1/bar/2 111 | // Output: []VariableSubstitution{{key: foo, value: 1},{key: baz, value: 2}} 112 | func findSubstitutions(tmplPath, inputPath string) ([]Transformer, error) { 113 | // First, generate a regexp with capture groups to find everywhere the 114 | // Route Path has a /:foo/ or /:foo value. 115 | pathSubRegexp := regexp.MustCompile(`(\/:\w+(?:\/|\z))+`) 116 | 117 | // An early exit here, if no matches, we can bail. 118 | pathMatches := pathSubRegexp.FindAllString(tmplPath, -1) 119 | if len(pathMatches) == 0 { 120 | return []Transformer{}, nil 121 | } 122 | 123 | // Next, use those captured segments to generate a new regexp with a 124 | // named capture group at each of those locations. 125 | captureRegexpString := fmt.Sprintf(`\A%s\z`, regexp.QuoteMeta(tmplPath)) 126 | for _, pm := range pathMatches { 127 | var cg string 128 | if strings.HasSuffix(pm, "/") { 129 | cg = fmt.Sprintf(`\/(?P<%s>\S+)\/`, strings.Trim(pm, "/:")) 130 | } else { 131 | cg = fmt.Sprintf(`\/(?P<%s>\S+)`, strings.TrimLeft(pm, "/:")) 132 | } 133 | captureRegexpString = strings.Replace(captureRegexpString, regexp.QuoteMeta(pm), cg, 1) 134 | } 135 | captureRegexp, err := regexp.Compile(captureRegexpString) 136 | if err != nil { 137 | return []Transformer{}, fmt.Errorf("error generating capture group regexp: %w", err) 138 | } 139 | 140 | // Finally, generate transformers using the capture groups to create names. 141 | cgMatches := captureRegexp.FindStringSubmatch(inputPath) 142 | 143 | // If there aren't enough matches to fulfil the capture, error. 144 | if len(cgMatches) != len(captureRegexp.SubexpNames()) { 145 | return []Transformer{}, fmt.Errorf("insufficient capture groups detected") 146 | } 147 | 148 | transformers := []Transformer{} 149 | for i, name := range captureRegexp.SubexpNames() { 150 | if i != 0 && name != "" { 151 | val, _ := url.PathUnescape(cgMatches[i]) 152 | transformers = append(transformers, &VariableSubstitution{ 153 | key: name, value: val, 154 | }) 155 | } 156 | } 157 | 158 | return transformers, nil 159 | } 160 | 161 | // match is a helper function that says if a single Route matches a single URL. 162 | func (r *Route) match(in *url.URL) bool { 163 | // Easy case, if the hosts don't match, they don't match 164 | if r.Host != in.Host { 165 | return false 166 | } 167 | 168 | switch r.Type { 169 | case "http": 170 | // Another easy out, if the Paths already match, then true. 171 | if r.Path == in.Path || (r.Path == "" && in.Path == "/") { 172 | return true 173 | } 174 | 175 | // If this satisfies the input subsititution algorithm, go with it. 176 | subs, err := findSubstitutions(r.Path, in.EscapedPath()) 177 | return err == nil && len(subs) != 0 178 | case "git": 179 | pathRequest := in.Path 180 | if len(in.RawQuery) != 0 { 181 | pathRequest = fmt.Sprintf("%s?%s", pathRequest, in.RawQuery) 182 | } 183 | switch pathRequest { 184 | case fmt.Sprintf("%s/info/refs?service=git-upload-pack", r.Path): 185 | return true 186 | case fmt.Sprintf("%s/git-upload-pack", r.Path): 187 | return true 188 | default: 189 | return false 190 | } 191 | default: 192 | // This is a bit oversimplified, but nice not to have to return an 193 | // error from this function. 194 | return false 195 | } 196 | } 197 | 198 | // MatchRoute returns the Route from a list of Routes that matches a given 199 | // input URL. 200 | func (rc RouteConfig) MatchRoute(in *url.URL) (*Route, error) { 201 | var match *Route 202 | var specificity int 203 | for _, route := range rc { 204 | if route.match(in) { 205 | // "specificity" is a measure of how many route components match 206 | // between the input and the matching route. 207 | currentSpecificity := len(strings.Split(route.Path, "/")) 208 | 209 | // An equally specific match is an error. Overlapping routes of 210 | // this type cannot be easily chosen between. 211 | if currentSpecificity == specificity { 212 | return nil, fmt.Errorf("multiple routes matched input: %s", in.String()) 213 | } 214 | 215 | // A more specific match replaces the current match. 216 | if currentSpecificity > specificity { 217 | specificity = currentSpecificity 218 | match = route 219 | } 220 | 221 | // A less specific match is ignored. 222 | } 223 | } 224 | 225 | return match, nil 226 | } 227 | -------------------------------------------------------------------------------- /pkg/mock/route_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParseURL(t *testing.T) { 12 | tcs := []struct { 13 | name string 14 | route *Route 15 | url string 16 | wantPath string 17 | wantTransformers []Transformer 18 | }{ 19 | { 20 | name: "index", 21 | route: &Route{ 22 | Host: "example.com", 23 | Path: "/", 24 | Type: "http", 25 | }, 26 | url: "http://example.com", 27 | wantPath: "example.com/index.mock", 28 | wantTransformers: []Transformer{}, 29 | }, 30 | { 31 | name: "simple", 32 | route: &Route{ 33 | Host: "example.com", 34 | Path: "/foo/bar", 35 | Type: "http", 36 | }, 37 | url: "http://example.com/foo/bar", 38 | wantPath: "example.com/foo/bar.mock", 39 | wantTransformers: []Transformer{}, 40 | }, 41 | { 42 | name: "with one transform", 43 | route: &Route{ 44 | Host: "example.com", 45 | Path: "/users/:foo/bars", 46 | Type: "http", 47 | }, 48 | url: "http://example.com/users/russell/bars", 49 | wantPath: "example.com/users/:foo/bars.mock", 50 | wantTransformers: []Transformer{ 51 | &VariableSubstitution{key: "foo", value: "russell"}, 52 | }, 53 | }, 54 | { 55 | name: "with multiple transforms", 56 | route: &Route{ 57 | Host: "example.com", 58 | Path: "/users/:user/settings/:setting/value", 59 | Type: "http", 60 | }, 61 | url: "http://example.com/users/russell/settings/locale/value", 62 | wantPath: "example.com/users/:user/settings/:setting/value.mock", 63 | wantTransformers: []Transformer{ 64 | &VariableSubstitution{key: "user", value: "russell"}, 65 | &VariableSubstitution{key: "setting", value: "locale"}, 66 | }, 67 | }, 68 | { 69 | name: "git", 70 | route: &Route{ 71 | Host: "github.com", 72 | Path: "example-repo", 73 | Type: "git", 74 | }, 75 | url: "http://github.com/example-repo", 76 | wantPath: "/git/github.com/example-repo/.git", 77 | wantTransformers: []Transformer{}, 78 | }, 79 | { 80 | name: "git strip ports", 81 | route: &Route{ 82 | Host: "gitlab.test:31080", 83 | Path: "/test/test-project", 84 | Type: "git", 85 | }, 86 | url: "http://gitlab.test:31080/test/test-project", 87 | wantPath: "/git/gitlab.test/test/test-project/.git", 88 | wantTransformers: []Transformer{}, 89 | }, 90 | } 91 | 92 | for _, tc := range tcs { 93 | tc := tc // capture range variable 94 | t.Run(tc.name, func(t *testing.T) { 95 | t.Parallel() 96 | 97 | testURL, err := url.Parse(tc.url) 98 | require.Nil(t, err) 99 | 100 | gotPath, gotTransformers, err := tc.route.ParseURL(testURL) 101 | require.Nil(t, err) 102 | 103 | assert.Equal(t, tc.wantPath, gotPath) 104 | assert.Equal(t, tc.wantTransformers, gotTransformers) 105 | }) 106 | } 107 | } 108 | 109 | func TestMatchRoute(t *testing.T) { 110 | tcs := []struct { 111 | name string 112 | routeConfig RouteConfig 113 | url string 114 | want *Route 115 | wantErr string 116 | }{ 117 | { 118 | name: "simple miss", 119 | routeConfig: []*Route{}, 120 | url: "http://example.com", 121 | want: nil, 122 | }, 123 | { 124 | name: "host miss", 125 | routeConfig: []*Route{ 126 | {Host: "example.com", Path: "", Type: "http"}, 127 | }, 128 | url: "http://mycoolwebsite.biz", 129 | want: nil, 130 | }, 131 | { 132 | name: "simple hit", 133 | routeConfig: []*Route{ 134 | {Host: "example.com", Path: "", Type: "http"}, 135 | }, 136 | url: "http://example.com", 137 | want: &Route{ 138 | Host: "example.com", 139 | Path: "", 140 | Type: "http", 141 | }, 142 | }, 143 | { 144 | name: "too many hits, overlaps are bad", 145 | routeConfig: []*Route{ 146 | {Host: "example.com", Path: "", Type: "http"}, 147 | {Host: "example.com", Path: "", Type: "http"}, 148 | }, 149 | url: "http://example.com", 150 | wantErr: "multiple routes matched input", 151 | }, 152 | { 153 | name: "hosts and paths must both match for simple cases", 154 | routeConfig: []*Route{ 155 | {Host: "example.com", Path: "/foo/bar", Type: "http"}, 156 | }, 157 | url: "http://example.com/baz/bing", 158 | want: nil, 159 | }, 160 | { 161 | name: "git paths match known git request patterns", 162 | routeConfig: []*Route{ 163 | {Host: "github.com", Path: "/example-repo", Type: "git"}, 164 | }, 165 | url: "http://github.com/example-repo/info/refs?service=git-upload-pack", 166 | want: &Route{ 167 | Host: "github.com", 168 | Path: "/example-repo", 169 | Type: "git", 170 | }, 171 | }, 172 | { 173 | name: "but not unknown ones", 174 | routeConfig: []*Route{ 175 | {Host: "github.com", Path: "/example-repo", Type: "git"}, 176 | }, 177 | url: "http://github.com/example-repo/otherinfo", 178 | want: nil, 179 | }, 180 | { 181 | name: "or the wrong repo", 182 | routeConfig: []*Route{ 183 | {Host: "github.com", Path: "/example-repo", Type: "git"}, 184 | }, 185 | url: "http://github.com/other-repo/info/refs?service=git-upload-pack", 186 | want: nil, 187 | }, 188 | { 189 | name: "http requests also work with substitutions logic", 190 | routeConfig: []*Route{ 191 | {Host: "example.com", Path: "/users/:user/settings/:setting/value", Type: "http"}, 192 | }, 193 | url: "http://example.com/users/russell/settings/locale/value", 194 | want: &Route{ 195 | Host: "example.com", 196 | Path: "/users/:user/settings/:setting/value", 197 | Type: "http", 198 | }, 199 | }, 200 | { 201 | name: "legal overlap", 202 | routeConfig: []*Route{ 203 | {Host: "api.github.com", Path: "/orgs/:org", Type: "http"}, 204 | {Host: "api.github.com", Path: "/orgs/:org/repos", Type: "http"}, 205 | {Host: "api.github.com", Path: "/orgs/:org/repos/tree", Type: "http"}, 206 | }, 207 | url: "http://api.github.com/orgs/hashicorp/repos", 208 | want: &Route{ 209 | Host: "api.github.com", 210 | Path: "/orgs/:org/repos", 211 | Type: "http", 212 | }, 213 | }, 214 | } 215 | 216 | for _, tc := range tcs { 217 | tc := tc // capture range variable 218 | t.Run(tc.name, func(t *testing.T) { 219 | t.Parallel() 220 | 221 | inURL, err := url.Parse(tc.url) 222 | require.Nil(t, err) 223 | 224 | got, err := tc.routeConfig.MatchRoute(inURL) 225 | if tc.wantErr == "" { 226 | require.Nil(t, err) 227 | assert.Equal(t, tc.want, got) 228 | } else { 229 | require.NotNil(t, err) 230 | assert.Contains(t, err.Error(), tc.wantErr) 231 | } 232 | }) 233 | } 234 | } 235 | 236 | func TestParseRoutes(t *testing.T) { 237 | tcs := []struct { 238 | name string 239 | input string 240 | want RouteConfig 241 | }{ 242 | { 243 | name: "simple", 244 | input: "testdata/routes.hcl", 245 | want: []*Route{ 246 | {Host: "example.com", Path: "/simple", Type: "http"}, 247 | {Host: "example.com", Path: "/substitutions", Type: "http"}, 248 | {Host: "example.com", Path: "/users/:name", Type: "http"}, 249 | }, 250 | }, 251 | } 252 | 253 | for _, tc := range tcs { 254 | tc := tc // capture range variable 255 | t.Run(tc.name, func(t *testing.T) { 256 | t.Parallel() 257 | 258 | got, err := ParseRoutes(tc.input) 259 | require.Nil(t, err) 260 | 261 | assert.Equal(t, tc.want, got) 262 | }) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /pkg/mock/testdata/example.com/simple.mock: -------------------------------------------------------------------------------- 1 | Hello, World! 2 | -------------------------------------------------------------------------------- /pkg/mock/testdata/example.com/substitutions.mock: -------------------------------------------------------------------------------- 1 | Hello, {{ .name }}! 2 | -------------------------------------------------------------------------------- /pkg/mock/testdata/example.com/users/:name.mock: -------------------------------------------------------------------------------- 1 | {{.name}} 2 | -------------------------------------------------------------------------------- /pkg/mock/testdata/routes.hcl: -------------------------------------------------------------------------------- 1 | route { 2 | host = "example.com" 3 | path = "/simple" 4 | type = "http" 5 | } 6 | 7 | route { 8 | host = "example.com" 9 | path = "/substitutions" 10 | type = "http" 11 | } 12 | 13 | route { 14 | host = "example.com" 15 | path = "/users/:name" 16 | type = "http" 17 | } 18 | -------------------------------------------------------------------------------- /pkg/mock/transform.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "text/template" 8 | templateparse "text/template/parse" 9 | ) 10 | 11 | // VariableSubstitution represents a single Golang type template value to be 12 | // replaced with a given value. 13 | type VariableSubstitution struct { 14 | key string 15 | value string 16 | } 17 | 18 | // NewVariableSubstitution is a creator for a new VariableSubstitution. 19 | func NewVariableSubstitution(key, value string) (*VariableSubstitution, error) { 20 | return &VariableSubstitution{key: key, value: value}, nil 21 | } 22 | 23 | // Transform is used to implement the Transformer interface. It takes an input 24 | // Reader, substitutes the "key" with the "value" using Golang templates and 25 | // returns a Reader that has that substitution performed. 26 | func (vs *VariableSubstitution) Transform(in io.Reader) (io.Reader, error) { 27 | subMap := map[string]string{ 28 | vs.key: vs.value, 29 | } 30 | 31 | b, err := ioutil.ReadAll(in) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | tmpl, err := template.New("var-substitution").Option("missingkey=error").Parse(string(b)) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // I'm pretty surprised this isn't a template option by default, but: 42 | // If the template has a substitution that is not in the map, just output 43 | // the original substitution string, allowing these transforms to be 44 | // chained together. 45 | for _, n := range tmpl.Root.Nodes { 46 | if n.Type() == templateparse.NodeAction { 47 | key := strings.TrimFunc(n.String(), func(r rune) bool { 48 | return r == '{' || r == '}' || r == ' ' || r == '.' 49 | }) 50 | 51 | _, ok := subMap[key] 52 | if !ok { 53 | subMap[key] = n.String() 54 | } 55 | } 56 | } 57 | 58 | pr, pw := io.Pipe() 59 | go func() { 60 | _ = pw.CloseWithError(tmpl.Execute(pw, subMap)) 61 | }() 62 | return pr, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/mock/transform_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestNewVariableSubstitution(t *testing.T) { 14 | tcs := []struct { 15 | name string 16 | key string 17 | value string 18 | want *VariableSubstitution 19 | }{ 20 | { 21 | name: "simple", 22 | key: "test_key", 23 | value: "test_value", 24 | want: &VariableSubstitution{ 25 | key: "test_key", 26 | value: "test_value", 27 | }, 28 | }, 29 | } 30 | 31 | for _, tc := range tcs { 32 | tc := tc // capture range variable 33 | t.Run(tc.name, func(t *testing.T) { 34 | t.Parallel() 35 | 36 | got, err := NewVariableSubstitution(tc.key, tc.value) 37 | require.Nil(t, err) 38 | 39 | assert.Equal(t, tc.want, got) 40 | }) 41 | } 42 | } 43 | 44 | func TestVariableSubstitutionTransform(t *testing.T) { 45 | tcs := []struct { 46 | name string 47 | vs []*VariableSubstitution 48 | input string 49 | want string 50 | }{ 51 | { 52 | name: "no handlebars in input", 53 | vs: []*VariableSubstitution{ 54 | { 55 | key: "test_key", 56 | value: "test_value", 57 | }, 58 | }, 59 | input: "just some input!", 60 | want: "just some input!", 61 | }, 62 | { 63 | name: "do a substitution", 64 | vs: []*VariableSubstitution{ 65 | { 66 | key: "test_key", 67 | value: "output", 68 | }, 69 | }, 70 | input: "just some {{ .test_key }}!", 71 | want: "just some output!", 72 | }, 73 | { 74 | name: "missing key", 75 | vs: []*VariableSubstitution{ 76 | {key: "a", value: "b"}, 77 | }, 78 | input: "just some {{.cool_key}}!", 79 | want: "just some {{.cool_key}}!", 80 | }, 81 | { 82 | name: "chain some transforms together", 83 | vs: []*VariableSubstitution{ 84 | {key: "a", value: "b"}, 85 | {key: "c", value: "d"}, 86 | }, 87 | input: "transform {{ .a }} and {{ .c }}", 88 | want: "transform b and d", 89 | }, 90 | } 91 | 92 | for _, tc := range tcs { 93 | tc := tc // capture range variable 94 | t.Run(tc.name, func(t *testing.T) { 95 | t.Parallel() 96 | 97 | ir := strings.NewReader(tc.input) 98 | 99 | var gotReader io.Reader = ir 100 | for _, transform := range tc.vs { 101 | gr, err := transform.Transform(gotReader) 102 | require.Nil(t, err) 103 | gotReader = gr 104 | } 105 | 106 | gotBytes, err := ioutil.ReadAll(gotReader) 107 | require.Nil(t, err) 108 | 109 | got := string(gotBytes) 110 | assert.Equal(t, tc.want, got) 111 | }) 112 | } 113 | } 114 | --------------------------------------------------------------------------------