mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
391 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e6eed42bd | |||
| 98cd4333a1 | |||
| aaf6794b31 | |||
| 1498132ea3 | |||
| 7f9b1f43c9 | |||
| 5e729c1e85 | |||
| 0a14f97e49 | |||
| 9864b138fb | |||
| 3d18bc56b9 | |||
| 886ba84baa | |||
| a9267791c4 | |||
| ef0aaca0d6 | |||
| 6891f7f421 | |||
| 499ad6d182 | |||
| 8e6bc36084 | |||
| 58970cae92 | |||
| 9e760e2e0c | |||
| 4b4e99bdb2 | |||
| 57d27c1b58 | |||
| 693e9b5283 | |||
| b687d7b967 | |||
| f7be0ee101 | |||
| f6900fcf53 | |||
| ec86a2f7a3 | |||
| e7fbee8c82 | |||
| e84e19a04e | |||
| 4a223f5203 | |||
| af7321511c | |||
| 0be3d99543 | |||
| 3017b245c9 | |||
| 2e4c09155a | |||
| dcc98da4d2 | |||
| 3ab648382d | |||
| 40b193fb79 | |||
| d543ad1ffd | |||
| a8bb4a665a | |||
| 3a1e0dbf47 | |||
| 77a77c0219 | |||
| db62942d63 | |||
| dadd4b59b0 | |||
| d230b33007 | |||
| 0d13173071 | |||
| c3a82f53d5 | |||
| 30b6d1f47a | |||
| bc15b4b0e7 | |||
| e2535233bb | |||
| 00234c8ac2 | |||
| 6512832f9f | |||
| 3e3bb00265 | |||
| e4ce40f8ff | |||
| afca242111 | |||
| 7d229665ed | |||
| 22d8edb984 | |||
| 734acc776a | |||
| b4f1a71397 | |||
| d06d0e79f8 | |||
| a58f240d3e | |||
| 4b75f3e2f0 | |||
| b8dbecb841 | |||
| 134b805644 | |||
| c9b5e7f77b | |||
| 79cbe7bfd0 | |||
| 55b4c12e04 | |||
| 2196c92c0e | |||
| c2327161f7 | |||
| c5fffb4ac2 | |||
| dc4d147388 | |||
| 93c99f6734 | |||
| 4e9fbee1e2 | |||
| a9c7e94a38 | |||
| 3d616e8c6d | |||
| b82e22b459 | |||
| bf6a1b7538 | |||
| c7d6c4cbb9 | |||
| d0b608af31 | |||
| d9b1d46325 | |||
| c8f2834b51 | |||
| ab0455922a | |||
| c50094fc9d | |||
| d058dee11d | |||
| 09ba9e994e | |||
| be82cc7aca | |||
| 2bb8550a4c | |||
| a72acd21b0 | |||
| a6199cf814 | |||
| ceef70dbc5 | |||
| f5e104944e | |||
| 6b385a36f9 | |||
| 9b7cdfa2f2 | |||
| 78e381b29f | |||
| de490c7cad | |||
| bbad6931e3 | |||
| 5bd96a6ac2 | |||
| ac14b64e08 | |||
| 15c95e9d5b | |||
| bc447e307f | |||
| 87a1f228b4 | |||
| acbee94708 | |||
| 7ea5b2a818 | |||
| 186fdba916 | |||
| 7778912d4e | |||
| c921e08296 | |||
| ddbb234d91 | |||
| 0de51593a6 | |||
| 26d633baf8 | |||
| ff137d17d0 | |||
| 57a708d189 | |||
| 32aad90938 | |||
| 40b54434f3 | |||
| 1d0425b26f | |||
| 7557d1d922 | |||
| ff74a0aa09 | |||
| 599c81d753 | |||
| 741b0502ee | |||
| 7ca5921a87 | |||
| da4a759bad | |||
| 042abeb431 | |||
| eb891d4683 | |||
| 44e5e9e43f | |||
| bf380d00ab | |||
| 94035c1797 | |||
| b3f7ce34b4 | |||
| a79b4055e5 | |||
| 5a07156894 | |||
| bcb7a19cd3 | |||
| 6e6ce2be6b | |||
| 1b7ff5d76c | |||
| 93a7a45e7e | |||
| 1a7a78a1f2 | |||
| 1feb65952a | |||
| 66de438a98 | |||
| 850e1605df | |||
| af1ac9cd2e | |||
| 64a3218f5c | |||
| c634bbe9cc | |||
| 4b9849c792 | |||
| 80d7a356b3 | |||
| b4bfa29be2 | |||
| 6cadb60fa2 | |||
| 2e46c2ac1d | |||
| 249adc1c87 | |||
| e9dde23024 | |||
| 3fe2c73dd0 | |||
| 5333c3528b | |||
| 180ae0cc48 | |||
| a1c41210d3 | |||
| ecac03cdcb | |||
| c04d24cafa | |||
| 81ee34e962 | |||
| 78b5356f2b | |||
| 6f9b6ad78e | |||
| 4906b9357a | |||
| e90d751732 | |||
| dce81e85d5 | |||
| a1b417c832 | |||
| 5bf0adad87 | |||
| 8e5aafa5cd | |||
| c133153447 | |||
| ec14ccdd40 | |||
| f55b123d63 | |||
| 0eb0b60f47 | |||
| 5e5af50e64 | |||
| 9ee68c1bd5 | |||
| 789efa5dee | |||
| 8887adb027 | |||
| bcac2beee7 | |||
| 1e10f6f725 | |||
| c8b5a81607 | |||
| eead337324 | |||
| 7d5047c1f1 | |||
| 7f364c777a | |||
| b47af6ef04 | |||
| e81369e220 | |||
| e7457b43e4 | |||
| f376a38b25 | |||
| 749e55c738 | |||
| 24fda7514d | |||
| 3385856966 | |||
| f73f55dba7 | |||
| 012d235314 | |||
| 997e41deae | |||
| 0ffb2229b0 | |||
| a21d5a001f | |||
| a2119c09e9 | |||
| 062657d0d8 | |||
| b092061591 | |||
| 64f8b557b1 | |||
| 95c035060f | |||
| c4790d7f9d | |||
| 837cdc566d | |||
| be5f77e84d | |||
| cbb045a121 | |||
| c48fadc4a7 | |||
| 059fc32f00 | |||
| e2d964ea30 | |||
| 501da21f20 | |||
| 3336faf254 | |||
| 16f752125f | |||
| 0a5f7a677f | |||
| d3a0259944 | |||
| 5fda9610f9 | |||
| 3f2c3ecf85 | |||
| 907e2d8d3a | |||
| 33c70f418f | |||
| 2ebfda1ae9 | |||
| 2392478bd3 | |||
| a437206643 | |||
| a779e1b383 | |||
| 46ab93be51 | |||
| e0fc46a911 | |||
| 9f6393c64c | |||
| 105dac8c2a | |||
| 4ebf100f09 | |||
| f43fd6f388 | |||
| 84b906a248 | |||
| 403732c433 | |||
| f6d5ec2fd6 | |||
| 19a55d6aeb | |||
| bfbc459c0a | |||
| f70a7578fa | |||
| 51f125bd44 | |||
| d74913f871 | |||
| ce5a45db45 | |||
| e0a6a1efff | |||
| c1cd192ee7 | |||
| a056fcd7ba | |||
| 9e333c39da | |||
| 8a974a4f8f | |||
| 6bc87ea2ff | |||
| 1b1e625c20 | |||
| a10910f398 | |||
| ab32440b21 | |||
| e6c29ce081 | |||
| 68c5c71659 | |||
| 569ecdbd02 | |||
| c131339c5c | |||
| b6f51254ea | |||
| 124ba1ba71 | |||
| 1c6c7714a3 | |||
| 46d99aba85 | |||
| 9e16e80f3c | |||
| d882211080 | |||
| 42e140b1b2 | |||
| 4245ceb67d | |||
| 0bdb8aa82d | |||
| 191dc86f9e | |||
| 81e5318021 | |||
| b3d35a4995 | |||
| 2de7e14e1c | |||
| 885a9aaf48 | |||
| 69c914483d | |||
| 9d4ed3a323 | |||
| fbd6560976 | |||
| 238914d70b | |||
| e8ae80adca | |||
| 32c284b54a | |||
| 7c68809f4e | |||
| 6d25261c22 | |||
| 8848df9c5d | |||
| 89aa3a5ef3 | |||
| 05656a60b3 | |||
| 1e92258dd6 | |||
| 76913b19ff | |||
| 4c2da18841 | |||
| f9b54454a1 | |||
| 658772ff24 | |||
| 323ffd2076 | |||
| 2a8109468c | |||
| 94b712009a | |||
| 7b500e74b4 | |||
| ecd5eeab38 | |||
| b4cef492cc | |||
| e3c369d452 | |||
| c052162203 | |||
| 7f26a6b3e5 | |||
| b82db994f3 | |||
| aef8d4decc | |||
| 37718560c1 | |||
| 2aefe15686 | |||
| dbe164d98a | |||
| bc22102478 | |||
| f5db41ce1d | |||
| 77764714ad | |||
| 61642b766b | |||
| 3cf443f0fe | |||
| d4b2f1bcee | |||
| a17c3b568d | |||
| 74f5d66c48 | |||
| efe84497d7 | |||
| e4a22de9d1 | |||
| e6f6d3a476 | |||
| ef7f15f3a4 | |||
| 6e0e3e1537 | |||
| 53ececda21 | |||
| 637fd8f67b | |||
| 956f01163d | |||
| ff6ca577ec | |||
| 9017557169 | |||
| 3a1e81dbf6 | |||
| a8d45277ca | |||
| 1e218e1d2e | |||
| 4d0474e3b8 | |||
| d789596bc0 | |||
| 96bb365929 | |||
| 00e12aa918 | |||
| 2250920e1d | |||
| 42b7134ffa | |||
| 3903642aa7 | |||
| 03b5debd95 | |||
| 3f6283b385 | |||
| 45fb7202ac | |||
| 66783eb4d9 | |||
| 1455d6bb69 | |||
| 3401f91dbe | |||
| eb3955a960 | |||
| d21e88ae3a | |||
| a0a7c60cb9 | |||
| 7da9241fd7 | |||
| e68dbe9cf8 | |||
| bd357bf005 | |||
| aac1ccf12d | |||
| f35a7fa466 | |||
| 75f797debd | |||
| 1c8ea00828 | |||
| d63d5ae1ce | |||
| a6bc58153b | |||
| 911c8a371a | |||
| 87fbc0783a | |||
| f1c36680fc | |||
| a87f757fcc | |||
| 0018b9be0d | |||
| a48c6205b7 | |||
| 28a4159933 | |||
| 0d7fe36007 | |||
| f137b82227 | |||
| 2a127ac3d1 | |||
| 802f80c382 | |||
| 51f35ba03f | |||
| ad8d01cb66 | |||
| 5bf0a55df4 | |||
| ec309c6d52 | |||
| ce5a0934a8 | |||
| b54fa41239 | |||
| 427bbe99d0 | |||
| a8fdc0a998 | |||
| f6bb02b303 | |||
| 6722ae3a83 | |||
| edb362aa96 | |||
| 5376e5113e | |||
| ec3ac840cf | |||
| fbd00e4b53 | |||
| bafb562991 | |||
| ed678235a4 | |||
| cc63c5805e | |||
| 51e3fdba77 | |||
| 5ef76ff3e6 | |||
| 653a0d3f6b | |||
| 0aefa7b047 | |||
| 8c291298c9 | |||
| bf50d7010a | |||
| 8ec90f1c40 | |||
| 90284e8017 | |||
| 2772ede43c | |||
| c986110678 | |||
| 55e49ff5c8 | |||
| e2940c8c03 | |||
| bef80cd806 | |||
| e2c5c28597 | |||
| ab80ff4fd2 | |||
| 3366384d93 | |||
| 1ac6351705 | |||
| 160d199999 | |||
| d68cff8eb6 | |||
| 8f6f9865d4 | |||
| 58e83a811b | |||
| f0c0f38ba5 | |||
| 59071ea15d | |||
| 14f50d9dfb | |||
| 0bf2046da7 | |||
| 88a38bd00d | |||
| 4f64105fbb | |||
| 09432ba64d | |||
| ef54483249 | |||
| c2b91dbd65 | |||
| 8b6fdc04da | |||
| f0216967dc | |||
| b1bec8c899 | |||
| 3c9256a1be | |||
| 7846bc1e06 | |||
| 144b65cf99 | |||
| c8557dc00b |
@@ -0,0 +1,5 @@
|
|||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
|
||||||
|
[caddytest/integration/caddyfile_adapt/*.txt]
|
||||||
|
indent_style = tab
|
||||||
+17
-14
@@ -23,13 +23,13 @@ Other menu items:
|
|||||||
|
|
||||||
### Contributing code
|
### Contributing code
|
||||||
|
|
||||||
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/-/search).
|
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
|
||||||
|
|
||||||
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
|
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
|
||||||
|
|
||||||
Here are some of the expectations we have of contributors:
|
Here are some of the expectations we have of contributors:
|
||||||
|
|
||||||
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it.
|
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it. A lot of valuable time can be saved by discussing a proposal first.
|
||||||
|
|
||||||
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
|
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
|
||||||
|
|
||||||
@@ -45,16 +45,18 @@ Here are some of the expectations we have of contributors:
|
|||||||
|
|
||||||
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
|
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
|
||||||
|
|
||||||
|
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a bit. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo.
|
||||||
|
|
||||||
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
|
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
|
||||||
|
|
||||||
|
|
||||||
#### HOW TO MAKE A PULL REQUEST TO CADDY
|
#### HOW TO MAKE A PULL REQUEST TO CADDY
|
||||||
|
|
||||||
Contributing to Go projects on GitHub is fun and easy. We recommend the following workflow:
|
Contributing to Go projects on GitHub is fun and easy. After you have proposed your change in an issue, we recommend the following workflow:
|
||||||
|
|
||||||
1. [Fork this repo](https://github.com/caddyserver/caddy). This makes a copy of the code you can write to.
|
1. [Fork this repo](https://github.com/caddyserver/caddy). This makes a copy of the code you can write to.
|
||||||
|
|
||||||
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, get it with `go get github.com/caddyserver/caddy/v2`.
|
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, clone it down: `git clone https://github.com/caddyserver/caddy.git`
|
||||||
|
|
||||||
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/<your-username>/caddy.git`
|
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/<your-username>/caddy.git`
|
||||||
|
|
||||||
@@ -85,9 +87,9 @@ Many people on the forums could benefit from your experience and expertise, too.
|
|||||||
|
|
||||||
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
|
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
|
||||||
|
|
||||||
**You can help stop bugs in their tracks!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
|
**You can help us fix bugs!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
|
||||||
|
|
||||||
Please follow the issue template so we have all the needed information. Unredacted—yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. The burden is on you to convince us that it is actually a bug in Caddy. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you!
|
We may reply with an issue template. Please follow the template so we have all the needed information. Unredacted—yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. If you don't, we might close your report. The burden is on you to make it easily reproducible and to convince us that it is actually a bug in Caddy. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you!
|
||||||
|
|
||||||
We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||||
|
|
||||||
@@ -98,11 +100,12 @@ Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're
|
|||||||
Maintainers---or more generally, developers---need three things to act on bugs:
|
Maintainers---or more generally, developers---need three things to act on bugs:
|
||||||
|
|
||||||
1. To agree or be convinced that it's a bug (reporter's responsibility).
|
1. To agree or be convinced that it's a bug (reporter's responsibility).
|
||||||
- A bug is undesired or surprising behavior which violates documentation or the spec.
|
- A bug is unintentional, undesired, or surprising behavior which violates documentation or relevant spec. It might be either a mistake in the documentation or a bug in the code.
|
||||||
|
- This project usually does not work around bugs in other software, systems, and dependencies; instead, we recommend that those bugs are fixed at their source. This sometimes means we close issues or reject PRs that attempt to fix, workaround, or hide bugs in other projects.
|
||||||
|
|
||||||
2. To be able to understand what is happening (mostly reporter's responsibility).
|
2. To be able to understand what is happening (mostly reporter's responsibility).
|
||||||
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix.
|
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix. This is the least amount of work for everyone and path to the fastest resolution.
|
||||||
- Otherwise, the burden is on the reporter to test possible solutions. This is discouraged because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
|
- Otherwise, the burden is on the reporter to test possible solutions. This is less preferable because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
|
||||||
|
|
||||||
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
|
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
|
||||||
- Sometimes the best solution is a documentation change.
|
- Sometimes the best solution is a documentation change.
|
||||||
@@ -112,7 +115,7 @@ Maintainers---or more generally, developers---need three things to act on bugs:
|
|||||||
|
|
||||||
Thus, at the very least, the reporter is expected to:
|
Thus, at the very least, the reporter is expected to:
|
||||||
|
|
||||||
1. Convince the reader that it's a bug (if it's not obvious).
|
1. Convince the reader that it's a bug in Caddy (if it's not obvious).
|
||||||
2. Reduce the problem down to the minimum specific steps required to reproduce it.
|
2. Reduce the problem down to the minimum specific steps required to reproduce it.
|
||||||
|
|
||||||
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
|
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
|
||||||
@@ -123,7 +126,7 @@ The maintainer is usually able to do the rest; but of course the reporter may in
|
|||||||
|
|
||||||
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
|
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
|
||||||
|
|
||||||
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
|
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core. Additionally, some features are bad ideas altogether (for either obvious or non-obvious reasons) which may be rejected. We'll try to explain why we reject a feature, but sometimes the best we can do is, "It's not a good fit for the project."
|
||||||
|
|
||||||
|
|
||||||
### Improving documentation
|
### Improving documentation
|
||||||
@@ -132,11 +135,11 @@ Caddy's documentation is available at [https://caddyserver.com/docs](https://cad
|
|||||||
|
|
||||||
Note that third-party module documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual module authors, and you will have to contact them to change their documentation.
|
Note that third-party module documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual module authors, and you will have to contact them to change their documentation.
|
||||||
|
|
||||||
|
Our documentation is scoped to the Caddy project only: it is not for describing how other software or systems work, even if they relate to Caddy or web servers. That kind of content [can be found in our community wiki](https://caddy.community/c/wiki/13), however.
|
||||||
|
|
||||||
## Collaborator Instructions
|
## Collaborator Instructions
|
||||||
|
|
||||||
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help.The expectations we have of collaborators are:
|
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help. The expectations we have of collaborators are:
|
||||||
|
|
||||||
- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider:
|
- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider:
|
||||||
- Can the change be made more elegant?
|
- Can the change be made more elegant?
|
||||||
@@ -167,7 +170,7 @@ Collaborators have push rights to the repository. We grant this permission after
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Values
|
## Values (WIP)
|
||||||
|
|
||||||
- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate.
|
- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate.
|
||||||
|
|
||||||
|
|||||||
+37
-5
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
||||||
|
|
||||||
Some security problems are more the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please report only vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing or BGP hijacks a vulnerability in the Caddy web server).
|
|
||||||
|
|
||||||
Please note that we consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems.
|
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
@@ -14,11 +11,46 @@ Please note that we consider publicly-registered domain names to be public infor
|
|||||||
| 1.x | :x: |
|
| 1.x | :x: |
|
||||||
| < 1.x | :x: |
|
| < 1.x | :x: |
|
||||||
|
|
||||||
|
|
||||||
|
## Acceptable Scope
|
||||||
|
|
||||||
|
A security report must demonstrate a security bug in the source code from this repository.
|
||||||
|
|
||||||
|
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
|
||||||
|
|
||||||
|
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that.
|
||||||
|
|
||||||
|
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
|
||||||
|
|
||||||
|
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
|
||||||
|
|
||||||
|
Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
||||||
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com].
|
We get a lot of difficult reports that turn out to be invalid. Clear, obvious reports tend to be the most credible (but are also rare).
|
||||||
|
|
||||||
We'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, resources permitting. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. Thank you for understanding.
|
First please ensure your report falls within the accepted scope of security bugs (above).
|
||||||
|
|
||||||
|
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
||||||
|
|
||||||
|
- Most minimal possible config (without redactions!)
|
||||||
|
- Command(s)
|
||||||
|
- Precise HTTP requests (`curl -v` and its output please)
|
||||||
|
- Full log output (please enable debug mode)
|
||||||
|
- Specific minimal steps to reproduce the issue from scratch
|
||||||
|
- A working patch
|
||||||
|
|
||||||
|
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
|
||||||
|
|
||||||
|
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
|
||||||
|
|
||||||
|
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
|
||||||
|
|
||||||
|
When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com.
|
||||||
|
|
||||||
|
Please don't encrypt the email body. It only makes the process more complicated.
|
||||||
|
|
||||||
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
|
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
|
||||||
|
|
||||||
|
|||||||
+31
-10
@@ -6,9 +6,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -17,12 +19,20 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||||
go: [ '1.14', '1.15' ]
|
go: [ '1.17', '1.18' ]
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
|
- go: '1.17'
|
||||||
|
GO_SEMVER: '~1.17.9'
|
||||||
|
|
||||||
|
- go: '1.18'
|
||||||
|
GO_SEMVER: '~1.18.1'
|
||||||
|
|
||||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||||
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||||
SUCCESS: 0
|
SUCCESS: 0
|
||||||
@@ -39,12 +49,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# These tools would be useful if we later decide to reinvestigate
|
# These tools would be useful if we later decide to reinvestigate
|
||||||
# publishing test/coverage reports to some tool for easier consumption
|
# publishing test/coverage reports to some tool for easier consumption
|
||||||
@@ -64,14 +75,23 @@ jobs:
|
|||||||
go env
|
go env
|
||||||
printf "\n\nSystem environment:\n\n"
|
printf "\n\nSystem environment:\n\n"
|
||||||
env
|
env
|
||||||
|
printf "Git version: $(git version)\n\n"
|
||||||
# Calculate the short SHA1 hash of the git commit
|
# Calculate the short SHA1 hash of the git commit
|
||||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
|
||||||
|
|
||||||
- name: Cache the build cache
|
- name: Cache the build cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
# In order:
|
||||||
|
# * Module download cache
|
||||||
|
# * Build cache (Linux)
|
||||||
|
# * Build cache (Mac)
|
||||||
|
# * Build cache (Windows)
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
~/Library/Caches/go-build
|
||||||
|
~\AppData\Local\go-build
|
||||||
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ matrix.go }}-go-ci
|
${{ runner.os }}-${{ matrix.go }}-go-ci
|
||||||
@@ -127,7 +147,7 @@ jobs:
|
|||||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code into the Go module directory
|
- name: Checkout code into the Go module directory
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||||
@@ -137,7 +157,7 @@ jobs:
|
|||||||
|
|
||||||
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
||||||
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
|
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . caddy-ci@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
|
||||||
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; CGO_ENABLED=0 /usr/local/go/bin/go test -v ./..."
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t caddy-ci@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..."
|
||||||
test_result=$?
|
test_result=$?
|
||||||
|
|
||||||
# There's no need leaving the files around
|
# There's no need leaving the files around
|
||||||
@@ -152,7 +172,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v2
|
- uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cross-build-test:
|
cross-build-test:
|
||||||
@@ -14,14 +16,22 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
||||||
go: [ '1.14', '1.15' ]
|
go: [ '1.18' ]
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
|
- go: '1.18'
|
||||||
|
GO_SEMVER: '~1.18.1'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Print Go version and environment
|
- name: Print Go version and environment
|
||||||
id: vars
|
id: vars
|
||||||
@@ -32,18 +42,22 @@ jobs:
|
|||||||
go env
|
go env
|
||||||
printf "\n\nSystem environment:\n\n"
|
printf "\n\nSystem environment:\n\n"
|
||||||
env
|
env
|
||||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
|
||||||
|
|
||||||
- name: Cache the build cache
|
- name: Cache the build cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
# In order:
|
||||||
|
# * Module download cache
|
||||||
|
# * Build cache (Linux)
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
|
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
|
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
|
||||||
|
|
||||||
- name: Checkout code into the Go module directory
|
- name: Checkout code into the Go module directory
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# From https://github.com/golangci/golangci-lint-action
|
# From https://github.com/golangci/golangci-lint-action
|
||||||
@@ -14,10 +16,15 @@ jobs:
|
|||||||
name: lint
|
name: lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: golangci-lint
|
- uses: actions/setup-go@v3
|
||||||
uses: golangci/golangci-lint-action@v2
|
|
||||||
with:
|
with:
|
||||||
version: v1.31
|
go-version: '~1.17.9'
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.44
|
||||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
# only-new-issues: true
|
# only-new-issues: true
|
||||||
|
|||||||
@@ -11,22 +11,30 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest ]
|
os: [ ubuntu-latest ]
|
||||||
go: [ '1.15' ]
|
go: [ '1.18' ]
|
||||||
|
|
||||||
|
include:
|
||||||
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
|
- go: '1.18'
|
||||||
|
GO_SEMVER: '~1.18.1'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
# tl;dr: actions/checkout@v2 runs this line:
|
# tl;dr: actions/checkout@v3 runs this line:
|
||||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||||
# git fetch --prune --unshallow
|
# git fetch --prune --unshallow
|
||||||
@@ -48,7 +56,6 @@ jobs:
|
|||||||
env
|
env
|
||||||
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
|
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
|
||||||
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
|
||||||
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
|
||||||
|
|
||||||
# Add "pip install" CLI tools to PATH
|
# Add "pip install" CLI tools to PATH
|
||||||
echo ~/.local/bin >> $GITHUB_PATH
|
echo ~/.local/bin >> $GITHUB_PATH
|
||||||
@@ -83,7 +90,12 @@ jobs:
|
|||||||
- name: Cache the build cache
|
- name: Cache the build cache
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
# In order:
|
||||||
|
# * Module download cache
|
||||||
|
# * Build cache (Linux)
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go${{ matrix.go }}-release
|
${{ runner.os }}-go${{ matrix.go }}-release
|
||||||
@@ -99,7 +111,7 @@ jobs:
|
|||||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||||
|
|
||||||
# Only publish on non-special tags (e.g. non-beta)
|
# Only publish on non-special tags (e.g. non-beta)
|
||||||
# We will continue to push to Gemfury for the forseeable future, although
|
# We will continue to push to Gemfury for the foreseeable future, although
|
||||||
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
|
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
|
||||||
# See https://gemfury.com/caddy/deb:caddy
|
# See https://gemfury.com/caddy/deb:caddy
|
||||||
- name: Publish .deb to Gemfury
|
- name: Publish .deb to Gemfury
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,7 @@
|
|||||||
_gitignore/
|
_gitignore/
|
||||||
*.log
|
*.log
|
||||||
Caddyfile
|
Caddyfile
|
||||||
|
Caddyfile.*
|
||||||
!caddyfile/
|
!caddyfile/
|
||||||
|
|
||||||
# artifacts from pprof tooling
|
# artifacts from pprof tooling
|
||||||
@@ -10,7 +11,6 @@ Caddyfile
|
|||||||
# build artifacts and helpers
|
# build artifacts and helpers
|
||||||
cmd/caddy/caddy
|
cmd/caddy/caddy
|
||||||
cmd/caddy/caddy.exe
|
cmd/caddy/caddy.exe
|
||||||
cmd/caddy/setcap*
|
|
||||||
|
|
||||||
# mac specific
|
# mac specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -22,3 +22,7 @@ vendor
|
|||||||
dist
|
dist
|
||||||
caddy-build
|
caddy-build
|
||||||
caddy-dist
|
caddy-dist
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
|
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
||||||
ignoretests: true
|
ignoretests: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
|
|||||||
+10
-8
@@ -6,11 +6,13 @@ before:
|
|||||||
# subsequently causes gorleaser to refuse running.
|
# subsequently causes gorleaser to refuse running.
|
||||||
- mkdir -p caddy-build
|
- mkdir -p caddy-build
|
||||||
- cp cmd/caddy/main.go caddy-build/main.go
|
- cp cmd/caddy/main.go caddy-build/main.go
|
||||||
- cp ./go.mod caddy-build/go.mod
|
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
||||||
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
|
|
||||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
||||||
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
||||||
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
||||||
|
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
|
||||||
|
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
|
||||||
|
- /bin/sh -c 'cd ./caddy-build && go mod tidy'
|
||||||
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
|
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
|
||||||
- go mod download
|
- go mod download
|
||||||
|
|
||||||
@@ -33,9 +35,9 @@ builds:
|
|||||||
- s390x
|
- s390x
|
||||||
- ppc64le
|
- ppc64le
|
||||||
goarm:
|
goarm:
|
||||||
- 5
|
- "5"
|
||||||
- 6
|
- "6"
|
||||||
- 7
|
- "7"
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm
|
goarch: arm
|
||||||
@@ -53,7 +55,7 @@ builds:
|
|||||||
goarch: s390x
|
goarch: s390x
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 5
|
goarm: "5"
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
@@ -72,11 +74,11 @@ nfpms:
|
|||||||
- id: default
|
- id: default
|
||||||
package_name: caddy
|
package_name: caddy
|
||||||
|
|
||||||
vendor: Light Code Labs
|
vendor: Dyanim
|
||||||
homepage: https://caddyserver.com
|
homepage: https://caddyserver.com
|
||||||
maintainer: Matthew Holt <mholt@users.noreply.github.com>
|
maintainer: Matthew Holt <mholt@users.noreply.github.com>
|
||||||
description: |
|
description: |
|
||||||
Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
|
Caddy - Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
|
||||||
formats:
|
formats:
|
||||||
|
|||||||
@@ -69,13 +69,13 @@
|
|||||||
|
|
||||||
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
|
The simplest, cross-platform way is to download from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
|
||||||
|
|
||||||
For other install options, see https://caddyserver.com/docs/download.
|
For other install options, see https://caddyserver.com/docs/install.
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- [Go 1.14 or newer](https://golang.org/dl/)
|
- [Go 1.17 or newer](https://golang.org/dl/)
|
||||||
|
|
||||||
### For development
|
### For development
|
||||||
|
|
||||||
@@ -87,17 +87,9 @@ $ cd caddy/cmd/caddy/
|
|||||||
$ go build
|
$ go build
|
||||||
```
|
```
|
||||||
|
|
||||||
When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges for this, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
||||||
|
|
||||||
If you prefer to use `go run` which creates temporary binaries, you can still do this. Make an executable file called `setcap.sh` (or whatever you want) with these contents:
|
If you prefer to use `go run` which only creates temporary binaries, you can still do this with the included `setcap.sh` like so:
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/sh
|
|
||||||
sudo setcap cap_net_bind_service=+ep "$1"
|
|
||||||
"$@"
|
|
||||||
```
|
|
||||||
|
|
||||||
then you can use `go run` like so:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ go run -exec ./setcap.sh main.go
|
$ go run -exec ./setcap.sh main.go
|
||||||
@@ -125,7 +117,7 @@ $ xcaddy build
|
|||||||
2. Change into it: `cd caddy`
|
2. Change into it: `cd caddy`
|
||||||
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
|
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
|
||||||
4. Initialize a Go module: `go mod init caddy`
|
4. Initialize a Go module: `go mod init caddy`
|
||||||
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag or commit.
|
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
|
||||||
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
|
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
|
||||||
7. Compile: `go build`
|
7. Compile: `go build`
|
||||||
|
|
||||||
@@ -174,7 +166,7 @@ The docs are also open source. You can contribute to them here: https://github.c
|
|||||||
|
|
||||||
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
|
- We **strongly recommend** that all professionals or companies using Caddy get a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
|
||||||
|
|
||||||
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way!
|
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! If Caddy is benefitting your company, please consider a sponsorship! This not only helps fund full-time work to ensure the longevity of the project, it's also a great look for your company to your customers and potential customers!
|
||||||
|
|
||||||
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
|
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
|
||||||
|
|
||||||
@@ -184,11 +176,13 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of apilayer GmbH.
|
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
|
||||||
|
|
||||||
|
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
|
||||||
|
|
||||||
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
||||||
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
|
- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
|
||||||
|
|
||||||
Caddy is a project of [ZeroSSL](https://zerossl.com), an [apilayer](https://apilayer.com) company.
|
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
|
||||||
|
|
||||||
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
|
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
|
||||||
+36
-18
@@ -17,9 +17,28 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var testCfg = []byte(`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"myserver": {
|
||||||
|
"listen": ["tcp/localhost:8080-8084"],
|
||||||
|
"read_timeout": "30s"
|
||||||
|
},
|
||||||
|
"yourserver": {
|
||||||
|
"listen": ["127.0.0.1:5000"],
|
||||||
|
"read_header_timeout": "15s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||||
// each test is performed in sequence, so
|
// each test is performed in sequence, so
|
||||||
// each change builds on the previous ones;
|
// each change builds on the previous ones;
|
||||||
@@ -108,25 +127,24 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLoadConcurrent exercises Load under concurrent conditions
|
||||||
|
// and is most useful under test with `-race` enabled.
|
||||||
|
func TestLoadConcurrent(t *testing.T) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
_ = Load(testCfg, true)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkLoad(b *testing.B) {
|
func BenchmarkLoad(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
cfg := []byte(`{
|
Load(testCfg, true)
|
||||||
"apps": {
|
|
||||||
"http": {
|
|
||||||
"servers": {
|
|
||||||
"myserver": {
|
|
||||||
"listen": ["tcp/localhost:8080-8084"],
|
|
||||||
"read_timeout": "30s"
|
|
||||||
},
|
|
||||||
"yourserver": {
|
|
||||||
"listen": ["127.0.0.1:5000"],
|
|
||||||
"read_header_timeout": "15s"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
Load(cfg, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -32,7 +32,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/notify"
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,14 +101,29 @@ func Run(cfg *Config) error {
|
|||||||
// if it is different from the current config or
|
// if it is different from the current config or
|
||||||
// forceReload is true.
|
// forceReload is true.
|
||||||
func Load(cfgJSON []byte, forceReload bool) error {
|
func Load(cfgJSON []byte, forceReload bool) error {
|
||||||
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
if err := notify.NotifyReloading(); err != nil {
|
||||||
|
Log().Error("unable to notify reloading to service manager", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := notify.NotifyReadiness(); err != nil {
|
||||||
|
Log().Error("unable to notify readiness to service manager", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
||||||
|
if errors.Is(err, errSameConfig) {
|
||||||
|
err = nil // not really an error
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeConfig changes the current config (rawCfg) according to the
|
// changeConfig changes the current config (rawCfg) according to the
|
||||||
// method, traversed via the given path, and uses the given input as
|
// method, traversed via the given path, and uses the given input as
|
||||||
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
||||||
// If the resulting config is the same as the previous, no reload will
|
// If the resulting config is the same as the previous, no reload will
|
||||||
// occur unless forceReload is true. This function is safe for
|
// occur unless forceReload is true. If the config is unchanged and not
|
||||||
|
// forcefully reloaded, then errConfigUnchanged This function is safe for
|
||||||
// concurrent use.
|
// concurrent use.
|
||||||
func changeConfig(method, path string, input []byte, forceReload bool) error {
|
func changeConfig(method, path string, input []byte, forceReload bool) error {
|
||||||
switch method {
|
switch method {
|
||||||
@@ -130,15 +147,15 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
|||||||
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
|
newCfg, err := json.Marshal(rawCfg[rawConfigKey])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{
|
return APIError{
|
||||||
Code: http.StatusBadRequest,
|
HTTPStatus: http.StatusBadRequest,
|
||||||
Err: fmt.Errorf("encoding new config: %v", err),
|
Err: fmt.Errorf("encoding new config: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if nothing changed, no need to do a whole reload unless the client forces it
|
// if nothing changed, no need to do a whole reload unless the client forces it
|
||||||
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
|
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
|
||||||
Log().Named("admin.api").Info("config is unchanged")
|
Log().Info("config is unchanged")
|
||||||
return nil
|
return errSameConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// find any IDs in this config and index them
|
// find any IDs in this config and index them
|
||||||
@@ -146,14 +163,14 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
|||||||
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{
|
return APIError{
|
||||||
Code: http.StatusInternalServerError,
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
Err: fmt.Errorf("indexing config: %v", err),
|
Err: fmt.Errorf("indexing config: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load this new config; if it fails, we need to revert to
|
// load this new config; if it fails, we need to revert to
|
||||||
// our old representation of caddy's actual config
|
// our old representation of caddy's actual config
|
||||||
err = unsyncedDecodeAndRun(newCfg)
|
err = unsyncedDecodeAndRun(newCfg, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(rawCfgJSON) > 0 {
|
if len(rawCfgJSON) > 0 {
|
||||||
// restore old config state to keep it consistent
|
// restore old config state to keep it consistent
|
||||||
@@ -233,8 +250,10 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
|
|||||||
// it as the new config, replacing any other current config.
|
// it as the new config, replacing any other current config.
|
||||||
// It does NOT update the raw config state, as this is a
|
// It does NOT update the raw config state, as this is a
|
||||||
// lower-level function; most callers will want to use Load
|
// lower-level function; most callers will want to use Load
|
||||||
// instead. A write lock on currentCfgMu is required!
|
// instead. A write lock on currentCfgMu is required! If
|
||||||
func unsyncedDecodeAndRun(cfgJSON []byte) error {
|
// allowPersist is false, it will not be persisted to disk,
|
||||||
|
// even if it is configured to.
|
||||||
|
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||||
// remove any @id fields from the JSON, which would cause
|
// remove any @id fields from the JSON, which would cause
|
||||||
// loading to break since the field wouldn't be recognized
|
// loading to break since the field wouldn't be recognized
|
||||||
strippedCfgJSON := RemoveMetaFields(cfgJSON)
|
strippedCfgJSON := RemoveMetaFields(cfgJSON)
|
||||||
@@ -245,6 +264,20 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prevent recursive config loads; that is a user error, and
|
||||||
|
// although frequent config loads should be safe, we cannot
|
||||||
|
// guarantee that in the presence of third party plugins, nor
|
||||||
|
// do we want this error to go unnoticed (we assume it was a
|
||||||
|
// pulled config if we're not allowed to persist it)
|
||||||
|
if !allowPersist &&
|
||||||
|
newCfg != nil &&
|
||||||
|
newCfg.Admin != nil &&
|
||||||
|
newCfg.Admin.Config != nil &&
|
||||||
|
newCfg.Admin.Config.LoadRaw != nil &&
|
||||||
|
newCfg.Admin.Config.LoadDelay <= 0 {
|
||||||
|
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
|
||||||
|
}
|
||||||
|
|
||||||
// run the new config and start all its apps
|
// run the new config and start all its apps
|
||||||
err = run(newCfg, true)
|
err = run(newCfg, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -259,7 +292,8 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
|
|||||||
unsyncedStop(oldCfg)
|
unsyncedStop(oldCfg)
|
||||||
|
|
||||||
// autosave a non-nil config, if not disabled
|
// autosave a non-nil config, if not disabled
|
||||||
if newCfg != nil &&
|
if allowPersist &&
|
||||||
|
newCfg != nil &&
|
||||||
(newCfg.Admin == nil ||
|
(newCfg.Admin == nil ||
|
||||||
newCfg.Admin.Config == nil ||
|
newCfg.Admin.Config == nil ||
|
||||||
newCfg.Admin.Config.Persist == nil ||
|
newCfg.Admin.Config.Persist == nil ||
|
||||||
@@ -271,9 +305,9 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
|
|||||||
zap.String("dir", dir),
|
zap.String("dir", dir),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
} else {
|
} else {
|
||||||
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
Log().Info("autosaved config", zap.String("file", ConfigAutosavePath))
|
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
||||||
} else {
|
} else {
|
||||||
Log().Error("unable to autosave config",
|
Log().Error("unable to autosave config",
|
||||||
zap.String("file", ConfigAutosavePath),
|
zap.String("file", ConfigAutosavePath),
|
||||||
@@ -309,21 +343,10 @@ func run(newCfg *Config, start bool) error {
|
|||||||
// been set by a short assignment
|
// been set by a short assignment
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// start the admin endpoint (and stop any prior one)
|
|
||||||
if start {
|
|
||||||
err = replaceAdmin(newCfg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("starting caddy administration endpoint: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newCfg == nil {
|
if newCfg == nil {
|
||||||
return nil
|
newCfg = new(Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare the new config for use
|
|
||||||
newCfg.apps = make(map[string]App)
|
|
||||||
|
|
||||||
// create a context within which to load
|
// create a context within which to load
|
||||||
// modules - essentially our new config's
|
// modules - essentially our new config's
|
||||||
// execution environment; be sure that
|
// execution environment; be sure that
|
||||||
@@ -357,6 +380,17 @@ func run(newCfg *Config, start bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start the admin endpoint (and stop any prior one)
|
||||||
|
if start {
|
||||||
|
err = replaceLocalAdminServer(newCfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the new config for use
|
||||||
|
newCfg.apps = make(map[string]App)
|
||||||
|
|
||||||
// set up global storage and make it CertMagic's default storage, too
|
// set up global storage and make it CertMagic's default storage, too
|
||||||
err = func() error {
|
err = func() error {
|
||||||
if newCfg.StorageRaw != nil {
|
if newCfg.StorageRaw != nil {
|
||||||
@@ -399,9 +433,16 @@ func run(newCfg *Config, start bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provision any admin routers which may need to access
|
||||||
|
// some of the other apps at runtime
|
||||||
|
err = newCfg.Admin.provisionAdminRouters(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
return func() error {
|
err = func() error {
|
||||||
var started []string
|
started := make([]string, 0, len(newCfg.apps))
|
||||||
for name, a := range newCfg.apps {
|
for name, a := range newCfg.apps {
|
||||||
err := a.Start()
|
err := a.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -420,6 +461,108 @@ func run(newCfg *Config, start bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// now that the user's config is running, finish setting up anything else,
|
||||||
|
// such as remote admin endpoint, config loader, etc.
|
||||||
|
return finishSettingUp(ctx, newCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finishSettingUp should be run after all apps have successfully started.
|
||||||
|
func finishSettingUp(ctx Context, cfg *Config) error {
|
||||||
|
// establish this server's identity (only after apps are loaded
|
||||||
|
// so that cert management of this endpoint doesn't prevent user's
|
||||||
|
// servers from starting which likely also use HTTP/HTTPS ports;
|
||||||
|
// but before remote management which may depend on these creds)
|
||||||
|
err := manageIdentity(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("provisioning remote admin endpoint: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace any remote admin endpoint
|
||||||
|
err = replaceRemoteAdminServer(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("provisioning remote admin endpoint: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if dynamic config is requested, set that up and run it
|
||||||
|
if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
|
||||||
|
val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config loader module: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := Log().Named("config_loader").With(
|
||||||
|
zap.String("module", val.(Module).CaddyModule().ID.Name()),
|
||||||
|
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
|
||||||
|
|
||||||
|
runLoadedConfig := func(config []byte) error {
|
||||||
|
logger.Info("applying dynamically-loaded config")
|
||||||
|
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false)
|
||||||
|
if errors.Is(err, errSameConfig) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("successfully applied dynamically-loaded config")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Admin.Config.LoadDelay > 0 {
|
||||||
|
go func() {
|
||||||
|
// the loop is here to iterate ONLY if there is an error, a no-op config load,
|
||||||
|
// or an unchanged config; in which case we simply wait the delay and try again
|
||||||
|
for {
|
||||||
|
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if loadedConfig == nil {
|
||||||
|
logger.Info("dynamically-loaded config was nil; will retry")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = runLoadedConfig(loadedConfig)
|
||||||
|
if errors.Is(err, errSameConfig) {
|
||||||
|
logger.Info("dynamically-loaded config was unchanged; will retry")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
|
logger.Info("stopping dynamic config loading")
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
// if no LoadDelay is provided, will load config synchronously
|
||||||
|
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
|
||||||
|
}
|
||||||
|
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
|
||||||
|
go func() { _ = runLoadedConfig(loadedConfig) }()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigLoader is a type that can load a Caddy config. If
|
||||||
|
// the return value is non-nil, it must be valid Caddy JSON;
|
||||||
|
// if nil or with non-nil error, it is considered to be a
|
||||||
|
// no-op load and may be retried later.
|
||||||
|
type ConfigLoader interface {
|
||||||
|
LoadConfig(Context) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops running the current configuration.
|
// Stop stops running the current configuration.
|
||||||
@@ -462,20 +605,6 @@ func unsyncedStop(cfg *Config) {
|
|||||||
cfg.cancelFunc()
|
cfg.cancelFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopAndCleanup calls stop and cleans up anything
|
|
||||||
// else that is expedient. This should only be used
|
|
||||||
// when stopping and not replacing with a new config.
|
|
||||||
func stopAndCleanup() error {
|
|
||||||
if err := Stop(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
certmagic.CleanUpOwnLocks()
|
|
||||||
if pidfile != "" {
|
|
||||||
return os.Remove(pidfile)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate loads, provisions, and validates
|
// Validate loads, provisions, and validates
|
||||||
// cfg, but does not start running it.
|
// cfg, but does not start running it.
|
||||||
func Validate(cfg *Config) error {
|
func Validate(cfg *Config) error {
|
||||||
@@ -486,6 +615,72 @@ func Validate(cfg *Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exitProcess exits the process as gracefully as possible,
|
||||||
|
// but it always exits, even if there are errors doing so.
|
||||||
|
// It stops all apps, cleans up external locks, removes any
|
||||||
|
// PID file, and shuts down admin endpoint(s) in a goroutine.
|
||||||
|
// Errors are logged along the way, and an appropriate exit
|
||||||
|
// code is emitted.
|
||||||
|
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||||
|
if logger == nil {
|
||||||
|
logger = Log()
|
||||||
|
}
|
||||||
|
logger.Warn("exiting; byeee!! 👋")
|
||||||
|
|
||||||
|
exitCode := ExitCodeSuccess
|
||||||
|
|
||||||
|
// stop all apps
|
||||||
|
if err := Stop(); err != nil {
|
||||||
|
logger.Error("failed to stop apps", zap.Error(err))
|
||||||
|
exitCode = ExitCodeFailedQuit
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up certmagic locks
|
||||||
|
certmagic.CleanUpOwnLocks(ctx, logger)
|
||||||
|
|
||||||
|
// remove pidfile
|
||||||
|
if pidfile != "" {
|
||||||
|
err := os.Remove(pidfile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("cleaning up PID file:",
|
||||||
|
zap.String("pidfile", pidfile),
|
||||||
|
zap.Error(err))
|
||||||
|
exitCode = ExitCodeFailedQuit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shut down admin endpoint(s) in goroutines so that
|
||||||
|
// if this function was called from an admin handler,
|
||||||
|
// it has a chance to return gracefully
|
||||||
|
// use goroutine so that we can finish responding to API request
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
logger = logger.With(zap.Int("exit_code", exitCode))
|
||||||
|
if exitCode == ExitCodeSuccess {
|
||||||
|
logger.Info("shutdown complete")
|
||||||
|
} else {
|
||||||
|
logger.Error("unclean shutdown")
|
||||||
|
}
|
||||||
|
os.Exit(exitCode)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if remoteAdminServer != nil {
|
||||||
|
err := stopAdminServer(remoteAdminServer)
|
||||||
|
if err != nil {
|
||||||
|
exitCode = ExitCodeFailedQuit
|
||||||
|
logger.Error("failed to stop remote admin server gracefully", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if localAdminServer != nil {
|
||||||
|
err := stopAdminServer(localAdminServer)
|
||||||
|
if err != nil {
|
||||||
|
exitCode = ExitCodeFailedQuit
|
||||||
|
logger.Error("failed to stop local admin server gracefully", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Duration can be an integer or a string. An integer is
|
// Duration can be an integer or a string. An integer is
|
||||||
// interpreted as nanoseconds. If a string, it is a Go
|
// interpreted as nanoseconds. If a string, it is a Go
|
||||||
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
|
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
|
||||||
@@ -536,6 +731,26 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||||||
return time.ParseDuration(s)
|
return time.ParseDuration(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstanceID returns the UUID for this instance, and generates one if it
|
||||||
|
// does not already exist. The UUID is stored in the local data directory,
|
||||||
|
// regardless of storage configuration, since each instance is intended to
|
||||||
|
// have its own unique ID.
|
||||||
|
func InstanceID() (uuid.UUID, error) {
|
||||||
|
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
|
||||||
|
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
uuid, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
|
||||||
|
return uuid, err
|
||||||
|
} else if err != nil {
|
||||||
|
return [16]byte{}, err
|
||||||
|
}
|
||||||
|
return uuid.ParseBytes(uuidFileBytes)
|
||||||
|
}
|
||||||
|
|
||||||
// GoModule returns the build info of this Caddy
|
// GoModule returns the build info of this Caddy
|
||||||
// build from debug.BuildInfo (requires Go modules).
|
// build from debug.BuildInfo (requires Go modules).
|
||||||
// If no version information is available, a non-nil
|
// If no version information is available, a non-nil
|
||||||
@@ -597,5 +812,10 @@ var (
|
|||||||
rawCfgIndex map[string]string
|
rawCfgIndex map[string]string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errSameConfig is returned if the new config is the same
|
||||||
|
// as the old one. This isn't usually an actual, actionable
|
||||||
|
// error; it's mostly a sentinel value.
|
||||||
|
var errSameConfig = errors.New("config is unchanged")
|
||||||
|
|
||||||
// ImportPath is the package import path for Caddy core.
|
// ImportPath is the package import path for Caddy core.
|
||||||
const ImportPath = "github.com/caddyserver/caddy/v2"
|
const ImportPath = "github.com/caddyserver/caddy/v2"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package caddyfile
|
package caddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@@ -51,15 +52,46 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
marshalFunc := json.Marshal
|
// lint check: see if input was properly formatted; sometimes messy files files parse
|
||||||
if options["pretty"] == "true" {
|
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
|
||||||
marshalFunc = caddyconfig.JSONIndent
|
if warning, different := formattingDifference(filename, body); different {
|
||||||
|
warnings = append(warnings, warning)
|
||||||
}
|
}
|
||||||
result, err := marshalFunc(cfg)
|
|
||||||
|
result, err := json.Marshal(cfg)
|
||||||
|
|
||||||
return result, warnings, err
|
return result, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formattingDifference returns a warning and true if the formatted version
|
||||||
|
// is any different from the input; empty warning and false otherwise.
|
||||||
|
// TODO: also perform this check on imported files
|
||||||
|
func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
|
||||||
|
// replace windows-style newlines to normalize comparison
|
||||||
|
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
|
||||||
|
|
||||||
|
formatted := Format(normalizedBody)
|
||||||
|
if bytes.Equal(formatted, normalizedBody) {
|
||||||
|
return caddyconfig.Warning{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// find where the difference is
|
||||||
|
line := 1
|
||||||
|
for i, ch := range normalizedBody {
|
||||||
|
if i >= len(formatted) || ch != formatted[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ch == '\n' {
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caddyconfig.Warning{
|
||||||
|
File: filename,
|
||||||
|
Line: line,
|
||||||
|
Message: "Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies",
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
// Unmarshaler is a type that can unmarshal
|
// Unmarshaler is a type that can unmarshal
|
||||||
// Caddyfile tokens to set itself up for a
|
// Caddyfile tokens to set itself up for a
|
||||||
// JSON encoding. The goal of an unmarshaler
|
// JSON encoding. The goal of an unmarshaler
|
||||||
@@ -87,5 +119,31 @@ type ServerType interface {
|
|||||||
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
|
Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalModule instantiates a module with the given ID and invokes
|
||||||
|
// UnmarshalCaddyfile on the new value using the immediate next segment
|
||||||
|
// of d as input. In other words, d's next token should be the first
|
||||||
|
// token of the module's Caddyfile input.
|
||||||
|
//
|
||||||
|
// This function is used when the next segment of Caddyfile tokens
|
||||||
|
// belongs to another Caddy module. The returned value is often
|
||||||
|
// type-asserted to the module's associated type for practical use
|
||||||
|
// when setting up a config.
|
||||||
|
func UnmarshalModule(d *Dispenser, moduleID string) (Unmarshaler, error) {
|
||||||
|
mod, err := caddy.GetModule(moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("getting module named '%s': %v", moduleID, err)
|
||||||
|
}
|
||||||
|
inst := mod.New()
|
||||||
|
unm, ok := inst.(Unmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("module %s is not a Caddyfile unmarshaler; is %T", mod.ID, inst)
|
||||||
|
}
|
||||||
|
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return unm, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ caddyconfig.Adapter = (*Adapter)(nil)
|
var _ caddyconfig.Adapter = (*Adapter)(nil)
|
||||||
|
|||||||
Executable → Regular
+126
-5
@@ -19,6 +19,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -201,6 +202,43 @@ func (d *Dispenser) Val() string {
|
|||||||
return d.tokens[d.cursor].Text
|
return d.tokens[d.cursor].Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValRaw gets the raw text of the current token (including quotes).
|
||||||
|
// If there is no token loaded, it returns empty string.
|
||||||
|
func (d *Dispenser) ValRaw() string {
|
||||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
quote := d.tokens[d.cursor].wasQuoted
|
||||||
|
if quote > 0 {
|
||||||
|
return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal
|
||||||
|
}
|
||||||
|
return d.tokens[d.cursor].Text
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarVal gets value of the current token, converted to the closest
|
||||||
|
// scalar type. If there is no token loaded, it returns nil.
|
||||||
|
func (d *Dispenser) ScalarVal() interface{} {
|
||||||
|
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
quote := d.tokens[d.cursor].wasQuoted
|
||||||
|
text := d.tokens[d.cursor].Text
|
||||||
|
|
||||||
|
if quote > 0 {
|
||||||
|
return text // string literal
|
||||||
|
}
|
||||||
|
if num, err := strconv.Atoi(text); err == nil {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
if num, err := strconv.ParseFloat(text, 64); err == nil {
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
if bool, err := strconv.ParseBool(text); err == nil {
|
||||||
|
return bool
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
// Line gets the line number of the current token.
|
// Line gets the line number of the current token.
|
||||||
// If there is no token loaded, it returns 0.
|
// If there is no token loaded, it returns 0.
|
||||||
func (d *Dispenser) Line() int {
|
func (d *Dispenser) Line() int {
|
||||||
@@ -249,6 +287,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountRemainingArgs counts the amount of remaining arguments
|
||||||
|
// (tokens on the same line) without consuming the tokens.
|
||||||
|
func (d *Dispenser) CountRemainingArgs() int {
|
||||||
|
count := 0
|
||||||
|
for d.NextArg() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
d.Prev()
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||||
// into a slice and returns them. Open curly brace tokens also indicate
|
// into a slice and returns them. Open curly brace tokens also indicate
|
||||||
// the end of arguments, and the curly brace is not included in
|
// the end of arguments, and the curly brace is not included in
|
||||||
@@ -261,6 +312,18 @@ func (d *Dispenser) RemainingArgs() []string {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
||||||
|
// retaining quotes) into a slice and returns them. Open curly brace
|
||||||
|
// tokens also indicate the end of arguments, and the curly brace is
|
||||||
|
// not included in the return value nor is it loaded.
|
||||||
|
func (d *Dispenser) RemainingArgsRaw() []string {
|
||||||
|
var args []string
|
||||||
|
for d.NextArg() {
|
||||||
|
args = append(args, d.ValRaw())
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
// NewFromNextSegment returns a new dispenser with a copy of
|
// NewFromNextSegment returns a new dispenser with a copy of
|
||||||
// the tokens from the current token until the end of the
|
// the tokens from the current token until the end of the
|
||||||
// "directive" whether that be to the end of the line or
|
// "directive" whether that be to the end of the line or
|
||||||
@@ -345,13 +408,17 @@ func (d *Dispenser) EOFErr() error {
|
|||||||
|
|
||||||
// Err generates a custom parse-time error with a message of msg.
|
// Err generates a custom parse-time error with a message of msg.
|
||||||
func (d *Dispenser) Err(msg string) error {
|
func (d *Dispenser) Err(msg string) error {
|
||||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
return d.Errf(msg)
|
||||||
return errors.New(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errf is like Err, but for formatted error messages
|
// Errf is like Err, but for formatted error messages
|
||||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||||
return d.Err(fmt.Sprintf(format, args...))
|
return d.WrapErr(fmt.Errorf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapErr takes an existing error and adds the Caddyfile file and line number.
|
||||||
|
func (d *Dispenser) WrapErr(err error) error {
|
||||||
|
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes the current token and returns the updated slice
|
// Delete deletes the current token and returns the updated slice
|
||||||
@@ -391,6 +458,60 @@ func (d *Dispenser) isNewLine() bool {
|
|||||||
if d.cursor > len(d.tokens)-1 {
|
if d.cursor > len(d.tokens)-1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
|
||||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
prev := d.tokens[d.cursor-1]
|
||||||
|
curr := d.tokens[d.cursor]
|
||||||
|
|
||||||
|
// If the previous token is from a different file,
|
||||||
|
// we can assume it's from a different line
|
||||||
|
if prev.File != curr.File {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The previous token may contain line breaks if
|
||||||
|
// it was quoted and spanned multiple lines. e.g:
|
||||||
|
//
|
||||||
|
// dir "foo
|
||||||
|
// bar
|
||||||
|
// baz"
|
||||||
|
prevLineBreaks := d.numLineBreaks(d.cursor - 1)
|
||||||
|
|
||||||
|
// If the previous token (incl line breaks) ends
|
||||||
|
// on a line earlier than the current token,
|
||||||
|
// then the current token is on a new line
|
||||||
|
return prev.Line+prevLineBreaks < curr.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNextOnNewLine determines whether the current token is on a different
|
||||||
|
// line (higher line number) than the next token. It handles imported
|
||||||
|
// tokens correctly. If there isn't a next token, it returns true.
|
||||||
|
func (d *Dispenser) isNextOnNewLine() bool {
|
||||||
|
if d.cursor < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if d.cursor >= len(d.tokens)-1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
curr := d.tokens[d.cursor]
|
||||||
|
next := d.tokens[d.cursor+1]
|
||||||
|
|
||||||
|
// If the next token is from a different file,
|
||||||
|
// we can assume it's from a different line
|
||||||
|
if curr.File != next.File {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current token may contain line breaks if
|
||||||
|
// it was quoted and spanned multiple lines. e.g:
|
||||||
|
//
|
||||||
|
// dir "foo
|
||||||
|
// bar
|
||||||
|
// baz"
|
||||||
|
currLineBreaks := d.numLineBreaks(d.cursor)
|
||||||
|
|
||||||
|
// If the current token (incl line breaks) ends
|
||||||
|
// on a line earlier than the next token,
|
||||||
|
// then the next token is on a new line
|
||||||
|
return curr.Line+currLineBreaks < next.Line
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
+7
@@ -15,6 +15,7 @@
|
|||||||
package caddyfile
|
package caddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -303,4 +304,10 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
|||||||
if !strings.Contains(err.Error(), "foobar") {
|
if !strings.Contains(err.Error(), "foobar") {
|
||||||
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrBarIsFull = errors.New("bar is full")
|
||||||
|
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
|
||||||
|
if !errors.Is(bookingError, ErrBarIsFull) {
|
||||||
|
t.Errorf("Errf(): should be able to unwrap the error chain")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ func Format(input []byte) []byte {
|
|||||||
if comment {
|
if comment {
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
comment = false
|
comment = false
|
||||||
|
space = true
|
||||||
nextLine()
|
nextLine()
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
func FuzzFormat(input []byte) int {
|
||||||
|
formatted := Format(input)
|
||||||
|
if bytes.Equal(formatted, Format(formatted)) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -179,6 +179,11 @@ d {
|
|||||||
{$F}
|
{$F}
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "env var placeholders with port",
|
||||||
|
input: `:{$PORT}`,
|
||||||
|
expect: `:{$PORT}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "comments",
|
description: "comments",
|
||||||
input: `#a "\n"
|
input: `#a "\n"
|
||||||
@@ -321,6 +326,44 @@ baz`,
|
|||||||
foo
|
foo
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "matthewpi/vscode-caddyfile-support#13",
|
||||||
|
input: `{
|
||||||
|
email {$ACMEEMAIL}
|
||||||
|
#debug
|
||||||
|
}
|
||||||
|
|
||||||
|
block {
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: `{
|
||||||
|
email {$ACMEEMAIL}
|
||||||
|
#debug
|
||||||
|
}
|
||||||
|
|
||||||
|
block {
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "matthewpi/vscode-caddyfile-support#13 - bad formatting",
|
||||||
|
input: `{
|
||||||
|
email {$ACMEEMAIL}
|
||||||
|
#debug
|
||||||
|
}
|
||||||
|
|
||||||
|
block {
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expect: `{
|
||||||
|
email {$ACMEEMAIL}
|
||||||
|
#debug
|
||||||
|
}
|
||||||
|
|
||||||
|
block {
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
// the formatter should output a trailing newline,
|
// the formatter should output a trailing newline,
|
||||||
// even if the tests aren't written to expect that
|
// even if the tests aren't written to expect that
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type adjacency map[string][]string
|
||||||
|
|
||||||
|
type importGraph struct {
|
||||||
|
nodes map[string]bool
|
||||||
|
edges adjacency
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *importGraph) addNode(name string) {
|
||||||
|
if i.nodes == nil {
|
||||||
|
i.nodes = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if _, exists := i.nodes[name]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i.nodes[name] = true
|
||||||
|
}
|
||||||
|
func (i *importGraph) addNodes(names []string) {
|
||||||
|
for _, name := range names {
|
||||||
|
i.addNode(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *importGraph) removeNode(name string) {
|
||||||
|
delete(i.nodes, name)
|
||||||
|
}
|
||||||
|
func (i *importGraph) removeNodes(names []string) {
|
||||||
|
for _, name := range names {
|
||||||
|
i.removeNode(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *importGraph) addEdge(from, to string) error {
|
||||||
|
if !i.exists(from) || !i.exists(to) {
|
||||||
|
return fmt.Errorf("one of the nodes does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.willCycle(to, from) {
|
||||||
|
return fmt.Errorf("a cycle of imports exists between %s and %s", from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.areConnected(from, to) {
|
||||||
|
// if connected, there's nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.nodes == nil {
|
||||||
|
i.nodes = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if i.edges == nil {
|
||||||
|
i.edges = make(adjacency)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.edges[from] = append(i.edges[from], to)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (i *importGraph) addEdges(from string, tos []string) error {
|
||||||
|
for _, to := range tos {
|
||||||
|
err := i.addEdge(from, to)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *importGraph) areConnected(from, to string) bool {
|
||||||
|
al, ok := i.edges[from]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v := range al {
|
||||||
|
if v == to {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *importGraph) willCycle(from, to string) bool {
|
||||||
|
collector := make(map[string]bool)
|
||||||
|
|
||||||
|
var visit func(string)
|
||||||
|
visit = func(start string) {
|
||||||
|
if !collector[start] {
|
||||||
|
collector[start] = true
|
||||||
|
for _, v := range i.edges[start] {
|
||||||
|
visit(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range i.edges[from] {
|
||||||
|
visit(v)
|
||||||
|
}
|
||||||
|
for k := range collector {
|
||||||
|
if to == k {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *importGraph) exists(key string) bool {
|
||||||
|
_, exists := i.nodes[key]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
Executable → Regular
+13
-9
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -35,9 +35,12 @@ type (
|
|||||||
|
|
||||||
// Token represents a single parsable unit.
|
// Token represents a single parsable unit.
|
||||||
Token struct {
|
Token struct {
|
||||||
File string
|
File string
|
||||||
Line int
|
Line int
|
||||||
Text string
|
Text string
|
||||||
|
wasQuoted rune // enclosing quote character, if any
|
||||||
|
inSnippet bool
|
||||||
|
snippetName string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,8 +79,9 @@ func (l *lexer) next() bool {
|
|||||||
var val []rune
|
var val []rune
|
||||||
var comment, quoted, btQuoted, escaped bool
|
var comment, quoted, btQuoted, escaped bool
|
||||||
|
|
||||||
makeToken := func() bool {
|
makeToken := func(quoted rune) bool {
|
||||||
l.token.Text = string(val)
|
l.token.Text = string(val)
|
||||||
|
l.token.wasQuoted = quoted
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ func (l *lexer) next() bool {
|
|||||||
ch, _, err := l.reader.ReadRune()
|
ch, _, err := l.reader.ReadRune()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(val) > 0 {
|
if len(val) > 0 {
|
||||||
return makeToken()
|
return makeToken(0)
|
||||||
}
|
}
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return false
|
return false
|
||||||
@@ -108,10 +112,10 @@ func (l *lexer) next() bool {
|
|||||||
escaped = false
|
escaped = false
|
||||||
} else {
|
} else {
|
||||||
if quoted && ch == '"' {
|
if quoted && ch == '"' {
|
||||||
return makeToken()
|
return makeToken('"')
|
||||||
}
|
}
|
||||||
if btQuoted && ch == '`' {
|
if btQuoted && ch == '`' {
|
||||||
return makeToken()
|
return makeToken('`')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
@@ -137,7 +141,7 @@ func (l *lexer) next() bool {
|
|||||||
comment = false
|
comment = false
|
||||||
}
|
}
|
||||||
if len(val) > 0 {
|
if len(val) > 0 {
|
||||||
return makeToken()
|
return makeToken(0)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package caddyfile
|
||||||
|
|
||||||
|
func FuzzTokenize(input []byte) int {
|
||||||
|
tokens, err := Tokenize(input, "Caddyfile")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Executable → Regular
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
|||||||
Executable → Regular
+92
-26
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -16,14 +16,15 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse parses the input just enough to group tokens, in
|
// Parse parses the input just enough to group tokens, in
|
||||||
@@ -36,15 +37,43 @@ import (
|
|||||||
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
||||||
// will be replaced before parsing begins.
|
// will be replaced before parsing begins.
|
||||||
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
||||||
tokens, err := allTokens(filename, input)
|
// unfortunately, we must copy the input because parsing must
|
||||||
|
// remain a read-only operation, but we have to expand environment
|
||||||
|
// variables before we parse, which changes the underlying array (#4422)
|
||||||
|
inputCopy := make([]byte, len(input))
|
||||||
|
copy(inputCopy, input)
|
||||||
|
|
||||||
|
tokens, err := allTokens(filename, inputCopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p := parser{Dispenser: NewDispenser(tokens)}
|
p := parser{
|
||||||
|
Dispenser: NewDispenser(tokens),
|
||||||
|
importGraph: importGraph{
|
||||||
|
nodes: make(map[string]bool),
|
||||||
|
edges: make(adjacency),
|
||||||
|
},
|
||||||
|
}
|
||||||
return p.parseAll()
|
return p.parseAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allTokens lexes the entire input, but does not parse it.
|
||||||
|
// It returns all the tokens from the input, unstructured
|
||||||
|
// and in order. It may mutate input as it expands env vars.
|
||||||
|
func allTokens(filename string, input []byte) ([]Token, error) {
|
||||||
|
inputCopy, err := replaceEnvVars(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokens, err := Tokenize(inputCopy, filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
// replaceEnvVars replaces all occurrences of environment variables.
|
// replaceEnvVars replaces all occurrences of environment variables.
|
||||||
|
// It mutates the underlying array and returns the updated slice.
|
||||||
func replaceEnvVars(input []byte) ([]byte, error) {
|
func replaceEnvVars(input []byte) ([]byte, error) {
|
||||||
var offset int
|
var offset int
|
||||||
for {
|
for {
|
||||||
@@ -89,27 +118,13 @@ func replaceEnvVars(input []byte) ([]byte, error) {
|
|||||||
return input, nil
|
return input, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// allTokens lexes the entire input, but does not parse it.
|
|
||||||
// It returns all the tokens from the input, unstructured
|
|
||||||
// and in order.
|
|
||||||
func allTokens(filename string, input []byte) ([]Token, error) {
|
|
||||||
input, err := replaceEnvVars(input)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tokens, err := Tokenize(input, filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tokens, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
*Dispenser
|
*Dispenser
|
||||||
block ServerBlock // current server block being parsed
|
block ServerBlock // current server block being parsed
|
||||||
eof bool // if we encounter a valid EOF in a hard place
|
eof bool // if we encounter a valid EOF in a hard place
|
||||||
definedSnippets map[string][]Token
|
definedSnippets map[string][]Token
|
||||||
nesting int
|
nesting int
|
||||||
|
importGraph importGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||||
@@ -165,6 +180,15 @@ func (p *parser) begin() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Just as we need to track which file the token comes from, we need to
|
||||||
|
// keep track of which snippets do the tokens come from. This is helpful
|
||||||
|
// in tracking import cycles across files/snippets by namespacing them. Without
|
||||||
|
// this we end up with false-positives in cycle-detection.
|
||||||
|
for k, v := range tokens {
|
||||||
|
v.inSnippet = true
|
||||||
|
v.snippetName = name
|
||||||
|
tokens[k] = v
|
||||||
|
}
|
||||||
p.definedSnippets[name] = tokens
|
p.definedSnippets[name] = tokens
|
||||||
// empty block keys so we don't save this block as a real server.
|
// empty block keys so we don't save this block as a real server.
|
||||||
p.block.Keys = nil
|
p.block.Keys = nil
|
||||||
@@ -194,9 +218,20 @@ func (p *parser) addresses() error {
|
|||||||
if expectingAnother {
|
if expectingAnother {
|
||||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
||||||
}
|
}
|
||||||
|
// Mark this server block as being defined with braces.
|
||||||
|
// This is used to provide a better error message when
|
||||||
|
// the user may have tried to define two server blocks
|
||||||
|
// without having used braces, which are required in
|
||||||
|
// that case.
|
||||||
|
p.block.HasBraces = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users commonly forget to place a space between the address and the '{'
|
||||||
|
if strings.HasSuffix(tkn, "{") {
|
||||||
|
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn)
|
||||||
|
}
|
||||||
|
|
||||||
if tkn != "" { // empty token possible if user typed ""
|
if tkn != "" { // empty token possible if user typed ""
|
||||||
// Trailing comma indicates another address will follow, which
|
// Trailing comma indicates another address will follow, which
|
||||||
// may possibly be on the next line
|
// may possibly be on the next line
|
||||||
@@ -207,6 +242,13 @@ func (p *parser) addresses() error {
|
|||||||
expectingAnother = false // but we may still see another one on this line
|
expectingAnother = false // but we may still see another one on this line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's a comma here, it's probably because they didn't use a space
|
||||||
|
// between their two domains, e.g. "foo.com,bar.com", which would not be
|
||||||
|
// parsed as two separate site addresses.
|
||||||
|
if strings.Contains(tkn, ",") {
|
||||||
|
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn)
|
||||||
|
}
|
||||||
|
|
||||||
p.block.Keys = append(p.block.Keys, tkn)
|
p.block.Keys = append(p.block.Keys, tkn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +346,7 @@ func (p *parser) doImport() error {
|
|||||||
args := p.RemainingArgs()
|
args := p.RemainingArgs()
|
||||||
|
|
||||||
// add args to the replacer
|
// add args to the replacer
|
||||||
repl := caddy.NewReplacer()
|
repl := caddy.NewEmptyReplacer()
|
||||||
for index, arg := range args {
|
for index, arg := range args {
|
||||||
repl.Set("args."+strconv.Itoa(index), arg)
|
repl.Set("args."+strconv.Itoa(index), arg)
|
||||||
}
|
}
|
||||||
@@ -314,10 +356,15 @@ func (p *parser) doImport() error {
|
|||||||
tokensBefore := p.tokens[:p.cursor-1-len(args)]
|
tokensBefore := p.tokens[:p.cursor-1-len(args)]
|
||||||
tokensAfter := p.tokens[p.cursor+1:]
|
tokensAfter := p.tokens[p.cursor+1:]
|
||||||
var importedTokens []Token
|
var importedTokens []Token
|
||||||
|
var nodes []string
|
||||||
|
|
||||||
// first check snippets. That is a simple, non-recursive replacement
|
// first check snippets. That is a simple, non-recursive replacement
|
||||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||||
importedTokens = p.definedSnippets[importPattern]
|
importedTokens = p.definedSnippets[importPattern]
|
||||||
|
if len(importedTokens) > 0 {
|
||||||
|
// just grab the first one
|
||||||
|
nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// make path relative to the file of the _token_ being processed rather
|
// make path relative to the file of the _token_ being processed rather
|
||||||
// than current working directory (issue #867) and then use glob to get
|
// than current working directory (issue #867) and then use glob to get
|
||||||
@@ -346,14 +393,13 @@ func (p *parser) doImport() error {
|
|||||||
}
|
}
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
if strings.ContainsAny(globPattern, "*?[]") {
|
if strings.ContainsAny(globPattern, "*?[]") {
|
||||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern))
|
||||||
} else {
|
} else {
|
||||||
return p.Errf("File to import not found: %s", importPattern)
|
return p.Errf("File to import not found: %s", importPattern)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect all the imported tokens
|
// collect all the imported tokens
|
||||||
|
|
||||||
for _, importFile := range matches {
|
for _, importFile := range matches {
|
||||||
newTokens, err := p.doSingleImport(importFile)
|
newTokens, err := p.doSingleImport(importFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,6 +407,18 @@ func (p *parser) doImport() error {
|
|||||||
}
|
}
|
||||||
importedTokens = append(importedTokens, newTokens...)
|
importedTokens = append(importedTokens, newTokens...)
|
||||||
}
|
}
|
||||||
|
nodes = matches
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeName := p.File()
|
||||||
|
if p.Token().inSnippet {
|
||||||
|
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
||||||
|
}
|
||||||
|
p.importGraph.addNode(nodeName)
|
||||||
|
p.importGraph.addNodes(nodes)
|
||||||
|
if err := p.importGraph.addEdges(nodeName, nodes); err != nil {
|
||||||
|
p.importGraph.removeNodes(nodes)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy the tokens so we don't overwrite p.definedSnippets
|
// copy the tokens so we don't overwrite p.definedSnippets
|
||||||
@@ -396,7 +454,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
input, err := ioutil.ReadAll(file)
|
input, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
||||||
}
|
}
|
||||||
@@ -436,6 +494,13 @@ func (p *parser) directive() error {
|
|||||||
for p.Next() {
|
for p.Next() {
|
||||||
if p.Val() == "{" {
|
if p.Val() == "{" {
|
||||||
p.nesting++
|
p.nesting++
|
||||||
|
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||||
|
return p.Err("Unexpected next token after '{' on same line")
|
||||||
|
}
|
||||||
|
} else if p.Val() == "{}" {
|
||||||
|
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||||
|
return p.Err("Unexpected '{}' at end of line")
|
||||||
|
}
|
||||||
} else if p.isNewLine() && p.nesting == 0 {
|
} else if p.isNewLine() && p.nesting == 0 {
|
||||||
p.cursor-- // read too far
|
p.cursor-- // read too far
|
||||||
break
|
break
|
||||||
@@ -526,8 +591,9 @@ func (p *parser) snippetTokens() ([]Token, error) {
|
|||||||
// head of the server block with tokens, which are
|
// head of the server block with tokens, which are
|
||||||
// grouped by segments.
|
// grouped by segments.
|
||||||
type ServerBlock struct {
|
type ServerBlock struct {
|
||||||
Keys []string
|
HasBraces bool
|
||||||
Segments []Segment
|
Keys []string
|
||||||
|
Segments []Segment
|
||||||
}
|
}
|
||||||
|
|
||||||
// DispenseDirective returns a dispenser that contains
|
// DispenseDirective returns a dispenser that contains
|
||||||
|
|||||||
Executable → Regular
+46
-7
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2015 Light Code Labs, LLC
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -16,7 +16,6 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -160,6 +159,10 @@ func TestParseOneAndImport(t *testing.T) {
|
|||||||
"localhost",
|
"localhost",
|
||||||
}, []int{}},
|
}, []int{}},
|
||||||
|
|
||||||
|
{`localhost{
|
||||||
|
dir1
|
||||||
|
}`, true, []string{}, []int{}},
|
||||||
|
|
||||||
{`localhost
|
{`localhost
|
||||||
dir1 {
|
dir1 {
|
||||||
nested {
|
nested {
|
||||||
@@ -188,6 +191,20 @@ func TestParseOneAndImport(t *testing.T) {
|
|||||||
|
|
||||||
{``, false, []string{}, []int{}},
|
{``, false, []string{}, []int{}},
|
||||||
|
|
||||||
|
// Unexpected next token after '{' on same line
|
||||||
|
{`localhost
|
||||||
|
dir1 { a b }`, true, []string{"localhost"}, []int{}},
|
||||||
|
// Workaround with quotes
|
||||||
|
{`localhost
|
||||||
|
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
|
||||||
|
|
||||||
|
// Unexpected '{}' at end of line
|
||||||
|
{`localhost
|
||||||
|
dir1 {}`, true, []string{"localhost"}, []int{}},
|
||||||
|
// Workaround with quotes
|
||||||
|
{`localhost
|
||||||
|
dir1 "{}"`, false, []string{"localhost"}, []int{2}},
|
||||||
|
|
||||||
// import with args
|
// import with args
|
||||||
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
|
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
|
||||||
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
|
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
|
||||||
@@ -276,7 +293,7 @@ func TestRecursiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test relative recursive import
|
// test relative recursive import
|
||||||
err = ioutil.WriteFile(recursiveFile1, []byte(
|
err = os.WriteFile(recursiveFile1, []byte(
|
||||||
`localhost
|
`localhost
|
||||||
dir1
|
dir1
|
||||||
import recursive_import_test2`), 0644)
|
import recursive_import_test2`), 0644)
|
||||||
@@ -285,7 +302,7 @@ func TestRecursiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.Remove(recursiveFile1)
|
defer os.Remove(recursiveFile1)
|
||||||
|
|
||||||
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
|
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -310,7 +327,7 @@ func TestRecursiveImport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// test absolute recursive import
|
// test absolute recursive import
|
||||||
err = ioutil.WriteFile(recursiveFile1, []byte(
|
err = os.WriteFile(recursiveFile1, []byte(
|
||||||
`localhost
|
`localhost
|
||||||
dir1
|
dir1
|
||||||
import `+recursiveFile2), 0644)
|
import `+recursiveFile2), 0644)
|
||||||
@@ -366,7 +383,7 @@ func TestDirectiveImport(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
|
err = os.WriteFile(directiveFile, []byte(`prop1 1
|
||||||
prop2 2`), 0644)
|
prop2 2`), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -444,6 +461,28 @@ func TestParseAll(t *testing.T) {
|
|||||||
|
|
||||||
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
|
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
|
||||||
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
|
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
|
||||||
|
|
||||||
|
// recursive self-import
|
||||||
|
{`import testdata/import_recursive0.txt`, true, [][]string{}},
|
||||||
|
{`import testdata/import_recursive3.txt
|
||||||
|
import testdata/import_recursive1.txt`, true, [][]string{}},
|
||||||
|
|
||||||
|
// cyclic imports
|
||||||
|
{`(A) {
|
||||||
|
import A
|
||||||
|
}
|
||||||
|
:80
|
||||||
|
import A
|
||||||
|
`, true, [][]string{}},
|
||||||
|
{`(A) {
|
||||||
|
import B
|
||||||
|
}
|
||||||
|
(B) {
|
||||||
|
import A
|
||||||
|
}
|
||||||
|
:80
|
||||||
|
import A
|
||||||
|
`, true, [][]string{}},
|
||||||
} {
|
} {
|
||||||
p := testParser(test.input)
|
p := testParser(test.input)
|
||||||
blocks, err := p.parseAll()
|
blocks, err := p.parseAll()
|
||||||
@@ -607,7 +646,7 @@ func TestSnippets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
||||||
file, err := ioutil.TempFile("", t.Name())
|
file, err := os.CreateTemp("", t.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // get a stack trace so we know where this was called from.
|
panic(err) // get a stack trace so we know where this was called from.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
import import_recursive0.txt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import import_recursive2.txt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import import_recursive3.txt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import import_recursive1.txt
|
||||||
@@ -35,6 +35,14 @@ type Warning struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w Warning) String() string {
|
||||||
|
var directive string
|
||||||
|
if w.Directive != "" {
|
||||||
|
directive = fmt.Sprintf(" (%s)", w.Directive)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%d%s: %s", w.File, w.Line, directive, w.Message)
|
||||||
|
}
|
||||||
|
|
||||||
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
|
// JSON encodes val as JSON, returning it as a json.RawMessage. Any
|
||||||
// marshaling errors (which are highly unlikely with correct code)
|
// marshaling errors (which are highly unlikely with correct code)
|
||||||
// are converted to warnings. This is convenient when filling config
|
// are converted to warnings. This is convenient when filling config
|
||||||
@@ -93,12 +101,6 @@ func JSONModuleObject(val interface{}, fieldName, fieldVal string, warnings *[]W
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONIndent is used to JSON-marshal the final resulting Caddy
|
|
||||||
// configuration in a consistent, human-readable way.
|
|
||||||
func JSONIndent(val interface{}) ([]byte, error) {
|
|
||||||
return json.MarshalIndent(val, "", "\t")
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterAdapter registers a config adapter with the given name.
|
// RegisterAdapter registers a config adapter with the given name.
|
||||||
// This should usually be done at init-time. It panics if the
|
// This should usually be done at init-time. It panics if the
|
||||||
// adapter cannot be registered successfully.
|
// adapter cannot be registered successfully.
|
||||||
|
|||||||
@@ -102,12 +102,20 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make a slice of the map keys so we can iterate in sorted order
|
||||||
|
addrs := make([]string, 0, len(addrToKeys))
|
||||||
|
for k := range addrToKeys {
|
||||||
|
addrs = append(addrs, k)
|
||||||
|
}
|
||||||
|
sort.Strings(addrs)
|
||||||
|
|
||||||
// now that we know which addresses serve which keys of this
|
// now that we know which addresses serve which keys of this
|
||||||
// server block, we iterate that mapping and create a list of
|
// server block, we iterate that mapping and create a list of
|
||||||
// new server blocks for each address where the keys of the
|
// new server blocks for each address where the keys of the
|
||||||
// server block are only the ones which use the address; but
|
// server block are only the ones which use the address; but
|
||||||
// the contents (tokens) are of course the same
|
// the contents (tokens) are of course the same
|
||||||
for addr, keys := range addrToKeys {
|
for _, addr := range addrs {
|
||||||
|
keys := addrToKeys[addr]
|
||||||
// parse keys so that we only have to do it once
|
// parse keys so that we only have to do it once
|
||||||
parsedKeys := make([]Address, 0, len(keys))
|
parsedKeys := make([]Address, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@@ -161,6 +169,7 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
|
|||||||
delete(addrToServerBlocks, otherAddr)
|
delete(addrToServerBlocks, otherAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sort.Strings(a.addresses)
|
||||||
|
|
||||||
sbaddrs = append(sbaddrs, a)
|
sbaddrs = append(sbaddrs, a)
|
||||||
}
|
}
|
||||||
@@ -208,12 +217,16 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the bind directive specifies hosts, but is optional
|
// the bind directive specifies hosts, but is optional
|
||||||
lnHosts := make([]string, 0, len(sblock.pile))
|
lnHosts := make([]string, 0, len(sblock.pile["bind"]))
|
||||||
for _, cfgVal := range sblock.pile["bind"] {
|
for _, cfgVal := range sblock.pile["bind"] {
|
||||||
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
|
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
|
||||||
}
|
}
|
||||||
if len(lnHosts) == 0 {
|
if len(lnHosts) == 0 {
|
||||||
lnHosts = []string{""}
|
if defaultBind, ok := options["default_bind"].([]string); ok {
|
||||||
|
lnHosts = defaultBind
|
||||||
|
} else {
|
||||||
|
lnHosts = []string{""}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use a map to prevent duplication
|
// use a map to prevent duplication
|
||||||
@@ -223,7 +236,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
|||||||
if err == nil && addr.IsUnixNetwork() {
|
if err == nil && addr.IsUnixNetwork() {
|
||||||
listeners[host] = struct{}{}
|
listeners[host] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
|
listeners[host+":"+lnPort] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +245,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
|||||||
for lnStr := range listeners {
|
for lnStr := range listeners {
|
||||||
listenersList = append(listenersList, lnStr)
|
listenersList = append(listenersList, lnStr)
|
||||||
}
|
}
|
||||||
|
sort.Strings(listenersList)
|
||||||
|
|
||||||
return listenersList, nil
|
return listenersList, nil
|
||||||
}
|
}
|
||||||
@@ -337,7 +351,9 @@ func (a Address) Normalize() Address {
|
|||||||
// ensure host is normalized if it's an IP address
|
// ensure host is normalized if it's an IP address
|
||||||
host := strings.TrimSpace(a.Host)
|
host := strings.TrimSpace(a.Host)
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
host = ip.String()
|
if ipv6 := ip.To16(); ipv6 != nil && ipv6.DefaultMask() == nil {
|
||||||
|
host = ipv6.String()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Address{
|
return Address{
|
||||||
@@ -349,28 +365,6 @@ func (a Address) Normalize() Address {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key returns a string form of a, much like String() does, but this
|
|
||||||
// method doesn't add anything default that wasn't in the original.
|
|
||||||
func (a Address) Key() string {
|
|
||||||
res := ""
|
|
||||||
if a.Scheme != "" {
|
|
||||||
res += a.Scheme + "://"
|
|
||||||
}
|
|
||||||
if a.Host != "" {
|
|
||||||
res += a.Host
|
|
||||||
}
|
|
||||||
// insert port only if the original has its own explicit port
|
|
||||||
if a.Port != "" &&
|
|
||||||
len(a.Original) >= len(res) &&
|
|
||||||
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
|
||||||
res += ":" + a.Port
|
|
||||||
}
|
|
||||||
if a.Path != "" {
|
|
||||||
res += a.Path
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// lowerExceptPlaceholders lowercases s except within
|
// lowerExceptPlaceholders lowercases s except within
|
||||||
// placeholders (substrings in non-escaped '{ }' spans).
|
// placeholders (substrings in non-escaped '{ }' spans).
|
||||||
// See https://github.com/caddyserver/caddy/issues/3264
|
// See https://github.com/caddyserver/caddy/issues/3264
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
//go:build gofuzz
|
||||||
// +build gofuzz
|
// +build gofuzz
|
||||||
|
|
||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|||||||
@@ -106,67 +106,128 @@ func TestAddressString(t *testing.T) {
|
|||||||
func TestKeyNormalization(t *testing.T) {
|
func TestKeyNormalization(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
input string
|
input string
|
||||||
expect string
|
expect Address
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
input: "example.com",
|
input: "example.com",
|
||||||
expect: "example.com",
|
expect: Address{
|
||||||
|
Host: "example.com",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "http://host:1234/path",
|
input: "http://host:1234/path",
|
||||||
expect: "http://host:1234/path",
|
expect: Address{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "host",
|
||||||
|
Port: "1234",
|
||||||
|
Path: "/path",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "HTTP://A/ABCDEF",
|
input: "HTTP://A/ABCDEF",
|
||||||
expect: "http://a/ABCDEF",
|
expect: Address{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: "a",
|
||||||
|
Path: "/ABCDEF",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "A/ABCDEF",
|
input: "A/ABCDEF",
|
||||||
expect: "a/ABCDEF",
|
expect: Address{
|
||||||
|
Host: "a",
|
||||||
|
Path: "/ABCDEF",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "A:2015/Path",
|
input: "A:2015/Path",
|
||||||
expect: "a:2015/Path",
|
expect: Address{
|
||||||
|
Host: "a",
|
||||||
|
Port: "2015",
|
||||||
|
Path: "/Path",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.{env.MY_DOMAIN}",
|
input: "sub.{env.MY_DOMAIN}",
|
||||||
expect: "sub.{env.MY_DOMAIN}",
|
expect: Address{
|
||||||
|
Host: "sub.{env.MY_DOMAIN}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.ExAmPle",
|
input: "sub.ExAmPle",
|
||||||
expect: "sub.example",
|
expect: Address{
|
||||||
|
Host: "sub.example",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.\\{env.MY_DOMAIN\\}",
|
input: "sub.\\{env.MY_DOMAIN\\}",
|
||||||
expect: "sub.\\{env.my_domain\\}",
|
expect: Address{
|
||||||
|
Host: "sub.\\{env.my_domain\\}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "sub.{env.MY_DOMAIN}.com",
|
input: "sub.{env.MY_DOMAIN}.com",
|
||||||
expect: "sub.{env.MY_DOMAIN}.com",
|
expect: Address{
|
||||||
|
Host: "sub.{env.MY_DOMAIN}.com",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":80",
|
input: ":80",
|
||||||
expect: ":80",
|
expect: Address{
|
||||||
|
Port: "80",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":443",
|
input: ":443",
|
||||||
expect: ":443",
|
expect: Address{
|
||||||
|
Port: "443",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":1234",
|
input: ":1234",
|
||||||
expect: ":1234",
|
expect: Address{
|
||||||
|
Port: "1234",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "",
|
input: "",
|
||||||
expect: "",
|
expect: Address{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: ":",
|
input: ":",
|
||||||
expect: "",
|
expect: Address{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "[::]",
|
input: "[::]",
|
||||||
expect: "::",
|
expect: Address{
|
||||||
|
Host: "::",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "127.0.0.1",
|
||||||
|
expect: Address{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:1234",
|
||||||
|
expect: Address{
|
||||||
|
Host: "2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
||||||
|
Port: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// IPv4 address in IPv6 form (#4381)
|
||||||
|
input: "[::ffff:cff4:e77d]:1234",
|
||||||
|
expect: Address{
|
||||||
|
Host: "::ffff:cff4:e77d",
|
||||||
|
Port: "1234",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "::ffff:cff4:e77d",
|
||||||
|
expect: Address{
|
||||||
|
Host: "::ffff:cff4:e77d",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, tc := range testCases {
|
for i, tc := range testCases {
|
||||||
@@ -175,9 +236,18 @@ func TestKeyNormalization(t *testing.T) {
|
|||||||
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
|
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if actual := addr.Normalize().Key(); actual != tc.expect {
|
actual := addr.Normalize()
|
||||||
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
|
if actual.Scheme != tc.expect.Scheme {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Scheme='%s' but got Scheme='%s'", i, tc.input, tc.expect.Scheme, actual.Scheme)
|
||||||
|
}
|
||||||
|
if actual.Host != tc.expect.Host {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Host='%s' but got Host='%s'", i, tc.input, tc.expect.Host, actual.Host)
|
||||||
|
}
|
||||||
|
if actual.Port != tc.expect.Port {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Port='%s' but got Port='%s'", i, tc.input, tc.expect.Port, actual.Port)
|
||||||
|
}
|
||||||
|
if actual.Path != tc.expect.Path {
|
||||||
|
t.Errorf("Test %d: Input '%s': Expected Path='%s' but got Path='%s'", i, tc.input, tc.expect.Path, actual.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -38,8 +39,11 @@ func init() {
|
|||||||
RegisterDirective("bind", parseBind)
|
RegisterDirective("bind", parseBind)
|
||||||
RegisterDirective("tls", parseTLS)
|
RegisterDirective("tls", parseTLS)
|
||||||
RegisterHandlerDirective("root", parseRoot)
|
RegisterHandlerDirective("root", parseRoot)
|
||||||
|
RegisterHandlerDirective("vars", parseVars)
|
||||||
RegisterHandlerDirective("redir", parseRedir)
|
RegisterHandlerDirective("redir", parseRedir)
|
||||||
RegisterHandlerDirective("respond", parseRespond)
|
RegisterHandlerDirective("respond", parseRespond)
|
||||||
|
RegisterHandlerDirective("abort", parseAbort)
|
||||||
|
RegisterHandlerDirective("error", parseError)
|
||||||
RegisterHandlerDirective("route", parseRoute)
|
RegisterHandlerDirective("route", parseRoute)
|
||||||
RegisterHandlerDirective("handle", parseHandle)
|
RegisterHandlerDirective("handle", parseHandle)
|
||||||
RegisterDirective("handle_errors", parseHandleErrors)
|
RegisterDirective("handle_errors", parseHandleErrors)
|
||||||
@@ -79,6 +83,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
|||||||
// on_demand
|
// on_demand
|
||||||
// eab <key_id> <mac_key>
|
// eab <key_id> <mac_key>
|
||||||
// issuer <module_name> [...]
|
// issuer <module_name> [...]
|
||||||
|
// get_certificate <module_name> [...]
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
@@ -87,8 +92,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
var folderLoader caddytls.FolderLoader
|
var folderLoader caddytls.FolderLoader
|
||||||
var certSelector caddytls.CustomCertSelectionPolicy
|
var certSelector caddytls.CustomCertSelectionPolicy
|
||||||
var acmeIssuer *caddytls.ACMEIssuer
|
var acmeIssuer *caddytls.ACMEIssuer
|
||||||
|
var keyType string
|
||||||
var internalIssuer *caddytls.InternalIssuer
|
var internalIssuer *caddytls.InternalIssuer
|
||||||
var issuers []certmagic.Issuer
|
var issuers []certmagic.Issuer
|
||||||
|
var certManagers []certmagic.Manager
|
||||||
var onDemand bool
|
var onDemand bool
|
||||||
|
|
||||||
for h.Next() {
|
for h.Next() {
|
||||||
@@ -122,10 +129,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
// must load each cert only once; otherwise, they each get a
|
// must load each cert only once; otherwise, they each get a
|
||||||
// different tag... since a cert loaded twice has the same
|
// different tag... since a cert loaded twice has the same
|
||||||
// bytes, it will overwrite the first one in the cache, and
|
// bytes, it will overwrite the first one in the cache, and
|
||||||
// only the last cert (and its tag) will survive, so a any conn
|
// only the last cert (and its tag) will survive, so any conn
|
||||||
// policy that is looking for any tag but the last one to be
|
// policy that is looking for any tag other than the last one
|
||||||
// loaded won't find it, and TLS handshakes will fail (see end)
|
// to be loaded won't find it, and TLS handshakes will fail
|
||||||
// of issue #3004)
|
// (see end of issue #3004)
|
||||||
//
|
//
|
||||||
// tlsCertTags maps certificate filenames to their tag.
|
// tlsCertTags maps certificate filenames to their tag.
|
||||||
// This is used to remember which tag is used for each
|
// This is used to remember which tag is used for each
|
||||||
@@ -226,7 +233,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
filename := h.Val()
|
filename := h.Val()
|
||||||
certDataPEM, err := ioutil.ReadFile(filename)
|
certDataPEM, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -267,6 +274,13 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
acmeIssuer.CA = arg[0]
|
acmeIssuer.CA = arg[0]
|
||||||
|
|
||||||
|
case "key_type":
|
||||||
|
arg := h.RemainingArgs()
|
||||||
|
if len(arg) != 1 {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
keyType = arg[0]
|
||||||
|
|
||||||
case "eab":
|
case "eab":
|
||||||
arg := h.RemainingArgs()
|
arg := h.RemainingArgs()
|
||||||
if len(arg) != 2 {
|
if len(arg) != 2 {
|
||||||
@@ -285,24 +299,33 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
modName := h.Val()
|
modName := h.Val()
|
||||||
mod, err := caddy.GetModule("tls.issuance." + modName)
|
modID := "tls.issuance." + modName
|
||||||
if err != nil {
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||||
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
|
|
||||||
}
|
|
||||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
||||||
if !ok {
|
|
||||||
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
|
||||||
}
|
|
||||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
issuer, ok := unm.(certmagic.Issuer)
|
issuer, ok := unm.(certmagic.Issuer)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
|
return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
|
||||||
}
|
}
|
||||||
issuers = append(issuers, issuer)
|
issuers = append(issuers, issuer)
|
||||||
|
|
||||||
|
case "get_certificate":
|
||||||
|
if !h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
modName := h.Val()
|
||||||
|
modID := "tls.get_certificate." + modName
|
||||||
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certManager, ok := unm.(certmagic.Manager)
|
||||||
|
if !ok {
|
||||||
|
return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm)
|
||||||
|
}
|
||||||
|
certManagers = append(certManagers, certManager)
|
||||||
|
|
||||||
case "dns":
|
case "dns":
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
@@ -313,20 +336,48 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
if acmeIssuer.Challenges == nil {
|
if acmeIssuer.Challenges == nil {
|
||||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
}
|
}
|
||||||
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
|
modID := "dns.providers." + provName
|
||||||
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
dnsProvModuleInstance := dnsProvModule.New()
|
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
|
||||||
if unm, ok := dnsProvModuleInstance.(caddyfile.Unmarshaler); ok {
|
|
||||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
case "resolvers":
|
||||||
if err != nil {
|
args := h.RemainingArgs()
|
||||||
return nil, err
|
if len(args) == 0 {
|
||||||
}
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModuleInstance, "name", provName, h.warnings)
|
if acmeIssuer == nil {
|
||||||
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges == nil {
|
||||||
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
acmeIssuer.Challenges.DNS.Resolvers = args
|
||||||
|
|
||||||
|
case "dns_challenge_override_domain":
|
||||||
|
arg := h.RemainingArgs()
|
||||||
|
if len(arg) != 1 {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
if acmeIssuer == nil {
|
||||||
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges == nil {
|
||||||
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
|
}
|
||||||
|
if acmeIssuer.Challenges.DNS == nil {
|
||||||
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||||
|
}
|
||||||
|
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
|
||||||
|
|
||||||
case "ca_root":
|
case "ca_root":
|
||||||
arg := h.RemainingArgs()
|
arg := h.RemainingArgs()
|
||||||
@@ -372,31 +423,64 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// some tls subdirectives are shortcuts that implicitly configure issuers, and the
|
||||||
|
// user can also configure issuers explicitly using the issuer subdirective; the
|
||||||
|
// logic to support both would likely be complex, or at least unintuitive
|
||||||
if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) {
|
if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) {
|
||||||
// some tls subdirectives are shortcuts that implicitly configure issuers, and the
|
|
||||||
// user can also configure issuers explicitly using the issuer subdirective; the
|
|
||||||
// logic to support both would likely be complex, or at least unintuitive
|
|
||||||
return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)")
|
return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)")
|
||||||
}
|
}
|
||||||
for _, issuer := range issuers {
|
if acmeIssuer != nil && internalIssuer != nil {
|
||||||
configVals = append(configVals, ConfigValue{
|
return nil, h.Err("cannot create both ACME and internal certificate issuers")
|
||||||
Class: "tls.cert_issuer",
|
|
||||||
Value: issuer,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if acmeIssuer != nil {
|
|
||||||
configVals = append(configVals, ConfigValue{
|
// now we should either have: explicitly-created issuers, or an implicitly-created
|
||||||
Class: "tls.cert_issuer",
|
// ACME or internal issuer, or no issuers at all
|
||||||
Value: disambiguateACMEIssuer(acmeIssuer),
|
switch {
|
||||||
})
|
case len(issuers) > 0:
|
||||||
}
|
for _, issuer := range issuers {
|
||||||
if internalIssuer != nil {
|
configVals = append(configVals, ConfigValue{
|
||||||
|
Class: "tls.cert_issuer",
|
||||||
|
Value: issuer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case acmeIssuer != nil:
|
||||||
|
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
||||||
|
defaultIssuers := caddytls.DefaultIssuers()
|
||||||
|
|
||||||
|
// if a CA endpoint was set, override multiple implicit issuers since it's a specific one
|
||||||
|
if acmeIssuer.CA != "" {
|
||||||
|
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, issuer := range defaultIssuers {
|
||||||
|
switch iss := issuer.(type) {
|
||||||
|
case *caddytls.ACMEIssuer:
|
||||||
|
issuer = acmeIssuer
|
||||||
|
case *caddytls.ZeroSSLIssuer:
|
||||||
|
iss.ACMEIssuer = acmeIssuer
|
||||||
|
}
|
||||||
|
configVals = append(configVals, ConfigValue{
|
||||||
|
Class: "tls.cert_issuer",
|
||||||
|
Value: issuer,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case internalIssuer != nil:
|
||||||
configVals = append(configVals, ConfigValue{
|
configVals = append(configVals, ConfigValue{
|
||||||
Class: "tls.cert_issuer",
|
Class: "tls.cert_issuer",
|
||||||
Value: internalIssuer,
|
Value: internalIssuer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// certificate key type
|
||||||
|
if keyType != "" {
|
||||||
|
configVals = append(configVals, ConfigValue{
|
||||||
|
Class: "tls.key_type",
|
||||||
|
Value: keyType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// on-demand TLS
|
// on-demand TLS
|
||||||
if onDemand {
|
if onDemand {
|
||||||
configVals = append(configVals, ConfigValue{
|
configVals = append(configVals, ConfigValue{
|
||||||
@@ -404,6 +488,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
Value: true,
|
Value: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
for _, certManager := range certManagers {
|
||||||
|
configVals = append(configVals, ConfigValue{
|
||||||
|
Class: "tls.cert_manager",
|
||||||
|
Value: certManager,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// custom certificate selection
|
// custom certificate selection
|
||||||
if len(certSelector.AnyTag) > 0 {
|
if len(certSelector.AnyTag) > 0 {
|
||||||
@@ -441,6 +531,13 @@ func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
return caddyhttp.VarsMiddleware{"root": root}, nil
|
return caddyhttp.VarsMiddleware{"root": root}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax.
|
||||||
|
func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
v := new(caddyhttp.VarsMiddleware)
|
||||||
|
err := v.UnmarshalCaddyfile(h.Dispenser)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
// parseRedir parses the redir directive. Syntax:
|
// parseRedir parses the redir directive. Syntax:
|
||||||
//
|
//
|
||||||
// redir [<matcher>] <to> [<code>]
|
// redir [<matcher>] <to> [<code>]
|
||||||
@@ -459,14 +556,14 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
code = h.Val()
|
code = h.Val()
|
||||||
}
|
}
|
||||||
if code == "permanent" {
|
|
||||||
code = "301"
|
|
||||||
}
|
|
||||||
if code == "temporary" || code == "" {
|
|
||||||
code = "302"
|
|
||||||
}
|
|
||||||
var body string
|
var body string
|
||||||
if code == "html" {
|
switch code {
|
||||||
|
case "permanent":
|
||||||
|
code = "301"
|
||||||
|
case "temporary", "":
|
||||||
|
code = "302"
|
||||||
|
case "html":
|
||||||
// Script tag comes first since that will better imitate a redirect in the browser's
|
// Script tag comes first since that will better imitate a redirect in the browser's
|
||||||
// history, but the meta tag is a fallback for most non-JS clients.
|
// history, but the meta tag is a fallback for most non-JS clients.
|
||||||
const metaRedir = `<!DOCTYPE html>
|
const metaRedir = `<!DOCTYPE html>
|
||||||
@@ -481,6 +578,27 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
`
|
`
|
||||||
safeTo := html.EscapeString(to)
|
safeTo := html.EscapeString(to)
|
||||||
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
|
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
|
||||||
|
code = "302"
|
||||||
|
default:
|
||||||
|
// Allow placeholders for the code
|
||||||
|
if strings.HasPrefix(code, "{") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Try to validate as an integer otherwise
|
||||||
|
codeInt, err := strconv.Atoi(code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
|
||||||
|
}
|
||||||
|
// Sometimes, a 401 with Location header is desirable because
|
||||||
|
// requests made with XHR will "eat" the 3xx redirect; so if
|
||||||
|
// the intent was to redirect to an auth page, a 3xx won't
|
||||||
|
// work. Responding with 401 allows JS code to read the
|
||||||
|
// Location header and do a window.location redirect manually.
|
||||||
|
// see https://stackoverflow.com/a/2573589/846934
|
||||||
|
// see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
|
||||||
|
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
|
||||||
|
return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return caddyhttp.StaticResponse{
|
return caddyhttp.StaticResponse{
|
||||||
@@ -500,6 +618,25 @@ func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
return sr, nil
|
return sr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseAbort parses the abort directive.
|
||||||
|
func parseAbort(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
h.Next() // consume directive
|
||||||
|
for h.Next() || h.NextBlock(0) {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
return &caddyhttp.StaticResponse{Abort: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseError parses the error directive.
|
||||||
|
func parseError(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
se := new(caddyhttp.StaticError)
|
||||||
|
err := se.UnmarshalCaddyfile(h.Dispenser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return se, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseRoute parses the route directive.
|
// parseRoute parses the route directive.
|
||||||
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
sr := new(caddyhttp.Subroute)
|
sr := new(caddyhttp.Subroute)
|
||||||
@@ -553,11 +690,50 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
func parseLog(h Helper) ([]ConfigValue, error) {
|
func parseLog(h Helper) ([]ConfigValue, error) {
|
||||||
|
return parseLogHelper(h, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLogHelper is used both for the parseLog directive within Server Blocks,
|
||||||
|
// as well as the global "log" option for configuring loggers at the global
|
||||||
|
// level. The parseAsGlobalOption parameter is used to distinguish any differing logic
|
||||||
|
// between the two.
|
||||||
|
func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue, error) {
|
||||||
|
// When the globalLogNames parameter is passed in, we make
|
||||||
|
// modifications to the parsing behavior.
|
||||||
|
parseAsGlobalOption := globalLogNames != nil
|
||||||
|
|
||||||
var configValues []ConfigValue
|
var configValues []ConfigValue
|
||||||
for h.Next() {
|
for h.Next() {
|
||||||
// log does not currently support any arguments
|
// Logic below expects that a name is always present when a
|
||||||
if h.NextArg() {
|
// global option is being parsed.
|
||||||
return nil, h.ArgErr()
|
var globalLogName string
|
||||||
|
if parseAsGlobalOption {
|
||||||
|
if h.NextArg() {
|
||||||
|
globalLogName = h.Val()
|
||||||
|
|
||||||
|
// Only a single argument is supported.
|
||||||
|
if h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If there is no log name specified, we
|
||||||
|
// reference the default logger. See the
|
||||||
|
// setupNewDefault function in the logging
|
||||||
|
// package for where this is configured.
|
||||||
|
globalLogName = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify this name is unused.
|
||||||
|
_, used := globalLogNames[globalLogName]
|
||||||
|
if used {
|
||||||
|
return nil, h.Err("duplicate global log option for: " + globalLogName)
|
||||||
|
}
|
||||||
|
globalLogNames[globalLogName] = struct{}{}
|
||||||
|
} else {
|
||||||
|
// No arguments are supported for the server block log directive
|
||||||
|
if h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cl := new(caddy.CustomLog)
|
cl := new(caddy.CustomLog)
|
||||||
@@ -583,21 +759,15 @@ func parseLog(h Helper) ([]ConfigValue, error) {
|
|||||||
case "discard":
|
case "discard":
|
||||||
wo = caddy.DiscardWriter{}
|
wo = caddy.DiscardWriter{}
|
||||||
default:
|
default:
|
||||||
mod, err := caddy.GetModule("caddy.logging.writers." + moduleName)
|
modID := "caddy.logging.writers." + moduleName
|
||||||
if err != nil {
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||||
return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err)
|
|
||||||
}
|
|
||||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
||||||
if !ok {
|
|
||||||
return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod)
|
|
||||||
}
|
|
||||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var ok bool
|
||||||
wo, ok = unm.(caddy.WriterOpener)
|
wo, ok = unm.(caddy.WriterOpener)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, h.Errf("module %s is not a WriterOpener", mod)
|
return nil, h.Errf("module %s (%T) is not a WriterOpener", modID, unm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
||||||
@@ -607,21 +777,14 @@ func parseLog(h Helper) ([]ConfigValue, error) {
|
|||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
moduleName := h.Val()
|
moduleName := h.Val()
|
||||||
mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
|
moduleID := "caddy.logging.encoders." + moduleName
|
||||||
if err != nil {
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID)
|
||||||
return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err)
|
|
||||||
}
|
|
||||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
||||||
if !ok {
|
|
||||||
return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
|
|
||||||
}
|
|
||||||
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
enc, ok := unm.(zapcore.Encoder)
|
enc, ok := unm.(zapcore.Encoder)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, h.Errf("module %s is not a zapcore.Encoder", mod)
|
return nil, h.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
|
||||||
}
|
}
|
||||||
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
|
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
|
||||||
|
|
||||||
@@ -634,22 +797,48 @@ func parseLog(h Helper) ([]ConfigValue, error) {
|
|||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "include":
|
||||||
|
// This configuration is only allowed in the global options
|
||||||
|
if !parseAsGlobalOption {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
for h.NextArg() {
|
||||||
|
cl.Include = append(cl.Include, h.Val())
|
||||||
|
}
|
||||||
|
|
||||||
|
case "exclude":
|
||||||
|
// This configuration is only allowed in the global options
|
||||||
|
if !parseAsGlobalOption {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
for h.NextArg() {
|
||||||
|
cl.Exclude = append(cl.Exclude, h.Val())
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
|
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var val namedCustomLog
|
var val namedCustomLog
|
||||||
|
// Skip handling of empty logging configs
|
||||||
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
||||||
logCounter, ok := h.State["logCounter"].(int)
|
if parseAsGlobalOption {
|
||||||
if !ok {
|
// Use indicated name for global log options
|
||||||
logCounter = 0
|
val.name = globalLogName
|
||||||
|
val.log = cl
|
||||||
|
} else {
|
||||||
|
// Construct a log name for server log streams
|
||||||
|
logCounter, ok := h.State["logCounter"].(int)
|
||||||
|
if !ok {
|
||||||
|
logCounter = 0
|
||||||
|
}
|
||||||
|
val.name = fmt.Sprintf("log%d", logCounter)
|
||||||
|
cl.Include = []string{"http.log.access." + val.name}
|
||||||
|
val.log = cl
|
||||||
|
logCounter++
|
||||||
|
h.State["logCounter"] = logCounter
|
||||||
}
|
}
|
||||||
val.name = fmt.Sprintf("log%d", logCounter)
|
|
||||||
cl.Include = []string{"http.log.access." + val.name}
|
|
||||||
val.log = cl
|
|
||||||
logCounter++
|
|
||||||
h.State["logCounter"] = logCounter
|
|
||||||
}
|
}
|
||||||
configValues = append(configValues, ConfigValue{
|
configValues = append(configValues, ConfigValue{
|
||||||
Class: "custom_log",
|
Class: "custom_log",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
func TestLogDirectiveSyntax(t *testing.T) {
|
func TestLogDirectiveSyntax(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
expectWarn bool
|
output string
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
log
|
log
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
output: `{"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -28,17 +28,34 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log /foo {
|
log {
|
||||||
|
format filter {
|
||||||
|
wrap console
|
||||||
|
fields {
|
||||||
|
request>remote_ip ip_mask {
|
||||||
|
ipv4 24
|
||||||
|
ipv6 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"request\u003eremote_ip":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
log invalid {
|
||||||
output file foo.log
|
output file foo.log
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -47,13 +64,149 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
ServerType: ServerType{},
|
ServerType: ServerType{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
|
out, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
if len(warnings) > 0 != tc.expectWarn {
|
if err != nil != tc.expectError {
|
||||||
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
|
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if string(out) != tc.output {
|
||||||
|
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirDirectiveSyntax(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
input string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir :8081
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /api/* :8081 300
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir :8081 300
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /api/* :8081 399
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir :8081 399
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /old.html /new.html
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /old.html /new.html temporary
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir https://example.com{uri} permanent
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /old.html /new.html permanent
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /old.html /new.html html
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// this is now allowed so a Location header
|
||||||
|
// can be written and consumed by JS
|
||||||
|
// in the case of XHR requests
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 401
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 402
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 {http.reverse_proxy.status_code}
|
||||||
|
}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /old.html /new.html htlm
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 200
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 temp
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 perm
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 php
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
if err != nil != tc.expectError {
|
if err != nil != tc.expectError {
|
||||||
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -37,32 +37,41 @@ import (
|
|||||||
// The header directive goes second so that headers
|
// The header directive goes second so that headers
|
||||||
// can be manipulated before doing redirects.
|
// can be manipulated before doing redirects.
|
||||||
var directiveOrder = []string{
|
var directiveOrder = []string{
|
||||||
|
"tracing",
|
||||||
|
|
||||||
"map",
|
"map",
|
||||||
|
"vars",
|
||||||
"root",
|
"root",
|
||||||
|
|
||||||
"header",
|
"header",
|
||||||
|
"copy_response_headers", // only in reverse_proxy's handle_response
|
||||||
"request_body",
|
"request_body",
|
||||||
|
|
||||||
"redir",
|
"redir",
|
||||||
"rewrite",
|
|
||||||
|
|
||||||
// URI manipulation
|
// incoming request manipulation
|
||||||
|
"method",
|
||||||
|
"rewrite",
|
||||||
"uri",
|
"uri",
|
||||||
"try_files",
|
"try_files",
|
||||||
|
|
||||||
// middleware handlers; some wrap responses
|
// middleware handlers; some wrap responses
|
||||||
"basicauth",
|
"basicauth",
|
||||||
|
"forward_auth",
|
||||||
"request_header",
|
"request_header",
|
||||||
"encode",
|
"encode",
|
||||||
|
"push",
|
||||||
"templates",
|
"templates",
|
||||||
|
|
||||||
// special routing & dispatching directives
|
// special routing & dispatching directives
|
||||||
"handle",
|
"handle",
|
||||||
"handle_path",
|
"handle_path",
|
||||||
"route",
|
"route",
|
||||||
"push",
|
|
||||||
|
|
||||||
// handlers that typically respond to requests
|
// handlers that typically respond to requests
|
||||||
|
"abort",
|
||||||
|
"error",
|
||||||
|
"copy_response", // only in reverse_proxy's handle_response
|
||||||
"respond",
|
"respond",
|
||||||
"metrics",
|
"metrics",
|
||||||
"reverse_proxy",
|
"reverse_proxy",
|
||||||
@@ -263,6 +272,13 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
|
|||||||
return []ConfigValue{{Class: "bind", Value: addrs}}
|
return []ConfigValue{{Class: "bind", Value: addrs}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithDispenser returns a new instance based on d. All others Helper
|
||||||
|
// fields are copied, so typically maps are shared with this new instance.
|
||||||
|
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
|
||||||
|
h.Dispenser = d
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
// ParseSegmentAsSubroute parses the segment such that its subdirectives
|
// ParseSegmentAsSubroute parses the segment such that its subdirectives
|
||||||
// are themselves treated as directives, from which a subroute is built
|
// are themselves treated as directives, from which a subroute is built
|
||||||
// and returned.
|
// and returned.
|
||||||
@@ -320,7 +336,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
dir := seg.Directive()
|
dir := seg.Directive()
|
||||||
dirFunc, ok := registeredDirectives[dir]
|
dirFunc, ok := registeredDirectives[dir]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, h.Errf("unrecognized directive: %s", dir)
|
return nil, h.Errf("unrecognized directive: %s - are you sure your Caddyfile structure (nesting and braces) is correct?", dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
subHelper := h
|
subHelper := h
|
||||||
@@ -331,6 +347,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dir = normalizeDirectiveName(dir)
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
result.directive = dir
|
result.directive = dir
|
||||||
allResults = append(allResults, result)
|
allResults = append(allResults, result)
|
||||||
@@ -406,14 +425,29 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
jPathLen = len(jPM[0])
|
jPathLen = len(jPM[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// if both directives have no path matcher, use whichever one
|
// some directives involve setting values which can overwrite
|
||||||
// has any kind of matcher defined first.
|
// eachother, so it makes most sense to reverse the order so
|
||||||
if iPathLen == 0 && jPathLen == 0 {
|
// that the lease specific matcher is first; everything else
|
||||||
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
// has most-specific matcher first
|
||||||
}
|
if iDir == "vars" {
|
||||||
|
// if both directives have no path matcher, use whichever one
|
||||||
|
// has no matcher first.
|
||||||
|
if iPathLen == 0 && jPathLen == 0 {
|
||||||
|
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
|
||||||
|
}
|
||||||
|
|
||||||
// sort with the most-specific (longest) path first
|
// sort with the least-specific (shortest) path first
|
||||||
return iPathLen > jPathLen
|
return iPathLen < jPathLen
|
||||||
|
} else {
|
||||||
|
// if both directives have no path matcher, use whichever one
|
||||||
|
// has any kind of matcher defined first.
|
||||||
|
if iPathLen == 0 && jPathLen == 0 {
|
||||||
|
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort with the most-specific (longest) path first
|
||||||
|
return iPathLen > jPathLen
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,6 +503,27 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
|||||||
return sblockHosts
|
return sblockHosts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
||||||
|
// ensure each entry in our list is unique
|
||||||
|
hostMap := make(map[string]struct{})
|
||||||
|
for _, addr := range sb.keys {
|
||||||
|
if addr.Host == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if addr.Scheme != "http" && addr.Port != httpPort {
|
||||||
|
hostMap[addr.Host] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert map to slice
|
||||||
|
sblockHosts := make([]string, 0, len(hostMap))
|
||||||
|
for host := range hostMap {
|
||||||
|
sblockHosts = append(sblockHosts, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sblockHosts
|
||||||
|
}
|
||||||
|
|
||||||
// hasHostCatchAllKey returns true if sb has a key that
|
// hasHostCatchAllKey returns true if sb has a key that
|
||||||
// omits a host portion, i.e. it "catches all" hosts.
|
// omits a host portion, i.e. it "catches all" hosts.
|
||||||
func (sb serverBlock) hasHostCatchAllKey() bool {
|
func (sb serverBlock) hasHostCatchAllKey() bool {
|
||||||
@@ -480,6 +535,17 @@ func (sb serverBlock) hasHostCatchAllKey() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAllHTTP returns true if all sb keys explicitly specify
|
||||||
|
// the http:// scheme
|
||||||
|
func (sb serverBlock) isAllHTTP() bool {
|
||||||
|
for _, addr := range sb.keys {
|
||||||
|
if addr.Scheme != "http" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// UnmarshalFunc is a function which can unmarshal Caddyfile
|
// UnmarshalFunc is a function which can unmarshal Caddyfile
|
||||||
// tokens into zero or more config values using a Helper type.
|
// tokens into zero or more config values using a Helper type.
|
||||||
@@ -498,9 +564,10 @@ type (
|
|||||||
UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)
|
UnmarshalHandlerFunc func(h Helper) (caddyhttp.MiddlewareHandler, error)
|
||||||
|
|
||||||
// UnmarshalGlobalFunc is a function which can unmarshal Caddyfile
|
// UnmarshalGlobalFunc is a function which can unmarshal Caddyfile
|
||||||
// tokens into a global option config value using a Helper type.
|
// tokens from a global option. It is passed the tokens to parse and
|
||||||
// These are passed in a call to RegisterGlobalOption.
|
// existing value from the previous instance of this global option
|
||||||
UnmarshalGlobalFunc func(d *caddyfile.Dispenser) (interface{}, error)
|
// (if any). It returns the value to associate with this global option.
|
||||||
|
UnmarshalGlobalFunc func(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error)
|
||||||
)
|
)
|
||||||
|
|
||||||
var registeredDirectives = make(map[string]UnmarshalFunc)
|
var registeredDirectives = make(map[string]UnmarshalFunc)
|
||||||
|
|||||||
@@ -27,13 +27,26 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
|
caddyconfig.RegisterAdapter("caddyfile", caddyfile.Adapter{ServerType: ServerType{}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App represents the configuration for a non-standard
|
||||||
|
// Caddy app module (e.g. third-party plugin) which was
|
||||||
|
// parsed from a global options block.
|
||||||
|
type App struct {
|
||||||
|
// The JSON key for the app being configured
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// The raw app config as JSON
|
||||||
|
Value json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
// ServerType can set up a config from an HTTP Caddyfile.
|
// ServerType can set up a config from an HTTP Caddyfile.
|
||||||
type ServerType struct {
|
type ServerType struct {
|
||||||
}
|
}
|
||||||
@@ -75,32 +88,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace shorthand placeholders (which are
|
// replace shorthand placeholders (which are convenient
|
||||||
// convenient when writing a Caddyfile) with
|
// when writing a Caddyfile) with their actual placeholder
|
||||||
// their actual placeholder identifiers or
|
// identifiers or variable names
|
||||||
// variable names
|
replacer := strings.NewReplacer(placeholderShorthands()...)
|
||||||
replacer := strings.NewReplacer(
|
|
||||||
"{dir}", "{http.request.uri.path.dir}",
|
|
||||||
"{file}", "{http.request.uri.path.file}",
|
|
||||||
"{host}", "{http.request.host}",
|
|
||||||
"{hostport}", "{http.request.hostport}",
|
|
||||||
"{port}", "{http.request.port}",
|
|
||||||
"{method}", "{http.request.method}",
|
|
||||||
"{path}", "{http.request.uri.path}",
|
|
||||||
"{query}", "{http.request.uri.query}",
|
|
||||||
"{remote}", "{http.request.remote}",
|
|
||||||
"{remote_host}", "{http.request.remote.host}",
|
|
||||||
"{remote_port}", "{http.request.remote.port}",
|
|
||||||
"{scheme}", "{http.request.scheme}",
|
|
||||||
"{uri}", "{http.request.uri}",
|
|
||||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
|
||||||
"{tls_version}", "{http.request.tls.version}",
|
|
||||||
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
|
||||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
|
||||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
|
||||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
|
||||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
|
||||||
)
|
|
||||||
|
|
||||||
// these are placeholders that allow a user-defined final
|
// these are placeholders that allow a user-defined final
|
||||||
// parameters, but we still want to provide a shorthand
|
// parameters, but we still want to provide a shorthand
|
||||||
@@ -114,6 +105,9 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
||||||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
||||||
|
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||||
|
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
||||||
|
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sb := range originalServerBlocks {
|
for _, sb := range originalServerBlocks {
|
||||||
@@ -156,7 +150,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
dirFunc, ok := registeredDirectives[dir]
|
dirFunc, ok := registeredDirectives[dir]
|
||||||
if !ok {
|
if !ok {
|
||||||
tkn := segment[0]
|
tkn := segment[0]
|
||||||
return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir)
|
message := "%s:%d: unrecognized directive: %s"
|
||||||
|
if !sb.block.HasBraces {
|
||||||
|
message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
|
||||||
|
}
|
||||||
|
return nil, warnings, fmt.Errorf(message, tkn.File, tkn.Line, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := Helper{
|
h := Helper{
|
||||||
@@ -174,13 +172,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
|
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// As a special case, we want "handle_path" to be sorted
|
dir = normalizeDirectiveName(dir)
|
||||||
// at the same level as "handle", so we force them to use
|
|
||||||
// the same directive name after their parsing is complete.
|
|
||||||
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
|
|
||||||
if dir == "handle_path" {
|
|
||||||
dir = "handle"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
result.directive = dir
|
result.directive = dir
|
||||||
@@ -207,9 +199,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
|
|
||||||
// now that each server is configured, make the HTTP app
|
// now that each server is configured, make the HTTP app
|
||||||
httpApp := caddyhttp.App{
|
httpApp := caddyhttp.App{
|
||||||
HTTPPort: tryInt(options["http_port"], &warnings),
|
HTTPPort: tryInt(options["http_port"], &warnings),
|
||||||
HTTPSPort: tryInt(options["https_port"], &warnings),
|
HTTPSPort: tryInt(options["https_port"], &warnings),
|
||||||
Servers: servers,
|
GracePeriod: tryDuration(options["grace_period"], &warnings),
|
||||||
|
Servers: servers,
|
||||||
}
|
}
|
||||||
|
|
||||||
// then make the TLS app
|
// then make the TLS app
|
||||||
@@ -218,24 +211,32 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// then make the PKI app
|
||||||
|
pkiApp, warnings, err := st.buildPKIApp(pairings, options, warnings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, warnings, err
|
||||||
|
}
|
||||||
|
|
||||||
// extract any custom logs, and enforce configured levels
|
// extract any custom logs, and enforce configured levels
|
||||||
var customLogs []namedCustomLog
|
var customLogs []namedCustomLog
|
||||||
var hasDefaultLog bool
|
var hasDefaultLog bool
|
||||||
for _, p := range pairings {
|
addCustomLog := func(ncl namedCustomLog) {
|
||||||
for _, sb := range p.serverBlocks {
|
if ncl.name == "" {
|
||||||
for _, clVal := range sb.pile["custom_log"] {
|
return
|
||||||
ncl := clVal.Value.(namedCustomLog)
|
}
|
||||||
if ncl.name == "" {
|
if ncl.name == "default" {
|
||||||
continue
|
hasDefaultLog = true
|
||||||
}
|
}
|
||||||
if ncl.name == "default" {
|
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
||||||
hasDefaultLog = true
|
ncl.log.Level = "DEBUG"
|
||||||
}
|
}
|
||||||
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
customLogs = append(customLogs, ncl)
|
||||||
ncl.log.Level = "DEBUG"
|
}
|
||||||
}
|
|
||||||
customLogs = append(customLogs, ncl)
|
// Apply global log options, when set
|
||||||
}
|
if options["log"] != nil {
|
||||||
|
for _, logValue := range options["log"].([]ConfigValue) {
|
||||||
|
addCustomLog(logValue.Value.(namedCustomLog))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,14 +251,37 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply server-specific log options
|
||||||
|
for _, p := range pairings {
|
||||||
|
for _, sb := range p.serverBlocks {
|
||||||
|
for _, clVal := range sb.pile["custom_log"] {
|
||||||
|
addCustomLog(clVal.Value.(namedCustomLog))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// annnd the top-level config, then we're done!
|
// annnd the top-level config, then we're done!
|
||||||
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
|
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
|
||||||
|
|
||||||
|
// loop through the configured options, and if any of
|
||||||
|
// them are an httpcaddyfile App, then we insert them
|
||||||
|
// into the config as raw Caddy apps
|
||||||
|
for _, opt := range options {
|
||||||
|
if app, ok := opt.(App); ok {
|
||||||
|
cfg.AppsRaw[app.Name] = app.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the standard Caddy apps into the config
|
||||||
if len(httpApp.Servers) > 0 {
|
if len(httpApp.Servers) > 0 {
|
||||||
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
|
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
|
if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
|
||||||
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
|
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
|
||||||
}
|
}
|
||||||
|
if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
|
||||||
|
cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
|
||||||
|
}
|
||||||
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
|
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
|
||||||
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
|
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
|
||||||
"module",
|
"module",
|
||||||
@@ -280,7 +304,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
// most users seem to prefer not writing access logs
|
// most users seem to prefer not writing access logs
|
||||||
// to the default log when they are directed to a
|
// to the default log when they are directed to a
|
||||||
// file or have any other special customization
|
// file or have any other special customization
|
||||||
if len(ncl.log.Include) > 0 {
|
if ncl.name != "default" && len(ncl.log.Include) > 0 {
|
||||||
defaultLog, ok := cfg.Logging.Logs["default"]
|
defaultLog, ok := cfg.Logging.Logs["default"]
|
||||||
if !ok {
|
if !ok {
|
||||||
defaultLog = new(caddy.CustomLog)
|
defaultLog = new(caddy.CustomLog)
|
||||||
@@ -315,7 +339,7 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
|||||||
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
|
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err = optFunc(disp)
|
val, err = optFunc(disp, options[opt])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
|
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
|
||||||
}
|
}
|
||||||
@@ -329,11 +353,25 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
|||||||
}
|
}
|
||||||
serverOpts, ok := val.(serverOptions)
|
serverOpts, ok := val.(serverOptions)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unexpected type from 'servers' global options")
|
return nil, fmt.Errorf("unexpected type from 'servers' global options: %T", val)
|
||||||
}
|
}
|
||||||
options[opt] = append(existingOpts, serverOpts)
|
options[opt] = append(existingOpts, serverOpts)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Additionally, fold multiple "log" options together into an
|
||||||
|
// array so that multiple loggers can be configured.
|
||||||
|
if opt == "log" {
|
||||||
|
existingOpts, ok := options[opt].([]ConfigValue)
|
||||||
|
if !ok {
|
||||||
|
existingOpts = []ConfigValue{}
|
||||||
|
}
|
||||||
|
logOpts, ok := val.([]ConfigValue)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected type from 'log' global options: %T", val)
|
||||||
|
}
|
||||||
|
options[opt] = append(existingOpts, logOpts...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
options[opt] = val
|
options[opt] = val
|
||||||
}
|
}
|
||||||
@@ -389,11 +427,26 @@ func (st *ServerType) serversFromPairings(
|
|||||||
// handle the auto_https global option
|
// handle the auto_https global option
|
||||||
if autoHTTPS != "on" {
|
if autoHTTPS != "on" {
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
||||||
if autoHTTPS == "off" {
|
switch autoHTTPS {
|
||||||
|
case "off":
|
||||||
srv.AutoHTTPS.Disabled = true
|
srv.AutoHTTPS.Disabled = true
|
||||||
}
|
case "disable_redirects":
|
||||||
if autoHTTPS == "disable_redirects" {
|
|
||||||
srv.AutoHTTPS.DisableRedir = true
|
srv.AutoHTTPS.DisableRedir = true
|
||||||
|
case "disable_certs":
|
||||||
|
srv.AutoHTTPS.DisableCerts = true
|
||||||
|
case "ignore_loaded_certs":
|
||||||
|
srv.AutoHTTPS.IgnoreLoadedCerts = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using paths in site addresses is deprecated
|
||||||
|
// See ParseAddress() where parsing should later reject paths
|
||||||
|
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
|
||||||
|
for _, sblock := range p.serverBlocks {
|
||||||
|
for _, addr := range sblock.keys {
|
||||||
|
if addr.Path != "" {
|
||||||
|
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +462,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
var iLongestHost, jLongestHost string
|
var iLongestHost, jLongestHost string
|
||||||
var iWildcardHost, jWildcardHost bool
|
var iWildcardHost, jWildcardHost bool
|
||||||
for _, addr := range p.serverBlocks[i].keys {
|
for _, addr := range p.serverBlocks[i].keys {
|
||||||
if strings.Contains(addr.Host, "*.") {
|
if strings.Contains(addr.Host, "*") || addr.Host == "" {
|
||||||
iWildcardHost = true
|
iWildcardHost = true
|
||||||
}
|
}
|
||||||
if specificity(addr.Host) > specificity(iLongestHost) {
|
if specificity(addr.Host) > specificity(iLongestHost) {
|
||||||
@@ -420,7 +473,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, addr := range p.serverBlocks[j].keys {
|
for _, addr := range p.serverBlocks[j].keys {
|
||||||
if strings.Contains(addr.Host, "*.") {
|
if strings.Contains(addr.Host, "*") || addr.Host == "" {
|
||||||
jWildcardHost = true
|
jWildcardHost = true
|
||||||
}
|
}
|
||||||
if specificity(addr.Host) > specificity(jLongestHost) {
|
if specificity(addr.Host) > specificity(jLongestHost) {
|
||||||
@@ -430,9 +483,12 @@ func (st *ServerType) serversFromPairings(
|
|||||||
jLongestPath = addr.Path
|
jLongestPath = addr.Path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// catch-all blocks (blocks with no hostname) should always go
|
||||||
|
// last, even after blocks with wildcard hosts
|
||||||
|
if specificity(iLongestHost) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if specificity(jLongestHost) == 0 {
|
if specificity(jLongestHost) == 0 {
|
||||||
// catch-all blocks (blocks with no hostname) should always go
|
|
||||||
// last, even after blocks with wildcard hosts
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if iWildcardHost != jWildcardHost {
|
if iWildcardHost != jWildcardHost {
|
||||||
@@ -459,6 +515,16 @@ func (st *ServerType) serversFromPairings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if needed, the ServerLogConfig is initialized beforehand so
|
||||||
|
// that all server blocks can populate it with data, even when not
|
||||||
|
// coming with a log directive
|
||||||
|
for _, sblock := range p.serverBlocks {
|
||||||
|
if len(sblock.pile["custom_log"]) != 0 {
|
||||||
|
srv.Logs = new(caddyhttp.ServerLogConfig)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// create a subroute for each site in the server block
|
// create a subroute for each site in the server block
|
||||||
for _, sblock := range p.serverBlocks {
|
for _, sblock := range p.serverBlocks {
|
||||||
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
|
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
|
||||||
@@ -468,6 +534,13 @@ func (st *ServerType) serversFromPairings(
|
|||||||
|
|
||||||
hosts := sblock.hostsFromKeys(false)
|
hosts := sblock.hostsFromKeys(false)
|
||||||
|
|
||||||
|
// emit warnings if user put unspecified IP addresses; they probably want the bind directive
|
||||||
|
for _, h := range hosts {
|
||||||
|
if h == "0.0.0.0" || h == "::" {
|
||||||
|
caddy.Log().Named("caddyfile").Warn("Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", zap.String("address", h))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// tls: connection policies
|
// tls: connection policies
|
||||||
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
||||||
// tls connection policies
|
// tls connection policies
|
||||||
@@ -500,16 +573,20 @@ func (st *ServerType) serversFromPairings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range sblock.keys {
|
for _, addr := range sblock.keys {
|
||||||
// exclude any hosts that were defined explicitly with "http://"
|
// if server only uses HTTP port, auto-HTTPS will not apply
|
||||||
// in the key from automated cert management (issue #2998)
|
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
|
||||||
if addr.Scheme == "http" && addr.Host != "" {
|
// exclude any hosts that were defined explicitly with "http://"
|
||||||
if srv.AutoHTTPS == nil {
|
// in the key from automated cert management (issue #2998)
|
||||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
if addr.Scheme == "http" && addr.Host != "" {
|
||||||
}
|
if srv.AutoHTTPS == nil {
|
||||||
if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
|
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
||||||
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
|
}
|
||||||
|
if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
|
||||||
|
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
||||||
// can add a TLS conn policy if necessary
|
// can add a TLS conn policy if necessary
|
||||||
if addr.Scheme == "https" ||
|
if addr.Scheme == "https" ||
|
||||||
@@ -562,9 +639,6 @@ func (st *ServerType) serversFromPairings(
|
|||||||
sblockLogHosts := sblock.hostsFromKeys(true)
|
sblockLogHosts := sblock.hostsFromKeys(true)
|
||||||
for _, cval := range sblock.pile["custom_log"] {
|
for _, cval := range sblock.pile["custom_log"] {
|
||||||
ncl := cval.Value.(namedCustomLog)
|
ncl := cval.Value.(namedCustomLog)
|
||||||
if srv.Logs == nil {
|
|
||||||
srv.Logs = new(caddyhttp.ServerLogConfig)
|
|
||||||
}
|
|
||||||
if sblock.hasHostCatchAllKey() {
|
if sblock.hasHostCatchAllKey() {
|
||||||
// all requests for hosts not able to be listed should use
|
// all requests for hosts not able to be listed should use
|
||||||
// this log because it's a catch-all-hosts server block
|
// this log because it's a catch-all-hosts server block
|
||||||
@@ -974,6 +1048,19 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
|
|||||||
return subroute, nil
|
return subroute, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeDirectiveName ensures directives that should be sorted
|
||||||
|
// at the same level are named the same before sorting happens.
|
||||||
|
func normalizeDirectiveName(directive string) string {
|
||||||
|
// As a special case, we want "handle_path" to be sorted
|
||||||
|
// at the same level as "handle", so we force them to use
|
||||||
|
// the same directive name after their parsing is complete.
|
||||||
|
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
|
||||||
|
if directive == "handle_path" {
|
||||||
|
directive = "handle"
|
||||||
|
}
|
||||||
|
return directive
|
||||||
|
}
|
||||||
|
|
||||||
// consolidateRoutes combines routes with the same properties
|
// consolidateRoutes combines routes with the same properties
|
||||||
// (same matchers, same Terminal and Group settings) for a
|
// (same matchers, same Terminal and Group settings) for a
|
||||||
// cleaner overall output.
|
// cleaner overall output.
|
||||||
@@ -1155,6 +1242,58 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
|
|||||||
return msEncoded, nil
|
return msEncoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// placeholderShorthands returns a slice of old-new string pairs,
|
||||||
|
// where the left of the pair is a placeholder shorthand that may
|
||||||
|
// be used in the Caddyfile, and the right is the replacement.
|
||||||
|
func placeholderShorthands() []string {
|
||||||
|
return []string{
|
||||||
|
"{dir}", "{http.request.uri.path.dir}",
|
||||||
|
"{file}", "{http.request.uri.path.file}",
|
||||||
|
"{host}", "{http.request.host}",
|
||||||
|
"{hostport}", "{http.request.hostport}",
|
||||||
|
"{port}", "{http.request.port}",
|
||||||
|
"{method}", "{http.request.method}",
|
||||||
|
"{path}", "{http.request.uri.path}",
|
||||||
|
"{query}", "{http.request.uri.query}",
|
||||||
|
"{remote}", "{http.request.remote}",
|
||||||
|
"{remote_host}", "{http.request.remote.host}",
|
||||||
|
"{remote_port}", "{http.request.remote.port}",
|
||||||
|
"{scheme}", "{http.request.scheme}",
|
||||||
|
"{uri}", "{http.request.uri}",
|
||||||
|
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||||
|
"{tls_version}", "{http.request.tls.version}",
|
||||||
|
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||||
|
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||||
|
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||||
|
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||||
|
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||||
|
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||||
|
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WasReplacedPlaceholderShorthand checks if a token string was
|
||||||
|
// likely a replaced shorthand of the known Caddyfile placeholder
|
||||||
|
// replacement outputs. Useful to prevent some user-defined map
|
||||||
|
// output destinations from overlapping with one of the
|
||||||
|
// predefined shorthands.
|
||||||
|
func WasReplacedPlaceholderShorthand(token string) string {
|
||||||
|
prev := ""
|
||||||
|
for i, item := range placeholderShorthands() {
|
||||||
|
// only look at every 2nd item, which is the replacement
|
||||||
|
if i%2 == 0 {
|
||||||
|
prev = item
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Trim(token, "{}") == strings.Trim(item, "{}") {
|
||||||
|
// we return the original shorthand so it
|
||||||
|
// can be used for an error message
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// tryInt tries to convert val to an integer. If it fails,
|
// tryInt tries to convert val to an integer. If it fails,
|
||||||
// it downgrades the error to a warning and returns 0.
|
// it downgrades the error to a warning and returns 0.
|
||||||
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
|
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
|
||||||
@@ -1173,6 +1312,14 @@ func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
|
|||||||
return stringVal
|
return stringVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration {
|
||||||
|
durationVal, ok := val.(caddy.Duration)
|
||||||
|
if val != nil && !ok && warnings != nil {
|
||||||
|
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
|
||||||
|
}
|
||||||
|
return durationVal
|
||||||
|
}
|
||||||
|
|
||||||
// sliceContains returns true if needle is in haystack.
|
// sliceContains returns true if needle is in haystack.
|
||||||
func sliceContains(haystack []string, needle string) bool {
|
func sliceContains(haystack []string, needle string) bool {
|
||||||
for _, s := range haystack {
|
for _, s := range haystack {
|
||||||
@@ -1183,6 +1330,26 @@ func sliceContains(haystack []string, needle string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenersUseAnyPortOtherThan returns true if there are any
|
||||||
|
// listeners in addresses that use a port which is not otherPort.
|
||||||
|
// Mostly borrowed from unexported method in caddyhttp package.
|
||||||
|
func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
|
||||||
|
otherPortInt, err := strconv.Atoi(otherPort)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, lnAddr := range addresses {
|
||||||
|
laddrs, err := caddy.ParseNetworkAddress(lnAddr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if uint(otherPortInt) > laddrs.EndPort || uint(otherPortInt) < laddrs.StartPort {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// specificity returns len(s) minus any wildcards (*) and
|
// specificity returns len(s) minus any wildcards (*) and
|
||||||
// placeholders ({...}). Basically, it's a length count
|
// placeholders ({...}). Basically, it's a length count
|
||||||
// that penalizes the use of wildcards and placeholders.
|
// that penalizes the use of wildcards and placeholders.
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
func TestMatcherSyntax(t *testing.T) {
|
func TestMatcherSyntax(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
expectWarn bool
|
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -18,7 +17,6 @@ func TestMatcherSyntax(t *testing.T) {
|
|||||||
query showdebug=1
|
query showdebug=1
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -27,7 +25,6 @@ func TestMatcherSyntax(t *testing.T) {
|
|||||||
query bad format
|
query bad format
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -38,7 +35,6 @@ func TestMatcherSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -47,14 +43,12 @@ func TestMatcherSyntax(t *testing.T) {
|
|||||||
not path /somepath*
|
not path /somepath*
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `http://localhost
|
input: `http://localhost
|
||||||
@debug not path /somepath*
|
@debug not path /somepath*
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -63,7 +57,6 @@ func TestMatcherSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
http://localhost
|
http://localhost
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -72,12 +65,7 @@ func TestMatcherSyntax(t *testing.T) {
|
|||||||
ServerType: ServerType{},
|
ServerType: ServerType{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
|
_, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
if len(warnings) > 0 != tc.expectWarn {
|
|
||||||
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil != tc.expectError {
|
if err != nil != tc.expectError {
|
||||||
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||||
@@ -119,7 +107,6 @@ func TestSpecificity(t *testing.T) {
|
|||||||
func TestGlobalOptions(t *testing.T) {
|
func TestGlobalOptions(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
expectWarn bool
|
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -129,7 +116,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,7 +125,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -149,7 +134,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -161,7 +145,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -174,7 +157,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -187,7 +169,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,7 +181,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -213,7 +193,6 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -222,12 +201,7 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
ServerType: ServerType{},
|
ServerType: ServerType{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, warnings, err := adapter.Adapt([]byte(tc.input), nil)
|
_, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
if len(warnings) > 0 != tc.expectWarn {
|
|
||||||
t.Errorf("Test %d warning expectation failed Expected: %v, got %v", i, tc.expectWarn, warnings)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil != tc.expectError {
|
if err != nil != tc.expectError {
|
||||||
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
@@ -28,15 +29,19 @@ func init() {
|
|||||||
RegisterGlobalOption("debug", parseOptTrue)
|
RegisterGlobalOption("debug", parseOptTrue)
|
||||||
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
||||||
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
||||||
|
RegisterGlobalOption("default_bind", parseOptStringList)
|
||||||
|
RegisterGlobalOption("grace_period", parseOptDuration)
|
||||||
RegisterGlobalOption("default_sni", parseOptSingleString)
|
RegisterGlobalOption("default_sni", parseOptSingleString)
|
||||||
RegisterGlobalOption("order", parseOptOrder)
|
RegisterGlobalOption("order", parseOptOrder)
|
||||||
RegisterGlobalOption("experimental_http3", parseOptTrue)
|
|
||||||
RegisterGlobalOption("storage", parseOptStorage)
|
RegisterGlobalOption("storage", parseOptStorage)
|
||||||
|
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
|
||||||
|
RegisterGlobalOption("renew_interval", parseOptDuration)
|
||||||
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
||||||
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
||||||
RegisterGlobalOption("acme_dns", parseOptSingleString)
|
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
|
||||||
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
|
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
|
||||||
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
|
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
|
||||||
|
RegisterGlobalOption("skip_install_trust", parseOptTrue)
|
||||||
RegisterGlobalOption("email", parseOptSingleString)
|
RegisterGlobalOption("email", parseOptSingleString)
|
||||||
RegisterGlobalOption("admin", parseOptAdmin)
|
RegisterGlobalOption("admin", parseOptAdmin)
|
||||||
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
|
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
|
||||||
@@ -44,13 +49,14 @@ func init() {
|
|||||||
RegisterGlobalOption("key_type", parseOptSingleString)
|
RegisterGlobalOption("key_type", parseOptSingleString)
|
||||||
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
||||||
RegisterGlobalOption("servers", parseServerOptions)
|
RegisterGlobalOption("servers", parseServerOptions)
|
||||||
|
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
||||||
|
RegisterGlobalOption("log", parseLogOptions)
|
||||||
|
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptTrue(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOptHTTPPort(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptHTTPPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
var httpPort int
|
var httpPort int
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
var httpPortStr string
|
var httpPortStr string
|
||||||
@@ -66,7 +72,7 @@ func parseOptHTTPPort(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return httpPort, nil
|
return httpPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
var httpsPort int
|
var httpsPort int
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
var httpsPortStr string
|
var httpsPortStr string
|
||||||
@@ -82,7 +88,7 @@ func parseOptHTTPSPort(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return httpsPort, nil
|
return httpsPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptOrder(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
newOrder := directiveOrder
|
newOrder := directiveOrder
|
||||||
|
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
@@ -158,34 +164,59 @@ func parseOptOrder(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return newOrder, nil
|
return newOrder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
if !d.Next() { // consume option name
|
if !d.Next() { // consume option name
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
if !d.Next() { // get storage module name
|
if !d.Next() { // get storage module name
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
modName := d.Val()
|
modID := "caddy.storage." + d.Val()
|
||||||
mod, err := caddy.GetModule("caddy.storage." + modName)
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("getting storage module '%s': %v", modName, err)
|
|
||||||
}
|
|
||||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
||||||
if !ok {
|
|
||||||
return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
|
||||||
}
|
|
||||||
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
storage, ok := unm.(caddy.StorageConverter)
|
storage, ok := unm.(caddy.StorageConverter)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
|
return nil, d.Errf("module %s is not a caddy.StorageConverter", modID)
|
||||||
}
|
}
|
||||||
return storage, nil
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
|
if !d.Next() { // consume option name
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
if !d.Next() { // get duration value
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return caddy.Duration(dur), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
|
if !d.Next() { // consume option name
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
if !d.Next() { // get DNS module name
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
modID := "dns.providers." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prov, ok := unm.(certmagic.ACMEDNSProvider)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
|
||||||
|
}
|
||||||
|
return prov, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptACMEEAB(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
eab := new(acme.EAB)
|
eab := new(acme.EAB)
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
@@ -213,34 +244,30 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return eab, nil
|
return eab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
|
||||||
if !d.Next() { // consume option name
|
var issuers []certmagic.Issuer
|
||||||
return nil, d.ArgErr()
|
if existing != nil {
|
||||||
|
issuers = existing.([]certmagic.Issuer)
|
||||||
}
|
}
|
||||||
if !d.Next() { // get issuer module name
|
for d.Next() { // consume option name
|
||||||
return nil, d.ArgErr()
|
if !d.Next() { // get issuer module name
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
modID := "tls.issuance." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
iss, ok := unm.(certmagic.Issuer)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
|
||||||
|
}
|
||||||
|
issuers = append(issuers, iss)
|
||||||
}
|
}
|
||||||
modName := d.Val()
|
return issuers, nil
|
||||||
mod, err := caddy.GetModule("tls.issuance." + modName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, d.Errf("getting issuer module '%s': %v", modName, err)
|
|
||||||
}
|
|
||||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
||||||
if !ok {
|
|
||||||
return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
|
||||||
}
|
|
||||||
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
iss, ok := unm.(certmagic.Issuer)
|
|
||||||
if !ok {
|
|
||||||
return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
|
|
||||||
}
|
|
||||||
return iss, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
d.Next() // consume parameter name
|
d.Next() // consume parameter name
|
||||||
if !d.Next() {
|
if !d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
@@ -252,7 +279,16 @@ func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptStringList(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
|
d.Next() // consume parameter name
|
||||||
|
val := d.RemainingArgs()
|
||||||
|
if len(val) == 0 {
|
||||||
|
return "", d.ArgErr()
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
adminCfg := new(caddy.AdminConfig)
|
adminCfg := new(caddy.AdminConfig)
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
@@ -288,7 +324,7 @@ func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return adminCfg, nil
|
return adminCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptOnDemand(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
var ond *caddytls.OnDemandConfig
|
var ond *caddytls.OnDemandConfig
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
@@ -348,7 +384,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return ond, nil
|
return ond, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
d.Next() // consume parameter name
|
d.Next() // consume parameter name
|
||||||
if !d.Next() {
|
if !d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
@@ -357,12 +393,79 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
if d.Next() {
|
if d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
}
|
}
|
||||||
if val != "off" && val != "disable_redirects" {
|
if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
|
||||||
return "", d.Errf("auto_https must be either 'off' or 'disable_redirects'")
|
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
|
||||||
}
|
}
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
return unmarshalCaddyfileServerOptions(d)
|
return unmarshalCaddyfileServerOptions(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOCSPStaplingOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
var val string
|
||||||
|
if !d.AllArgs(&val) {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
if val != "off" {
|
||||||
|
return nil, d.Errf("invalid argument '%s'", val)
|
||||||
|
}
|
||||||
|
return certmagic.OCSPConfig{
|
||||||
|
DisableStapling: val == "off",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLogOptions parses the global log option. Syntax:
|
||||||
|
//
|
||||||
|
// log [name] {
|
||||||
|
// output <writer_module> ...
|
||||||
|
// format <encoder_module> ...
|
||||||
|
// level <level>
|
||||||
|
// include <namespaces...>
|
||||||
|
// exclude <namespaces...>
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// When the name argument is unspecified, this directive modifies the default
|
||||||
|
// logger.
|
||||||
|
//
|
||||||
|
func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
|
||||||
|
currentNames := make(map[string]struct{})
|
||||||
|
if existingVal != nil {
|
||||||
|
innerVals, ok := existingVal.([]ConfigValue)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("existing log values of unexpected type: %T", existingVal)
|
||||||
|
}
|
||||||
|
for _, rawVal := range innerVals {
|
||||||
|
val, ok := rawVal.Value.(namedCustomLog)
|
||||||
|
if !ok {
|
||||||
|
return nil, d.Errf("existing log value of unexpected type: %T", existingVal)
|
||||||
|
}
|
||||||
|
currentNames[val.name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var warnings []caddyconfig.Warning
|
||||||
|
// Call out the same parser that handles server-specific log configuration.
|
||||||
|
configValues, err := parseLogHelper(
|
||||||
|
Helper{
|
||||||
|
Dispenser: d,
|
||||||
|
warnings: &warnings,
|
||||||
|
},
|
||||||
|
currentNames,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
return nil, d.Errf("warnings found in parsing global log options: %+v", warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configValues, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
|
d.Next()
|
||||||
|
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package httpcaddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGlobalLogOptionSyntax(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
input string
|
||||||
|
output string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
// NOTE: Additional test cases of successful Caddyfile parsing
|
||||||
|
// are present in: caddytest/integration/caddyfile_adapt/
|
||||||
|
{
|
||||||
|
input: `{
|
||||||
|
log default
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
output: `{}`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{
|
||||||
|
log example {
|
||||||
|
output file foo.log
|
||||||
|
}
|
||||||
|
log example {
|
||||||
|
format json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{
|
||||||
|
log example /foo {
|
||||||
|
output file foo.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
|
if err != nil != tc.expectError {
|
||||||
|
t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(out) != tc.output {
|
||||||
|
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package httpcaddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterGlobalOption("pki", parsePKIApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePKIApp parses the global log option. Syntax:
|
||||||
|
//
|
||||||
|
// pki {
|
||||||
|
// ca [<id>] {
|
||||||
|
// name <name>
|
||||||
|
// root_cn <name>
|
||||||
|
// intermediate_cn <name>
|
||||||
|
// root {
|
||||||
|
// cert <path>
|
||||||
|
// key <path>
|
||||||
|
// format <format>
|
||||||
|
// }
|
||||||
|
// intermediate {
|
||||||
|
// cert <path>
|
||||||
|
// key <path>
|
||||||
|
// format <format>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// When the CA ID is unspecified, 'local' is assumed.
|
||||||
|
//
|
||||||
|
func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
|
||||||
|
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "ca":
|
||||||
|
pkiCa := new(caddypki.CA)
|
||||||
|
if d.NextArg() {
|
||||||
|
pkiCa.ID = d.Val()
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pkiCa.ID == "" {
|
||||||
|
pkiCa.ID = caddypki.DefaultCAID
|
||||||
|
}
|
||||||
|
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "name":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Name = d.Val()
|
||||||
|
|
||||||
|
case "root_cn":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.RootCommonName = d.Val()
|
||||||
|
|
||||||
|
case "intermediate_cn":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.IntermediateCommonName = d.Val()
|
||||||
|
|
||||||
|
case "root":
|
||||||
|
if pkiCa.Root == nil {
|
||||||
|
pkiCa.Root = new(caddypki.KeyPair)
|
||||||
|
}
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "cert":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Root.Certificate = d.Val()
|
||||||
|
|
||||||
|
case "key":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Root.PrivateKey = d.Val()
|
||||||
|
|
||||||
|
case "format":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Root.Format = d.Val()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "intermediate":
|
||||||
|
if pkiCa.Intermediate == nil {
|
||||||
|
pkiCa.Intermediate = new(caddypki.KeyPair)
|
||||||
|
}
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "cert":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Intermediate.Certificate = d.Val()
|
||||||
|
|
||||||
|
case "key":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Intermediate.PrivateKey = d.Val()
|
||||||
|
|
||||||
|
case "format":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
pkiCa.Intermediate.Format = d.Val()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pki.CAs[pkiCa.ID] = pkiCa
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pki, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st ServerType) buildPKIApp(
|
||||||
|
pairings []sbAddrAssociation,
|
||||||
|
options map[string]interface{},
|
||||||
|
warnings []caddyconfig.Warning,
|
||||||
|
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||||
|
|
||||||
|
skipInstallTrust := false
|
||||||
|
if _, ok := options["skip_install_trust"]; ok {
|
||||||
|
skipInstallTrust = true
|
||||||
|
}
|
||||||
|
falseBool := false
|
||||||
|
|
||||||
|
// Load the PKI app configured via global options
|
||||||
|
var pkiApp *caddypki.PKI
|
||||||
|
unwrappedPki, ok := options["pki"].(*caddypki.PKI)
|
||||||
|
if ok {
|
||||||
|
pkiApp = unwrappedPki
|
||||||
|
} else {
|
||||||
|
pkiApp = &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||||
|
}
|
||||||
|
for _, ca := range pkiApp.CAs {
|
||||||
|
if skipInstallTrust {
|
||||||
|
ca.InstallTrust = &falseBool
|
||||||
|
}
|
||||||
|
pkiApp.CAs[ca.ID] = ca
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in the CAs configured via directives
|
||||||
|
for _, p := range pairings {
|
||||||
|
for _, sblock := range p.serverBlocks {
|
||||||
|
// find all the CAs that were defined and add them to the app config
|
||||||
|
// i.e. from any "acme_server" directives
|
||||||
|
for _, caCfgValue := range sblock.pile["pki.ca"] {
|
||||||
|
ca := caCfgValue.Value.(*caddypki.CA)
|
||||||
|
if skipInstallTrust {
|
||||||
|
ca.InstallTrust = &falseBool
|
||||||
|
}
|
||||||
|
|
||||||
|
// the CA might already exist from global options, so
|
||||||
|
// don't overwrite it in that case
|
||||||
|
if _, ok := pkiApp.CAs[ca.ID]; !ok {
|
||||||
|
pkiApp.CAs[ca.ID] = ca
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there was no CAs defined in any of the servers,
|
||||||
|
// and we were requested to not install trust, then
|
||||||
|
// add one for the default/local CA to do so
|
||||||
|
if len(pkiApp.CAs) == 0 && skipInstallTrust {
|
||||||
|
ca := new(caddypki.CA)
|
||||||
|
ca.ID = caddypki.DefaultCAID
|
||||||
|
ca.InstallTrust = &falseBool
|
||||||
|
pkiApp.CAs[ca.ID] = ca
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkiApp, warnings, nil
|
||||||
|
}
|
||||||
@@ -33,15 +33,16 @@ type serverOptions struct {
|
|||||||
ListenerAddress string
|
ListenerAddress string
|
||||||
|
|
||||||
// These will all map 1:1 to the caddyhttp.Server struct
|
// These will all map 1:1 to the caddyhttp.Server struct
|
||||||
ListenerWrappersRaw []json.RawMessage
|
ListenerWrappersRaw []json.RawMessage
|
||||||
ReadTimeout caddy.Duration
|
ReadTimeout caddy.Duration
|
||||||
ReadHeaderTimeout caddy.Duration
|
ReadHeaderTimeout caddy.Duration
|
||||||
WriteTimeout caddy.Duration
|
WriteTimeout caddy.Duration
|
||||||
IdleTimeout caddy.Duration
|
IdleTimeout caddy.Duration
|
||||||
MaxHeaderBytes int
|
MaxHeaderBytes int
|
||||||
AllowH2C bool
|
AllowH2C bool
|
||||||
ExperimentalHTTP3 bool
|
ExperimentalHTTP3 bool
|
||||||
StrictSNIHost *bool
|
StrictSNIHost *bool
|
||||||
|
ShouldLogCredentials bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
|
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
|
||||||
@@ -57,21 +58,14 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
|
|||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
case "listener_wrappers":
|
case "listener_wrappers":
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
mod, err := caddy.GetModule("caddy.listeners." + d.Val())
|
modID := "caddy.listeners." + d.Val()
|
||||||
if err != nil {
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
return nil, fmt.Errorf("finding listener module '%s': %v", d.Val(), err)
|
|
||||||
}
|
|
||||||
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("listener module '%s' is not a Caddyfile unmarshaler", mod)
|
|
||||||
}
|
|
||||||
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
|
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("module %s is not a listener wrapper", mod)
|
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
|
||||||
}
|
}
|
||||||
jsonListenerWrapper := caddyconfig.JSONModuleObject(
|
jsonListenerWrapper := caddyconfig.JSONModuleObject(
|
||||||
listenerWrapper,
|
listenerWrapper,
|
||||||
@@ -141,6 +135,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
|
|||||||
}
|
}
|
||||||
serverOpts.MaxHeaderBytes = int(size)
|
serverOpts.MaxHeaderBytes = int(size)
|
||||||
|
|
||||||
|
case "log_credentials":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.ShouldLogCredentials = true
|
||||||
|
|
||||||
case "protocol":
|
case "protocol":
|
||||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
switch d.Val() {
|
switch d.Val() {
|
||||||
@@ -157,11 +157,14 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
|
|||||||
serverOpts.ExperimentalHTTP3 = true
|
serverOpts.ExperimentalHTTP3 = true
|
||||||
|
|
||||||
case "strict_sni_host":
|
case "strict_sni_host":
|
||||||
if d.NextArg() {
|
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
|
||||||
return nil, d.ArgErr()
|
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
|
||||||
}
|
}
|
||||||
trueBool := true
|
boolVal := true
|
||||||
serverOpts.StrictSNIHost = &trueBool
|
if d.Val() == "insecure_off" {
|
||||||
|
boolVal = false
|
||||||
|
}
|
||||||
|
serverOpts.StrictSNIHost = &boolVal
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
|
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
|
||||||
@@ -229,6 +232,12 @@ func applyServerOptions(
|
|||||||
server.AllowH2C = opts.AllowH2C
|
server.AllowH2C = opts.AllowH2C
|
||||||
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
|
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
|
||||||
server.StrictSNIHost = opts.StrictSNIHost
|
server.StrictSNIHost = opts.StrictSNIHost
|
||||||
|
if opts.ShouldLogCredentials {
|
||||||
|
if server.Logs == nil {
|
||||||
|
server.Logs = &caddyhttp.ServerLogConfig{}
|
||||||
|
}
|
||||||
|
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||||
var certLoaders []caddytls.CertificateLoader
|
var certLoaders []caddytls.CertificateLoader
|
||||||
|
|
||||||
|
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
|
||||||
|
if hp, ok := options["http_port"].(int); ok {
|
||||||
|
httpPort = strconv.Itoa(hp)
|
||||||
|
}
|
||||||
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
||||||
if hsp, ok := options["https_port"].(int); ok {
|
if hsp, ok := options["https_port"].(int); ok {
|
||||||
httpsPort = strconv.Itoa(hsp)
|
httpsPort = strconv.Itoa(hsp)
|
||||||
@@ -50,7 +54,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
// a hostless key, so that they don't get forgotten/omitted
|
// a hostless key, so that they don't get forgotten/omitted
|
||||||
// by auto-HTTPS (since they won't appear in route matchers)
|
// by auto-HTTPS (since they won't appear in route matchers)
|
||||||
var serverBlocksWithTLSHostlessKey int
|
var serverBlocksWithTLSHostlessKey int
|
||||||
hostsSharedWithHostlessKey := make(map[string]struct{})
|
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
|
||||||
for _, pair := range pairings {
|
for _, pair := range pairings {
|
||||||
for _, sb := range pair.serverBlocks {
|
for _, sb := range pair.serverBlocks {
|
||||||
for _, addr := range sb.keys {
|
for _, addr := range sb.keys {
|
||||||
@@ -66,8 +70,8 @@ func (st ServerType) buildTLSApp(
|
|||||||
if otherAddr.Original == addr.Original {
|
if otherAddr.Original == addr.Original {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if otherAddr.Host != "" {
|
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
|
||||||
hostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -91,7 +95,18 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
|
// avoid setting up TLS automation policies for a server that is HTTP-only
|
||||||
|
if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, sblock := range p.serverBlocks {
|
for _, sblock := range p.serverBlocks {
|
||||||
|
// check the scheme of all the site addresses,
|
||||||
|
// skip building AP if they all had http://
|
||||||
|
if sblock.isAllHTTP() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// get values that populate an automation policy for this block
|
// get values that populate an automation policy for this block
|
||||||
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,17 +123,29 @@ func (st ServerType) buildTLSApp(
|
|||||||
ap.OnDemand = true
|
ap.OnDemand = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
|
||||||
|
ap.KeyType = keyTypeVals[0].Value.(string)
|
||||||
|
}
|
||||||
|
|
||||||
// certificate issuers
|
// certificate issuers
|
||||||
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
||||||
var issuers []certmagic.Issuer
|
var issuers []certmagic.Issuer
|
||||||
for _, issuerVal := range issuerVals {
|
for _, issuerVal := range issuerVals {
|
||||||
ap.Issuers = append(ap.Issuers, issuerVal.Value.(certmagic.Issuer))
|
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
|
||||||
}
|
}
|
||||||
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
|
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuers, issuers) {
|
||||||
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
|
return nil, warnings, fmt.Errorf("automation policy from site block is also default/catch-all policy because of key without hostname, and the two are in conflict: %#v != %#v", ap.Issuers, issuers)
|
||||||
}
|
}
|
||||||
|
ap.Issuers = issuers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// certificate managers
|
||||||
|
if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
|
||||||
|
for _, certManager := range certManagerVals {
|
||||||
|
certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
|
||||||
|
ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
|
||||||
|
}
|
||||||
|
}
|
||||||
// custom bind host
|
// custom bind host
|
||||||
for _, cfgVal := range sblock.pile["bind"] {
|
for _, cfgVal := range sblock.pile["bind"] {
|
||||||
for _, iss := range ap.Issuers {
|
for _, iss := range ap.Issuers {
|
||||||
@@ -175,7 +202,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// associate our new automation policy with this server block's hosts
|
// associate our new automation policy with this server block's hosts
|
||||||
ap.Subjects = sblockHosts
|
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
|
||||||
sort.Strings(ap.Subjects) // solely for deterministic test results
|
sort.Strings(ap.Subjects) // solely for deterministic test results
|
||||||
|
|
||||||
// if a combination of public and internal names were given
|
// if a combination of public and internal names were given
|
||||||
@@ -197,7 +224,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
// it that we would need to check here) since the hostname is known at handshake;
|
// it that we would need to check here) since the hostname is known at handshake;
|
||||||
// and it is unexpected to switch to internal issuer when the user wants to get
|
// and it is unexpected to switch to internal issuer when the user wants to get
|
||||||
// regular certificates on-demand for a class of certs like *.*.tld.
|
// regular certificates on-demand for a class of certs like *.*.tld.
|
||||||
if !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) {
|
if subjectQualifiesForPublicCert(ap, s) {
|
||||||
external = append(external, s)
|
external = append(external, s)
|
||||||
} else {
|
} else {
|
||||||
internal = append(internal, s)
|
internal = append(internal, s)
|
||||||
@@ -264,6 +291,27 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.Automation.OnDemand = onDemand
|
tlsApp.Automation.OnDemand = onDemand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set the storage clean interval if configured
|
||||||
|
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
|
||||||
|
if tlsApp.Automation == nil {
|
||||||
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
|
}
|
||||||
|
tlsApp.Automation.StorageCleanInterval = storageCleanInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the expired certificates renew interval if configured
|
||||||
|
if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
|
||||||
|
if tlsApp.Automation == nil {
|
||||||
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
|
}
|
||||||
|
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// set whether OCSP stapling should be disabled for manually-managed certificates
|
||||||
|
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
|
||||||
|
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||||
|
}
|
||||||
|
|
||||||
// if any hostnames appear on the same server block as a key with
|
// if any hostnames appear on the same server block as a key with
|
||||||
// no host, they will not be used with route matchers because the
|
// no host, they will not be used with route matchers because the
|
||||||
// hostless key matches all hosts, therefore, it wouldn't be
|
// hostless key matches all hosts, therefore, it wouldn't be
|
||||||
@@ -275,7 +323,7 @@ func (st ServerType) buildTLSApp(
|
|||||||
internalAP := &caddytls.AutomationPolicy{
|
internalAP := &caddytls.AutomationPolicy{
|
||||||
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
||||||
}
|
}
|
||||||
for h := range hostsSharedWithHostlessKey {
|
for h := range httpsHostsSharedWithHostlessKey {
|
||||||
al = append(al, h)
|
al = append(al, h)
|
||||||
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
||||||
internalAP.Subjects = append(internalAP.Subjects, h)
|
internalAP.Subjects = append(internalAP.Subjects, h)
|
||||||
@@ -299,17 +347,23 @@ func (st ServerType) buildTLSApp(
|
|||||||
globalACMECARoot := options["acme_ca_root"]
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
globalACMEDNS := options["acme_dns"]
|
globalACMEDNS := options["acme_dns"]
|
||||||
globalACMEEAB := options["acme_eab"]
|
globalACMEEAB := options["acme_eab"]
|
||||||
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil
|
globalPreferredChains := options["preferred_chains"]
|
||||||
|
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
|
||||||
if hasGlobalACMEDefaults {
|
if hasGlobalACMEDefaults {
|
||||||
for _, ap := range tlsApp.Automation.Policies {
|
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
|
||||||
if len(ap.Issuers) == 0 {
|
ap := tlsApp.Automation.Policies[i]
|
||||||
acme, zerosslACME := new(caddytls.ACMEIssuer), new(caddytls.ACMEIssuer)
|
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
||||||
zerossl := &caddytls.ZeroSSLIssuer{ACMEIssuer: zerosslACME}
|
// for public names, create default issuers which will later be filled in with configured global defaults
|
||||||
ap.Issuers = []certmagic.Issuer{acme, zerossl} // TODO: keep this in sync with Caddy's other issuer defaults elsewhere, like in caddytls/automation.go (DefaultIssuers).
|
// (internal names will implicitly use the internal issuer at auto-https time)
|
||||||
|
ap.Issuers = caddytls.DefaultIssuers()
|
||||||
|
|
||||||
// if a non-ZeroSSL endpoint is specified, we assume we can't use the ZeroSSL issuer successfully
|
// if a specific endpoint is configured, can't use multiple default issuers
|
||||||
if globalACMECA != nil && !strings.Contains(globalACMECA.(string), "zerossl") {
|
if globalACMECA != nil {
|
||||||
ap.Issuers = []certmagic.Issuer{acme}
|
if strings.Contains(globalACMECA.(string), "zerossl") {
|
||||||
|
ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
|
||||||
|
} else {
|
||||||
|
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,6 +435,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
globalACMECARoot := options["acme_ca_root"]
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
globalACMEDNS := options["acme_dns"]
|
globalACMEDNS := options["acme_dns"]
|
||||||
globalACMEEAB := options["acme_eab"]
|
globalACMEEAB := options["acme_eab"]
|
||||||
|
globalPreferredChains := options["preferred_chains"]
|
||||||
|
|
||||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||||
acmeIssuer.Email = globalEmail.(string)
|
acmeIssuer.Email = globalEmail.(string)
|
||||||
@@ -392,20 +447,18 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||||
}
|
}
|
||||||
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
||||||
provName := globalACMEDNS.(string)
|
|
||||||
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
|
|
||||||
}
|
|
||||||
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||||
DNS: &caddytls.DNSChallengeConfig{
|
DNS: &caddytls.DNSChallengeConfig{
|
||||||
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, nil),
|
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
||||||
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
|
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
|
||||||
}
|
}
|
||||||
|
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
|
||||||
|
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,11 +468,12 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
|||||||
// returned if there are no default/global options. However, if always is
|
// returned if there are no default/global options. However, if always is
|
||||||
// true, a non-nil value will always be returned (unless there is an error).
|
// true, a non-nil value will always be returned (unless there is an error).
|
||||||
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
|
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
|
||||||
issuer, hasIssuer := options["cert_issuer"]
|
issuers, hasIssuers := options["cert_issuer"]
|
||||||
_, hasLocalCerts := options["local_certs"]
|
_, hasLocalCerts := options["local_certs"]
|
||||||
keyType, hasKeyType := options["key_type"]
|
keyType, hasKeyType := options["key_type"]
|
||||||
|
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||||
|
|
||||||
hasGlobalAutomationOpts := hasIssuer || hasLocalCerts || hasKeyType
|
hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling
|
||||||
|
|
||||||
// if there are no global options related to automation policies
|
// if there are no global options related to automation policies
|
||||||
// set, then we can just return right away
|
// set, then we can just return right away
|
||||||
@@ -435,30 +489,23 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
|
|||||||
ap.KeyType = keyType.(string)
|
ap.KeyType = keyType.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasIssuer && hasLocalCerts {
|
if hasIssuers && hasLocalCerts {
|
||||||
return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
|
return nil, fmt.Errorf("global options are ambiguous: local_certs is confusing when combined with cert_issuer, because local_certs is also a specific kind of issuer")
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasIssuer {
|
if hasIssuers {
|
||||||
ap.Issuers = []certmagic.Issuer{issuer.(certmagic.Issuer)}
|
ap.Issuers = issuers.([]certmagic.Issuer)
|
||||||
} else if hasLocalCerts {
|
} else if hasLocalCerts {
|
||||||
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ap, nil
|
if hasOCSPStapling {
|
||||||
}
|
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
|
||||||
|
ap.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||||
// disambiguateACMEIssuer returns an issuer based on the properties of acmeIssuer.
|
ap.OCSPOverrides = ocspConfig.ResponderOverrides
|
||||||
// If acmeIssuer implicitly configures a certain kind of ACMEIssuer (for example,
|
|
||||||
// ZeroSSL), the proper wrapper over acmeIssuer will be returned instead.
|
|
||||||
func disambiguateACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) certmagic.Issuer {
|
|
||||||
// as a special case, we integrate with ZeroSSL's ACME endpoint if it looks like an
|
|
||||||
// implicit ZeroSSL configuration (this requires a wrapper type over ACMEIssuer
|
|
||||||
// because of the EAB generation; if EAB is provided, we can use plain ACMEIssuer)
|
|
||||||
if strings.Contains(acmeIssuer.CA, "acme.zerossl.com") && acmeIssuer.ExternalAccount == nil {
|
|
||||||
return &caddytls.ZeroSSLIssuer{ACMEIssuer: acmeIssuer}
|
|
||||||
}
|
}
|
||||||
return acmeIssuer
|
|
||||||
|
return ap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// consolidateAutomationPolicies combines automation policies that are the same,
|
// consolidateAutomationPolicies combines automation policies that are the same,
|
||||||
@@ -475,25 +522,39 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove any empty policies (except subjects, of course)
|
emptyAPCount := 0
|
||||||
|
origLenAPs := len(aps)
|
||||||
|
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||||
emptyAP := new(caddytls.AutomationPolicy)
|
emptyAP := new(caddytls.AutomationPolicy)
|
||||||
for i := 0; i < len(aps); i++ {
|
for i := 0; i < len(aps); i++ {
|
||||||
emptyAP.Subjects = aps[i].Subjects
|
emptyAP.Subjects = aps[i].Subjects
|
||||||
if reflect.DeepEqual(aps[i], emptyAP) {
|
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||||
aps = append(aps[:i], aps[i+1:]...)
|
emptyAPCount++
|
||||||
i--
|
if !automationPolicyHasAllPublicNames(aps[i]) {
|
||||||
|
// if this automation policy has internal names, we might as well remove it
|
||||||
|
// so auto-https can implicitly use the internal issuer
|
||||||
|
aps = append(aps[:i], aps[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If all policies are empty, we can return nil, as there is no need to set any policy
|
||||||
|
if emptyAPCount == origLenAPs {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// remove or combine duplicate policies
|
// remove or combine duplicate policies
|
||||||
|
outer:
|
||||||
for i := 0; i < len(aps); i++ {
|
for i := 0; i < len(aps); i++ {
|
||||||
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
|
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
|
||||||
for j := i + 1; j < len(aps); j++ {
|
for j := i + 1; j < len(aps); j++ {
|
||||||
// if they're exactly equal in every way, just keep one of them
|
// if they're exactly equal in every way, just keep one of them
|
||||||
if reflect.DeepEqual(aps[i], aps[j]) {
|
if reflect.DeepEqual(aps[i], aps[j]) {
|
||||||
aps = append(aps[:j], aps[j+1:]...)
|
aps = append(aps[:j], aps[j+1:]...)
|
||||||
|
// must re-evaluate current i against next j; can't skip it!
|
||||||
|
// even if i decrements to -1, will be incremented to 0 immediately
|
||||||
i--
|
i--
|
||||||
break
|
continue outer
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the policy is the same, we can keep just one, but we have
|
// if the policy is the same, we can keep just one, but we have
|
||||||
@@ -518,6 +579,8 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
// '*.com', which might be different (yes we've seen this happen)
|
// '*.com', which might be different (yes we've seen this happen)
|
||||||
if automationPolicyShadows(i, aps) >= j {
|
if automationPolicyShadows(i, aps) >= j {
|
||||||
aps = append(aps[:i], aps[i+1:]...)
|
aps = append(aps[:i], aps[i+1:]...)
|
||||||
|
i--
|
||||||
|
continue outer
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// avoid repeated subjects
|
// avoid repeated subjects
|
||||||
@@ -574,3 +637,21 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
|
||||||
|
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
|
||||||
|
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
|
||||||
|
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
|
||||||
|
return !certmagic.SubjectIsIP(subj) &&
|
||||||
|
!certmagic.SubjectIsInternal(subj) &&
|
||||||
|
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
||||||
|
for _, subj := range ap.Subjects {
|
||||||
|
if !subjectQualifiesForPublicCert(ap, subj) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterModule(HTTPLoader{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
|
||||||
|
// based on the Content-Type header of the HTTP response.
|
||||||
|
type HTTPLoader struct {
|
||||||
|
// The method for the request. Default: GET
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
|
||||||
|
// The URL of the request.
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
|
||||||
|
// HTTP headers to add to the request.
|
||||||
|
Headers http.Header `json:"header,omitempty"`
|
||||||
|
|
||||||
|
// Maximum time allowed for a complete connection and request.
|
||||||
|
Timeout caddy.Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
TLS *struct {
|
||||||
|
// Present this instance's managed remote identity credentials to the server.
|
||||||
|
UseServerIdentity bool `json:"use_server_identity,omitempty"`
|
||||||
|
|
||||||
|
// PEM-encoded client certificate filename to present to the server.
|
||||||
|
ClientCertificateFile string `json:"client_certificate_file,omitempty"`
|
||||||
|
|
||||||
|
// PEM-encoded key to use with the client certificate.
|
||||||
|
ClientCertificateKeyFile string `json:"client_certificate_key_file,omitempty"`
|
||||||
|
|
||||||
|
// List of PEM-encoded CA certificate files to add to the same trust
|
||||||
|
// store as RootCAPool (or root_ca_pool in the JSON).
|
||||||
|
RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
|
||||||
|
} `json:"tls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
ID: "caddy.config_loaders.http",
|
||||||
|
New: func() caddy.Module { return new(HTTPLoader) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads a Caddy config.
|
||||||
|
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
|
||||||
|
client, err := hl.makeClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
method := repl.ReplaceAll(hl.Method, "")
|
||||||
|
if method == "" {
|
||||||
|
method = http.MethodGet
|
||||||
|
}
|
||||||
|
|
||||||
|
url := repl.ReplaceAll(hl.URL, "")
|
||||||
|
req, err := http.NewRequest(method, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for key, vals := range hl.Headers {
|
||||||
|
for _, val := range vals {
|
||||||
|
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, warn := range warnings {
|
||||||
|
ctx.Logger(hl).Warn(warn.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Duration(hl.Timeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
if hl.TLS != nil {
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
|
||||||
|
// client authentication
|
||||||
|
if hl.TLS.UseServerIdentity {
|
||||||
|
certs, err := ctx.IdentityCredentials(ctx.Logger(hl))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting server identity credentials: %v", err)
|
||||||
|
}
|
||||||
|
if tlsConfig == nil {
|
||||||
|
tlsConfig = new(tls.Config)
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = certs
|
||||||
|
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tlsConfig == nil {
|
||||||
|
tlsConfig = new(tls.Config)
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||||
|
}
|
||||||
|
|
||||||
|
// trusted server certs
|
||||||
|
if len(hl.TLS.RootCAPEMFiles) > 0 {
|
||||||
|
rootPool := x509.NewCertPool()
|
||||||
|
for _, pemFile := range hl.TLS.RootCAPEMFiles {
|
||||||
|
pemData, err := os.ReadFile(pemFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed reading ca cert: %v", err)
|
||||||
|
}
|
||||||
|
rootPool.AppendCertsFromPEM(pemData)
|
||||||
|
}
|
||||||
|
if tlsConfig == nil {
|
||||||
|
tlsConfig = new(tls.Config)
|
||||||
|
}
|
||||||
|
tlsConfig.RootCAs = rootPool
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Transport = &http.Transport{TLSClientConfig: tlsConfig}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ caddy.ConfigLoader = (*HTTPLoader)(nil)
|
||||||
+55
-38
@@ -69,8 +69,8 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
|
|||||||
func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
Code: http.StatusMethodNotAllowed,
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
Err: fmt.Errorf("method not allowed"),
|
Err: fmt.Errorf("method not allowed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +81,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
_, err := io.Copy(buf, r.Body)
|
_, err := io.Copy(buf, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
Code: http.StatusBadRequest,
|
HTTPStatus: http.StatusBadRequest,
|
||||||
Err: fmt.Errorf("reading request body: %v", err),
|
Err: fmt.Errorf("reading request body: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := buf.Bytes()
|
body := buf.Bytes()
|
||||||
@@ -90,45 +90,21 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
// if the config is formatted other than Caddy's native
|
// if the config is formatted other than Caddy's native
|
||||||
// JSON, we need to adapt it before loading it
|
// JSON, we need to adapt it before loading it
|
||||||
if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
|
if ctHeader := r.Header.Get("Content-Type"); ctHeader != "" {
|
||||||
ct, _, err := mime.ParseMediaType(ctHeader)
|
result, warnings, err := adaptByContentType(ctHeader, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
Code: http.StatusBadRequest,
|
HTTPStatus: http.StatusBadRequest,
|
||||||
Err: fmt.Errorf("invalid Content-Type: %v", err),
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(ct, "/json") {
|
if len(warnings) > 0 {
|
||||||
slashIdx := strings.Index(ct, "/")
|
respBody, err := json.Marshal(warnings)
|
||||||
if slashIdx < 0 {
|
|
||||||
return caddy.APIError{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Err: fmt.Errorf("malformed Content-Type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
adapterName := ct[slashIdx+1:]
|
|
||||||
cfgAdapter := GetAdapter(adapterName)
|
|
||||||
if cfgAdapter == nil {
|
|
||||||
return caddy.APIError{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Err: fmt.Errorf("unrecognized config adapter '%s'", adapterName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result, warnings, err := cfgAdapter.Adapt(body, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.APIError{
|
caddy.Log().Named("admin.api.load").Error(err.Error())
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Err: fmt.Errorf("adapting config using %s adapter: %v", adapterName, err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(warnings) > 0 {
|
_, _ = w.Write(respBody)
|
||||||
respBody, err := json.Marshal(warnings)
|
|
||||||
if err != nil {
|
|
||||||
caddy.Log().Named("admin.api.load").Error(err.Error())
|
|
||||||
}
|
|
||||||
_, _ = w.Write(respBody)
|
|
||||||
}
|
|
||||||
body = result
|
|
||||||
}
|
}
|
||||||
|
body = result
|
||||||
}
|
}
|
||||||
|
|
||||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||||
@@ -136,8 +112,8 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
err = caddy.Load(body, forceReload)
|
err = caddy.Load(body, forceReload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
Code: http.StatusBadRequest,
|
HTTPStatus: http.StatusBadRequest,
|
||||||
Err: fmt.Errorf("loading config: %v", err),
|
Err: fmt.Errorf("loading config: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +122,47 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
|
||||||
|
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
|
||||||
|
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
|
||||||
|
// assume JSON as the default
|
||||||
|
if contentType == "" {
|
||||||
|
return body, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ct, _, err := mime.ParseMediaType(contentType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, caddy.APIError{
|
||||||
|
HTTPStatus: http.StatusBadRequest,
|
||||||
|
Err: fmt.Errorf("invalid Content-Type: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if already JSON, no need to adapt
|
||||||
|
if strings.HasSuffix(ct, "/json") {
|
||||||
|
return body, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapter name should be suffix of MIME type
|
||||||
|
slashIdx := strings.Index(ct, "/")
|
||||||
|
if slashIdx < 0 {
|
||||||
|
return nil, nil, fmt.Errorf("malformed Content-Type")
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterName := ct[slashIdx+1:]
|
||||||
|
cfgAdapter := GetAdapter(adapterName)
|
||||||
|
if cfgAdapter == nil {
|
||||||
|
return nil, nil, fmt.Errorf("unrecognized config adapter '%s'", adapterName)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, warnings, err := cfgAdapter.Adapt(body, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("adapting config using %s adapter: %v", adapterName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, warnings, nil
|
||||||
|
}
|
||||||
|
|
||||||
var bufPool = sync.Pool{
|
var bufPool = sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() interface{} {
|
||||||
return new(bytes.Buffer)
|
return new(bytes.Buffer)
|
||||||
|
|||||||
+73
-30
@@ -7,13 +7,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -98,6 +99,10 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
|
|||||||
tc.t.Logf("failed to load config: %s", err)
|
tc.t.Logf("failed to load config: %s", err)
|
||||||
tc.t.Fail()
|
tc.t.Fail()
|
||||||
}
|
}
|
||||||
|
if err := tc.ensureConfigRunning(rawConfig, configType); err != nil {
|
||||||
|
tc.t.Logf("failed ensurng config is running: %s", err)
|
||||||
|
tc.t.Fail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitServer this will configure the server with a configurion of a specific
|
// InitServer this will configure the server with a configurion of a specific
|
||||||
@@ -124,7 +129,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
_ = json.Indent(&out, body, "", " ")
|
_ = json.Indent(&out, body, "", " ")
|
||||||
@@ -157,7 +162,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
timeElapsed(start, "caddytest: config load time")
|
timeElapsed(start, "caddytest: config load time")
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("unable to read response. %s", err)
|
tc.t.Errorf("unable to read response. %s", err)
|
||||||
return err
|
return err
|
||||||
@@ -171,20 +176,57 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasValidated bool
|
func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error {
|
||||||
var arePrerequisitesValid bool
|
expectedBytes := []byte(prependCaddyFilePath(rawConfig))
|
||||||
|
if configType != "json" {
|
||||||
func validateTestPrerequisites() error {
|
adapter := caddyconfig.GetAdapter(configType)
|
||||||
|
if adapter == nil {
|
||||||
if hasValidated {
|
return fmt.Errorf("adapter of config type is missing: %s", configType)
|
||||||
if !arePrerequisitesValid {
|
|
||||||
return errors.New("caddy integration prerequisites failed. see first error")
|
|
||||||
}
|
}
|
||||||
return nil
|
expectedBytes, _, _ = adapter.Adapt([]byte(rawConfig), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidated = true
|
var expected interface{}
|
||||||
arePrerequisitesValid = false
|
err := json.Unmarshal(expectedBytes, &expected)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: Default.LoadRequestTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchConfig := func(client *http.Client) interface{} {
|
||||||
|
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
actualBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var actual interface{}
|
||||||
|
err = json.Unmarshal(actualBytes, &actual)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return actual
|
||||||
|
}
|
||||||
|
|
||||||
|
for retries := 4; retries > 0; retries-- {
|
||||||
|
if reflect.DeepEqual(expected, fetchConfig(client)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
tc.t.Errorf("POSTed configuration isn't active")
|
||||||
|
return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTestPrerequisites ensures the certificates are available in the
|
||||||
|
// designated path and Caddy sub-process is running.
|
||||||
|
func validateTestPrerequisites() error {
|
||||||
|
|
||||||
// check certificates are found
|
// check certificates are found
|
||||||
for _, certName := range Default.Certifcates {
|
for _, certName := range Default.Certifcates {
|
||||||
@@ -200,20 +242,14 @@ func validateTestPrerequisites() error {
|
|||||||
caddycmd.Main()
|
caddycmd.Main()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// wait for caddy to start
|
// wait for caddy to start serving the initial config
|
||||||
retries := 4
|
for retries := 4; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
|
||||||
for ; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// assert that caddy is running
|
// one more time to return the error
|
||||||
if err := isCaddyAdminRunning(); err != nil {
|
return isCaddyAdminRunning()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
arePrerequisitesValid = true
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCaddyAdminRunning() error {
|
func isCaddyAdminRunning() error {
|
||||||
@@ -223,7 +259,7 @@ func isCaddyAdminRunning() error {
|
|||||||
}
|
}
|
||||||
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("caddy integration test caddy server not running. Expected to be listening on localhost:2019")
|
return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort)
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
@@ -327,7 +363,7 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompareAdapt adapts a config and then compares it against an expected result
|
// CompareAdapt adapts a config and then compares it against an expected result
|
||||||
func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) bool {
|
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
@@ -336,7 +372,6 @@ func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
options := make(map[string]interface{})
|
options := make(map[string]interface{})
|
||||||
options["pretty"] = "true"
|
|
||||||
|
|
||||||
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
|
result, warnings, err := cfgAdapter.Adapt([]byte(rawConfig), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -344,9 +379,17 @@ func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedRe
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prettify results to keep tests human-manageable
|
||||||
|
var prettyBuf bytes.Buffer
|
||||||
|
err = json.Indent(&prettyBuf, result, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
result = prettyBuf.Bytes()
|
||||||
|
|
||||||
if len(warnings) > 0 {
|
if len(warnings) > 0 {
|
||||||
for _, w := range warnings {
|
for _, w := range warnings {
|
||||||
t.Logf("warning: directive: %s : %s", w.Directive, w.Message)
|
t.Logf("warning: %s:%d: %s: %s", filename, w.Line, w.Directive, w.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +424,7 @@ func CompareAdapt(t *testing.T, rawConfig string, adapterName string, expectedRe
|
|||||||
|
|
||||||
// AssertAdapt adapts a config and then tests it against an expected result
|
// AssertAdapt adapts a config and then tests it against an expected result
|
||||||
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
|
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) {
|
||||||
ok := CompareAdapt(t, rawConfig, adapterName, expectedResponse)
|
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
@@ -428,7 +471,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
|||||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
bytes, err := ioutil.ReadAll(resp.Body)
|
bytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to read the response body %s", err)
|
tc.t.Fatalf("unable to read the response body %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,3 +80,46 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
|||||||
`, "json")
|
`, "json")
|
||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
http://:9080 {
|
||||||
|
respond "Foo"
|
||||||
|
}
|
||||||
|
http://baz.localhost:9080 {
|
||||||
|
respond "Baz"
|
||||||
|
}
|
||||||
|
bar.localhost {
|
||||||
|
respond "Bar"
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
|
||||||
|
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
||||||
|
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
http://:9080 {
|
||||||
|
respond "Foo"
|
||||||
|
}
|
||||||
|
bar.localhost {
|
||||||
|
respond "Bar"
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
|
||||||
|
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
||||||
|
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
auto_https ignore_loaded_certs
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"automatic_https": {
|
||||||
|
"ignore_loaded_certificates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
example.com {
|
||||||
|
bind tcp6/[::]
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
"tcp6/[::]:443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
# All the options
|
||||||
|
encode gzip zstd {
|
||||||
|
minimum_length 256
|
||||||
|
match {
|
||||||
|
status 2xx 4xx 500
|
||||||
|
header Content-Type text/*
|
||||||
|
header Content-Type application/json*
|
||||||
|
header Content-Type application/javascript*
|
||||||
|
header Content-Type application/xhtml+xml*
|
||||||
|
header Content-Type application/atom+xml*
|
||||||
|
header Content-Type application/rss+xml*
|
||||||
|
header Content-Type image/svg+xml*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Long way with a block for each encoding
|
||||||
|
encode {
|
||||||
|
zstd
|
||||||
|
gzip 5
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"encodings": {
|
||||||
|
"gzip": {},
|
||||||
|
"zstd": {}
|
||||||
|
},
|
||||||
|
"handler": "encode",
|
||||||
|
"match": {
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": [
|
||||||
|
"text/*",
|
||||||
|
"application/json*",
|
||||||
|
"application/javascript*",
|
||||||
|
"application/xhtml+xml*",
|
||||||
|
"application/atom+xml*",
|
||||||
|
"application/rss+xml*",
|
||||||
|
"image/svg+xml*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": [
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
500
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum_length": 256,
|
||||||
|
"prefer": [
|
||||||
|
"gzip",
|
||||||
|
"zstd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"encodings": {
|
||||||
|
"gzip": {
|
||||||
|
"level": 5
|
||||||
|
},
|
||||||
|
"zstd": {}
|
||||||
|
},
|
||||||
|
"handler": "encode",
|
||||||
|
"prefer": [
|
||||||
|
"zstd",
|
||||||
|
"gzip"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
example.com {
|
||||||
|
root * /srv
|
||||||
|
|
||||||
|
# Trigger errors for certain paths
|
||||||
|
error /private* "Unauthorized" 403
|
||||||
|
error /hidden* "Not found" 404
|
||||||
|
|
||||||
|
# Handle the error by serving an HTML page
|
||||||
|
handle_errors {
|
||||||
|
rewrite * /{http.error.status_code}.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 403
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/private*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"error": "Not found",
|
||||||
|
"handler": "error",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/hidden*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"group": "group0",
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"uri": "/{http.error.status_code}.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
example.com
|
||||||
|
|
||||||
|
@a expression {http.error.status_code} == 400
|
||||||
|
abort @a
|
||||||
|
|
||||||
|
@b expression {http.error.status_code} == "401"
|
||||||
|
abort @b
|
||||||
|
|
||||||
|
@c expression {http.error.status_code} == `402`
|
||||||
|
abort @c
|
||||||
|
|
||||||
|
@d expression "{http.error.status_code} == 403"
|
||||||
|
abort @d
|
||||||
|
|
||||||
|
@e expression `{http.error.status_code} == 404`
|
||||||
|
abort @e
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == 400"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == \"401\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == `402`"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == 403"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"abort": true,
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.error.status_code} == 404"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
file_server {
|
||||||
|
disable_canonical_uris
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"canonical_uris": false,
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
file_server {
|
||||||
|
pass_thru
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
],
|
||||||
|
"pass_thru": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
file_server {
|
||||||
|
precompressed zstd br gzip
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
],
|
||||||
|
"precompressed": {
|
||||||
|
"br": {},
|
||||||
|
"gzip": {},
|
||||||
|
"zstd": {}
|
||||||
|
},
|
||||||
|
"precompressed_order": [
|
||||||
|
"zstd",
|
||||||
|
"br",
|
||||||
|
"gzip"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
localhost
|
||||||
|
|
||||||
|
root * /srv
|
||||||
|
|
||||||
|
handle /nope* {
|
||||||
|
file_server {
|
||||||
|
status 403
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /custom-status* {
|
||||||
|
file_server {
|
||||||
|
status {env.CUSTOM_STATUS}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/srv"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "group2",
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
],
|
||||||
|
"status_code": "{env.CUSTOM_STATUS}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/custom-status*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "group2",
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
],
|
||||||
|
"status_code": 403
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/nope*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
app.example.com {
|
||||||
|
forward_auth authelia:9091 {
|
||||||
|
uri /api/verify?rd=https://authelia.example.com
|
||||||
|
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy backend:8080
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"app.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"Remote-Email": [
|
||||||
|
"{http.reverse_proxy.header.Remote-Email}"
|
||||||
|
],
|
||||||
|
"Remote-Groups": [
|
||||||
|
"{http.reverse_proxy.header.Remote-Groups}"
|
||||||
|
],
|
||||||
|
"Remote-Name": [
|
||||||
|
"{http.reverse_proxy.header.Remote-Name}"
|
||||||
|
],
|
||||||
|
"Remote-User": [
|
||||||
|
"{http.reverse_proxy.header.Remote-User}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"exclude": [
|
||||||
|
"Connection",
|
||||||
|
"Keep-Alive",
|
||||||
|
"Te",
|
||||||
|
"Trailers",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"Upgrade"
|
||||||
|
],
|
||||||
|
"handler": "copy_response_headers"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "copy_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"headers": {
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"X-Forwarded-Method": [
|
||||||
|
"{http.request.method}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-Uri": [
|
||||||
|
"{http.request.uri}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rewrite": {
|
||||||
|
"method": "GET",
|
||||||
|
"uri": "/api/verify?rd=https://authelia.example.com"
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "authelia:9091"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "backend:8080"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
debug
|
debug
|
||||||
http_port 8080
|
http_port 8080
|
||||||
https_port 8443
|
https_port 8443
|
||||||
|
grace_period 5s
|
||||||
default_sni localhost
|
default_sni localhost
|
||||||
order root first
|
order root first
|
||||||
storage file_system {
|
storage file_system {
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
}
|
}
|
||||||
acme_ca https://example.com
|
acme_ca https://example.com
|
||||||
acme_ca_root /path/to/ca.crt
|
acme_ca_root /path/to/ca.crt
|
||||||
|
ocsp_stapling off
|
||||||
|
|
||||||
email test@example.com
|
email test@example.com
|
||||||
admin off
|
admin off
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"http_port": 8080,
|
"http_port": 8080,
|
||||||
"https_port": 8443,
|
"https_port": 8443,
|
||||||
|
"grace_period": 5000000000,
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
@@ -59,7 +62,8 @@
|
|||||||
"module": "internal"
|
"module": "internal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"key_type": "ed25519"
|
"key_type": "ed25519",
|
||||||
|
"disable_ocsp_stapling": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
@@ -69,7 +73,8 @@
|
|||||||
},
|
},
|
||||||
"ask": "https://example.com"
|
"ask": "https://example.com"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"disable_ocsp_stapling": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
}
|
}
|
||||||
acme_ca https://example.com
|
acme_ca https://example.com
|
||||||
acme_eab {
|
acme_eab {
|
||||||
key_id 4K2scIVbBpNd-78scadB2g
|
key_id 4K2scIVbBpNd-78scadB2g
|
||||||
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
|
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
|
||||||
}
|
}
|
||||||
acme_ca_root /path/to/ca.crt
|
acme_ca_root /path/to/ca.crt
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
interval 30s
|
interval 30s
|
||||||
burst 20
|
burst 20
|
||||||
}
|
}
|
||||||
|
storage_clean_interval 7d
|
||||||
|
renew_interval 1d
|
||||||
|
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
}
|
}
|
||||||
@@ -80,7 +82,9 @@
|
|||||||
"burst": 20
|
"burst": 20
|
||||||
},
|
},
|
||||||
"ask": "https://example.com"
|
"ask": "https://example.com"
|
||||||
}
|
},
|
||||||
|
"renew_interval": 86400000000000,
|
||||||
|
"storage_clean_interval": 604800000000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
debug
|
||||||
|
}
|
||||||
|
|
||||||
|
:8881 {
|
||||||
|
log {
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"exclude": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"log0": {
|
||||||
|
"encoder": {
|
||||||
|
"format": "console"
|
||||||
|
},
|
||||||
|
"level": "DEBUG",
|
||||||
|
"include": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8881"
|
||||||
|
],
|
||||||
|
"logs": {
|
||||||
|
"default_logger_name": "log0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
default_bind tcp4/0.0.0.0 tcp6/[::]
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
example.org:12345 {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
"tcp4/0.0.0.0:12345",
|
||||||
|
"tcp6/[::]:12345"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.org"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
"tcp4/0.0.0.0:443",
|
||||||
|
"tcp6/[::]:443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
log {
|
||||||
|
output file caddy.log
|
||||||
|
include some-log-source
|
||||||
|
exclude admin.api admin2.api
|
||||||
|
}
|
||||||
|
log custom-logger {
|
||||||
|
output file caddy.log
|
||||||
|
level WARN
|
||||||
|
include custom-log-source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8884 {
|
||||||
|
log {
|
||||||
|
format json
|
||||||
|
output file access.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"custom-logger": {
|
||||||
|
"writer": {
|
||||||
|
"filename": "caddy.log",
|
||||||
|
"output": "file"
|
||||||
|
},
|
||||||
|
"level": "WARN",
|
||||||
|
"include": [
|
||||||
|
"custom-log-source"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"writer": {
|
||||||
|
"filename": "caddy.log",
|
||||||
|
"output": "file"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"some-log-source"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"admin.api",
|
||||||
|
"admin2.api",
|
||||||
|
"custom-log-source",
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"log0": {
|
||||||
|
"writer": {
|
||||||
|
"filename": "access.log",
|
||||||
|
"output": "file"
|
||||||
|
},
|
||||||
|
"encoder": {
|
||||||
|
"format": "json"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"logs": {
|
||||||
|
"default_logger_name": "log0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
log {
|
||||||
|
output file foo.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"writer": {
|
||||||
|
"filename": "foo.log",
|
||||||
|
"output": "file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
log custom-logger {
|
||||||
|
format filter {
|
||||||
|
wrap console
|
||||||
|
fields {
|
||||||
|
request>remote_ip ip_mask {
|
||||||
|
ipv4 24
|
||||||
|
ipv6 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"custom-logger": {
|
||||||
|
"encoder": {
|
||||||
|
"fields": {
|
||||||
|
"request\u003eremote_ip": {
|
||||||
|
"filter": "ip_mask",
|
||||||
|
"ipv4_cidr": 24,
|
||||||
|
"ipv6_cidr": 32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"format": "filter",
|
||||||
|
"wrap": {
|
||||||
|
"format": "console"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
log first {
|
||||||
|
output file foo.log
|
||||||
|
}
|
||||||
|
log second {
|
||||||
|
format json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"first": {
|
||||||
|
"writer": {
|
||||||
|
"filename": "foo.log",
|
||||||
|
"output": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"second": {
|
||||||
|
"encoder": {
|
||||||
|
"format": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
preferred_chains smallest
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "acme",
|
||||||
|
"preferred_chains": {
|
||||||
|
"smallest": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "zerossl",
|
||||||
|
"preferred_chains": {
|
||||||
|
"smallest": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
pki {
|
||||||
|
ca {
|
||||||
|
name "Local"
|
||||||
|
root_cn "Custom Local Root Name"
|
||||||
|
intermediate_cn "Custom Local Intermediate Name"
|
||||||
|
root {
|
||||||
|
cert /path/to/cert.pem
|
||||||
|
key /path/to/key.pem
|
||||||
|
format pem_file
|
||||||
|
}
|
||||||
|
intermediate {
|
||||||
|
cert /path/to/cert.pem
|
||||||
|
key /path/to/key.pem
|
||||||
|
format pem_file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ca foo {
|
||||||
|
name "Foo"
|
||||||
|
root_cn "Custom Foo Root Name"
|
||||||
|
intermediate_cn "Custom Foo Intermediate Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.example.com {
|
||||||
|
tls internal
|
||||||
|
}
|
||||||
|
|
||||||
|
acme.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca foo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acme-bar.example.com {
|
||||||
|
acme_server {
|
||||||
|
ca bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme-bar.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "bar",
|
||||||
|
"handler": "acme_server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"acme.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"ca": "foo",
|
||||||
|
"handler": "acme_server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"a.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"bar": {
|
||||||
|
"install_trust": false
|
||||||
|
},
|
||||||
|
"foo": {
|
||||||
|
"name": "Foo",
|
||||||
|
"root_common_name": "Custom Foo Root Name",
|
||||||
|
"intermediate_common_name": "Custom Foo Intermediate Name",
|
||||||
|
"install_trust": false
|
||||||
|
},
|
||||||
|
"local": {
|
||||||
|
"name": "Local",
|
||||||
|
"root_common_name": "Custom Local Root Name",
|
||||||
|
"intermediate_common_name": "Custom Local Intermediate Name",
|
||||||
|
"install_trust": false,
|
||||||
|
"root": {
|
||||||
|
"certificate": "/path/to/cert.pem",
|
||||||
|
"private_key": "/path/to/key.pem",
|
||||||
|
"format": "pem_file"
|
||||||
|
},
|
||||||
|
"intermediate": {
|
||||||
|
"certificate": "/path/to/cert.pem",
|
||||||
|
"private_key": "/path/to/key.pem",
|
||||||
|
"format": "pem_file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"acme-bar.example.com",
|
||||||
|
"acme.example.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"a.example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
timeouts {
|
timeouts {
|
||||||
idle 90s
|
idle 90s
|
||||||
}
|
}
|
||||||
|
protocol {
|
||||||
|
strict_sni_host insecure_off
|
||||||
|
}
|
||||||
}
|
}
|
||||||
servers :80 {
|
servers :80 {
|
||||||
timeouts {
|
timeouts {
|
||||||
@@ -13,10 +16,13 @@
|
|||||||
timeouts {
|
timeouts {
|
||||||
idle 30s
|
idle 30s
|
||||||
}
|
}
|
||||||
|
protocol {
|
||||||
|
strict_sni_host
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foo.com {
|
foo.com {
|
||||||
}
|
}
|
||||||
|
|
||||||
http://bar.com {
|
http://bar.com {
|
||||||
@@ -46,7 +52,8 @@ http://bar.com {
|
|||||||
],
|
],
|
||||||
"terminal": true
|
"terminal": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"strict_sni_host": true
|
||||||
},
|
},
|
||||||
"srv1": {
|
"srv1": {
|
||||||
"listen": [
|
"listen": [
|
||||||
@@ -64,18 +71,14 @@ http://bar.com {
|
|||||||
],
|
],
|
||||||
"terminal": true
|
"terminal": true
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"automatic_https": {
|
|
||||||
"skip": [
|
|
||||||
"bar.com"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"srv2": {
|
"srv2": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":8080"
|
":8080"
|
||||||
],
|
],
|
||||||
"idle_timeout": 90000000000
|
"idle_timeout": 90000000000,
|
||||||
|
"strict_sni_host": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
servers {
|
servers {
|
||||||
listener_wrappers {
|
listener_wrappers {
|
||||||
|
http_redirect
|
||||||
tls
|
tls
|
||||||
}
|
}
|
||||||
timeouts {
|
timeouts {
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
idle 30s
|
idle 30s
|
||||||
}
|
}
|
||||||
max_header_size 100MB
|
max_header_size 100MB
|
||||||
|
log_credentials
|
||||||
protocol {
|
protocol {
|
||||||
allow_h2c
|
allow_h2c
|
||||||
experimental_http3
|
experimental_http3
|
||||||
@@ -18,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foo.com {
|
foo.com {
|
||||||
}
|
}
|
||||||
|
|
||||||
----------
|
----------
|
||||||
@@ -31,6 +33,9 @@ foo.com {
|
|||||||
":443"
|
":443"
|
||||||
],
|
],
|
||||||
"listener_wrappers": [
|
"listener_wrappers": [
|
||||||
|
{
|
||||||
|
"wrapper": "http_redirect"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"wrapper": "tls"
|
"wrapper": "tls"
|
||||||
}
|
}
|
||||||
@@ -53,6 +58,9 @@ foo.com {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"strict_sni_host": true,
|
"strict_sni_host": true,
|
||||||
|
"logs": {
|
||||||
|
"should_log_credentials": true
|
||||||
|
},
|
||||||
"experimental_http3": true,
|
"experimental_http3": true,
|
||||||
"allow_h2c": true
|
"allow_h2c": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
header ?John "von Neumann"
|
header ?John "von Neumann"
|
||||||
header -Wolfram
|
header -Wolfram
|
||||||
header {
|
header {
|
||||||
Grace: "Hopper" # some users habitually suffix field names with a colon
|
Grace: "Hopper" # some users habitually suffix field names with a colon
|
||||||
+Ray "Solomonoff"
|
+Ray "Solomonoff"
|
||||||
?Tim "Berners-Lee"
|
?Tim "Berners-Lee"
|
||||||
defer
|
defer
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
header @images {
|
header @images {
|
||||||
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
||||||
}
|
}
|
||||||
|
header {
|
||||||
|
+Link "Foo"
|
||||||
|
+Link "Bar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
@@ -121,6 +125,17 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"add": {
|
||||||
|
"Link": [
|
||||||
|
"Foo",
|
||||||
|
"Bar"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# https://github.com/caddyserver/caddy/issues/3977
|
||||||
|
http://* {
|
||||||
|
respond "Hello, world!"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Hello, world!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,12 +46,7 @@ http://a.caddy.localhost {
|
|||||||
],
|
],
|
||||||
"terminal": true
|
"terminal": true
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"automatic_https": {
|
|
||||||
"skip": [
|
|
||||||
"a.caddy.localhost"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Issue #4113
|
||||||
|
:80, http://example.com {
|
||||||
|
respond "foo"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "foo",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
(foo) {
|
||||||
|
respond {env.FOO}
|
||||||
|
}
|
||||||
|
|
||||||
|
:80 {
|
||||||
|
import foo
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "{env.FOO}",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,24 @@ log {
|
|||||||
format filter {
|
format filter {
|
||||||
wrap console
|
wrap console
|
||||||
fields {
|
fields {
|
||||||
request>headers>Authorization delete
|
uri query {
|
||||||
|
replace foo REDACTED
|
||||||
|
delete bar
|
||||||
|
hash baz
|
||||||
|
}
|
||||||
|
request>headers>Authorization replace REDACTED
|
||||||
request>headers>Server delete
|
request>headers>Server delete
|
||||||
request>remote_addr ip_mask {
|
request>headers>Cookie cookie {
|
||||||
|
replace foo REDACTED
|
||||||
|
delete bar
|
||||||
|
hash baz
|
||||||
|
}
|
||||||
|
request>remote_ip ip_mask {
|
||||||
ipv4 24
|
ipv4 24
|
||||||
ipv6 32
|
ipv6 32
|
||||||
}
|
}
|
||||||
|
request>headers>Regexp regexp secret REDACTED
|
||||||
|
request>headers>Hash hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,15 +42,60 @@ log {
|
|||||||
"encoder": {
|
"encoder": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"request\u003eheaders\u003eAuthorization": {
|
"request\u003eheaders\u003eAuthorization": {
|
||||||
"filter": "delete"
|
"filter": "replace",
|
||||||
|
"value": "REDACTED"
|
||||||
|
},
|
||||||
|
"request\u003eheaders\u003eCookie": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
"type": "replace",
|
||||||
|
"value": "REDACTED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bar",
|
||||||
|
"type": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "baz",
|
||||||
|
"type": "hash"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filter": "cookie"
|
||||||
|
},
|
||||||
|
"request\u003eheaders\u003eHash": {
|
||||||
|
"filter": "hash"
|
||||||
|
},
|
||||||
|
"request\u003eheaders\u003eRegexp": {
|
||||||
|
"filter": "regexp",
|
||||||
|
"regexp": "secret",
|
||||||
|
"value": "REDACTED"
|
||||||
},
|
},
|
||||||
"request\u003eheaders\u003eServer": {
|
"request\u003eheaders\u003eServer": {
|
||||||
"filter": "delete"
|
"filter": "delete"
|
||||||
},
|
},
|
||||||
"request\u003eremote_addr": {
|
"request\u003eremote_ip": {
|
||||||
"filter": "ip_mask",
|
"filter": "ip_mask",
|
||||||
"ipv4_cidr": 24,
|
"ipv4_cidr": 24,
|
||||||
"ipv6_cidr": 32
|
"ipv6_cidr": 32
|
||||||
|
},
|
||||||
|
"uri": {
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"parameter": "foo",
|
||||||
|
"type": "replace",
|
||||||
|
"value": "REDACTED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "bar",
|
||||||
|
"type": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "baz",
|
||||||
|
"type": "hash"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filter": "query"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"format": "filter",
|
"format": "filter",
|
||||||
@@ -66,4 +123,4 @@ log {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
log {
|
log {
|
||||||
output file /var/log/access.log {
|
output file /var/log/access.log {
|
||||||
roll_size 1gb
|
roll_size 1gb
|
||||||
|
roll_uncompressed
|
||||||
|
roll_local_time
|
||||||
roll_keep 5
|
roll_keep 5
|
||||||
roll_keep_for 90d
|
roll_keep_for 90d
|
||||||
}
|
}
|
||||||
@@ -20,8 +22,10 @@ log {
|
|||||||
"writer": {
|
"writer": {
|
||||||
"filename": "/var/log/access.log",
|
"filename": "/var/log/access.log",
|
||||||
"output": "file",
|
"output": "file",
|
||||||
|
"roll_gzip": false,
|
||||||
"roll_keep": 5,
|
"roll_keep": 5,
|
||||||
"roll_keep_days": 90,
|
"roll_keep_days": 90,
|
||||||
|
"roll_local_time": true,
|
||||||
"roll_size_mb": 954
|
"roll_size_mb": 954
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
one.example.com {
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
two.example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
three.example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"three.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"one.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"two.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"logs": {
|
||||||
|
"skip_hosts": [
|
||||||
|
"three.example.com",
|
||||||
|
"two.example.com",
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
example.com
|
||||||
|
|
||||||
|
map {host} {my_placeholder} {magic_number} {
|
||||||
|
# Should output boolean "true" and an integer
|
||||||
|
example.com true 3
|
||||||
|
|
||||||
|
# Should output a string and null
|
||||||
|
foo.example.com "string value"
|
||||||
|
|
||||||
|
# Should output two strings (quoted int)
|
||||||
|
(.*)\.example.com "${1} subdomain" "5"
|
||||||
|
|
||||||
|
# Should output null and a string (quoted int)
|
||||||
|
~.*\.net$ - `7`
|
||||||
|
|
||||||
|
# Should output a float and the string "false"
|
||||||
|
~.*\.xyz$ 123.456 "false"
|
||||||
|
|
||||||
|
# Should output two strings, second being escaped quote
|
||||||
|
default "unknown domain" \"""
|
||||||
|
}
|
||||||
|
|
||||||
|
vars foo bar
|
||||||
|
vars {
|
||||||
|
abc true
|
||||||
|
def 1
|
||||||
|
ghi 2.3
|
||||||
|
jkl "mn op"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"defaults": [
|
||||||
|
"unknown domain",
|
||||||
|
"\""
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"{my_placeholder}",
|
||||||
|
"{magic_number}"
|
||||||
|
],
|
||||||
|
"handler": "map",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"input": "example.com",
|
||||||
|
"outputs": [
|
||||||
|
true,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "foo.example.com",
|
||||||
|
"outputs": [
|
||||||
|
"string value",
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "(.*)\\.example.com",
|
||||||
|
"outputs": [
|
||||||
|
"${1} subdomain",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_regexp": ".*\\.net$",
|
||||||
|
"outputs": [
|
||||||
|
null,
|
||||||
|
"7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input_regexp": ".*\\.xyz$",
|
||||||
|
"outputs": [
|
||||||
|
123.456,
|
||||||
|
"false"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "{http.request.host}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"handler": "vars"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"abc": true,
|
||||||
|
"def": 1,
|
||||||
|
"ghi": 2.3,
|
||||||
|
"handler": "vars",
|
||||||
|
"jkl": "mn op"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
header Bar foo
|
header Bar foo
|
||||||
}
|
}
|
||||||
respond @matcher9 "header matcher with null field matcher"
|
respond @matcher9 "header matcher with null field matcher"
|
||||||
|
|
||||||
|
@matcher10 remote_ip private_ranges
|
||||||
|
respond @matcher10 "remote_ip matcher with private ranges"
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
@@ -101,7 +104,9 @@
|
|||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"vars": {
|
"vars": {
|
||||||
"{http.request.uri}": "/vars-matcher"
|
"{http.request.uri}": [
|
||||||
|
"/vars-matcher"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -207,6 +212,28 @@
|
|||||||
"handler": "static_response"
|
"handler": "static_response"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"remote_ip": {
|
||||||
|
"ranges": [
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"172.16.0.0/12",
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"127.0.0.1/8",
|
||||||
|
"fd00::/8",
|
||||||
|
"::1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "remote_ip matcher with private ranges",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
:80 {
|
:80 {
|
||||||
route {
|
route {
|
||||||
# unused matchers should not panic
|
# unused matchers should not panic
|
||||||
# see https://github.com/caddyserver/caddy/issues/3745
|
# see https://github.com/caddyserver/caddy/issues/3745
|
||||||
@matcher1 path /path1
|
@matcher1 path /path1
|
||||||
@matcher2 path /path2
|
@matcher2 path /path2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
:8080 {
|
||||||
|
method FOO
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8080"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"method": "FOO"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
:8881 {
|
||||||
|
php_fastcgi app:9000 {
|
||||||
|
env FOO bar
|
||||||
|
|
||||||
|
@error status 4xx
|
||||||
|
handle_response @error {
|
||||||
|
root * /errors
|
||||||
|
rewrite * /{http.reverse_proxy.status_code}.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8881"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"file": {
|
||||||
|
"try_files": [
|
||||||
|
"{http.request.uri.path}/index.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"not": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"*/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"{http.request.uri.path}/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 308
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"file": {
|
||||||
|
"try_files": [
|
||||||
|
"{http.request.uri.path}",
|
||||||
|
"{http.request.uri.path}/index.php",
|
||||||
|
"index.php"
|
||||||
|
],
|
||||||
|
"split_path": [
|
||||||
|
".php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"uri": "{http.matchers.file.relative}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"*.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"root": "/errors"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "group0",
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"uri": "/{http.reverse_proxy.status_code}.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"transport": {
|
||||||
|
"env": {
|
||||||
|
"FOO": "bar"
|
||||||
|
},
|
||||||
|
"protocol": "fastcgi",
|
||||||
|
"split_path": [
|
||||||
|
".php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "app:9000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
:8884
|
:8884
|
||||||
|
|
||||||
php_fastcgi localhost:9000 {
|
php_fastcgi localhost:9000 {
|
||||||
# some php_fastcgi-specific subdirectives
|
# some php_fastcgi-specific subdirectives
|
||||||
split .php .php5
|
split .php .php5
|
||||||
env VAR1 value1
|
env VAR1 value1
|
||||||
env VAR2 value2
|
env VAR2 value2
|
||||||
root /var/www
|
root /var/www
|
||||||
index index.php5
|
index index.php5
|
||||||
|
|
||||||
# passed through to reverse_proxy (directive order doesn't matter!)
|
# passed through to reverse_proxy (directive order doesn't matter!)
|
||||||
lb_policy random
|
lb_policy random
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
php_fastcgi localhost:9000 {
|
||||||
|
# some php_fastcgi-specific subdirectives
|
||||||
|
split .php .php5
|
||||||
|
env VAR1 value1
|
||||||
|
env VAR2 value2
|
||||||
|
root /var/www
|
||||||
|
try_files {path} {path}/index.php =404
|
||||||
|
dial_timeout 3s
|
||||||
|
read_timeout 10s
|
||||||
|
write_timeout 20s
|
||||||
|
|
||||||
|
# passed through to reverse_proxy (directive order doesn't matter!)
|
||||||
|
lb_policy random
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"file": {
|
||||||
|
"try_files": [
|
||||||
|
"{http.request.uri.path}/index.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"not": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"*/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"{http.request.uri.path}/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 308
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"file": {
|
||||||
|
"try_files": [
|
||||||
|
"{http.request.uri.path}",
|
||||||
|
"{http.request.uri.path}/index.php",
|
||||||
|
"=404"
|
||||||
|
],
|
||||||
|
"split_path": [
|
||||||
|
".php",
|
||||||
|
".php5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"uri": "{http.matchers.file.relative}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"*.php",
|
||||||
|
"*.php5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"load_balancing": {
|
||||||
|
"selection_policy": {
|
||||||
|
"policy": "random"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"dial_timeout": 3000000000,
|
||||||
|
"env": {
|
||||||
|
"VAR1": "value1",
|
||||||
|
"VAR2": "value2"
|
||||||
|
},
|
||||||
|
"protocol": "fastcgi",
|
||||||
|
"read_timeout": 10000000000,
|
||||||
|
"root": "/var/www",
|
||||||
|
"split_path": [
|
||||||
|
".php",
|
||||||
|
".php5"
|
||||||
|
],
|
||||||
|
"write_timeout": 20000000000
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:9000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
localhost
|
localhost
|
||||||
|
|
||||||
request_body {
|
request_body {
|
||||||
max_size 1MB
|
max_size 1MB
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
@matcher path /something*
|
||||||
|
request_header @matcher Denis "Ritchie"
|
||||||
|
|
||||||
|
request_header +Edsger "Dijkstra"
|
||||||
|
request_header -Wolfram
|
||||||
|
|
||||||
|
@images path /images/*
|
||||||
|
request_header @images Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/something*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"Denis": [
|
||||||
|
"Ritchie"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/images/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"Cache-Control": [
|
||||||
|
"public, max-age=3600, stale-while-revalidate=86400"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"add": {
|
||||||
|
"Edsger": [
|
||||||
|
"Dijkstra"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"Wolfram"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
:8884 {
|
||||||
|
reverse_proxy {
|
||||||
|
dynamic a foo 9000
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy {
|
||||||
|
dynamic a {
|
||||||
|
name foo
|
||||||
|
port 9000
|
||||||
|
refresh 5m
|
||||||
|
resolvers 8.8.8.8 8.8.4.4
|
||||||
|
dial_timeout 2s
|
||||||
|
dial_fallback_delay 300ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:8885 {
|
||||||
|
reverse_proxy {
|
||||||
|
dynamic srv _api._tcp.example.com
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy {
|
||||||
|
dynamic srv {
|
||||||
|
service api
|
||||||
|
proto tcp
|
||||||
|
name example.com
|
||||||
|
refresh 5m
|
||||||
|
resolvers 8.8.8.8 8.8.4.4
|
||||||
|
dial_timeout 1s
|
||||||
|
dial_fallback_delay -1s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"dynamic_upstreams": {
|
||||||
|
"name": "foo",
|
||||||
|
"port": "9000",
|
||||||
|
"source": "a"
|
||||||
|
},
|
||||||
|
"handler": "reverse_proxy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dynamic_upstreams": {
|
||||||
|
"dial_fallback_delay": 300000000,
|
||||||
|
"dial_timeout": 2000000000,
|
||||||
|
"name": "foo",
|
||||||
|
"port": "9000",
|
||||||
|
"refresh": 300000000000,
|
||||||
|
"resolver": {
|
||||||
|
"addresses": [
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source": "a"
|
||||||
|
},
|
||||||
|
"handler": "reverse_proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":8885"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"dynamic_upstreams": {
|
||||||
|
"name": "_api._tcp.example.com",
|
||||||
|
"source": "srv"
|
||||||
|
},
|
||||||
|
"handler": "reverse_proxy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dynamic_upstreams": {
|
||||||
|
"dial_fallback_delay": -1000000000,
|
||||||
|
"dial_timeout": 1000000000,
|
||||||
|
"name": "example.com",
|
||||||
|
"proto": "tcp",
|
||||||
|
"refresh": 300000000000,
|
||||||
|
"resolver": {
|
||||||
|
"addresses": [
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"service": "api",
|
||||||
|
"source": "srv"
|
||||||
|
},
|
||||||
|
"handler": "reverse_proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
:8884
|
:8884
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:65535 {
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
transport fastcgi
|
transport fastcgi
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
:8884
|
:8884
|
||||||
|
|
||||||
reverse_proxy h2c://localhost:8080
|
reverse_proxy h2c://localhost:8080
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":8884"
|
":8884"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
"transport": {
|
"transport": {
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"versions": [
|
"versions": [
|
||||||
"h2c",
|
"h2c",
|
||||||
"2"
|
"2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{
|
{
|
||||||
"dial": "localhost:8080"
|
"dial": "localhost:8080"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
@500 status 500
|
||||||
|
replace_status @500 400
|
||||||
|
|
||||||
|
@all status 2xx 3xx 4xx 5xx
|
||||||
|
replace_status @all {http.error.status_code}
|
||||||
|
|
||||||
|
replace_status {http.error.status_code}
|
||||||
|
|
||||||
|
@accel header X-Accel-Redirect *
|
||||||
|
handle_response @accel {
|
||||||
|
respond "Header X-Accel-Redirect!"
|
||||||
|
}
|
||||||
|
|
||||||
|
@another {
|
||||||
|
header X-Another *
|
||||||
|
}
|
||||||
|
handle_response @another {
|
||||||
|
respond "Header X-Another!"
|
||||||
|
}
|
||||||
|
|
||||||
|
@401 status 401
|
||||||
|
handle_response @401 {
|
||||||
|
respond "Status 401!"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_response {
|
||||||
|
respond "Any! This should be last in the JSON!"
|
||||||
|
}
|
||||||
|
|
||||||
|
@403 {
|
||||||
|
status 403
|
||||||
|
}
|
||||||
|
handle_response @403 {
|
||||||
|
respond "Status 403!"
|
||||||
|
}
|
||||||
|
|
||||||
|
@multi {
|
||||||
|
status 401 403
|
||||||
|
status 404
|
||||||
|
header Foo *
|
||||||
|
header Bar *
|
||||||
|
}
|
||||||
|
handle_response @multi {
|
||||||
|
respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
|
||||||
|
}
|
||||||
|
|
||||||
|
@200 status 200
|
||||||
|
handle_response @200 {
|
||||||
|
copy_response_headers {
|
||||||
|
include Foo Bar
|
||||||
|
}
|
||||||
|
respond "Copied headers from the response"
|
||||||
|
}
|
||||||
|
|
||||||
|
@201 status 201
|
||||||
|
handle_response @201 {
|
||||||
|
header Foo "Copying the response"
|
||||||
|
copy_response 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
500
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": "{http.error.status_code}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"headers": {
|
||||||
|
"X-Accel-Redirect": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Header X-Accel-Redirect!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"headers": {
|
||||||
|
"X-Another": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Header X-Another!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
401
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Status 401!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
403
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Status 403!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"headers": {
|
||||||
|
"Bar": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"Foo": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": [
|
||||||
|
401,
|
||||||
|
403,
|
||||||
|
404
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
200
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "copy_response_headers",
|
||||||
|
"include": [
|
||||||
|
"Foo",
|
||||||
|
"Bar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"body": "Copied headers from the response",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
201
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"set": {
|
||||||
|
"Foo": [
|
||||||
|
"Copying the response"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "copy_response",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status_code": "{http.error.status_code}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Any! This should be last in the JSON!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
health_headers {
|
||||||
|
Host example.com
|
||||||
|
X-Header-Key 95ca39e3cbe7
|
||||||
|
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
|
||||||
|
X-Empty-Value
|
||||||
|
}
|
||||||
|
health_uri /health
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"active": {
|
||||||
|
"headers": {
|
||||||
|
"Host": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
|
"X-Empty-Value": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"X-Header-Key": [
|
||||||
|
"95ca39e3cbe7"
|
||||||
|
],
|
||||||
|
"X-Header-Keys": [
|
||||||
|
"VbG4NZwWnipo",
|
||||||
|
"335Q9/MhqcNU3s2TO"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uri": "/health"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Health with query in the uri
|
||||||
|
:8443 {
|
||||||
|
reverse_proxy localhost:54321 {
|
||||||
|
health_uri /health?ready=1
|
||||||
|
health_status 2xx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health without query in the uri
|
||||||
|
:8444 {
|
||||||
|
reverse_proxy localhost:54321 {
|
||||||
|
health_uri /health
|
||||||
|
health_status 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"active": {
|
||||||
|
"expect_status": 2,
|
||||||
|
"uri": "/health?ready=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:54321"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":8444"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"active": {
|
||||||
|
"expect_status": 200,
|
||||||
|
"uri": "/health"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:54321"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
https://example.com {
|
https://example.com {
|
||||||
reverse_proxy /path http://localhost:54321 {
|
reverse_proxy /path https://localhost:54321 {
|
||||||
header_up Host {host}
|
header_up Host {upstream_hostport}
|
||||||
header_up X-Real-IP {remote}
|
header_up Foo bar
|
||||||
header_up X-Forwarded-For {remote}
|
|
||||||
header_up X-Forwarded-Port {server_port}
|
method GET
|
||||||
header_up X-Forwarded-Proto "http"
|
rewrite /rewritten?uri={uri}
|
||||||
|
|
||||||
buffer_requests
|
buffer_requests
|
||||||
|
|
||||||
@@ -17,11 +17,14 @@ https://example.com {
|
|||||||
dial_fallback_delay 5s
|
dial_fallback_delay 5s
|
||||||
response_header_timeout 8s
|
response_header_timeout 8s
|
||||||
expect_continue_timeout 9s
|
expect_continue_timeout 9s
|
||||||
|
resolvers 8.8.8.8 8.8.4.4
|
||||||
|
|
||||||
versions h2c 2
|
versions h2c 2
|
||||||
compression off
|
compression off
|
||||||
max_conns_per_host 5
|
max_conns_per_host 5
|
||||||
max_idle_conns_per_host 2
|
keepalive_idle_conns_per_host 2
|
||||||
|
keepalive_interval 30s
|
||||||
|
renegotiation freely
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,35 +59,42 @@ https://example.com {
|
|||||||
"headers": {
|
"headers": {
|
||||||
"request": {
|
"request": {
|
||||||
"set": {
|
"set": {
|
||||||
|
"Foo": [
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
"Host": [
|
"Host": [
|
||||||
"{http.request.host}"
|
"{http.reverse_proxy.upstream.hostport}"
|
||||||
],
|
|
||||||
"X-Forwarded-For": [
|
|
||||||
"{http.request.remote}"
|
|
||||||
],
|
|
||||||
"X-Forwarded-Port": [
|
|
||||||
"{server_port}"
|
|
||||||
],
|
|
||||||
"X-Forwarded-Proto": [
|
|
||||||
"http"
|
|
||||||
],
|
|
||||||
"X-Real-Ip": [
|
|
||||||
"{http.request.remote}"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rewrite": {
|
||||||
|
"method": "GET",
|
||||||
|
"uri": "/rewritten?uri={http.request.uri}"
|
||||||
|
},
|
||||||
"transport": {
|
"transport": {
|
||||||
"compression": false,
|
"compression": false,
|
||||||
"dial_fallback_delay": 5000000000,
|
"dial_fallback_delay": 5000000000,
|
||||||
"dial_timeout": 3000000000,
|
"dial_timeout": 3000000000,
|
||||||
"expect_continue_timeout": 9000000000,
|
"expect_continue_timeout": 9000000000,
|
||||||
|
"keep_alive": {
|
||||||
|
"max_idle_conns_per_host": 2,
|
||||||
|
"probe_interval": 30000000000
|
||||||
|
},
|
||||||
"max_conns_per_host": 5,
|
"max_conns_per_host": 5,
|
||||||
"max_idle_conns_per_host": 2,
|
|
||||||
"max_response_header_size": 30000000,
|
"max_response_header_size": 30000000,
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"read_buffer_size": 10000000,
|
"read_buffer_size": 10000000,
|
||||||
|
"resolver": {
|
||||||
|
"addresses": [
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4"
|
||||||
|
]
|
||||||
|
},
|
||||||
"response_header_timeout": 8000000000,
|
"response_header_timeout": 8000000000,
|
||||||
|
"tls": {
|
||||||
|
"renegotiation": "freely"
|
||||||
|
},
|
||||||
"versions": [
|
"versions": [
|
||||||
"h2c",
|
"h2c",
|
||||||
"2"
|
"2"
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
trusted_proxies 127.0.0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
trusted_proxies private_ranges
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"trusted_proxies": [
|
||||||
|
"127.0.0.1"
|
||||||
|
],
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"trusted_proxies": [
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"172.16.0.0/12",
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"127.0.0.1/8",
|
||||||
|
"fd00::/8",
|
||||||
|
"::1"
|
||||||
|
],
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user