mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 16:52:40 -04:00
Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1b453dd4fb | |||
| ebc278ec98 | |||
| 79f3af9927 | |||
| d8bcf5be4e | |||
| 38a83ca6f8 | |||
| 2b90cdba52 | |||
| 635f075f18 | |||
| e384f07a3c | |||
| 132525de3b | |||
| deedf8abb0 | |||
| 63bda6a0dc | |||
| b8a799df9f | |||
| a748151666 | |||
| c898a37f40 | |||
| 31fbcd7401 | |||
| 7e719157d9 | |||
| 6e9ac248dd | |||
| 5643dc3fb9 | |||
| 3d0e046238 | |||
| bac82073d0 | |||
| e7a5a3850f | |||
| aca7ef0d4c | |||
| 792fca40f1 | |||
| 9157051f45 | |||
| 4cff36d731 | |||
| a26f70a12b | |||
| 4afcdc49d1 | |||
| 7d7434c9ce | |||
| 53aa60afff | |||
| b0f8fc7aae | |||
| 03d853e2ec | |||
| 63afffc2e3 | |||
| 2d5498ee6f | |||
| 0a7721dcfe | |||
| c5197f5999 | |||
| 06ba006f9b | |||
| c6dec30535 | |||
| 3cfefeb0f7 | |||
| 4a641f6c6f | |||
| bd17eb205d | |||
| 1e480b818b | |||
| 96058538f0 | |||
| 6e0849d4c2 | |||
| b0d5c2c8ae | |||
| 12cc69ab7a | |||
| 349457cc1b | |||
| 6ea6f3ebe0 | |||
| 1438e4dbc8 | |||
| 4fc570711e | |||
| 99b8f44486 | |||
| 670b723e38 | |||
| 13781e67ab | |||
| 7a3d9d81fe | |||
| 95af4262a8 | |||
| 3db60e6cba | |||
| 7c28ecb5f4 | |||
| 9e28f60aab | |||
| b4f49e2962 | |||
| dd26875ffc | |||
| eda9a1b377 | |||
| 860cc6adfe | |||
| 8d038ca515 | |||
| 937ec34201 | |||
| 966d5e6b42 | |||
| b66099379d | |||
| c9fdff9976 | |||
| db4f1c0277 | |||
| b6e96d6f4a | |||
| b6686a54d8 | |||
| 97caf368ee | |||
| 385adf5d87 | |||
| c7efb0307d | |||
| e34d9f1244 | |||
| ef8a372a1c | |||
| 0fc47e8357 | |||
| 25d2b4bf29 | |||
| 023d702f30 | |||
| 6722426f1a | |||
| 3b9eae70c9 | |||
| aa9c3eb732 | |||
| fdfdc03339 | |||
| dadfe1933b | |||
| 85152679ce | |||
| a33e4b5426 | |||
| f197cec7f3 | |||
| be6daa5fd4 | |||
| fe27f9cf0c | |||
| b1d456d8ab | |||
| d16ede358a | |||
| c82c231ba7 | |||
| 3ee663dee1 | |||
| 8ec51bbede | |||
| bc453fa6ae | |||
| e3324aa6de | |||
| d55d50b3b3 | |||
| b95b87381a | |||
| b01bb275b3 | |||
| 309c1fec62 | |||
| b88e2b6a49 | |||
| 4217217bad | |||
| 1c5969b576 | |||
| 0ee4378227 | |||
| 9859ab8148 | |||
| 00e6b77fe4 | |||
| d4f249741e | |||
| 04f50a9759 | |||
| 4cd7ae35b3 | |||
| 24f34780b6 | |||
| 724b74d981 | |||
| 4940325844 | |||
| 744d04c258 | |||
| ecbc1f85c5 | |||
| 997ef522bc | |||
| 0279a57ac4 | |||
| c94f5bb7dd | |||
| 0afbab8667 | |||
| fc65320e9c | |||
| e385be9225 | |||
| 66863aad3b | |||
| c42bfaf31e | |||
| e2f913bb7f | |||
| 65a09524c3 | |||
| c6d6a775a1 | |||
| 4accf737a6 | |||
| ff19bddac5 | |||
| 584eba94a4 | |||
| 904f149e5b | |||
| 8b80a3201f | |||
| 68529e2f9e | |||
| 399eff415c | |||
| c054a818a1 | |||
| af5c148ed1 | |||
| 514eef33fe | |||
| 3860b235d0 | |||
| 6f73a358f4 | |||
| 6a14e2c2a8 | |||
| 2bc30bb780 | |||
| 28d870c193 | |||
| fb9d874fa9 | |||
| 6cea1f239d | |||
| 2ae8c11927 | |||
| e9b1d7dcb4 | |||
| bd9d796e6e | |||
| 246a31aacd | |||
| 0665a86eb7 | |||
| 3fdaf50785 | |||
| 19cc2bd3c3 | |||
| 705de11bef | |||
| 8a0fff58aa | |||
| 6f0f159ba5 | |||
| 6eafd4e82f | |||
| eda54c22a6 | |||
| 2c71fb116b | |||
| 724613a1be | |||
| 735c86658d | |||
| a2dae1d43f | |||
| efc0cc5e85 | |||
| 0bf2565c37 | |||
| 7bfe5b6c95 | |||
| 2a5599e2ad | |||
| c35820012b | |||
| 2d0f8831f8 | |||
| d7dbf85525 | |||
| 77f233a484 | |||
| ddd690de4c | |||
| 6004d3f779 | |||
| caca55e582 |
+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.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ Please note that we consider publicly-registered domain names to be public infor
|
|||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 2.x | :white_check_mark: |
|
| 2.x | :white_check_mark: |
|
||||||
| 1.x | :white_check_mark: (deprecating soon) |
|
| 1.x | :x: |
|
||||||
| < 1.x | :x: |
|
| < 1.x | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
@@ -22,6 +22,6 @@ We'll need enough information to verify the bug and make a patch. It will speed
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use.
|
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use and/or your GitHub username. If you don't provide this we can't credit you.
|
||||||
|
|
||||||
Thanks for responsibly helping Caddy—and thousands of websites—be more secure!
|
Thanks for responsibly helping Caddy—and thousands of websites—be more secure!
|
||||||
|
|||||||
+14
-24
@@ -1,14 +1,16 @@
|
|||||||
# Used as inspiration: https://github.com/mvdan/github-actions-golang
|
# Used as inspiration: https://github.com/mvdan/github-actions-golang
|
||||||
|
|
||||||
name: Cross-Platform
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- 2.*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -17,7 +19,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||||
go-version: [ 1.14.x ]
|
go: [ '1.15', '1.16' ]
|
||||||
|
|
||||||
# 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
|
||||||
@@ -39,9 +41,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -64,17 +66,18 @@ 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)"
|
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
||||||
|
|
||||||
- name: Cache the build cache
|
- name: Cache the build cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
path: ${{ steps.vars.outputs.go_cache }}
|
||||||
key: ${{ runner.os }}-go-ci-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-ci
|
${{ runner.os }}-${{ matrix.go }}-go-ci
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Get dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -91,7 +94,7 @@ jobs:
|
|||||||
- name: Publish Build Artifact
|
- name: Publish Build Artifact
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: caddy_v2_${{ runner.os }}_${{ steps.vars.outputs.short_sha }}
|
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
||||||
path: ${{ matrix.CADDY_BIN_PATH }}
|
path: ${{ matrix.CADDY_BIN_PATH }}
|
||||||
|
|
||||||
# Commented bits below were useful to allow the job to continue
|
# Commented bits below were useful to allow the job to continue
|
||||||
@@ -124,6 +127,7 @@ jobs:
|
|||||||
name: test (s390x on IBM Z)
|
name: test (s390x on IBM Z)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
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@v2
|
||||||
@@ -136,7 +140,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
|
||||||
@@ -147,26 +151,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
|
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
|
||||||
|
|
||||||
# From https://github.com/reviewdog/action-golangci-lint
|
|
||||||
golangci-lint:
|
|
||||||
name: runner / golangci-lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code into the Go module directory
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Run golangci-lint
|
|
||||||
uses: reviewdog/action-golangci-lint@v1
|
|
||||||
# uses: docker://reviewdog/action-golangci-lint:v1 # pre-build docker image
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.github_token }}
|
|
||||||
|
|
||||||
goreleaser-check:
|
goreleaser-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- uses: goreleaser/goreleaser-action@v1
|
- uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: Cross-Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- 2.*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- 2.*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cross-build-test:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
||||||
|
go: [ '1.15', '1.16' ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Print Go version and environment
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
printf "Using go at: $(which go)\n"
|
||||||
|
printf "Go version: $(go version)\n"
|
||||||
|
printf "\n\nGo environment:\n\n"
|
||||||
|
go env
|
||||||
|
printf "\n\nSystem environment:\n\n"
|
||||||
|
env
|
||||||
|
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
||||||
|
|
||||||
|
- name: Cache the build cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.vars.outputs.go_cache }}
|
||||||
|
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
|
||||||
|
|
||||||
|
- name: Checkout code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Run Build
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
shell: bash
|
||||||
|
continue-on-error: true
|
||||||
|
working-directory: ./cmd/caddy
|
||||||
|
run: |
|
||||||
|
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "::warning ::$GOOS Build Failed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
name: Fuzzing
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Daily midnight fuzzing
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fuzzing:
|
|
||||||
name: Fuzzing
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ ubuntu-latest ]
|
|
||||||
go-version: [ 1.14.x ]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v1
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Download go-fuzz tools and the Fuzzit CLI, move Fuzzit CLI to GOBIN
|
|
||||||
# If we decide we need to prevent this from running on forks, we can use this line:
|
|
||||||
# if: github.repository == 'caddyserver/caddy'
|
|
||||||
run: |
|
|
||||||
|
|
||||||
go get -v github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
|
||||||
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.77/fuzzit_Linux_x86_64
|
|
||||||
chmod a+x fuzzit
|
|
||||||
mv fuzzit $(go env GOPATH)/bin
|
|
||||||
echo "::add-path::$(go env GOPATH)/bin"
|
|
||||||
|
|
||||||
- name: Generate fuzzers & submit them to Fuzzit
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
|
||||||
FUZZIT_API_KEY: ${{ secrets.FUZZIT_API_KEY }}
|
|
||||||
SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.ref }}
|
|
||||||
BUILD_SOURCEVERSION: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
# debug
|
|
||||||
echo "PR Source Branch: $SYSTEM_PULLREQUEST_SOURCEBRANCH"
|
|
||||||
echo "Source version: $BUILD_SOURCEVERSION"
|
|
||||||
|
|
||||||
declare -A fuzzers_funcs=(\
|
|
||||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="FuzzParseAddress" \
|
|
||||||
["./listeners_fuzz.go"]="FuzzParseNetworkAddress" \
|
|
||||||
["./replacer_fuzz.go"]="FuzzReplacer" \
|
|
||||||
)
|
|
||||||
|
|
||||||
declare -A fuzzers_targets=(\
|
|
||||||
["./caddyconfig/httpcaddyfile/addresses_fuzz.go"]="parse-address" \
|
|
||||||
["./listeners_fuzz.go"]="parse-network-address" \
|
|
||||||
["./replacer_fuzz.go"]="replacer" \
|
|
||||||
)
|
|
||||||
|
|
||||||
fuzz_type="fuzzing"
|
|
||||||
|
|
||||||
for f in $(find . -name \*_fuzz.go); do
|
|
||||||
FUZZER_DIRECTORY=$(dirname "$f")
|
|
||||||
|
|
||||||
echo "go-fuzz-build func ${fuzzers_funcs[$f]} residing in $f"
|
|
||||||
|
|
||||||
go-fuzz-build -func "${fuzzers_funcs[$f]}" -o "$FUZZER_DIRECTORY/${fuzzers_targets[$f]}.zip" "$FUZZER_DIRECTORY"
|
|
||||||
|
|
||||||
fuzzit create job --engine go-fuzz caddyserver/"${fuzzers_targets[$f]}" "$FUZZER_DIRECTORY"/"${fuzzers_targets[$f]}.zip" --api-key "${FUZZIT_API_KEY}" --type "${fuzz_type}" --branch "${SYSTEM_PULLREQUEST_SOURCEBRANCH}" --revision "${BUILD_SOURCEVERSION}"
|
|
||||||
|
|
||||||
echo "Completed $f"
|
|
||||||
done
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- 2.*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- 2.*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# From https://github.com/golangci/golangci-lint-action
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
version: v1.31
|
||||||
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
|
# only-new-issues: true
|
||||||
@@ -11,21 +11,30 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest ]
|
os: [ ubuntu-latest ]
|
||||||
go-version: [ 1.14.x ]
|
go: [ '1.16' ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
# So GoReleaser can generate the changelog properly
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
- name: Unshallowify the repo clone
|
# tl;dr: actions/checkout@v2 runs this line:
|
||||||
run: git fetch --prune --unshallow
|
# 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:
|
||||||
|
# git fetch --prune --unshallow
|
||||||
|
# which doesn't overwrite that tag because that would be destructive.
|
||||||
|
# Credit to @francislavoie for the investigation.
|
||||||
|
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
|
||||||
|
- name: Force fetch upstream tags
|
||||||
|
run: git fetch --tags --force
|
||||||
|
|
||||||
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||||
- name: Print Go version and environment
|
- name: Print Go version and environment
|
||||||
@@ -41,6 +50,9 @@ jobs:
|
|||||||
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)"
|
echo "::set-output name=go_cache::$(go env GOCACHE)"
|
||||||
|
|
||||||
|
# Add "pip install" CLI tools to PATH
|
||||||
|
echo ~/.local/bin >> $GITHUB_PATH
|
||||||
|
|
||||||
# Parse semver
|
# Parse semver
|
||||||
TAG=${GITHUB_REF/refs\/tags\//}
|
TAG=${GITHUB_REF/refs\/tags\//}
|
||||||
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
|
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
|
||||||
@@ -53,17 +65,32 @@ jobs:
|
|||||||
echo "::set-output name=tag_patch::${TAG_PATCH}"
|
echo "::set-output name=tag_patch::${TAG_PATCH}"
|
||||||
echo "::set-output name=tag_special::${TAG_SPECIAL}"
|
echo "::set-output name=tag_special::${TAG_SPECIAL}"
|
||||||
|
|
||||||
|
# Cloudsmith CLI tooling for pushing releases
|
||||||
|
# See https://help.cloudsmith.io/docs/cli
|
||||||
|
- name: Install Cloudsmith CLI
|
||||||
|
run: pip install --upgrade cloudsmith-cli
|
||||||
|
|
||||||
|
- name: Validate commits and tag signatures
|
||||||
|
run: |
|
||||||
|
|
||||||
|
# Import Matt Holt's key
|
||||||
|
curl 'https://github.com/mholt.gpg' | gpg --import
|
||||||
|
|
||||||
|
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
|
||||||
|
# tags are only accepted if signed by Matt's key
|
||||||
|
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
|
||||||
|
|
||||||
- name: Cache the build cache
|
- name: Cache the build cache
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.vars.outputs.go_cache }}
|
path: ${{ steps.vars.outputs.go_cache }}
|
||||||
key: ${{ runner.os }}-go-release-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-release
|
${{ runner.os }}-go${{ matrix.go }}-release
|
||||||
|
|
||||||
# GoReleaser will take care of publishing those artifacts into the release
|
# GoReleaser will take care of publishing those artifacts into the release
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v1
|
uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist
|
args: release --rm-dist
|
||||||
@@ -72,12 +99,59 @@ 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
|
||||||
|
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
|
||||||
|
# See https://gemfury.com/caddy/deb:caddy
|
||||||
- name: Publish .deb to Gemfury
|
- name: Publish .deb to Gemfury
|
||||||
if: ${{ steps.vars.outputs.tag_special == '' }}
|
if: ${{ steps.vars.outputs.tag_special == '' }}
|
||||||
env:
|
env:
|
||||||
GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }}
|
GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
for filename in dist/*.deb; do
|
for filename in dist/*.deb; do
|
||||||
|
# armv6 and armv7 are both "armhf" so we can skip the duplicate
|
||||||
|
if [[ "$filename" == *"armv6"* ]]; then
|
||||||
|
echo "Skipping $filename"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
|
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Publish only special tags (unstable/beta/rc) to the "testing" repo
|
||||||
|
# See https://cloudsmith.io/~caddy/repos/testing/
|
||||||
|
- name: Publish .deb to Cloudsmith (special tags)
|
||||||
|
if: ${{ steps.vars.outputs.tag_special != '' }}
|
||||||
|
env:
|
||||||
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
|
run: |
|
||||||
|
for filename in dist/*.deb; do
|
||||||
|
# armv6 and armv7 are both "armhf" so we can skip the duplicate
|
||||||
|
if [[ "$filename" == *"armv6"* ]]; then
|
||||||
|
echo "Skipping $filename"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pushing $filename to 'testing'"
|
||||||
|
cloudsmith push deb caddy/testing/any-distro/any-version $filename
|
||||||
|
done
|
||||||
|
|
||||||
|
# Publish stable tags to Cloudsmith to both repos, "stable" and "testing"
|
||||||
|
# See https://cloudsmith.io/~caddy/repos/stable/
|
||||||
|
- name: Publish .deb to Cloudsmith (stable tags)
|
||||||
|
if: ${{ steps.vars.outputs.tag_special == '' }}
|
||||||
|
env:
|
||||||
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
|
run: |
|
||||||
|
for filename in dist/*.deb; do
|
||||||
|
# armv6 and armv7 are both "armhf" so we can skip the duplicate
|
||||||
|
if [[ "$filename" == *"armv6"* ]]; then
|
||||||
|
echo "Skipping $filename"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pushing $filename to 'stable'"
|
||||||
|
cloudsmith push deb caddy/stable/any-distro/any-version $filename
|
||||||
|
|
||||||
|
echo "Pushing $filename to 'testing'"
|
||||||
|
cloudsmith push deb caddy/testing/any-distro/any-version $filename
|
||||||
|
done
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/dist
|
repository: caddyserver/dist
|
||||||
event-type: release-tagged
|
event-type: release-tagged
|
||||||
client-payload: '{"tag": "${{ github.release.tag_name }}"}'
|
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/caddy-docker
|
- name: Trigger event on caddyserver/caddy-docker
|
||||||
uses: peter-evans/repository-dispatch@v1
|
uses: peter-evans/repository-dispatch@v1
|
||||||
@@ -30,5 +30,5 @@ jobs:
|
|||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/caddy-docker
|
repository: caddyserver/caddy-docker
|
||||||
event-type: release-tagged
|
event-type: release-tagged
|
||||||
client-payload: '{"tag": "${{ github.release.tag_name }}"}'
|
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -7,7 +7,7 @@ Caddyfile
|
|||||||
*.prof
|
*.prof
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
# build artifacts
|
# build artifacts and helpers
|
||||||
cmd/caddy/caddy
|
cmd/caddy/caddy
|
||||||
cmd/caddy/caddy.exe
|
cmd/caddy/caddy.exe
|
||||||
|
|
||||||
@@ -21,3 +21,7 @@ vendor
|
|||||||
dist
|
dist
|
||||||
caddy-build
|
caddy-build
|
||||||
caddy-dist
|
caddy-dist
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|||||||
+52
-5
@@ -1,21 +1,68 @@
|
|||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
ignore: fmt:.*,io/ioutil:^Read.*,github.com/caddyserver/caddy/v2/caddyconfig:RegisterAdapter,github.com/caddyserver/caddy/v2:RegisterModule
|
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
|
||||||
ignoretests: true
|
ignoretests: true
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- prealloc
|
- deadcode
|
||||||
- unconvert
|
|
||||||
- errcheck
|
- errcheck
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
- gosec
|
- gosec
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
|
- prealloc
|
||||||
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
|
- typecheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- varcheck
|
||||||
|
# these are implicitly disabled:
|
||||||
|
# - asciicheck
|
||||||
|
# - depguard
|
||||||
|
# - dogsled
|
||||||
|
# - dupl
|
||||||
|
# - exhaustive
|
||||||
|
# - exportloopref
|
||||||
|
# - funlen
|
||||||
|
# - gci
|
||||||
|
# - gochecknoglobals
|
||||||
|
# - gochecknoinits
|
||||||
|
# - gocognit
|
||||||
|
# - goconst
|
||||||
|
# - gocritic
|
||||||
|
# - gocyclo
|
||||||
|
# - godot
|
||||||
|
# - godox
|
||||||
|
# - goerr113
|
||||||
|
# - gofumpt
|
||||||
|
# - goheader
|
||||||
|
# - golint
|
||||||
|
# - gomnd
|
||||||
|
# - gomodguard
|
||||||
|
# - goprintffuncname
|
||||||
|
# - interfacer
|
||||||
|
# - lll
|
||||||
|
# - maligned
|
||||||
|
# - nakedret
|
||||||
|
# - nestif
|
||||||
|
# - nlreturn
|
||||||
|
# - noctx
|
||||||
|
# - nolintlint
|
||||||
|
# - rowserrcheck
|
||||||
|
# - scopelint
|
||||||
|
# - sqlclosecheck
|
||||||
|
# - stylecheck
|
||||||
|
# - testpackage
|
||||||
|
# - unparam
|
||||||
|
# - whitespace
|
||||||
|
# - wsl
|
||||||
|
|
||||||
run:
|
run:
|
||||||
# default concurrency is a available CPU number.
|
# default concurrency is a available CPU number.
|
||||||
|
|||||||
+20
-7
@@ -11,6 +11,9 @@ before:
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -84,13 +87,22 @@ nfpms:
|
|||||||
# - rpm
|
# - rpm
|
||||||
|
|
||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
files:
|
contents:
|
||||||
./caddy-dist/init/caddy.service: /lib/systemd/system/caddy.service
|
- src: ./caddy-dist/init/caddy.service
|
||||||
./caddy-dist/init/caddy-api.service: /lib/systemd/system/caddy-api.service
|
dst: /lib/systemd/system/caddy.service
|
||||||
./caddy-dist/welcome/index.html: /usr/share/caddy/index.html
|
|
||||||
./caddy-dist/scripts/completions/bash-completion: /etc/bash_completion.d/caddy
|
- src: ./caddy-dist/init/caddy-api.service
|
||||||
config_files:
|
dst: /lib/systemd/system/caddy-api.service
|
||||||
./caddy-dist/config/Caddyfile: /etc/caddy/Caddyfile
|
|
||||||
|
- src: ./caddy-dist/welcome/index.html
|
||||||
|
dst: /usr/share/caddy/index.html
|
||||||
|
|
||||||
|
- src: ./caddy-dist/scripts/completions/bash-completion
|
||||||
|
dst: /etc/bash_completion.d/caddy
|
||||||
|
|
||||||
|
- src: ./caddy-dist/config/Caddyfile
|
||||||
|
dst: /etc/caddy/Caddyfile
|
||||||
|
type: config
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: ./caddy-dist/scripts/postinstall.sh
|
postinstall: ./caddy-dist/scripts/postinstall.sh
|
||||||
@@ -112,5 +124,6 @@ changelog:
|
|||||||
- '^chore:'
|
- '^chore:'
|
||||||
- '^ci:'
|
- '^ci:'
|
||||||
- '^docs?:'
|
- '^docs?:'
|
||||||
|
- '^readme:'
|
||||||
- '^tests?:'
|
- '^tests?:'
|
||||||
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
|
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
|
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
|
||||||
|
<br>
|
||||||
|
<h3 align="center">a <a href="https://zerossl.com"><img src="https://caddyserver.com/resources/images/zerossl-logo.svg" height="28" valign="middle"></a> project</h3>
|
||||||
</p>
|
</p>
|
||||||
|
<hr>
|
||||||
<h3 align="center">Every site on HTTPS</h3>
|
<h3 align="center">Every site on HTTPS</h3>
|
||||||
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
|
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a>
|
||||||
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
|
||||||
<a href="https://app.fuzzit.dev/orgs/caddyserver-gh/dashboard"><img src="https://app.fuzzit.dev/badge?org_id=caddyserver-gh"></a>
|
|
||||||
<br>
|
<br>
|
||||||
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
|
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
|
||||||
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||||
|
<br>
|
||||||
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
|
||||||
|
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/caddyserver/caddy/releases">Download</a> ·
|
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
|
||||||
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
<a href="https://caddyserver.com/docs/">Documentation</a> ·
|
||||||
<a href="https://caddy.community">Community</a>
|
<a href="https://caddy.community">Get Help</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +27,7 @@
|
|||||||
### Menu
|
### Menu
|
||||||
|
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
|
- [Install](#install)
|
||||||
- [Build from source](#build-from-source)
|
- [Build from source](#build-from-source)
|
||||||
- [For development](#for-development)
|
- [For development](#for-development)
|
||||||
- [With version information and/or plugins](#with-version-information-andor-plugins)
|
- [With version information and/or plugins](#with-version-information-andor-plugins)
|
||||||
@@ -39,45 +44,68 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## [Features](https://caddyserver.com/v2)
|
||||||
|
|
||||||
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
|
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
|
||||||
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
|
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
|
||||||
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
|
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
|
||||||
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
|
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
|
||||||
- **Automatic HTTPS** by default
|
- **Automatic HTTPS** by default
|
||||||
- [Let's Encrypt](https://letsencrypt.org) for public sites
|
- [ZeroSSL](https://zerossl.com) and [Let's Encrypt](https://letsencrypt.org) for public names
|
||||||
- Fully-managed local CA for internal names & IPs
|
- Fully-managed local CA for internal names & IPs
|
||||||
- Can coordinate with other Caddy instances in a cluster
|
- Can coordinate with other Caddy instances in a cluster
|
||||||
|
- Multi-issuer fallback
|
||||||
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
|
||||||
|
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
|
||||||
|
- **Scales to tens of thousands of sites** ... and probably more
|
||||||
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
|
- **HTTP/1.1, HTTP/2, and experimental HTTP/3** support
|
||||||
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
|
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
|
||||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||||
- Written in Go, a language with higher **memory safety guarantees** than other servers
|
- Written in Go, a language with higher **memory safety guarantees** than other servers
|
||||||
- Actually **fun to use**
|
- Actually **fun to use**
|
||||||
- So, so much more to discover
|
- So, so much more to [discover](https://caddyserver.com/v2)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
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/install.
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- [Go 1.14 or newer](https://golang.org/dl/)
|
- [Go 1.15 or newer](https://golang.org/dl/)
|
||||||
|
|
||||||
### For development
|
### For development
|
||||||
|
|
||||||
|
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git clone "https://github.com/caddyserver/caddy.git"
|
$ git clone "https://github.com/caddyserver/caddy.git"
|
||||||
$ cd caddy/cmd/caddy/
|
$ cd caddy/cmd/caddy/
|
||||||
$ go build
|
$ go build
|
||||||
```
|
```
|
||||||
|
|
||||||
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions below._
|
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 only creates temporary binaries, you can still do this with the included `setcap.sh` like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go run -exec ./setcap.sh main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't want to type your password for `setcap`, use `sudo visudo` to edit your sudoers file and allow your user account to run that command without a password, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap
|
||||||
|
```
|
||||||
|
|
||||||
|
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
|
||||||
|
|
||||||
### With version information and/or plugins
|
### With version information and/or plugins
|
||||||
|
|
||||||
Using [our builder tool](https://github.com/caddyserver/xcaddy)...
|
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
|
||||||
|
|
||||||
```
|
```
|
||||||
$ xcaddy build
|
$ xcaddy build
|
||||||
@@ -89,8 +117,8 @@ $ 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@TAG` replacing `TAG` 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"`
|
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
|
||||||
7. Compile: `go build`
|
7. Compile: `go build`
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +128,7 @@ $ xcaddy build
|
|||||||
|
|
||||||
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
|
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
|
||||||
|
|
||||||
**We recommend that all users do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
|
**We recommend that all users -- regardless of experience level -- do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
|
||||||
|
|
||||||
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
|
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
|
||||||
|
|
||||||
@@ -119,7 +147,7 @@ The primary way to configure Caddy is through [its API](https://caddyserver.com/
|
|||||||
|
|
||||||
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
|
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
|
||||||
|
|
||||||
To wield the power of this design, you need to know how the config document is structured. Please see the [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
|
To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
|
||||||
|
|
||||||
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
|
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
|
||||||
|
|
||||||
@@ -138,6 +166,8 @@ 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! 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!
|
||||||
|
|
||||||
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
|
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
|
||||||
@@ -146,7 +176,11 @@ 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 Light Code Labs, LLC.
|
**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), 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.
|
||||||
@@ -17,7 +17,12 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"expvar"
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -34,11 +39,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/notify"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
|
|
||||||
|
|
||||||
// AdminConfig configures Caddy's API endpoint, which is used
|
// AdminConfig configures Caddy's API endpoint, which is used
|
||||||
// to manage Caddy while it is running.
|
// to manage Caddy while it is running.
|
||||||
type AdminConfig struct {
|
type AdminConfig struct {
|
||||||
@@ -56,84 +62,180 @@ type AdminConfig struct {
|
|||||||
// If true, CORS headers will be emitted, and requests to the
|
// If true, CORS headers will be emitted, and requests to the
|
||||||
// API will be rejected if their `Host` and `Origin` headers
|
// API will be rejected if their `Host` and `Origin` headers
|
||||||
// do not match the expected value(s). Use `origins` to
|
// do not match the expected value(s). Use `origins` to
|
||||||
// customize which origins/hosts are allowed.If `origins` is
|
// customize which origins/hosts are allowed. If `origins` is
|
||||||
// not set, the listen address is the only value allowed by
|
// not set, the listen address is the only value allowed by
|
||||||
// default.
|
// default. Enforced only on local (plaintext) endpoint.
|
||||||
EnforceOrigin bool `json:"enforce_origin,omitempty"`
|
EnforceOrigin bool `json:"enforce_origin,omitempty"`
|
||||||
|
|
||||||
// The list of allowed origins/hosts for API requests. Only needed
|
// The list of allowed origins/hosts for API requests. Only needed
|
||||||
// if accessing the admin endpoint from a host different from the
|
// if accessing the admin endpoint from a host different from the
|
||||||
// socket's network interface or if `enforce_origin` is true. If not
|
// socket's network interface or if `enforce_origin` is true. If not
|
||||||
// set, the listener address will be the default value. If set but
|
// set, the listener address will be the default value. If set but
|
||||||
// empty, no origins will be allowed.
|
// empty, no origins will be allowed. Enforced only on local
|
||||||
|
// (plaintext) endpoint.
|
||||||
Origins []string `json:"origins,omitempty"`
|
Origins []string `json:"origins,omitempty"`
|
||||||
|
|
||||||
// Options related to configuration management.
|
// Options pertaining to configuration management.
|
||||||
Config *ConfigSettings `json:"config,omitempty"`
|
Config *ConfigSettings `json:"config,omitempty"`
|
||||||
|
|
||||||
|
// Options that establish this server's identity. Identity refers to
|
||||||
|
// credentials which can be used to uniquely identify and authenticate
|
||||||
|
// this server instance. This is required if remote administration is
|
||||||
|
// enabled (but does not require remote administration to be enabled).
|
||||||
|
// Default: no identity management.
|
||||||
|
Identity *IdentityConfig `json:"identity,omitempty"`
|
||||||
|
|
||||||
|
// Options pertaining to remote administration. By default, remote
|
||||||
|
// administration is disabled. If enabled, identity management must
|
||||||
|
// also be configured, as that is how the endpoint is secured.
|
||||||
|
// See the neighboring "identity" object.
|
||||||
|
//
|
||||||
|
// EXPERIMENTAL: This feature is subject to change.
|
||||||
|
Remote *RemoteAdmin `json:"remote,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigSettings configures the, uh, configuration... and
|
// ConfigSettings configures the management of configuration.
|
||||||
// management thereof.
|
|
||||||
type ConfigSettings struct {
|
type ConfigSettings struct {
|
||||||
// Whether to keep a copy of the active config on disk. Default is true.
|
// Whether to keep a copy of the active config on disk. Default is true.
|
||||||
|
// Note that "pulled" dynamic configs (using the neighboring "load" module)
|
||||||
|
// are not persisted; only configs that are pushed to Caddy get persisted.
|
||||||
Persist *bool `json:"persist,omitempty"`
|
Persist *bool `json:"persist,omitempty"`
|
||||||
|
|
||||||
|
// Loads a configuration to use. This is helpful if your configs are
|
||||||
|
// managed elsewhere, and you want Caddy to pull its config dynamically
|
||||||
|
// when it starts. The pulled config completely replaces the current
|
||||||
|
// one, just like any other config load. It is an error if a pulled
|
||||||
|
// config is configured to pull another config.
|
||||||
|
//
|
||||||
|
// EXPERIMENTAL: Subject to change.
|
||||||
|
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// listenAddr extracts a singular listen address from ac.Listen,
|
// IdentityConfig configures management of this server's identity. An identity
|
||||||
// returning the network and the address of the listener.
|
// consists of credentials that uniquely verify this instance; for example,
|
||||||
func (admin AdminConfig) listenAddr() (NetworkAddress, error) {
|
// TLS certificates (public + private key pairs).
|
||||||
input := admin.Listen
|
type IdentityConfig struct {
|
||||||
if input == "" {
|
// List of names or IP addresses which refer to this server.
|
||||||
input = DefaultAdminListen
|
// Certificates will be obtained for these identifiers so
|
||||||
}
|
// secure TLS connections can be made using them.
|
||||||
listenAddr, err := ParseNetworkAddress(input)
|
Identifiers []string `json:"identifiers,omitempty"`
|
||||||
if err != nil {
|
|
||||||
return NetworkAddress{}, fmt.Errorf("parsing admin listener address: %v", err)
|
// Issuers that can provide this admin endpoint its identity
|
||||||
}
|
// certificate(s). Default: ACME issuers configured for
|
||||||
if listenAddr.PortRangeSize() != 1 {
|
// ZeroSSL and Let's Encrypt. Be sure to change this if you
|
||||||
return NetworkAddress{}, fmt.Errorf("admin endpoint must have exactly one address; cannot listen on %v", listenAddr)
|
// require credentials for private identifiers.
|
||||||
}
|
IssuersRaw []json.RawMessage `json:"issuers,omitempty" caddy:"namespace=tls.issuance inline_key=module"`
|
||||||
return listenAddr, nil
|
|
||||||
|
issuers []certmagic.Issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteAdmin enables and configures remote administration. If enabled,
|
||||||
|
// a secure listener enforcing mutual TLS authentication will be started
|
||||||
|
// on a different port from the standard plaintext admin server.
|
||||||
|
//
|
||||||
|
// This endpoint is secured using identity management, which must be
|
||||||
|
// configured separately (because identity management does not depend
|
||||||
|
// on remote administration). See the admin/identity config struct.
|
||||||
|
//
|
||||||
|
// EXPERIMENTAL: Subject to change.
|
||||||
|
type RemoteAdmin struct {
|
||||||
|
// The address on which to start the secure listener.
|
||||||
|
// Default: :2021
|
||||||
|
Listen string `json:"listen,omitempty"`
|
||||||
|
|
||||||
|
// List of access controls for this secure admin endpoint.
|
||||||
|
// This configures TLS mutual authentication (i.e. authorized
|
||||||
|
// client certificates), but also application-layer permissions
|
||||||
|
// like which paths and methods each identity is authorized for.
|
||||||
|
AccessControl []*AdminAccess `json:"access_control,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminAccess specifies what permissions an identity or group
|
||||||
|
// of identities are granted.
|
||||||
|
type AdminAccess struct {
|
||||||
|
// Base64-encoded DER certificates containing public keys to accept.
|
||||||
|
// (The contents of PEM certificate blocks are base64-encoded DER.)
|
||||||
|
// Any of these public keys can appear in any part of a verified chain.
|
||||||
|
PublicKeys []string `json:"public_keys,omitempty"`
|
||||||
|
|
||||||
|
// Limits what the associated identities are allowed to do.
|
||||||
|
// If unspecified, all permissions are granted.
|
||||||
|
Permissions []AdminPermissions `json:"permissions,omitempty"`
|
||||||
|
|
||||||
|
publicKeys []crypto.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminPermissions specifies what kinds of requests are allowed
|
||||||
|
// to be made to the admin endpoint.
|
||||||
|
type AdminPermissions struct {
|
||||||
|
// The API paths allowed. Paths are simple prefix matches.
|
||||||
|
// Any subpath of the specified paths will be allowed.
|
||||||
|
Paths []string `json:"paths,omitempty"`
|
||||||
|
|
||||||
|
// The HTTP methods allowed for the given paths.
|
||||||
|
Methods []string `json:"methods,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||||
func (admin AdminConfig) newAdminHandler(addr NetworkAddress) adminHandler {
|
func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
|
||||||
muxWrap := adminHandler{
|
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||||
enforceOrigin: admin.EnforceOrigin,
|
|
||||||
enforceHost: !addr.isWildcardInterface(),
|
// secure the local or remote endpoint respectively
|
||||||
allowedOrigins: admin.allowedOrigins(addr),
|
if remote {
|
||||||
mux: http.NewServeMux(),
|
muxWrap.remoteControl = admin.Remote
|
||||||
|
} else {
|
||||||
|
muxWrap.enforceHost = !addr.isWildcardInterface()
|
||||||
|
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
|
||||||
|
labels := prometheus.Labels{"path": pattern, "handler": handlerLabel}
|
||||||
|
h = instrumentHandlerCounter(
|
||||||
|
adminMetrics.requestCount.MustCurryWith(labels),
|
||||||
|
h,
|
||||||
|
)
|
||||||
|
muxWrap.mux.Handle(pattern, h)
|
||||||
|
}
|
||||||
// addRoute just calls muxWrap.mux.Handle after
|
// addRoute just calls muxWrap.mux.Handle after
|
||||||
// wrapping the handler with error handling
|
// wrapping the handler with error handling
|
||||||
addRoute := func(pattern string, h AdminHandler) {
|
addRoute := func(pattern string, handlerLabel string, h AdminHandler) {
|
||||||
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
wrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
err := h.ServeHTTP(w, r)
|
err := h.ServeHTTP(w, r)
|
||||||
|
if err != nil {
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"path": pattern,
|
||||||
|
"handler": handlerLabel,
|
||||||
|
"method": strings.ToUpper(r.Method),
|
||||||
|
}
|
||||||
|
adminMetrics.requestErrors.With(labels).Inc()
|
||||||
|
}
|
||||||
muxWrap.handleError(w, r, err)
|
muxWrap.handleError(w, r, err)
|
||||||
})
|
})
|
||||||
muxWrap.mux.Handle(pattern, wrapper)
|
addRouteWithMetrics(pattern, handlerLabel, wrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlerLabel = "admin"
|
||||||
|
|
||||||
// register standard config control endpoints
|
// register standard config control endpoints
|
||||||
addRoute("/"+rawConfigKey+"/", AdminHandlerFunc(handleConfig))
|
addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
|
||||||
addRoute("/id/", AdminHandlerFunc(handleConfigID))
|
addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
|
||||||
addRoute("/stop", AdminHandlerFunc(handleStop))
|
addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
|
||||||
|
|
||||||
// register debugging endpoints
|
// register debugging endpoints
|
||||||
muxWrap.mux.HandleFunc("/debug/pprof/", pprof.Index)
|
addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
|
||||||
muxWrap.mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
addRouteWithMetrics("/debug/pprof/cmdline", handlerLabel, http.HandlerFunc(pprof.Cmdline))
|
||||||
muxWrap.mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
addRouteWithMetrics("/debug/pprof/profile", handlerLabel, http.HandlerFunc(pprof.Profile))
|
||||||
muxWrap.mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
addRouteWithMetrics("/debug/pprof/symbol", handlerLabel, http.HandlerFunc(pprof.Symbol))
|
||||||
muxWrap.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
addRouteWithMetrics("/debug/pprof/trace", handlerLabel, http.HandlerFunc(pprof.Trace))
|
||||||
muxWrap.mux.Handle("/debug/vars", expvar.Handler())
|
addRouteWithMetrics("/debug/vars", handlerLabel, expvar.Handler())
|
||||||
|
|
||||||
// register third-party module endpoints
|
// register third-party module endpoints
|
||||||
for _, m := range GetModules("admin.api") {
|
for _, m := range GetModules("admin.api") {
|
||||||
router := m.New().(AdminRouter)
|
router := m.New().(AdminRouter)
|
||||||
|
handlerLabel := m.ID.Name()
|
||||||
for _, route := range router.Routes() {
|
for _, route := range router.Routes() {
|
||||||
addRoute(route.Pattern, route.Handler)
|
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,18 +278,18 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
|
|||||||
return allowed
|
return allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceAdmin replaces the running admin server according
|
// replaceLocalAdminServer replaces the running local admin server
|
||||||
// to the relevant configuration in cfg. If no configuration
|
// according to the relevant configuration in cfg. If no configuration
|
||||||
// for the admin endpoint exists in cfg, a default one is
|
// for the admin endpoint exists in cfg, a default one is used, so
|
||||||
// used, so that there is always an admin server (unless it
|
// that there is always an admin server (unless it is explicitly
|
||||||
// is explicitly configured to be disabled).
|
// configured to be disabled).
|
||||||
func replaceAdmin(cfg *Config) error {
|
func replaceLocalAdminServer(cfg *Config) error {
|
||||||
// always be sure to close down the old admin endpoint
|
// always be sure to close down the old admin endpoint
|
||||||
// as gracefully as possible, even if the new one is
|
// as gracefully as possible, even if the new one is
|
||||||
// disabled -- careful to use reference to the current
|
// disabled -- careful to use reference to the current
|
||||||
// (old) admin endpoint since it will be different
|
// (old) admin endpoint since it will be different
|
||||||
// when the function returns
|
// when the function returns
|
||||||
oldAdminServer := adminServer
|
oldAdminServer := localAdminServer
|
||||||
defer func() {
|
defer func() {
|
||||||
// do the shutdown asynchronously so that any
|
// do the shutdown asynchronously so that any
|
||||||
// current API request gets a response; this
|
// current API request gets a response; this
|
||||||
@@ -215,19 +317,20 @@ func replaceAdmin(cfg *Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extract a singular listener address
|
// extract a singular listener address
|
||||||
addr, err := adminConfig.listenAddr()
|
addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := adminConfig.newAdminHandler(addr)
|
handler := adminConfig.newAdminHandler(addr, false)
|
||||||
|
|
||||||
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
adminServer = &http.Server{
|
localAdminServer = &http.Server{
|
||||||
|
Addr: addr.String(), // for logging purposes only
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
@@ -235,21 +338,272 @@ func replaceAdmin(cfg *Config) error {
|
|||||||
MaxHeaderBytes: 1024 * 64,
|
MaxHeaderBytes: 1024 * 64,
|
||||||
}
|
}
|
||||||
|
|
||||||
go adminServer.Serve(ln)
|
adminLogger := Log().Named("admin")
|
||||||
|
go func() {
|
||||||
|
if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
Log().Named("admin").Info("admin endpoint started",
|
adminLogger.Info("admin endpoint started",
|
||||||
zap.String("address", addr.String()),
|
zap.String("address", addr.String()),
|
||||||
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
||||||
zap.Strings("origins", handler.allowedOrigins))
|
zap.Strings("origins", handler.allowedOrigins))
|
||||||
|
|
||||||
if !handler.enforceHost {
|
if !handler.enforceHost {
|
||||||
Log().Named("admin").Warn("admin endpoint on open interface; host checking disabled",
|
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
|
||||||
zap.String("address", addr.String()))
|
zap.String("address", addr.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// manageIdentity sets up automated identity management for this server.
|
||||||
|
func manageIdentity(ctx Context, cfg *Config) error {
|
||||||
|
if cfg == nil || cfg.Admin == nil || cfg.Admin.Identity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldIdentityCertCache := identityCertCache
|
||||||
|
if oldIdentityCertCache != nil {
|
||||||
|
defer oldIdentityCertCache.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default issuers; this is pretty hacky because we can't
|
||||||
|
// import the caddytls package -- but it works
|
||||||
|
if cfg.Admin.Identity.IssuersRaw == nil {
|
||||||
|
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
|
||||||
|
json.RawMessage(`{"module": "zerossl"}`),
|
||||||
|
json.RawMessage(`{"module": "acme"}`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load and provision issuer modules
|
||||||
|
if cfg.Admin.Identity.IssuersRaw != nil {
|
||||||
|
val, err := ctx.LoadModule(cfg.Admin.Identity, "IssuersRaw")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading identity issuer modules: %s", err)
|
||||||
|
}
|
||||||
|
for _, issVal := range val.([]interface{}) {
|
||||||
|
cfg.Admin.Identity.issuers = append(cfg.Admin.Identity.issuers, issVal.(certmagic.Issuer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := Log().Named("admin.identity")
|
||||||
|
cmCfg := cfg.Admin.Identity.certmagicConfig(logger)
|
||||||
|
|
||||||
|
// issuers have circular dependencies with the configs because,
|
||||||
|
// as explained in the caddytls package, they need access to the
|
||||||
|
// correct storage and cache to solve ACME challenges
|
||||||
|
for _, issuer := range cfg.Admin.Identity.issuers {
|
||||||
|
// avoid import cycle with caddytls package, so manually duplicate the interface here, yuck
|
||||||
|
if annoying, ok := issuer.(interface{ SetConfig(cfg *certmagic.Config) }); ok {
|
||||||
|
annoying.SetConfig(cmCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obtain and renew server identity certificate(s)
|
||||||
|
return cmCfg.ManageAsync(ctx, cfg.Admin.Identity.Identifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceRemoteAdminServer replaces the running remote admin server
|
||||||
|
// according to the relevant configuration in cfg. It stops any previous
|
||||||
|
// remote admin server and only starts a new one if configured.
|
||||||
|
func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteLogger := Log().Named("admin.remote")
|
||||||
|
|
||||||
|
oldAdminServer := remoteAdminServer
|
||||||
|
defer func() {
|
||||||
|
if oldAdminServer != nil {
|
||||||
|
go func(oldAdminServer *http.Server) {
|
||||||
|
err := stopAdminServer(oldAdminServer)
|
||||||
|
if err != nil {
|
||||||
|
Log().Named("admin").Error("stopping current secure admin endpoint", zap.Error(err))
|
||||||
|
}
|
||||||
|
}(oldAdminServer)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if cfg.Admin == nil || cfg.Admin.Remote == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := parseAdminListenAddr(cfg.Admin.Remote.Listen, DefaultRemoteAdminListen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the HTTP handler but disable Host/Origin enforcement
|
||||||
|
// because we are using TLS authentication instead
|
||||||
|
handler := cfg.Admin.newAdminHandler(addr, true)
|
||||||
|
|
||||||
|
// create client certificate pool for TLS mutual auth, and extract public keys
|
||||||
|
// so that we can enforce access controls at the application layer
|
||||||
|
clientCertPool := x509.NewCertPool()
|
||||||
|
for i, accessControl := range cfg.Admin.Remote.AccessControl {
|
||||||
|
for j, certBase64 := range accessControl.PublicKeys {
|
||||||
|
cert, err := decodeBase64DERCert(certBase64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("access control %d public key %d: parsing base64 certificate DER: %v", i, j, err)
|
||||||
|
}
|
||||||
|
accessControl.publicKeys = append(accessControl.publicKeys, cert.PublicKey)
|
||||||
|
clientCertPool.AddCert(cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create TLS config that will enforce mutual authentication
|
||||||
|
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger)
|
||||||
|
tlsConfig := cmCfg.TLSConfig()
|
||||||
|
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
|
||||||
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
tlsConfig.ClientCAs = clientCertPool
|
||||||
|
|
||||||
|
// convert logger to stdlib so it can be used by HTTP server
|
||||||
|
serverLogger, err := zap.NewStdLogAt(remoteLogger, zap.DebugLevel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create secure HTTP server
|
||||||
|
remoteAdminServer = &http.Server{
|
||||||
|
Addr: addr.String(), // for logging purposes only
|
||||||
|
Handler: handler,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
MaxHeaderBytes: 1024 * 64,
|
||||||
|
ErrorLog: serverLogger,
|
||||||
|
}
|
||||||
|
|
||||||
|
// start listener
|
||||||
|
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ln = tls.NewListener(ln, tlsConfig)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := remoteAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
remoteLogger.Info("secure admin remote control endpoint started",
|
||||||
|
zap.String("address", addr.String()))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Config {
|
||||||
|
if ident == nil {
|
||||||
|
// user might not have configured identity; that's OK, we can still make a
|
||||||
|
// certmagic config, although it'll be mostly useless for remote management
|
||||||
|
ident = new(IdentityConfig)
|
||||||
|
}
|
||||||
|
cmCfg := &certmagic.Config{
|
||||||
|
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
|
||||||
|
Logger: logger,
|
||||||
|
Issuers: ident.issuers,
|
||||||
|
}
|
||||||
|
if identityCertCache == nil {
|
||||||
|
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
|
||||||
|
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
|
return cmCfg, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return certmagic.New(identityCertCache, *cmCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdentityCredentials returns this instance's configured, managed identity credentials
|
||||||
|
// that can be used in TLS client authentication.
|
||||||
|
func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, error) {
|
||||||
|
if ctx.cfg == nil || ctx.cfg.Admin == nil || ctx.cfg.Admin.Identity == nil {
|
||||||
|
return nil, fmt.Errorf("no server identity configured")
|
||||||
|
}
|
||||||
|
ident := ctx.cfg.Admin.Identity
|
||||||
|
if len(ident.Identifiers) == 0 {
|
||||||
|
return nil, fmt.Errorf("no identifiers configured")
|
||||||
|
}
|
||||||
|
if logger == nil {
|
||||||
|
logger = Log()
|
||||||
|
}
|
||||||
|
magic := ident.certmagicConfig(logger)
|
||||||
|
return magic.ClientCredentials(ctx, ident.Identifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceAccessControls enforces application-layer access controls for r based on remote.
|
||||||
|
// It expects that the TLS server has already established at least one verified chain of
|
||||||
|
// trust, and then looks for a matching, authorized public key that is allowed to access
|
||||||
|
// the defined path(s) using the defined method(s).
|
||||||
|
func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
||||||
|
for _, chain := range r.TLS.VerifiedChains {
|
||||||
|
for _, peerCert := range chain {
|
||||||
|
for _, adminAccess := range remote.AccessControl {
|
||||||
|
for _, allowedKey := range adminAccess.publicKeys {
|
||||||
|
// see if we found a matching public key; the TLS server already verified the chain
|
||||||
|
// so we know the client possesses the associated private key; this handy interface
|
||||||
|
// doesn't appear to be defined anywhere in the std lib, but was implemented here:
|
||||||
|
// https://github.com/golang/go/commit/b5f2c0f50297fa5cd14af668ddd7fd923626cf8c
|
||||||
|
comparer, ok := peerCert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
|
||||||
|
if !ok || !comparer.Equal(allowedKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// key recognized; make sure its HTTP request is permitted
|
||||||
|
for _, accessPerm := range adminAccess.Permissions {
|
||||||
|
// verify method
|
||||||
|
methodFound := accessPerm.Methods == nil
|
||||||
|
for _, method := range accessPerm.Methods {
|
||||||
|
if method == r.Method {
|
||||||
|
methodFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !methodFound {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusForbidden,
|
||||||
|
Message: "not authorized to use this method",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify path
|
||||||
|
pathFound := accessPerm.Paths == nil
|
||||||
|
for _, allowedPath := range accessPerm.Paths {
|
||||||
|
if strings.HasPrefix(r.URL.Path, allowedPath) {
|
||||||
|
pathFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pathFound {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusForbidden,
|
||||||
|
Message: "not authorized to access this path",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public key authorized, method and path allowed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// in theory, this should never happen; with an unverified chain, the TLS server
|
||||||
|
// should not accept the connection in the first place, and the acceptable cert
|
||||||
|
// pool is configured using the same list of public keys we verify against
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusUnauthorized,
|
||||||
|
Message: "client identity not authorized",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func stopAdminServer(srv *http.Server) error {
|
func stopAdminServer(srv *http.Server) error {
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
return fmt.Errorf("no admin server")
|
return fmt.Errorf("no admin server")
|
||||||
@@ -260,7 +614,7 @@ func stopAdminServer(srv *http.Server) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("shutting down admin server: %v", err)
|
return fmt.Errorf("shutting down admin server: %v", err)
|
||||||
}
|
}
|
||||||
Log().Named("admin").Info("stopped previous server")
|
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,22 +630,38 @@ type AdminRoute struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type adminHandler struct {
|
type adminHandler struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
|
||||||
|
// security for local/plaintext) endpoint, on by default
|
||||||
enforceOrigin bool
|
enforceOrigin bool
|
||||||
enforceHost bool
|
enforceHost bool
|
||||||
allowedOrigins []string
|
allowedOrigins []string
|
||||||
mux *http.ServeMux
|
|
||||||
|
// security for remote/encrypted endpoint
|
||||||
|
remoteControl *RemoteAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP is the external entry point for API requests.
|
// ServeHTTP is the external entry point for API requests.
|
||||||
// It will only be called once per request.
|
// It will only be called once per request.
|
||||||
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
Log().Named("admin.api").Info("received request",
|
log := Log().Named("admin.api").With(
|
||||||
zap.String("method", r.Method),
|
zap.String("method", r.Method),
|
||||||
zap.String("host", r.Host),
|
zap.String("host", r.Host),
|
||||||
zap.String("uri", r.RequestURI),
|
zap.String("uri", r.RequestURI),
|
||||||
zap.String("remote_addr", r.RemoteAddr),
|
zap.String("remote_addr", r.RemoteAddr),
|
||||||
zap.Reflect("headers", r.Header),
|
zap.Reflect("headers", r.Header),
|
||||||
)
|
)
|
||||||
|
if r.TLS != nil {
|
||||||
|
log = log.With(
|
||||||
|
zap.Bool("secure", true),
|
||||||
|
zap.Int("verified_chains", len(r.TLS.VerifiedChains)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if r.RequestURI == "/metrics" {
|
||||||
|
log.Debug("received request")
|
||||||
|
} else {
|
||||||
|
log.Info("received request")
|
||||||
|
}
|
||||||
h.serveHTTP(w, r)
|
h.serveHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +669,14 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// be called more than once per request, for example if a request
|
// be called more than once per request, for example if a request
|
||||||
// is rewritten (i.e. internal redirect).
|
// is rewritten (i.e. internal redirect).
|
||||||
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.remoteControl != nil {
|
||||||
|
// enforce access controls on secure endpoint
|
||||||
|
if err := h.remoteControl.enforceAccessControls(r); err != nil {
|
||||||
|
h.handleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
|
||||||
// I've never been able demonstrate a vulnerability myself, but apparently
|
// I've never been able demonstrate a vulnerability myself, but apparently
|
||||||
// WebSocket connections originating from browsers aren't subject to CORS
|
// WebSocket connections originating from browsers aren't subject to CORS
|
||||||
@@ -332,8 +710,6 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: authentication & authorization, if configured
|
|
||||||
|
|
||||||
h.mux.ServeHTTP(w, r)
|
h.mux.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +717,7 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err == ErrInternalRedir {
|
if err == errInternalRedir {
|
||||||
h.serveHTTP(w, r)
|
h.serveHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -349,12 +725,12 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
|||||||
apiErr, ok := err.(APIError)
|
apiErr, ok := err.(APIError)
|
||||||
if !ok {
|
if !ok {
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: http.StatusInternalServerError,
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if apiErr.Code == 0 {
|
if apiErr.HTTPStatus == 0 {
|
||||||
apiErr.Code = http.StatusInternalServerError
|
apiErr.HTTPStatus = http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
if apiErr.Message == "" && apiErr.Err != nil {
|
if apiErr.Message == "" && apiErr.Err != nil {
|
||||||
apiErr.Message = apiErr.Err.Error()
|
apiErr.Message = apiErr.Err.Error()
|
||||||
@@ -362,12 +738,15 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
|||||||
|
|
||||||
Log().Named("admin.api").Error("request error",
|
Log().Named("admin.api").Error("request error",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Int("status_code", apiErr.Code),
|
zap.Int("status_code", apiErr.HTTPStatus),
|
||||||
)
|
)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(apiErr.Code)
|
w.WriteHeader(apiErr.HTTPStatus)
|
||||||
json.NewEncoder(w).Encode(apiErr)
|
encErr := json.NewEncoder(w).Encode(apiErr)
|
||||||
|
if encErr != nil {
|
||||||
|
Log().Named("admin.api").Error("failed to encode error response", zap.Error(encErr))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkHost returns a handler that wraps next such that
|
// checkHost returns a handler that wraps next such that
|
||||||
@@ -384,8 +763,8 @@ func (h adminHandler) checkHost(r *http.Request) error {
|
|||||||
}
|
}
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return APIError{
|
return APIError{
|
||||||
Code: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
Err: fmt.Errorf("host not allowed: %s", r.Host),
|
Err: fmt.Errorf("host not allowed: %s", r.Host),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -399,14 +778,14 @@ func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
|||||||
origin := h.getOriginHost(r)
|
origin := h.getOriginHost(r)
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
return origin, APIError{
|
return origin, APIError{
|
||||||
Code: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
Err: fmt.Errorf("missing required Origin header"),
|
Err: fmt.Errorf("missing required Origin header"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !h.originAllowed(origin) {
|
if !h.originAllowed(origin) {
|
||||||
return origin, APIError{
|
return origin, APIError{
|
||||||
Code: http.StatusForbidden,
|
HTTPStatus: http.StatusForbidden,
|
||||||
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return origin, nil
|
return origin, nil
|
||||||
@@ -446,7 +825,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
err := readConfig(r.URL.Path, w)
|
err := readConfig(r.URL.Path, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{Code: http.StatusBadRequest, Err: err}
|
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -461,8 +840,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
if r.Method != http.MethodDelete {
|
if r.Method != http.MethodDelete {
|
||||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
|
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
|
||||||
return APIError{
|
return APIError{
|
||||||
Code: http.StatusBadRequest,
|
HTTPStatus: http.StatusBadRequest,
|
||||||
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
|
Err: fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,8 +852,8 @@ func handleConfig(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 APIError{
|
return 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()
|
||||||
@@ -489,8 +868,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
return APIError{
|
return APIError{
|
||||||
Code: http.StatusMethodNotAllowed,
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
Err: fmt.Errorf("method %s not allowed", r.Method),
|
Err: fmt.Errorf("method %s not allowed", r.Method),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,46 +900,22 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
|||||||
parts = append([]string{expanded}, parts[3:]...)
|
parts = append([]string{expanded}, parts[3:]...)
|
||||||
r.URL.Path = path.Join(parts...)
|
r.URL.Path = path.Join(parts...)
|
||||||
|
|
||||||
return ErrInternalRedir
|
return errInternalRedir
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStop(w http.ResponseWriter, r *http.Request) error {
|
func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||||
err := handleUnload(w, r)
|
|
||||||
if err != nil {
|
|
||||||
Log().Named("admin.api").Error("unload error", zap.Error(err))
|
|
||||||
}
|
|
||||||
if adminServer != nil {
|
|
||||||
// use goroutine so that we can finish responding to API request
|
|
||||||
go func() {
|
|
||||||
err := stopAdminServer(adminServer)
|
|
||||||
var exitCode int
|
|
||||||
if err != nil {
|
|
||||||
exitCode = ExitCodeFailedQuit
|
|
||||||
Log().Named("admin.api").Error("failed to stop admin server gracefully", zap.Error(err))
|
|
||||||
}
|
|
||||||
Log().Named("admin.api").Info("stopping now, bye!! 👋")
|
|
||||||
os.Exit(exitCode)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleUnload stops the current configuration that is running.
|
|
||||||
// Note that doing this can also be accomplished with DELETE /config/
|
|
||||||
// but we leave this function because handleStop uses it.
|
|
||||||
func handleUnload(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
return APIError{
|
return APIError{
|
||||||
Code: http.StatusMethodNotAllowed,
|
HTTPStatus: http.StatusMethodNotAllowed,
|
||||||
Err: fmt.Errorf("method not allowed"),
|
Err: fmt.Errorf("method not allowed"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log().Named("admin.api").Info("unloading")
|
|
||||||
if err := stopAndCleanup(); err != nil {
|
if err := notify.NotifyStopping(); err != nil {
|
||||||
Log().Named("admin.api").Error("error unloading", zap.Error(err))
|
Log().Error("unable to notify stopping to service manager", zap.Error(err))
|
||||||
} else {
|
|
||||||
Log().Named("admin.api").Info("unloading completed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exitProcess(Log().Named("admin.api"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,9 +1127,9 @@ func (f AdminHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) erro
|
|||||||
// and client responses. If Message is unset, then
|
// and client responses. If Message is unset, then
|
||||||
// Err.Error() will be serialized in its place.
|
// Err.Error() will be serialized in its place.
|
||||||
type APIError struct {
|
type APIError struct {
|
||||||
Code int `json:"-"`
|
HTTPStatus int `json:"-"`
|
||||||
Err error `json:"-"`
|
Err error `json:"-"`
|
||||||
Message string `json:"error"`
|
Message string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e APIError) Error() string {
|
func (e APIError) Error() string {
|
||||||
@@ -784,20 +1139,44 @@ func (e APIError) Error() string {
|
|||||||
return e.Message
|
return e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseAdminListenAddr extracts a singular listen address from either addr
|
||||||
|
// or defaultAddr, returning the network and the address of the listener.
|
||||||
|
func parseAdminListenAddr(addr string, defaultAddr string) (NetworkAddress, error) {
|
||||||
|
input := addr
|
||||||
|
if input == "" {
|
||||||
|
input = defaultAddr
|
||||||
|
}
|
||||||
|
listenAddr, err := ParseNetworkAddress(input)
|
||||||
|
if err != nil {
|
||||||
|
return NetworkAddress{}, fmt.Errorf("parsing listener address: %v", err)
|
||||||
|
}
|
||||||
|
if listenAddr.PortRangeSize() != 1 {
|
||||||
|
return NetworkAddress{}, fmt.Errorf("must be exactly one listener address; cannot listen on: %s", listenAddr)
|
||||||
|
}
|
||||||
|
return listenAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
|
||||||
|
func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
|
||||||
|
derBytes, err := base64.StdEncoding.DecodeString(certStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x509.ParseCertificate(derBytes)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultAdminListen is the address for the admin
|
// DefaultAdminListen is the address for the local admin
|
||||||
// listener, if none is specified at startup.
|
// listener, if none is specified at startup.
|
||||||
DefaultAdminListen = "localhost:2019"
|
DefaultAdminListen = "localhost:2019"
|
||||||
|
|
||||||
// ErrInternalRedir indicates an internal redirect
|
// DefaultRemoteAdminListen is the address for the remote
|
||||||
// and is useful when admin API handlers rewrite
|
// (TLS-authenticated) admin listener, if enabled and not
|
||||||
// the request; in that case, authentication and
|
// specified otherwise.
|
||||||
// authorization needs to happen again for the
|
DefaultRemoteAdminListen = ":2021"
|
||||||
// rewritten request.
|
|
||||||
ErrInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
|
|
||||||
|
|
||||||
// DefaultAdminConfig is the default configuration
|
// DefaultAdminConfig is the default configuration
|
||||||
// for the administration endpoint.
|
// for the local administration endpoint.
|
||||||
DefaultAdminConfig = &AdminConfig{
|
DefaultAdminConfig = &AdminConfig{
|
||||||
Listen: DefaultAdminListen,
|
Listen: DefaultAdminListen,
|
||||||
}
|
}
|
||||||
@@ -807,7 +1186,7 @@ var (
|
|||||||
// will get deleted before the process gracefully exits.
|
// will get deleted before the process gracefully exits.
|
||||||
func PIDFile(filename string) error {
|
func PIDFile(filename string) error {
|
||||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||||
err := ioutil.WriteFile(filename, pid, 0644)
|
err := ioutil.WriteFile(filename, pid, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -824,6 +1203,13 @@ var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0
|
|||||||
// pidfile is the name of the pidfile, if any.
|
// pidfile is the name of the pidfile, if any.
|
||||||
var pidfile string
|
var pidfile string
|
||||||
|
|
||||||
|
// errInternalRedir indicates an internal redirect
|
||||||
|
// and is useful when admin API handlers rewrite
|
||||||
|
// the request; in that case, authentication and
|
||||||
|
// authorization needs to happen again for the
|
||||||
|
// rewritten request.
|
||||||
|
var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rawConfigKey = "config"
|
rawConfigKey = "config"
|
||||||
idKey = "@id"
|
idKey = "@id"
|
||||||
@@ -835,4 +1221,8 @@ var bufPool = sync.Pool{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var adminServer *http.Server
|
// keep a reference to admin endpoint singletons while they're active
|
||||||
|
var (
|
||||||
|
localAdminServer, remoteAdminServer *http.Server
|
||||||
|
identityCertCache *certmagic.Cache
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,6 +101,16 @@ 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 {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,8 +142,8 @@ 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +158,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 +245,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 +259,19 @@ 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 {
|
||||||
|
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs")
|
||||||
|
}
|
||||||
|
|
||||||
// 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 +286,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 ||
|
||||||
@@ -273,7 +301,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error {
|
|||||||
} else {
|
} else {
|
||||||
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
err := ioutil.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 +337,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 +374,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 {
|
||||||
@@ -400,7 +428,7 @@ func run(newCfg *Config, start bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
return func() error {
|
err = func() error {
|
||||||
var started []string
|
var started []string
|
||||||
for name, a := range newCfg.apps {
|
for name, a := range newCfg.apps {
|
||||||
err := a.Start()
|
err := a.Start()
|
||||||
@@ -420,6 +448,64 @@ 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)
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()))
|
||||||
|
currentCfgMu.Lock()
|
||||||
|
err := unsyncedDecodeAndRun(loadedConfig, false)
|
||||||
|
currentCfgMu.Unlock()
|
||||||
|
if err == nil {
|
||||||
|
Log().Info("dynamically-loaded config applied successfully")
|
||||||
|
} else {
|
||||||
|
Log().Error("running dynamically-loaded config failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigLoader is a type that can load a Caddy config. The
|
||||||
|
// returned config must be valid Caddy JSON.
|
||||||
|
type ConfigLoader interface {
|
||||||
|
LoadConfig(Context) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops running the current configuration.
|
// Stop stops running the current configuration.
|
||||||
@@ -462,20 +548,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 != "" {
|
|
||||||
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 +558,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(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(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 +674,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 := ioutil.ReadFile(uuidFilePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
uuid, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
err = ioutil.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
|
||||||
|
|||||||
@@ -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: "input is not formatted with 'caddy fmt'",
|
||||||
|
}, 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)
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ func Format(input []byte) []byte {
|
|||||||
if comment {
|
if comment {
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
comment = false
|
comment = false
|
||||||
|
space = true
|
||||||
|
nextLine()
|
||||||
|
continue
|
||||||
} else {
|
} else {
|
||||||
write(ch)
|
write(ch)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package caddyfile
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
func FuzzFormat(input []byte) int {
|
||||||
|
formatted := Format(input)
|
||||||
|
if bytes.Equal(formatted, Format(formatted)) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -310,6 +310,55 @@ baz`,
|
|||||||
input: `redir / /some/#/path`,
|
input: `redir / /some/#/path`,
|
||||||
expect: `redir / /some/#/path`,
|
expect: `redir / /some/#/path`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "brace does not fold into comment above",
|
||||||
|
input: `# comment
|
||||||
|
{
|
||||||
|
foo
|
||||||
|
}`,
|
||||||
|
expect: `# comment
|
||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
@@ -34,9 +35,11 @@ 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
|
||||||
|
inSnippet bool
|
||||||
|
snippetName string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,3 +171,21 @@ func (l *lexer) next() bool {
|
|||||||
val = append(val, ch)
|
val = append(val, ch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tokenize takes bytes as input and lexes it into
|
||||||
|
// a list of tokens that can be parsed as a Caddyfile.
|
||||||
|
// Also takes a filename to fill the token's File as
|
||||||
|
// the source of the tokens, which is important to
|
||||||
|
// determine relative paths for `import` directives.
|
||||||
|
func Tokenize(input []byte, filename string) ([]Token, error) {
|
||||||
|
l := lexer{}
|
||||||
|
if err := l.load(bytes.NewReader(input)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tokens []Token
|
||||||
|
for l.next() {
|
||||||
|
l.token.File = filename
|
||||||
|
tokens = append(tokens, l.token)
|
||||||
|
}
|
||||||
|
return tokens, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,26 +12,17 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
// +build !windows
|
// +build gofuzz
|
||||||
|
|
||||||
package caddycmd
|
package caddyfile
|
||||||
|
|
||||||
import (
|
func FuzzTokenize(input []byte) int {
|
||||||
"fmt"
|
tokens, err := Tokenize(input, "Caddyfile")
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func gracefullyStopProcess(pid int) error {
|
|
||||||
fmt.Print("Graceful stop... ")
|
|
||||||
err := syscall.Kill(pid, syscall.SIGINT)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("kill: %v", err)
|
return 0
|
||||||
}
|
}
|
||||||
return nil
|
if len(tokens) == 0 {
|
||||||
}
|
return -1
|
||||||
|
}
|
||||||
func getProcessName() string {
|
return 1
|
||||||
return filepath.Base(os.Args[0])
|
|
||||||
}
|
}
|
||||||
@@ -15,37 +15,35 @@
|
|||||||
package caddyfile
|
package caddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type lexerTestCase struct {
|
type lexerTestCase struct {
|
||||||
input string
|
input []byte
|
||||||
expected []Token
|
expected []Token
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLexer(t *testing.T) {
|
func TestLexer(t *testing.T) {
|
||||||
testCases := []lexerTestCase{
|
testCases := []lexerTestCase{
|
||||||
{
|
{
|
||||||
input: `host:123`,
|
input: []byte(`host:123`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "host:123"},
|
{Line: 1, Text: "host:123"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `host:123
|
input: []byte(`host:123
|
||||||
|
|
||||||
directive`,
|
directive`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "host:123"},
|
{Line: 1, Text: "host:123"},
|
||||||
{Line: 3, Text: "directive"},
|
{Line: 3, Text: "directive"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `host:123 {
|
input: []byte(`host:123 {
|
||||||
directive
|
directive
|
||||||
}`,
|
}`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "host:123"},
|
{Line: 1, Text: "host:123"},
|
||||||
{Line: 1, Text: "{"},
|
{Line: 1, Text: "{"},
|
||||||
@@ -54,7 +52,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `host:123 { directive }`,
|
input: []byte(`host:123 { directive }`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "host:123"},
|
{Line: 1, Text: "host:123"},
|
||||||
{Line: 1, Text: "{"},
|
{Line: 1, Text: "{"},
|
||||||
@@ -63,12 +61,12 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `host:123 {
|
input: []byte(`host:123 {
|
||||||
#comment
|
#comment
|
||||||
directive
|
directive
|
||||||
# comment
|
# comment
|
||||||
foobar # another comment
|
foobar # another comment
|
||||||
}`,
|
}`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "host:123"},
|
{Line: 1, Text: "host:123"},
|
||||||
{Line: 1, Text: "{"},
|
{Line: 1, Text: "{"},
|
||||||
@@ -78,10 +76,10 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `host:123 {
|
input: []byte(`host:123 {
|
||||||
# hash inside string is not a comment
|
# hash inside string is not a comment
|
||||||
redir / /some/#/path
|
redir / /some/#/path
|
||||||
}`,
|
}`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "host:123"},
|
{Line: 1, Text: "host:123"},
|
||||||
{Line: 1, Text: "{"},
|
{Line: 1, Text: "{"},
|
||||||
@@ -92,14 +90,14 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "# comment at beginning of file\n# comment at beginning of line\nhost:123",
|
input: []byte("# comment at beginning of file\n# comment at beginning of line\nhost:123"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 3, Text: "host:123"},
|
{Line: 3, Text: "host:123"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `a "quoted value" b
|
input: []byte(`a "quoted value" b
|
||||||
foobar`,
|
foobar`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "a"},
|
{Line: 1, Text: "a"},
|
||||||
{Line: 1, Text: "quoted value"},
|
{Line: 1, Text: "quoted value"},
|
||||||
@@ -108,7 +106,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `A "quoted \"value\" inside" B`,
|
input: []byte(`A "quoted \"value\" inside" B`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "A"},
|
{Line: 1, Text: "A"},
|
||||||
{Line: 1, Text: `quoted "value" inside`},
|
{Line: 1, Text: `quoted "value" inside`},
|
||||||
@@ -116,7 +114,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "An escaped \"newline\\\ninside\" quotes",
|
input: []byte("An escaped \"newline\\\ninside\" quotes"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "An"},
|
{Line: 1, Text: "An"},
|
||||||
{Line: 1, Text: "escaped"},
|
{Line: 1, Text: "escaped"},
|
||||||
@@ -125,7 +123,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "An escaped newline\\\noutside quotes",
|
input: []byte("An escaped newline\\\noutside quotes"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "An"},
|
{Line: 1, Text: "An"},
|
||||||
{Line: 1, Text: "escaped"},
|
{Line: 1, Text: "escaped"},
|
||||||
@@ -135,7 +133,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "line1\\\nescaped\nline2\nline3",
|
input: []byte("line1\\\nescaped\nline2\nline3"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "line1"},
|
{Line: 1, Text: "line1"},
|
||||||
{Line: 1, Text: "escaped"},
|
{Line: 1, Text: "escaped"},
|
||||||
@@ -144,7 +142,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "line1\\\nescaped1\\\nescaped2\nline4\nline5",
|
input: []byte("line1\\\nescaped1\\\nescaped2\nline4\nline5"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "line1"},
|
{Line: 1, Text: "line1"},
|
||||||
{Line: 1, Text: "escaped1"},
|
{Line: 1, Text: "escaped1"},
|
||||||
@@ -154,34 +152,34 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `"unescapable\ in quotes"`,
|
input: []byte(`"unescapable\ in quotes"`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `unescapable\ in quotes`},
|
{Line: 1, Text: `unescapable\ in quotes`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `"don't\escape"`,
|
input: []byte(`"don't\escape"`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `don't\escape`},
|
{Line: 1, Text: `don't\escape`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `"don't\\escape"`,
|
input: []byte(`"don't\\escape"`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `don't\\escape`},
|
{Line: 1, Text: `don't\\escape`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `un\escapable`,
|
input: []byte(`un\escapable`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `un\escapable`},
|
{Line: 1, Text: `un\escapable`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `A "quoted value with line
|
input: []byte(`A "quoted value with line
|
||||||
break inside" {
|
break inside" {
|
||||||
foobar
|
foobar
|
||||||
}`,
|
}`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "A"},
|
{Line: 1, Text: "A"},
|
||||||
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||||
@@ -191,13 +189,13 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `"C:\php\php-cgi.exe"`,
|
input: []byte(`"C:\php\php-cgi.exe"`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `C:\php\php-cgi.exe`},
|
{Line: 1, Text: `C:\php\php-cgi.exe`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `empty "" string`,
|
input: []byte(`empty "" string`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `empty`},
|
{Line: 1, Text: `empty`},
|
||||||
{Line: 1, Text: ``},
|
{Line: 1, Text: ``},
|
||||||
@@ -205,7 +203,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "skip those\r\nCR characters",
|
input: []byte("skip those\r\nCR characters"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: "skip"},
|
{Line: 1, Text: "skip"},
|
||||||
{Line: 1, Text: "those"},
|
{Line: 1, Text: "those"},
|
||||||
@@ -214,13 +212,13 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
|
input: []byte("\xEF\xBB\xBF:8080"), // test with leading byte order mark
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: ":8080"},
|
{Line: 1, Text: ":8080"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "simple `backtick quoted` string",
|
input: []byte("simple `backtick quoted` string"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `simple`},
|
{Line: 1, Text: `simple`},
|
||||||
{Line: 1, Text: `backtick quoted`},
|
{Line: 1, Text: `backtick quoted`},
|
||||||
@@ -228,7 +226,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "multiline `backtick\nquoted\n` string",
|
input: []byte("multiline `backtick\nquoted\n` string"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `multiline`},
|
{Line: 1, Text: `multiline`},
|
||||||
{Line: 1, Text: "backtick\nquoted\n"},
|
{Line: 1, Text: "backtick\nquoted\n"},
|
||||||
@@ -236,7 +234,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "nested `\"quotes inside\" backticks` string",
|
input: []byte("nested `\"quotes inside\" backticks` string"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `nested`},
|
{Line: 1, Text: `nested`},
|
||||||
{Line: 1, Text: `"quotes inside" backticks`},
|
{Line: 1, Text: `"quotes inside" backticks`},
|
||||||
@@ -244,7 +242,7 @@ func TestLexer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "reverse-nested \"`backticks` inside\" quotes",
|
input: []byte("reverse-nested \"`backticks` inside\" quotes"),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `reverse-nested`},
|
{Line: 1, Text: `reverse-nested`},
|
||||||
{Line: 1, Text: "`backticks` inside"},
|
{Line: 1, Text: "`backticks` inside"},
|
||||||
@@ -254,22 +252,14 @@ func TestLexer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
actual := tokenize(testCase.input)
|
actual, err := Tokenize(testCase.input, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
}
|
||||||
lexerCompare(t, i, testCase.expected, actual)
|
lexerCompare(t, i, testCase.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenize(input string) (tokens []Token) {
|
|
||||||
l := lexer{}
|
|
||||||
if err := l.load(strings.NewReader(input)); err != nil {
|
|
||||||
log.Printf("[ERROR] load failed: %v", err)
|
|
||||||
}
|
|
||||||
for l.next() {
|
|
||||||
tokens = append(tokens, l.token)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
|
||||||
if len(expected) != len(actual) {
|
if len(expected) != len(actual) {
|
||||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package caddyfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -40,7 +41,13 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
|||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,21 +67,31 @@ func replaceEnvVars(input []byte) ([]byte, error) {
|
|||||||
end += begin + len(spanOpen) // make end relative to input, not begin
|
end += begin + len(spanOpen) // make end relative to input, not begin
|
||||||
|
|
||||||
// get the name; if there is no name, skip it
|
// get the name; if there is no name, skip it
|
||||||
envVarName := input[begin+len(spanOpen) : end]
|
envString := input[begin+len(spanOpen) : end]
|
||||||
if len(envVarName) == 0 {
|
if len(envString) == 0 {
|
||||||
offset = end + len(spanClose)
|
offset = end + len(spanClose)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// split the string into a key and an optional default
|
||||||
|
envParts := strings.SplitN(string(envString), envVarDefaultDelimiter, 2)
|
||||||
|
|
||||||
|
// do a lookup for the env var, replace with the default if not found
|
||||||
|
envVarValue, found := os.LookupEnv(envParts[0])
|
||||||
|
if !found && len(envParts) == 2 {
|
||||||
|
envVarValue = envParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
// get the value of the environment variable
|
// get the value of the environment variable
|
||||||
envVarValue := []byte(os.ExpandEnv(os.Getenv(string(envVarName))))
|
// note that this causes one-level deep chaining
|
||||||
|
envVarBytes := []byte(envVarValue)
|
||||||
|
|
||||||
// splice in the value
|
// splice in the value
|
||||||
input = append(input[:begin],
|
input = append(input[:begin],
|
||||||
append(envVarValue, input[end+len(spanClose):]...)...)
|
append(envVarBytes, input[end+len(spanClose):]...)...)
|
||||||
|
|
||||||
// continue at the end of the replacement
|
// continue at the end of the replacement
|
||||||
offset = begin + len(envVarValue)
|
offset = begin + len(envVarBytes)
|
||||||
}
|
}
|
||||||
return input, nil
|
return input, nil
|
||||||
}
|
}
|
||||||
@@ -87,16 +104,10 @@ func allTokens(filename string, input []byte) ([]Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
l := new(lexer)
|
tokens, err := Tokenize(input, filename)
|
||||||
err = l.load(bytes.NewReader(input))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tokens []Token
|
|
||||||
for l.next() {
|
|
||||||
l.token.File = filename
|
|
||||||
tokens = append(tokens, l.token)
|
|
||||||
}
|
|
||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +117,7 @@ type parser struct {
|
|||||||
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) {
|
||||||
@@ -161,6 +173,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
|
||||||
@@ -193,6 +214,11 @@ func (p *parser) addresses() error {
|
|||||||
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
|
||||||
@@ -300,7 +326,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)
|
||||||
}
|
}
|
||||||
@@ -310,10 +336,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
|
||||||
@@ -349,7 +380,6 @@ func (p *parser) doImport() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -357,6 +387,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
|
||||||
@@ -554,4 +596,7 @@ func (s Segment) Directive() string {
|
|||||||
|
|
||||||
// spanOpen and spanClose are used to bound spans that
|
// spanOpen and spanClose are used to bound spans that
|
||||||
// contain the name of an environment variable.
|
// contain the name of an environment variable.
|
||||||
var spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
|
var (
|
||||||
|
spanOpen, spanClose = []byte{'{', '$'}, []byte{'}'}
|
||||||
|
envVarDefaultDelimiter = ":"
|
||||||
|
)
|
||||||
|
|||||||
@@ -160,6 +160,10 @@ func TestParseOneAndImport(t *testing.T) {
|
|||||||
"localhost",
|
"localhost",
|
||||||
}, []int{}},
|
}, []int{}},
|
||||||
|
|
||||||
|
{`localhost{
|
||||||
|
dir1
|
||||||
|
}`, true, []string{}, []int{}},
|
||||||
|
|
||||||
{`localhost
|
{`localhost
|
||||||
dir1 {
|
dir1 {
|
||||||
nested {
|
nested {
|
||||||
@@ -444,6 +448,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()
|
||||||
@@ -478,6 +504,7 @@ func TestParseAll(t *testing.T) {
|
|||||||
|
|
||||||
func TestEnvironmentReplacement(t *testing.T) {
|
func TestEnvironmentReplacement(t *testing.T) {
|
||||||
os.Setenv("FOOBAR", "foobar")
|
os.Setenv("FOOBAR", "foobar")
|
||||||
|
os.Setenv("CHAINED", "$FOOBAR")
|
||||||
|
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
input string
|
input string
|
||||||
@@ -523,6 +550,22 @@ func TestEnvironmentReplacement(t *testing.T) {
|
|||||||
input: "{$FOOBAR}{$FOOBAR}",
|
input: "{$FOOBAR}{$FOOBAR}",
|
||||||
expect: "foobarfoobar",
|
expect: "foobarfoobar",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: "{$CHAINED}",
|
||||||
|
expect: "$FOOBAR", // should not chain env expands
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "{$FOO:default}",
|
||||||
|
expect: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "foo{$BAR:bar}baz",
|
||||||
|
expect: "foobarbaz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "foo{$BAR:$FOOBAR}baz",
|
||||||
|
expect: "foo$FOOBARbaz", // should not chain env expands
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "{$FOOBAR",
|
input: "{$FOOBAR",
|
||||||
expect: "{$FOOBAR",
|
expect: "{$FOOBAR",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -163,6 +164,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
|
|||||||
|
|
||||||
sbaddrs = append(sbaddrs, a)
|
sbaddrs = append(sbaddrs, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sort them by their first address (we know there will always be at least one)
|
||||||
|
// to avoid problems with non-deterministic ordering (makes tests flaky)
|
||||||
|
sort.Slice(sbaddrs, func(i, j int) bool {
|
||||||
|
return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
|
||||||
|
})
|
||||||
|
|
||||||
return sbaddrs
|
return sbaddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
@@ -29,6 +30,8 @@ import (
|
|||||||
"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/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,6 +41,8 @@ func init() {
|
|||||||
RegisterHandlerDirective("root", parseRoot)
|
RegisterHandlerDirective("root", parseRoot)
|
||||||
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)
|
||||||
@@ -73,8 +78,10 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
|||||||
// load <paths...>
|
// load <paths...>
|
||||||
// ca <acme_ca_endpoint>
|
// ca <acme_ca_endpoint>
|
||||||
// ca_root <pem_file>
|
// ca_root <pem_file>
|
||||||
// dns <provider_name>
|
// dns <provider_name> [...]
|
||||||
// on_demand
|
// on_demand
|
||||||
|
// eab <key_id> <mac_key>
|
||||||
|
// issuer <module_name> [...]
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
@@ -83,7 +90,9 @@ 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 onDemand bool
|
var onDemand bool
|
||||||
|
|
||||||
for h.Next() {
|
for h.Next() {
|
||||||
@@ -117,10 +126,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
|
||||||
@@ -262,6 +271,42 @@ 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":
|
||||||
|
arg := h.RemainingArgs()
|
||||||
|
if len(arg) != 2 {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
if acmeIssuer == nil {
|
||||||
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||||
|
}
|
||||||
|
acmeIssuer.ExternalAccount = &acme.EAB{
|
||||||
|
KeyID: arg[0],
|
||||||
|
MACKey: arg[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
case "issuer":
|
||||||
|
if !h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
modName := h.Val()
|
||||||
|
modID := "tls.issuance." + modName
|
||||||
|
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
issuer, ok := unm.(certmagic.Issuer)
|
||||||
|
if !ok {
|
||||||
|
return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
|
||||||
|
}
|
||||||
|
issuers = append(issuers, issuer)
|
||||||
|
|
||||||
case "dns":
|
case "dns":
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
@@ -272,20 +317,32 @@ 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 "ca_root":
|
case "ca_root":
|
||||||
arg := h.RemainingArgs()
|
arg := h.RemainingArgs()
|
||||||
@@ -315,7 +372,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// begin building the final config values
|
// begin building the final config values
|
||||||
var configVals []ConfigValue
|
configVals := []ConfigValue{}
|
||||||
|
|
||||||
// certificate loaders
|
// certificate loaders
|
||||||
if len(fileLoader) > 0 {
|
if len(fileLoader) > 0 {
|
||||||
@@ -331,34 +388,64 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// issuer
|
// some tls subdirectives are shortcuts that implicitly configure issuers, and the
|
||||||
if acmeIssuer != nil && internalIssuer != nil {
|
// user can also configure issuers explicitly using the issuer subdirective; the
|
||||||
// the logic to support this would be complex
|
// logic to support both would likely be complex, or at least unintuitive
|
||||||
return nil, h.Err("cannot use both ACME and internal issuers in same server block")
|
if len(issuers) > 0 && (acmeIssuer != nil || internalIssuer != nil) {
|
||||||
|
return nil, h.Err("cannot mix issuer subdirective (explicit issuers) with other issuer-specific subdirectives (implicit issuers)")
|
||||||
}
|
}
|
||||||
if acmeIssuer != nil {
|
if acmeIssuer != nil && internalIssuer != nil {
|
||||||
// fill in global defaults, if configured
|
return nil, h.Err("cannot create both ACME and internal certificate issuers")
|
||||||
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
|
}
|
||||||
acmeIssuer.Email = email.(string)
|
|
||||||
}
|
// now we should either have: explicitly-created issuers, or an implicitly-created
|
||||||
if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
|
// ACME or internal issuer, or no issuers at all
|
||||||
acmeIssuer.CA = acmeCA.(string)
|
switch {
|
||||||
}
|
case len(issuers) > 0:
|
||||||
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
|
for _, issuer := range issuers {
|
||||||
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
|
configVals = append(configVals, ConfigValue{
|
||||||
|
Class: "tls.cert_issuer",
|
||||||
|
Value: issuer,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
configVals = append(configVals, ConfigValue{
|
case acmeIssuer != nil:
|
||||||
Class: "tls.cert_issuer",
|
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
||||||
Value: acmeIssuer,
|
defaultIssuers := caddytls.DefaultIssuers()
|
||||||
})
|
|
||||||
} else if internalIssuer != nil {
|
// 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{
|
||||||
@@ -421,14 +508,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>
|
||||||
@@ -443,6 +530,15 @@ 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:
|
||||||
|
codeInt, err := strconv.Atoi(code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
|
||||||
|
}
|
||||||
|
if codeInt < 300 || codeInt > 399 {
|
||||||
|
return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return caddyhttp.StaticResponse{
|
return caddyhttp.StaticResponse{
|
||||||
@@ -462,40 +558,46 @@ 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)
|
||||||
|
|
||||||
for h.Next() {
|
allResults, err := parseSegmentAsConfig(h)
|
||||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
if err != nil {
|
||||||
dir := h.Val()
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
dirFunc, ok := registeredDirectives[dir]
|
for _, result := range allResults {
|
||||||
if !ok {
|
switch handler := result.Value.(type) {
|
||||||
return nil, h.Errf("unrecognized directive: %s", dir)
|
case caddyhttp.Route:
|
||||||
}
|
sr.Routes = append(sr.Routes, handler)
|
||||||
|
case caddyhttp.Subroute:
|
||||||
subHelper := h
|
// directives which return a literal subroute instead of a route
|
||||||
subHelper.Dispenser = h.NewFromNextSegment()
|
// means they intend to keep those handlers together without
|
||||||
|
// them being reordered; we're doing that anyway since we're in
|
||||||
results, err := dirFunc(subHelper)
|
// the route directive, so just append its handlers
|
||||||
if err != nil {
|
sr.Routes = append(sr.Routes, handler.Routes...)
|
||||||
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
default:
|
||||||
}
|
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
|
||||||
for _, result := range results {
|
|
||||||
switch handler := result.Value.(type) {
|
|
||||||
case caddyhttp.Route:
|
|
||||||
sr.Routes = append(sr.Routes, handler)
|
|
||||||
case caddyhttp.Subroute:
|
|
||||||
// directives which return a literal subroute instead of a route
|
|
||||||
// means they intend to keep those handlers together without
|
|
||||||
// them being reordered; we're doing that anyway since we're in
|
|
||||||
// the route directive, so just append its handlers
|
|
||||||
sr.Routes = append(sr.Routes, handler.Routes...)
|
|
||||||
default:
|
|
||||||
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", dir, result.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,11 +630,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)
|
||||||
@@ -558,21 +699,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)
|
||||||
@@ -582,21 +717,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)
|
||||||
|
|
||||||
@@ -609,22 +737,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,35 @@ 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 {
|
||||||
|
common_log delete
|
||||||
|
request>remote_addr ip_mask {
|
||||||
|
ipv4 24
|
||||||
|
ipv6 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"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 +65,134 @@ 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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir /old.html /new.html htlm
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 200
|
||||||
|
}`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `:8080 {
|
||||||
|
redir * :8081 400
|
||||||
|
}`,
|
||||||
|
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,9 +37,11 @@ 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{
|
||||||
|
"map",
|
||||||
"root",
|
"root",
|
||||||
|
|
||||||
"header",
|
"header",
|
||||||
|
"request_body",
|
||||||
|
|
||||||
"redir",
|
"redir",
|
||||||
"rewrite",
|
"rewrite",
|
||||||
@@ -54,17 +56,21 @@ var directiveOrder = []string{
|
|||||||
"encode",
|
"encode",
|
||||||
"templates",
|
"templates",
|
||||||
|
|
||||||
// special routing 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
|
||||||
"respond",
|
"respond",
|
||||||
|
"metrics",
|
||||||
"reverse_proxy",
|
"reverse_proxy",
|
||||||
"php_fastcgi",
|
"php_fastcgi",
|
||||||
"file_server",
|
"file_server",
|
||||||
"acme_server",
|
"acme_server",
|
||||||
|
"abort",
|
||||||
|
"error",
|
||||||
}
|
}
|
||||||
|
|
||||||
// directiveIsOrdered returns true if dir is
|
// directiveIsOrdered returns true if dir is
|
||||||
@@ -99,20 +105,11 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
|
|||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
matcherSet, ok, err := h.MatcherToken()
|
matcherSet, err := h.ExtractMatcherSet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ok {
|
|
||||||
// strip matcher token; we don't need to
|
|
||||||
// use the return value here because a
|
|
||||||
// new dispenser should have been made
|
|
||||||
// solely for this directive's tokens,
|
|
||||||
// with no other uses of same slice
|
|
||||||
h.Dispenser.Delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Dispenser.Reset() // pretend this lookahead never happened
|
|
||||||
val, err := setupFunc(h)
|
val, err := setupFunc(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -197,7 +194,12 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if hasMatcher {
|
if hasMatcher {
|
||||||
h.Dispenser.Delete() // strip matcher token
|
// strip matcher token; we don't need to
|
||||||
|
// use the return value here because a
|
||||||
|
// new dispenser should have been made
|
||||||
|
// solely for this directive's tokens,
|
||||||
|
// with no other uses of same slice
|
||||||
|
h.Dispenser.Delete()
|
||||||
}
|
}
|
||||||
h.Dispenser.Reset() // pretend this lookahead never happened
|
h.Dispenser.Reset() // pretend this lookahead never happened
|
||||||
return matcherSet, nil
|
return matcherSet, nil
|
||||||
@@ -263,13 +265,37 @@ 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.
|
||||||
func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
allResults, err := parseSegmentAsConfig(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildSubroute(allResults, h.groupCounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSegmentAsConfig parses the segment such that its subdirectives
|
||||||
|
// are themselves treated as directives, including named matcher definitions,
|
||||||
|
// and the raw Config structs are returned.
|
||||||
|
func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
||||||
var allResults []ConfigValue
|
var allResults []ConfigValue
|
||||||
|
|
||||||
for h.Next() {
|
for h.Next() {
|
||||||
|
// don't allow non-matcher args on the first line
|
||||||
|
if h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
// slice the linear list of tokens into top-level segments
|
// slice the linear list of tokens into top-level segments
|
||||||
var segments []caddyfile.Segment
|
var segments []caddyfile.Segment
|
||||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||||
@@ -284,13 +310,17 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// find and extract any embedded matcher definitions in this scope
|
// find and extract any embedded matcher definitions in this scope
|
||||||
for i, seg := range segments {
|
for i := 0; i < len(segments); i++ {
|
||||||
|
seg := segments[i]
|
||||||
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
|
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
|
||||||
|
// parse, then add the matcher to matcherDefs
|
||||||
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
|
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// remove the matcher segment (consumed), then step back the loop
|
||||||
segments = append(segments[:i], segments[i+1:]...)
|
segments = append(segments[:i], segments[i+1:]...)
|
||||||
|
i--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +347,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildSubroute(allResults, h.groupCounter)
|
return allResults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigValue represents a value to be added to the final
|
// ConfigValue represents a value to be added to the final
|
||||||
@@ -384,6 +414,14 @@ func sortRoutes(routes []ConfigValue) {
|
|||||||
if len(jPM) > 0 {
|
if len(jPM) > 0 {
|
||||||
jPathLen = len(jPM[0])
|
jPathLen = len(jPM[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
return iPathLen > jPathLen
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -469,9 +507,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)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -27,6 +28,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +36,17 @@ 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 {
|
||||||
}
|
}
|
||||||
@@ -99,6 +112,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
"{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
|
||||||
@@ -172,6 +186,15 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
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
|
||||||
|
// 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
|
||||||
sb.pile[result.Class] = append(sb.pile[result.Class], result)
|
sb.pile[result.Class] = append(sb.pile[result.Class], result)
|
||||||
@@ -197,9 +220,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
|
||||||
@@ -208,30 +232,38 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if experimental HTTP/3 is enabled, enable it on each server
|
// then make the PKI app
|
||||||
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
|
pkiApp, warnings, err := st.buildPKIApp(pairings, options, warnings)
|
||||||
for _, srv := range httpApp.Servers {
|
if err != nil {
|
||||||
srv.ExperimentalHTTP3 = true
|
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
|
||||||
|
addCustomLog := func(ncl namedCustomLog) {
|
||||||
|
if ncl.name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ncl.name == "default" {
|
||||||
|
hasDefaultLog = true
|
||||||
|
}
|
||||||
|
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Apply server-specific log options
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
for _, sb := range p.serverBlocks {
|
for _, sb := range p.serverBlocks {
|
||||||
for _, clVal := range sb.pile["custom_log"] {
|
for _, clVal := range sb.pile["custom_log"] {
|
||||||
ncl := clVal.Value.(namedCustomLog)
|
addCustomLog(clVal.Value.(namedCustomLog))
|
||||||
if ncl.name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ncl.name == "default" {
|
|
||||||
hasDefaultLog = true
|
|
||||||
}
|
|
||||||
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
|
||||||
ncl.log.Level = "DEBUG"
|
|
||||||
}
|
|
||||||
customLogs = append(customLogs, ncl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,24 +281,34 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||||||
|
|
||||||
// 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",
|
||||||
storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
|
storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
|
||||||
&warnings)
|
&warnings)
|
||||||
}
|
}
|
||||||
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
|
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
|
||||||
if adminConfig == "off" {
|
cfg.Admin = adminConfig
|
||||||
cfg.Admin = &caddy.AdminConfig{Disabled: true}
|
|
||||||
} else {
|
|
||||||
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(customLogs) > 0 {
|
if len(customLogs) > 0 {
|
||||||
if cfg.Logging == nil {
|
if cfg.Logging == nil {
|
||||||
@@ -281,7 +323,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)
|
||||||
@@ -305,23 +347,68 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, segment := range serverBlocks[0].block.Segments {
|
for _, segment := range serverBlocks[0].block.Segments {
|
||||||
dir := segment.Directive()
|
opt := segment.Directive()
|
||||||
var val interface{}
|
var val interface{}
|
||||||
var err error
|
var err error
|
||||||
disp := caddyfile.NewDispenser(segment)
|
disp := caddyfile.NewDispenser(segment)
|
||||||
|
|
||||||
dirFunc, ok := registeredGlobalOptions[dir]
|
optFunc, ok := registeredGlobalOptions[opt]
|
||||||
if !ok {
|
if !ok {
|
||||||
tkn := segment[0]
|
tkn := segment[0]
|
||||||
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, dir)
|
return nil, fmt.Errorf("%s:%d: unrecognized global option: %s", tkn.File, tkn.Line, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err = dirFunc(disp)
|
val, err = optFunc(disp, options[opt])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
|
return nil, fmt.Errorf("parsing caddyfile tokens for '%s': %v", opt, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
options[dir] = val
|
// As a special case, fold multiple "servers" options together
|
||||||
|
// in an array instead of overwriting a possible existing value
|
||||||
|
if opt == "servers" {
|
||||||
|
existingOpts, ok := options[opt].([]serverOptions)
|
||||||
|
if !ok {
|
||||||
|
existingOpts = []serverOptions{}
|
||||||
|
}
|
||||||
|
serverOpts, ok := val.(serverOptions)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected type from 'servers' global options: %T", val)
|
||||||
|
}
|
||||||
|
options[opt] = append(existingOpts, serverOpts)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got "servers" options, we'll sort them by their listener address
|
||||||
|
if serverOpts, ok := options["servers"].([]serverOptions); ok {
|
||||||
|
sort.Slice(serverOpts, func(i, j int) bool {
|
||||||
|
return len(serverOpts[i].ListenerAddress) > len(serverOpts[j].ListenerAddress)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reject the config if there are duplicate listener address
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, entry := range serverOpts {
|
||||||
|
if _, alreadySeen := seen[entry.ListenerAddress]; alreadySeen {
|
||||||
|
return nil, fmt.Errorf("cannot have 'servers' global options with duplicate listener addresses: %s", entry.ListenerAddress)
|
||||||
|
}
|
||||||
|
seen[entry.ListenerAddress] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverBlocks[1:], nil
|
return serverBlocks[1:], nil
|
||||||
@@ -365,6 +452,9 @@ func (st *ServerType) serversFromPairings(
|
|||||||
if autoHTTPS == "disable_redirects" {
|
if autoHTTPS == "disable_redirects" {
|
||||||
srv.AutoHTTPS.DisableRedir = true
|
srv.AutoHTTPS.DisableRedir = true
|
||||||
}
|
}
|
||||||
|
if autoHTTPS == "ignore_loaded_certs" {
|
||||||
|
srv.AutoHTTPS.IgnoreLoadedCerts = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort server blocks by their keys; this is important because
|
// sort server blocks by their keys; this is important because
|
||||||
@@ -379,7 +469,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) {
|
||||||
@@ -390,7 +480,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) {
|
||||||
@@ -400,9 +490,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 {
|
||||||
@@ -420,6 +513,15 @@ func (st *ServerType) serversFromPairings(
|
|||||||
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
|
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
|
||||||
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
|
autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
|
||||||
|
|
||||||
|
// if a catch-all server block (one which accepts all hostnames) exists in this pairing,
|
||||||
|
// we need to know that so that we can configure logs properly (see #3878)
|
||||||
|
var catchAllSblockExists bool
|
||||||
|
for _, sblock := range p.serverBlocks {
|
||||||
|
if len(sblock.hostsFromKeys(false)) == 0 {
|
||||||
|
catchAllSblockExists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -429,6 +531,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 == "::" {
|
||||||
|
log.Printf("[WARNING] Site block has unspecified IP address %s which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", 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
|
||||||
@@ -450,27 +559,31 @@ func (st *ServerType) serversFromPairings(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cp.DefaultSNI = defaultSNI
|
cp.DefaultSNI = defaultSNI
|
||||||
hasCatchAllTLSConnPolicy = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// only append this policy if it actually changes something
|
// only append this policy if it actually changes something
|
||||||
if !cp.SettingsEmpty() {
|
if !cp.SettingsEmpty() {
|
||||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
||||||
|
hasCatchAllTLSConnPolicy = len(hosts) == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, addr := range sblock.keys {
|
for _, addr := range sblock.keys {
|
||||||
// exclude any hosts that were defined explicitly with "http://"
|
// if server only uses HTTPS 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" ||
|
||||||
@@ -533,13 +646,13 @@ func (st *ServerType) serversFromPairings(
|
|||||||
} else {
|
} else {
|
||||||
// map each host to the user's desired logger name
|
// map each host to the user's desired logger name
|
||||||
for _, h := range sblockLogHosts {
|
for _, h := range sblockLogHosts {
|
||||||
// if the custom logger name is non-empty, add it to
|
// if the custom logger name is non-empty, add it to the map;
|
||||||
// the map; otherwise, only map to an empty logger
|
// otherwise, only map to an empty logger name if this or
|
||||||
// name if the server block has a catch-all host (in
|
// another site block on this server has a catch-all host (in
|
||||||
// which case only requests with mapped hostnames will
|
// which case only requests with mapped hostnames will be
|
||||||
// be access-logged, so it'll be necessary to add them
|
// access-logged, so it'll be necessary to add them to the
|
||||||
// to the map even if they use default logger)
|
// map even if they use default logger)
|
||||||
if ncl.name != "" || len(hosts) == 0 {
|
if ncl.name != "" || catchAllSblockExists {
|
||||||
if srv.Logs.LoggerNames == nil {
|
if srv.Logs.LoggerNames == nil {
|
||||||
srv.Logs.LoggerNames = make(map[string]string)
|
srv.Logs.LoggerNames = make(map[string]string)
|
||||||
}
|
}
|
||||||
@@ -596,6 +709,11 @@ func (st *ServerType) serversFromPairings(
|
|||||||
servers[fmt.Sprintf("srv%d", i)] = srv
|
servers[fmt.Sprintf("srv%d", i)] = srv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := applyServerOptions(servers, options, warnings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return servers, nil
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,9 +775,15 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// consolidateConnPolicies removes empty TLS connection policies and combines
|
// consolidateConnPolicies sorts any catch-all policy to the end, removes empty TLS connection
|
||||||
// equivalent ones for a cleaner overall output.
|
// policies, and combines equivalent ones for a cleaner overall output.
|
||||||
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
|
func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.ConnectionPolicies, error) {
|
||||||
|
// catch-all policies (those without any matcher) should be at the
|
||||||
|
// end, otherwise it nullifies any more specific policies
|
||||||
|
sort.SliceStable(cps, func(i, j int) bool {
|
||||||
|
return cps[j].MatchersRaw == nil && cps[i].MatchersRaw != nil
|
||||||
|
})
|
||||||
|
|
||||||
for i := 0; i < len(cps); i++ {
|
for i := 0; i < len(cps); i++ {
|
||||||
// compare it to the others
|
// compare it to the others
|
||||||
for j := 0; j < len(cps); j++ {
|
for j := 0; j < len(cps); j++ {
|
||||||
@@ -852,7 +976,18 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
|
|||||||
// root directives would overwrite previously-matched ones; they should not cascade
|
// root directives would overwrite previously-matched ones; they should not cascade
|
||||||
"root": {},
|
"root": {},
|
||||||
}
|
}
|
||||||
for meDir, info := range mutuallyExclusiveDirs {
|
|
||||||
|
// we need to deterministically loop over each of these directives
|
||||||
|
// in order to keep the group numbers consistent
|
||||||
|
keys := make([]string, 0, len(mutuallyExclusiveDirs))
|
||||||
|
for k := range mutuallyExclusiveDirs {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, meDir := range keys {
|
||||||
|
info := mutuallyExclusiveDirs[meDir]
|
||||||
|
|
||||||
// see how many instances of the directive there are
|
// see how many instances of the directive there are
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
if r.directive == meDir {
|
if r.directive == meDir {
|
||||||
@@ -1112,6 +1247,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 {
|
||||||
@@ -1122,6 +1265,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,54 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
:80
|
:80
|
||||||
`,
|
`,
|
||||||
expectWarn: false,
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
admin {
|
||||||
|
enforce_origin
|
||||||
|
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:80
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
admin 127.0.0.1:2020 {
|
||||||
|
enforce_origin
|
||||||
|
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:80
|
||||||
|
`,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
admin 192.168.1.1:2020 127.0.0.1:2020 {
|
||||||
|
enforce_origin
|
||||||
|
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:80
|
||||||
|
`,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `
|
||||||
|
{
|
||||||
|
admin off {
|
||||||
|
enforce_origin
|
||||||
|
origins 192.168.1.1:2020 127.0.0.1:2020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:80
|
||||||
|
`,
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -170,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,35 +18,41 @@ 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/mholt/acmez/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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("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("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("email", parseOptSingleString)
|
RegisterGlobalOption("email", parseOptSingleString)
|
||||||
RegisterGlobalOption("admin", parseOptAdmin)
|
RegisterGlobalOption("admin", parseOptAdmin)
|
||||||
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
|
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
|
||||||
RegisterGlobalOption("local_certs", parseOptTrue)
|
RegisterGlobalOption("local_certs", parseOptTrue)
|
||||||
RegisterGlobalOption("key_type", parseOptSingleString)
|
RegisterGlobalOption("key_type", parseOptSingleString)
|
||||||
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
|
||||||
|
RegisterGlobalOption("servers", parseServerOptions)
|
||||||
|
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
||||||
|
RegisterGlobalOption("log", parseLogOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -62,7 +68,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
|
||||||
@@ -78,7 +84,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() {
|
||||||
@@ -154,35 +160,60 @@ 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) {
|
||||||
eab := new(caddytls.ExternalAccountBinding)
|
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)
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
@@ -195,11 +226,11 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
eab.KeyID = d.Val()
|
eab.KeyID = d.Val()
|
||||||
|
|
||||||
case "hmac":
|
case "mac_key":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
eab.HMAC = d.Val()
|
eab.MACKey = d.Val()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||||
@@ -209,7 +240,30 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return eab, nil
|
return eab, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptCertIssuer(d *caddyfile.Dispenser, existing interface{}) (interface{}, error) {
|
||||||
|
var issuers []certmagic.Issuer
|
||||||
|
if existing != nil {
|
||||||
|
issuers = existing.([]certmagic.Issuer)
|
||||||
|
}
|
||||||
|
for d.Next() { // consume option name
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return issuers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -221,21 +275,43 @@ func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptAdmin(d *caddyfile.Dispenser) (interface{}, error) {
|
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
if d.Next() {
|
adminCfg := new(caddy.AdminConfig)
|
||||||
var listenAddress string
|
for d.Next() {
|
||||||
if !d.AllArgs(&listenAddress) {
|
if d.NextArg() {
|
||||||
return "", d.ArgErr()
|
listenAddress := d.Val()
|
||||||
|
if listenAddress == "off" {
|
||||||
|
adminCfg.Disabled = true
|
||||||
|
if d.Next() { // Do not accept any remaining options including block
|
||||||
|
return nil, d.Err("No more option is allowed after turning off admin config")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
adminCfg.Listen = listenAddress
|
||||||
|
if d.NextArg() { // At most 1 arg is allowed
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if listenAddress == "" {
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
listenAddress = caddy.DefaultAdminListen
|
switch d.Val() {
|
||||||
|
case "enforce_origin":
|
||||||
|
adminCfg.EnforceOrigin = true
|
||||||
|
|
||||||
|
case "origins":
|
||||||
|
adminCfg.Origins = d.RemainingArgs()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return listenAddress, nil
|
|
||||||
}
|
}
|
||||||
return "", nil
|
if adminCfg.Listen == "" && !adminCfg.Disabled {
|
||||||
|
adminCfg.Listen = caddy.DefaultAdminListen
|
||||||
|
}
|
||||||
|
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() {
|
||||||
@@ -295,7 +371,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()
|
||||||
@@ -304,8 +380,74 @@ 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 != "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' or 'ignore_loaded_certs'")
|
||||||
}
|
}
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseServerOptions(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,41 @@
|
|||||||
|
// 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/modules/caddypki"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (st ServerType) buildPKIApp(
|
||||||
|
pairings []sbAddrAssociation,
|
||||||
|
options map[string]interface{},
|
||||||
|
warnings []caddyconfig.Warning,
|
||||||
|
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||||
|
|
||||||
|
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||||
|
|
||||||
|
for _, p := range pairings {
|
||||||
|
for _, sblock := range p.serverBlocks {
|
||||||
|
// find all the CAs that were defined and add them to the app config
|
||||||
|
for _, caCfgValue := range sblock.pile["pki.ca"] {
|
||||||
|
ca := caCfgValue.Value.(*caddypki.CA)
|
||||||
|
pkiApp.CAs[ca.ID] = ca
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkiApp, warnings, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
// 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 (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serverOptions collects server config overrides parsed from Caddyfile global options
|
||||||
|
type serverOptions struct {
|
||||||
|
// If set, will only apply these options to servers that contain a
|
||||||
|
// listener address that matches exactly. If empty, will apply to all
|
||||||
|
// servers that were not already matched by another serverOptions.
|
||||||
|
ListenerAddress string
|
||||||
|
|
||||||
|
// These will all map 1:1 to the caddyhttp.Server struct
|
||||||
|
ListenerWrappersRaw []json.RawMessage
|
||||||
|
ReadTimeout caddy.Duration
|
||||||
|
ReadHeaderTimeout caddy.Duration
|
||||||
|
WriteTimeout caddy.Duration
|
||||||
|
IdleTimeout caddy.Duration
|
||||||
|
MaxHeaderBytes int
|
||||||
|
AllowH2C bool
|
||||||
|
ExperimentalHTTP3 bool
|
||||||
|
StrictSNIHost *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
|
||||||
|
serverOpts := serverOptions{}
|
||||||
|
for d.Next() {
|
||||||
|
if d.NextArg() {
|
||||||
|
serverOpts.ListenerAddress = d.Val()
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "listener_wrappers":
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
modID := "caddy.listeners." + d.Val()
|
||||||
|
unm, err := caddyfile.UnmarshalModule(d, modID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
|
||||||
|
}
|
||||||
|
jsonListenerWrapper := caddyconfig.JSONModuleObject(
|
||||||
|
listenerWrapper,
|
||||||
|
"wrapper",
|
||||||
|
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "timeouts":
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "read_body":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing read_body timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.ReadTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "read_header":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing read_header timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "write":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing write timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.WriteTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
case "idle":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
dur, err := caddy.ParseDuration(d.Val())
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing idle timeout duration: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.IdleTimeout = caddy.Duration(dur)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "max_header_size":
|
||||||
|
var sizeStr string
|
||||||
|
if !d.AllArgs(&sizeStr) {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
size, err := humanize.ParseBytes(sizeStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, d.Errf("parsing max_header_size: %v", err)
|
||||||
|
}
|
||||||
|
serverOpts.MaxHeaderBytes = int(size)
|
||||||
|
|
||||||
|
case "protocol":
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "allow_h2c":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.AllowH2C = true
|
||||||
|
|
||||||
|
case "experimental_http3":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.ExperimentalHTTP3 = true
|
||||||
|
|
||||||
|
case "strict_sni_host":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
trueBool := true
|
||||||
|
serverOpts.StrictSNIHost = &trueBool
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverOpts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyServerOptions sets the server options on the appropriate servers
|
||||||
|
func applyServerOptions(
|
||||||
|
servers map[string]*caddyhttp.Server,
|
||||||
|
options map[string]interface{},
|
||||||
|
warnings *[]caddyconfig.Warning,
|
||||||
|
) error {
|
||||||
|
// If experimental HTTP/3 is enabled, enable it on each server.
|
||||||
|
// We already know there won't be a conflict with serverOptions because
|
||||||
|
// we validated earlier that "experimental_http3" cannot be set at the same
|
||||||
|
// time as "servers"
|
||||||
|
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
|
||||||
|
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
|
||||||
|
for _, srv := range servers {
|
||||||
|
srv.ExperimentalHTTP3 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverOpts, ok := options["servers"].([]serverOptions)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
// find the options that apply to this server
|
||||||
|
opts := func() *serverOptions {
|
||||||
|
for _, entry := range serverOpts {
|
||||||
|
if entry.ListenerAddress == "" {
|
||||||
|
return &entry
|
||||||
|
}
|
||||||
|
for _, listener := range server.Listen {
|
||||||
|
if entry.ListenerAddress == listener {
|
||||||
|
return &entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
// if none apply, then move to the next server
|
||||||
|
if opts == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// set all the options
|
||||||
|
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
|
||||||
|
server.ReadTimeout = opts.ReadTimeout
|
||||||
|
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
|
||||||
|
server.WriteTimeout = opts.WriteTimeout
|
||||||
|
server.IdleTimeout = opts.IdleTimeout
|
||||||
|
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||||
|
server.AllowH2C = opts.AllowH2C
|
||||||
|
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
|
||||||
|
server.StrictSNIHost = opts.StrictSNIHost
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+337
-200
@@ -21,12 +21,14 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (st ServerType) buildTLSApp(
|
func (st ServerType) buildTLSApp(
|
||||||
@@ -38,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)
|
||||||
@@ -48,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 {
|
||||||
@@ -64,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
|
||||||
@@ -74,163 +80,158 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a catch-all automation policy is used as a "default" for all subjects that
|
||||||
|
// don't have custom configuration explicitly associated with them; this
|
||||||
|
// is only to add if the global settings or defaults are non-empty
|
||||||
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
|
catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
if catchAllAP != nil {
|
||||||
|
if tlsApp.Automation == nil {
|
||||||
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
|
}
|
||||||
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
// get values that populate an automation policy for this block
|
// get values that populate an automation policy for this block
|
||||||
var ap *caddytls.AutomationPolicy
|
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, warnings, err
|
||||||
|
}
|
||||||
|
|
||||||
sblockHosts := sblock.hostsFromKeys(false)
|
sblockHosts := sblock.hostsFromKeys(false)
|
||||||
if len(sblockHosts) == 0 {
|
if len(sblockHosts) == 0 && catchAllAP != nil {
|
||||||
ap = catchAllAP
|
ap = catchAllAP
|
||||||
}
|
}
|
||||||
|
|
||||||
// on-demand tls
|
// on-demand tls
|
||||||
if _, ok := sblock.pile["tls.on_demand"]; ok {
|
if _, ok := sblock.pile["tls.on_demand"]; ok {
|
||||||
if ap == nil {
|
|
||||||
var err error
|
|
||||||
ap, err = newBaseAutomationPolicy(options, warnings, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, warnings, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
||||||
for _, issuerVal := range issuerVals {
|
for _, issuerVal := range issuerVals {
|
||||||
issuer := issuerVal.Value.(certmagic.Issuer)
|
issuers = append(issuers, issuerVal.Value.(certmagic.Issuer))
|
||||||
if ap == nil {
|
|
||||||
var err error
|
|
||||||
ap, err = newBaseAutomationPolicy(options, warnings, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, warnings, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ap == catchAllAP && !reflect.DeepEqual(ap.Issuer, issuer) {
|
|
||||||
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.Issuer, issuer)
|
|
||||||
}
|
|
||||||
ap.Issuer = issuer
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
ap.Issuers = issuers
|
||||||
}
|
}
|
||||||
|
|
||||||
// custom bind host
|
// custom bind host
|
||||||
for _, cfgVal := range sblock.pile["bind"] {
|
for _, cfgVal := range sblock.pile["bind"] {
|
||||||
// either an existing issuer is already configured (and thus, ap is not
|
for _, iss := range ap.Issuers {
|
||||||
// nil), or we need to configure an issuer, so we need ap to be non-nil
|
// if an issuer was already configured and it is NOT an ACME issuer,
|
||||||
if ap == nil {
|
// skip, since we intend to adjust only ACME issuers; ensure we
|
||||||
ap, err = newBaseAutomationPolicy(options, warnings, true)
|
// include any issuer that embeds/wraps an underlying ACME issuer
|
||||||
if err != nil {
|
var acmeIssuer *caddytls.ACMEIssuer
|
||||||
return nil, warnings, err
|
if acmeWrapper, ok := iss.(acmeCapable); ok {
|
||||||
|
acmeIssuer = acmeWrapper.GetACMEIssuer()
|
||||||
|
}
|
||||||
|
if acmeIssuer == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if an issuer was already configured and it is NOT an ACME
|
// proceed to configure the ACME issuer's bind host, without
|
||||||
// issuer, skip, since we intend to adjust only ACME issuers
|
// overwriting any existing settings
|
||||||
var acmeIssuer *caddytls.ACMEIssuer
|
if acmeIssuer.Challenges == nil {
|
||||||
if ap.Issuer != nil {
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||||
var ok bool
|
}
|
||||||
if acmeIssuer, ok = ap.Issuer.(*caddytls.ACMEIssuer); !ok {
|
if acmeIssuer.Challenges.BindHost == "" {
|
||||||
break
|
// only binding to one host is supported
|
||||||
|
var bindHost string
|
||||||
|
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
|
||||||
|
bindHost = bindHosts[0]
|
||||||
|
}
|
||||||
|
acmeIssuer.Challenges.BindHost = bindHost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// proceed to configure the ACME issuer's bind host, without
|
|
||||||
// overwriting any existing settings
|
|
||||||
if acmeIssuer == nil {
|
|
||||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
|
||||||
}
|
|
||||||
if acmeIssuer.Challenges == nil {
|
|
||||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
|
||||||
}
|
|
||||||
if acmeIssuer.Challenges.BindHost == "" {
|
|
||||||
// only binding to one host is supported
|
|
||||||
var bindHost string
|
|
||||||
if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
|
|
||||||
bindHost = bindHosts[0]
|
|
||||||
}
|
|
||||||
acmeIssuer.Challenges.BindHost = bindHost
|
|
||||||
}
|
|
||||||
ap.Issuer = acmeIssuer // we'll encode it later
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ap != nil {
|
// first make sure this block is allowed to create an automation policy;
|
||||||
if ap.Issuer != nil {
|
// doing so is forbidden if it has a key with no host (i.e. ":443")
|
||||||
// encode issuer now that it's all set up
|
// and if there is a different server block that also has a key with no
|
||||||
issuerName := ap.Issuer.(caddy.Module).CaddyModule().ID.Name()
|
// host -- since a key with no host matches any host, we need its
|
||||||
ap.IssuerRaw = caddyconfig.JSONModuleObject(ap.Issuer, "module", issuerName, &warnings)
|
// associated automation policy to have an empty Subjects list, i.e. no
|
||||||
|
// host filter, which is indistinguishable between the two server blocks
|
||||||
|
// because automation is not done in the context of a particular server...
|
||||||
|
// this is an example of a poor mapping from Caddyfile to JSON but that's
|
||||||
|
// the least-leaky abstraction I could figure out
|
||||||
|
if len(sblockHosts) == 0 {
|
||||||
|
if serverBlocksWithTLSHostlessKey > 1 {
|
||||||
|
// this server block and at least one other has a key with no host,
|
||||||
|
// making the two indistinguishable; it is misleading to define such
|
||||||
|
// a policy within one server block since it actually will apply to
|
||||||
|
// others as well
|
||||||
|
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
|
||||||
}
|
}
|
||||||
|
if catchAllAP == nil {
|
||||||
|
// this server block has a key with no hosts, but there is not yet
|
||||||
|
// a catch-all automation policy (probably because no global options
|
||||||
|
// were set), so this one becomes it
|
||||||
|
catchAllAP = ap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// first make sure this block is allowed to create an automation policy;
|
// associate our new automation policy with this server block's hosts
|
||||||
// doing so is forbidden if it has a key with no host (i.e. ":443")
|
ap.Subjects = sblockHosts
|
||||||
// and if there is a different server block that also has a key with no
|
sort.Strings(ap.Subjects) // solely for deterministic test results
|
||||||
// host -- since a key with no host matches any host, we need its
|
|
||||||
// associated automation policy to have an empty Subjects list, i.e. no
|
// if a combination of public and internal names were given
|
||||||
// host filter, which is indistinguishable between the two server blocks
|
// for this same server block and no issuer was specified, we
|
||||||
// because automation is not done in the context of a particular server...
|
// need to separate them out in the automation policies so
|
||||||
// this is an example of a poor mapping from Caddyfile to JSON but that's
|
// that the internal names can use the internal issuer and
|
||||||
// the least-leaky abstraction I could figure out
|
// the other names can use the default/public/ACME issuer
|
||||||
if len(sblockHosts) == 0 {
|
var ap2 *caddytls.AutomationPolicy
|
||||||
if serverBlocksWithTLSHostlessKey > 1 {
|
if len(ap.Issuers) == 0 {
|
||||||
// this server block and at least one other has a key with no host,
|
var internal, external []string
|
||||||
// making the two indistinguishable; it is misleading to define such
|
for _, s := range ap.Subjects {
|
||||||
// a policy within one server block since it actually will apply to
|
if !certmagic.SubjectQualifiesForCert(s) {
|
||||||
// others as well
|
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
|
||||||
return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other TLS-enabled server block addresses lacking a host")
|
|
||||||
}
|
}
|
||||||
if catchAllAP == nil {
|
// we don't use certmagic.SubjectQualifiesForPublicCert() because of one nuance:
|
||||||
// this server block has a key with no hosts, but there is not yet
|
// names like *.*.tld that may not qualify for a public certificate are actually
|
||||||
// a catch-all automation policy (probably because no global options
|
// fine when used with OnDemand, since OnDemand (currently) does not obtain
|
||||||
// were set), so this one becomes it
|
// wildcards (if it ever does, there will be a separate config option to enable
|
||||||
catchAllAP = ap
|
// 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
|
||||||
|
// regular certificates on-demand for a class of certs like *.*.tld.
|
||||||
|
if !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) {
|
||||||
|
external = append(external, s)
|
||||||
|
} else {
|
||||||
|
internal = append(internal, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(external) > 0 && len(internal) > 0 {
|
||||||
// associate our new automation policy with this server block's hosts,
|
ap.Subjects = external
|
||||||
// unless, of course, the server block has a key with no hosts, in which
|
apCopy := *ap
|
||||||
// case its automation policy becomes or blends with the default/global
|
ap2 = &apCopy
|
||||||
// automation policy because, of necessity, it applies to all hostnames
|
ap2.Subjects = internal
|
||||||
// (i.e. it has no Subjects filter) -- in that case, we'll append it last
|
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
|
||||||
if ap != catchAllAP {
|
|
||||||
ap.Subjects = sblockHosts
|
|
||||||
|
|
||||||
// if a combination of public and internal names were given
|
|
||||||
// for this same server block and no issuer was specified, we
|
|
||||||
// need to separate them out in the automation policies so
|
|
||||||
// that the internal names can use the internal issuer and
|
|
||||||
// the other names can use the default/public/ACME issuer
|
|
||||||
var ap2 *caddytls.AutomationPolicy
|
|
||||||
if ap.Issuer == nil {
|
|
||||||
var internal, external []string
|
|
||||||
for _, s := range ap.Subjects {
|
|
||||||
if certmagic.SubjectQualifiesForPublicCert(s) {
|
|
||||||
external = append(external, s)
|
|
||||||
} else {
|
|
||||||
internal = append(internal, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(external) > 0 && len(internal) > 0 {
|
|
||||||
ap.Subjects = external
|
|
||||||
apCopy := *ap
|
|
||||||
ap2 = &apCopy
|
|
||||||
ap2.Subjects = internal
|
|
||||||
ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tlsApp.Automation == nil {
|
|
||||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
|
||||||
}
|
|
||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
|
|
||||||
if ap2 != nil {
|
|
||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if tlsApp.Automation == nil {
|
||||||
|
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||||
|
}
|
||||||
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
|
||||||
|
if ap2 != nil {
|
||||||
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
|
||||||
|
}
|
||||||
|
|
||||||
// certificate loaders
|
// certificate loaders
|
||||||
if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
|
if clVals, ok := sblock.pile["tls.cert_loader"]; ok {
|
||||||
@@ -277,6 +278,14 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -286,9 +295,9 @@ func (st ServerType) buildTLSApp(
|
|||||||
// get internal certificates by default rather than ACME
|
// get internal certificates by default rather than ACME
|
||||||
var al caddytls.AutomateLoader
|
var al caddytls.AutomateLoader
|
||||||
internalAP := &caddytls.AutomationPolicy{
|
internalAP := &caddytls.AutomationPolicy{
|
||||||
IssuerRaw: 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)
|
||||||
@@ -304,23 +313,56 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, internalAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is a global/catch-all automation policy, ensure it goes last
|
// if there are any global options set for issuers (ACME ones in particular), make sure they
|
||||||
if catchAllAP != nil {
|
// take effect in every automation policy that does not have any issuers
|
||||||
// first, encode its issuer, if there is one
|
if tlsApp.Automation != nil {
|
||||||
if catchAllAP.Issuer != nil {
|
globalEmail := options["email"]
|
||||||
issuerName := catchAllAP.Issuer.(caddy.Module).CaddyModule().ID.Name()
|
globalACMECA := options["acme_ca"]
|
||||||
catchAllAP.IssuerRaw = caddyconfig.JSONModuleObject(catchAllAP.Issuer, "module", issuerName, &warnings)
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
}
|
globalACMEDNS := options["acme_dns"]
|
||||||
|
globalACMEEAB := options["acme_eab"]
|
||||||
|
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil
|
||||||
|
if hasGlobalACMEDefaults {
|
||||||
|
for _, ap := range tlsApp.Automation.Policies {
|
||||||
|
if len(ap.Issuers) == 0 {
|
||||||
|
ap.Issuers = caddytls.DefaultIssuers()
|
||||||
|
|
||||||
// then append it to the end of the policies list
|
// if a specific endpoint is configured, can't use multiple default issuers
|
||||||
if tlsApp.Automation == nil {
|
if globalACMECA != nil {
|
||||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
if strings.Contains(globalACMECA.(string), "zerossl") {
|
||||||
|
ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
|
||||||
|
} else {
|
||||||
|
ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do a little verification & cleanup
|
// finalize and verify policies; do cleanup
|
||||||
if tlsApp.Automation != nil {
|
if tlsApp.Automation != nil {
|
||||||
|
for i, ap := range tlsApp.Automation.Policies {
|
||||||
|
// ensure all issuers have global defaults filled in
|
||||||
|
for j, issuer := range ap.Issuers {
|
||||||
|
err := fillInGlobalACMEDefaults(issuer, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, warnings, fmt.Errorf("filling in global issuer defaults for AP %d, issuer %d: %v", i, j, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode all issuer values we created, so they will be rendered in the output
|
||||||
|
if len(ap.Issuers) > 0 && ap.IssuersRaw == nil {
|
||||||
|
for _, iss := range ap.Issuers {
|
||||||
|
issuerName := iss.(caddy.Module).CaddyModule().ID.Name()
|
||||||
|
ap.IssuersRaw = append(ap.IssuersRaw, caddyconfig.JSONModuleObject(iss, "module", issuerName, &warnings))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consolidate automation policies that are the exact same
|
||||||
|
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
|
||||||
|
|
||||||
// ensure automation policies don't overlap subjects (this should be
|
// ensure automation policies don't overlap subjects (this should be
|
||||||
// an error at provision-time as well, but catch it in the adapt phase
|
// an error at provision-time as well, but catch it in the adapt phase
|
||||||
// for convenience)
|
// for convenience)
|
||||||
@@ -334,29 +376,70 @@ func (st ServerType) buildTLSApp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// consolidate automation policies that are the exact same
|
// if nothing remains, remove any excess values to clean up the resulting config
|
||||||
tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
|
if len(tlsApp.Automation.Policies) == 0 {
|
||||||
|
tlsApp.Automation.Policies = nil
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(tlsApp.Automation, new(caddytls.AutomationConfig)) {
|
||||||
|
tlsApp.Automation = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tlsApp, warnings, nil
|
return tlsApp, warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
|
||||||
|
|
||||||
|
func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interface{}) error {
|
||||||
|
acmeWrapper, ok := issuer.(acmeCapable)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
acmeIssuer := acmeWrapper.GetACMEIssuer()
|
||||||
|
if acmeIssuer == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
globalEmail := options["email"]
|
||||||
|
globalACMECA := options["acme_ca"]
|
||||||
|
globalACMECARoot := options["acme_ca_root"]
|
||||||
|
globalACMEDNS := options["acme_dns"]
|
||||||
|
globalACMEEAB := options["acme_eab"]
|
||||||
|
|
||||||
|
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||||
|
acmeIssuer.Email = globalEmail.(string)
|
||||||
|
}
|
||||||
|
if globalACMECA != nil && acmeIssuer.CA == "" {
|
||||||
|
acmeIssuer.CA = globalACMECA.(string)
|
||||||
|
}
|
||||||
|
if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
|
||||||
|
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
|
||||||
|
}
|
||||||
|
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
|
||||||
|
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
|
||||||
|
DNS: &caddytls.DNSChallengeConfig{
|
||||||
|
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
||||||
|
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||||
// its values from the global options map. It should be used as the base
|
// its values from the global options map. It should be used as the base
|
||||||
// for any other automation policies. A nil policy (and no error) will be
|
// for any other automation policies. A nil policy (and no error) will be
|
||||||
// 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) {
|
||||||
acmeCA, hasACMECA := options["acme_ca"]
|
issuers, hasIssuers := options["cert_issuer"]
|
||||||
acmeCARoot, hasACMECARoot := options["acme_ca_root"]
|
_, hasLocalCerts := options["local_certs"]
|
||||||
acmeDNS, hasACMEDNS := options["acme_dns"]
|
|
||||||
acmeEAB, hasACMEEAB := options["acme_eab"]
|
|
||||||
|
|
||||||
email, hasEmail := options["email"]
|
|
||||||
localCerts, hasLocalCerts := options["local_certs"]
|
|
||||||
keyType, hasKeyType := options["key_type"]
|
keyType, hasKeyType := options["key_type"]
|
||||||
|
ocspStapling, hasOCSPStapling := options["ocsp_stapling"]
|
||||||
|
|
||||||
hasGlobalAutomationOpts := hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || 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
|
||||||
@@ -368,43 +451,24 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
|
|||||||
}
|
}
|
||||||
|
|
||||||
ap := new(caddytls.AutomationPolicy)
|
ap := new(caddytls.AutomationPolicy)
|
||||||
|
if hasKeyType {
|
||||||
|
ap.KeyType = keyType.(string)
|
||||||
|
}
|
||||||
|
|
||||||
if localCerts != nil {
|
if hasIssuers && hasLocalCerts {
|
||||||
// internal issuer enabled trumps any ACME configurations; useful in testing
|
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")
|
||||||
ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later
|
}
|
||||||
} else {
|
|
||||||
if acmeCA == nil {
|
if hasIssuers {
|
||||||
acmeCA = ""
|
ap.Issuers = issuers.([]certmagic.Issuer)
|
||||||
}
|
} else if hasLocalCerts {
|
||||||
if email == nil {
|
ap.Issuers = []certmagic.Issuer{new(caddytls.InternalIssuer)}
|
||||||
email = ""
|
}
|
||||||
}
|
|
||||||
mgr := &caddytls.ACMEIssuer{
|
if hasOCSPStapling {
|
||||||
CA: acmeCA.(string),
|
ocspConfig := ocspStapling.(certmagic.OCSPConfig)
|
||||||
Email: email.(string),
|
ap.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||||
}
|
ap.OCSPOverrides = ocspConfig.ResponderOverrides
|
||||||
if acmeDNS != nil {
|
|
||||||
provName := acmeDNS.(string)
|
|
||||||
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
|
|
||||||
}
|
|
||||||
mgr.Challenges = &caddytls.ChallengesConfig{
|
|
||||||
DNS: &caddytls.DNSChallengeConfig{
|
|
||||||
ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, &warnings),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if acmeCARoot != nil {
|
|
||||||
mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
|
|
||||||
}
|
|
||||||
if acmeEAB != nil {
|
|
||||||
mgr.ExternalAccount = acmeEAB.(*caddytls.ExternalAccountBinding)
|
|
||||||
}
|
|
||||||
if keyType != nil {
|
|
||||||
ap.KeyType = keyType.(string)
|
|
||||||
}
|
|
||||||
ap.Issuer = mgr // we'll encode it later
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ap, nil
|
return ap, nil
|
||||||
@@ -413,17 +477,43 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
|
|||||||
// consolidateAutomationPolicies combines automation policies that are the same,
|
// consolidateAutomationPolicies combines automation policies that are the same,
|
||||||
// for a cleaner overall output.
|
// for a cleaner overall output.
|
||||||
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
|
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
|
||||||
for i := 0; i < len(aps); i++ {
|
// sort from most specific to least specific; we depend on this ordering
|
||||||
for j := 0; j < len(aps); j++ {
|
sort.SliceStable(aps, func(i, j int) bool {
|
||||||
if j == i {
|
if automationPolicyIsSubset(aps[i], aps[j]) {
|
||||||
continue
|
return true
|
||||||
}
|
}
|
||||||
|
if automationPolicyIsSubset(aps[j], aps[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
||||||
|
})
|
||||||
|
|
||||||
|
emptyAPCount := 0
|
||||||
|
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||||
|
emptyAP := new(caddytls.AutomationPolicy)
|
||||||
|
for i := 0; i < len(aps); i++ {
|
||||||
|
emptyAP.Subjects = aps[i].Subjects
|
||||||
|
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||||
|
emptyAPCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If all policies are empty, we can return nil, as there is no need to set any policy
|
||||||
|
if emptyAPCount == len(aps) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove or combine duplicate policies
|
||||||
|
outer:
|
||||||
|
for i := 0; i < len(aps); i++ {
|
||||||
|
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
|
||||||
|
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
|
||||||
@@ -432,30 +522,77 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
// otherwise the one without any subjects (a catch-all) would be
|
// otherwise the one without any subjects (a catch-all) would be
|
||||||
// eaten up by the one with subjects; and if both have subjects, we
|
// eaten up by the one with subjects; and if both have subjects, we
|
||||||
// need to combine their lists
|
// need to combine their lists
|
||||||
if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
|
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||||
aps[i].MustStaple == aps[j].MustStaple &&
|
aps[i].MustStaple == aps[j].MustStaple &&
|
||||||
aps[i].KeyType == aps[j].KeyType &&
|
aps[i].KeyType == aps[j].KeyType &&
|
||||||
aps[i].OnDemand == aps[j].OnDemand &&
|
aps[i].OnDemand == aps[j].OnDemand &&
|
||||||
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
|
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
|
||||||
if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 {
|
if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
|
||||||
aps = append(aps[:j], aps[j+1:]...)
|
// later policy (at j) has no subjects ("catch-all"), so we can
|
||||||
} else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
|
// remove the identical-but-more-specific policy that comes first
|
||||||
aps = append(aps[:i], aps[i+1:]...)
|
// AS LONG AS it is not shadowed by another policy before it; e.g.
|
||||||
|
// if policy i is for example.com, policy i+1 is '*.com', and policy
|
||||||
|
// j is catch-all, we cannot remove policy i because that would
|
||||||
|
// cause example.com to be served by the less specific policy for
|
||||||
|
// '*.com', which might be different (yes we've seen this happen)
|
||||||
|
if automationPolicyShadows(i, aps) >= j {
|
||||||
|
aps = append(aps[:i], aps[i+1:]...)
|
||||||
|
i--
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...)
|
// avoid repeated subjects
|
||||||
|
for _, subj := range aps[j].Subjects {
|
||||||
|
if !sliceContains(aps[i].Subjects, subj) {
|
||||||
|
aps[i].Subjects = append(aps[i].Subjects, subj)
|
||||||
|
}
|
||||||
|
}
|
||||||
aps = append(aps[:j], aps[j+1:]...)
|
aps = append(aps[:j], aps[j+1:]...)
|
||||||
|
j--
|
||||||
}
|
}
|
||||||
i--
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure any catch-all policies go last
|
|
||||||
sort.SliceStable(aps, func(i, j int) bool {
|
|
||||||
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
|
||||||
})
|
|
||||||
|
|
||||||
return aps
|
return aps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automationPolicyIsSubset returns true if a's subjects are a subset
|
||||||
|
// of b's subjects.
|
||||||
|
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
|
||||||
|
if len(b.Subjects) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(a.Subjects) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, aSubj := range a.Subjects {
|
||||||
|
var inSuperset bool
|
||||||
|
for _, bSubj := range b.Subjects {
|
||||||
|
if certmagic.MatchWildcard(aSubj, bSubj) {
|
||||||
|
inSuperset = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inSuperset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// automationPolicyShadows returns the index of a policy that aps[i] shadows;
|
||||||
|
// in other words, for all policies after position i, if that policy covers
|
||||||
|
// the same subjects but is less specific, that policy's position is returned,
|
||||||
|
// or -1 if no shadowing is found. For example, if policy i is for
|
||||||
|
// "foo.example.com" and policy i+2 is for "*.example.com", then i+2 will be
|
||||||
|
// returned, since that policy is shadowed by i, which is in front.
|
||||||
|
func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
|
||||||
|
for j := i + 1; j < len(aps); j++ {
|
||||||
|
if automationPolicyIsSubset(aps[i], aps[j]) {
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package httpcaddyfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAutomationPolicyIsSubset(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
a, b []string
|
||||||
|
expect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
a: []string{"example.com"},
|
||||||
|
b: []string{},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: []string{},
|
||||||
|
b: []string{"example.com"},
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: []string{"foo.example.com"},
|
||||||
|
b: []string{"*.example.com"},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: []string{"foo.example.com"},
|
||||||
|
b: []string{"foo.example.com"},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: []string{"foo.example.com"},
|
||||||
|
b: []string{"example.com"},
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: []string{"example.com", "foo.example.com"},
|
||||||
|
b: []string{"*.com", "*.*.com"},
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
a: []string{"example.com", "foo.example.com"},
|
||||||
|
b: []string{"*.com"},
|
||||||
|
expect: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
apA := &caddytls.AutomationPolicy{Subjects: test.a}
|
||||||
|
apB := &caddytls.AutomationPolicy{Subjects: test.b}
|
||||||
|
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
|
||||||
|
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"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) {
|
||||||
|
client, err := hl.makeClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
method := hl.Method
|
||||||
|
if method == "" {
|
||||||
|
method = http.MethodGet
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, hl.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header = hl.Headers
|
||||||
|
|
||||||
|
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 := ioutil.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 := ioutil.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)
|
||||||
|
|||||||
+82
-34
@@ -14,6 +14,7 @@ import (
|
|||||||
"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,10 +129,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, _ := ioutil.ReadAll(res.Body)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
json.Indent(&out, body, "", " ")
|
_ = json.Indent(&out, body, "", " ")
|
||||||
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
|
tc.t.Logf("----------- failed with config -----------\n%s", out.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -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 := ioutil.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 {
|
||||||
@@ -221,10 +257,11 @@ func isCaddyAdminRunning() error {
|
|||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: Default.LoadRequestTimeout,
|
Timeout: Default.LoadRequestTimeout,
|
||||||
}
|
}
|
||||||
_, 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()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -272,7 +309,7 @@ func CreateTestingTransport() *http.Transport {
|
|||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
TLSHandshakeTimeout: 5 * time.Second,
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,16 +350,20 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
|
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got error: %s", requestURI, expectedToLocation, err)
|
||||||
}
|
}
|
||||||
|
if loc == nil && expectedToLocation != "" {
|
||||||
if expectedToLocation != loc.String() {
|
tc.t.Errorf("requesting \"%s\" expected a Location header, but didn't get one", requestURI)
|
||||||
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
|
}
|
||||||
|
if loc != nil {
|
||||||
|
if expectedToLocation != loc.String() {
|
||||||
|
tc.t.Errorf("requesting \"%s\" expected location: \"%s\" but got \"%s\"", requestURI, expectedToLocation, loc.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -331,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 {
|
||||||
@@ -339,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,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()
|
||||||
}
|
}
|
||||||
@@ -430,7 +478,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
|||||||
|
|
||||||
body := string(bytes)
|
body := string(bytes)
|
||||||
|
|
||||||
if !strings.Contains(body, expectedBody) {
|
if body != expectedBody {
|
||||||
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
|
tc.t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
}
|
||||||
|
localhost
|
||||||
|
respond "Yahaha! You found me!"
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPtoHTTPSRedirectsExplicitPortSameAsHTTPSPort(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
}
|
||||||
|
localhost:9443
|
||||||
|
respond "Yahaha! You found me!"
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
}
|
||||||
|
localhost:1234
|
||||||
|
respond "Yahaha! You found me!"
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 9080,
|
||||||
|
"https_port": 9443,
|
||||||
|
"servers": {
|
||||||
|
"ingress_server": {
|
||||||
|
"listen": [
|
||||||
|
":9080",
|
||||||
|
":9443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": ["localhost"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "json")
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -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,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,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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"http": {
|
"http": {
|
||||||
"http_port": 8080,
|
"http_port": 8080,
|
||||||
"https_port": 8443,
|
"https_port": 8443,
|
||||||
|
"grace_period": 5000000000,
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
@@ -54,9 +56,12 @@
|
|||||||
"automation": {
|
"automation": {
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"issuer": {
|
"issuers": [
|
||||||
"module": "internal"
|
{
|
||||||
}
|
"module": "internal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"key_type": "ed25519"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
acme_ca https://example.com
|
acme_ca https://example.com
|
||||||
acme_eab {
|
acme_eab {
|
||||||
key_id 4K2scIVbBpNd-78scadB2g
|
key_id 4K2scIVbBpNd-78scadB2g
|
||||||
hmac abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
|
mac_key abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh
|
||||||
}
|
}
|
||||||
acme_ca_root /path/to/ca.crt
|
acme_ca_root /path/to/ca.crt
|
||||||
email test@example.com
|
email test@example.com
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
interval 30s
|
interval 30s
|
||||||
burst 20
|
burst 20
|
||||||
}
|
}
|
||||||
|
storage_clean_interval 7d
|
||||||
|
|
||||||
key_type ed25519
|
key_type ed25519
|
||||||
}
|
}
|
||||||
@@ -57,18 +58,20 @@
|
|||||||
"automation": {
|
"automation": {
|
||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"issuer": {
|
"issuers": [
|
||||||
"ca": "https://example.com",
|
{
|
||||||
"email": "test@example.com",
|
"ca": "https://example.com",
|
||||||
"external_account": {
|
"email": "test@example.com",
|
||||||
"hmac": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh",
|
"external_account": {
|
||||||
"key_id": "4K2scIVbBpNd-78scadB2g"
|
"key_id": "4K2scIVbBpNd-78scadB2g",
|
||||||
},
|
"mac_key": "abcdefghijklmnopqrstuvwx-abcdefghijklnopqrstuvwxyz12ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh"
|
||||||
"module": "acme",
|
},
|
||||||
"trusted_roots_pem_files": [
|
"module": "acme",
|
||||||
"/path/to/ca.crt"
|
"trusted_roots_pem_files": [
|
||||||
]
|
"/path/to/ca.crt"
|
||||||
},
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"key_type": "ed25519"
|
"key_type": "ed25519"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -78,7 +81,8 @@
|
|||||||
"burst": 20
|
"burst": 20
|
||||||
},
|
},
|
||||||
"ask": "https://example.com"
|
"ask": "https://example.com"
|
||||||
}
|
},
|
||||||
|
"storage_clean_interval": 604800000000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
debug
|
||||||
|
http_port 8080
|
||||||
|
https_port 8443
|
||||||
|
default_sni localhost
|
||||||
|
order root first
|
||||||
|
storage file_system {
|
||||||
|
root /data
|
||||||
|
}
|
||||||
|
acme_ca https://example.com
|
||||||
|
acme_ca_root /path/to/ca.crt
|
||||||
|
|
||||||
|
email test@example.com
|
||||||
|
admin {
|
||||||
|
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
|
||||||
|
}
|
||||||
|
on_demand_tls {
|
||||||
|
ask https://example.com
|
||||||
|
interval 30s
|
||||||
|
burst 20
|
||||||
|
}
|
||||||
|
local_certs
|
||||||
|
key_type ed25519
|
||||||
|
}
|
||||||
|
|
||||||
|
:80
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2019",
|
||||||
|
"origins": [
|
||||||
|
"localhost:2019",
|
||||||
|
"[::1]:2019",
|
||||||
|
"127.0.0.1:2019",
|
||||||
|
"192.168.10.128"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"level": "DEBUG"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"module": "file_system",
|
||||||
|
"root": "/data"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 8080,
|
||||||
|
"https_port": 8443,
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"key_type": "ed25519"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"on_demand": {
|
||||||
|
"rate_limit": {
|
||||||
|
"interval": 30000000000,
|
||||||
|
"burst": 20
|
||||||
|
},
|
||||||
|
"ask": "https://example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,39 @@
|
|||||||
|
{
|
||||||
|
log custom-logger {
|
||||||
|
format filter {
|
||||||
|
wrap console
|
||||||
|
fields {
|
||||||
|
common_log delete
|
||||||
|
request>remote_addr ip_mask {
|
||||||
|
ipv4 24
|
||||||
|
ipv6 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"custom-logger": {
|
||||||
|
"encoder": {
|
||||||
|
"fields": {
|
||||||
|
"common_log": {
|
||||||
|
"filter": "delete"
|
||||||
|
},
|
||||||
|
"request\u003eremote_addr": {
|
||||||
|
"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,78 @@
|
|||||||
|
{
|
||||||
|
servers {
|
||||||
|
timeouts {
|
||||||
|
idle 90s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
servers :80 {
|
||||||
|
timeouts {
|
||||||
|
idle 60s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
servers :443 {
|
||||||
|
timeouts {
|
||||||
|
idle 30s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
http://bar.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
:8080 {
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"idle_timeout": 30000000000,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"foo.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"idle_timeout": 60000000000,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"bar.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv2": {
|
||||||
|
"listen": [
|
||||||
|
":8080"
|
||||||
|
],
|
||||||
|
"idle_timeout": 90000000000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
servers {
|
||||||
|
listener_wrappers {
|
||||||
|
tls
|
||||||
|
}
|
||||||
|
timeouts {
|
||||||
|
read_body 30s
|
||||||
|
read_header 30s
|
||||||
|
write 30s
|
||||||
|
idle 30s
|
||||||
|
}
|
||||||
|
max_header_size 100MB
|
||||||
|
protocol {
|
||||||
|
allow_h2c
|
||||||
|
experimental_http3
|
||||||
|
strict_sni_host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"listener_wrappers": [
|
||||||
|
{
|
||||||
|
"wrapper": "tls"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"read_timeout": 30000000000,
|
||||||
|
"read_header_timeout": 30000000000,
|
||||||
|
"write_timeout": 30000000000,
|
||||||
|
"idle_timeout": 30000000000,
|
||||||
|
"max_header_bytes": 100000000,
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"foo.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strict_sni_host": true,
|
||||||
|
"experimental_http3": true,
|
||||||
|
"allow_h2c": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,52 @@
|
|||||||
:80
|
:80
|
||||||
handle_path /api/v1/* {
|
handle_path /api/v1/* {
|
||||||
respond "API v1"
|
respond "API v1"
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":80"
|
":80"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"path": [
|
"path": [
|
||||||
"/api/v1/*"
|
"/api/v1/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"handler": "subroute",
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "rewrite",
|
"handler": "rewrite",
|
||||||
"strip_path_prefix": "/api/v1"
|
"strip_path_prefix": "/api/v1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"body": "API v1",
|
"body": "API v1",
|
||||||
"handler": "static_response"
|
"handler": "static_response"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
:80 {
|
||||||
|
handle /api/* {
|
||||||
|
respond "api"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_path /static/* {
|
||||||
|
respond "static"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
respond "handle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"group": "group3",
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/static/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"strip_path_prefix": "/static"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "static",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "group3",
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/api/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "api",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "group3",
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "handle",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
:80 {
|
||||||
|
header Denis "Ritchie"
|
||||||
|
header +Edsger "Dijkstra"
|
||||||
|
header ?John "von Neumann"
|
||||||
|
header -Wolfram
|
||||||
|
header {
|
||||||
|
Grace: "Hopper" # some users habitually suffix field names with a colon
|
||||||
|
+Ray "Solomonoff"
|
||||||
|
?Tim "Berners-Lee"
|
||||||
|
defer
|
||||||
|
}
|
||||||
|
@images path /images/*
|
||||||
|
header @images {
|
||||||
|
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/images/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"set": {
|
||||||
|
"Cache-Control": [
|
||||||
|
"public, max-age=3600, stale-while-revalidate=86400"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"set": {
|
||||||
|
"Denis": [
|
||||||
|
"Ritchie"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"add": {
|
||||||
|
"Edsger": [
|
||||||
|
"Dijkstra"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"require": {
|
||||||
|
"headers": {
|
||||||
|
"John": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"John": [
|
||||||
|
"von Neumann"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"deferred": true,
|
||||||
|
"delete": [
|
||||||
|
"Wolfram"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"add": {
|
||||||
|
"Ray": [
|
||||||
|
"Solomonoff"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"deferred": true,
|
||||||
|
"set": {
|
||||||
|
"Grace": [
|
||||||
|
"Hopper"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"require": {
|
||||||
|
"headers": {
|
||||||
|
"Tim": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"Tim": [
|
||||||
|
"Berners-Lee"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
example.com
|
example.com
|
||||||
|
|
||||||
import testdata/import_respond.txt Groot Rocket
|
import testdata/import_respond.txt Groot Rocket
|
||||||
import testdata/import_respond.txt you "the confused man"
|
import testdata/import_respond.txt you "the confused man"
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":443"
|
":443"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"host": [
|
"host": [
|
||||||
"example.com"
|
"example.com"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"handler": "subroute",
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"body": "'I am Groot', hears Rocket",
|
"body": "'I am Groot', hears Rocket",
|
||||||
"handler": "static_response"
|
"handler": "static_response"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"body": "'I am you', hears the confused man",
|
"body": "'I am you', hears the confused man",
|
||||||
"handler": "static_response"
|
"handler": "static_response"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"terminal": true
|
"terminal": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,83 +1,83 @@
|
|||||||
(logging) {
|
(logging) {
|
||||||
log {
|
log {
|
||||||
output file /var/log/caddy/{args.0}.access.log
|
output file /var/log/caddy/{args.0}.access.log
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.example.com {
|
a.example.com {
|
||||||
import logging a.example.com
|
import logging a.example.com
|
||||||
}
|
}
|
||||||
|
|
||||||
b.example.com {
|
b.example.com {
|
||||||
import logging b.example.com
|
import logging b.example.com
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"logging": {
|
"logging": {
|
||||||
"logs": {
|
"logs": {
|
||||||
"default": {
|
"default": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"http.log.access.log0",
|
"http.log.access.log0",
|
||||||
"http.log.access.log1"
|
"http.log.access.log1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"log0": {
|
"log0": {
|
||||||
"writer": {
|
"writer": {
|
||||||
"filename": "/var/log/caddy/a.example.com.access.log",
|
"filename": "/var/log/caddy/a.example.com.access.log",
|
||||||
"output": "file"
|
"output": "file"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"http.log.access.log0"
|
"http.log.access.log0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"log1": {
|
"log1": {
|
||||||
"writer": {
|
"writer": {
|
||||||
"filename": "/var/log/caddy/b.example.com.access.log",
|
"filename": "/var/log/caddy/b.example.com.access.log",
|
||||||
"output": "file"
|
"output": "file"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"http.log.access.log1"
|
"http.log.access.log1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":443"
|
":443"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"host": [
|
"host": [
|
||||||
"a.example.com"
|
"a.example.com"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"terminal": true
|
"terminal": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"host": [
|
"host": [
|
||||||
"b.example.com"
|
"b.example.com"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"terminal": true
|
"terminal": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"logs": {
|
"logs": {
|
||||||
"logger_names": {
|
"logger_names": {
|
||||||
"a.example.com": "log0",
|
"a.example.com": "log0",
|
||||||
"b.example.com": "log1"
|
"b.example.com": "log1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
http://localhost:2020 {
|
||||||
|
log
|
||||||
|
respond 200
|
||||||
|
}
|
||||||
|
|
||||||
|
:2020 {
|
||||||
|
respond 418
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":2020"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 418
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"automatic_https": {
|
||||||
|
"skip": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"logger_names": {
|
||||||
|
"localhost:2020": ""
|
||||||
|
},
|
||||||
|
"skip_unmapped_hosts": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format filter {
|
||||||
|
wrap console
|
||||||
|
fields {
|
||||||
|
request>headers>Authorization replace REDACTED
|
||||||
|
request>headers>Server delete
|
||||||
|
request>remote_addr ip_mask {
|
||||||
|
ipv4 24
|
||||||
|
ipv6 32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"exclude": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"log0": {
|
||||||
|
"writer": {
|
||||||
|
"output": "stdout"
|
||||||
|
},
|
||||||
|
"encoder": {
|
||||||
|
"fields": {
|
||||||
|
"request\u003eheaders\u003eAuthorization": {
|
||||||
|
"filter": "replace",
|
||||||
|
"value": "REDACTED"
|
||||||
|
},
|
||||||
|
"request\u003eheaders\u003eServer": {
|
||||||
|
"filter": "delete"
|
||||||
|
},
|
||||||
|
"request\u003eremote_addr": {
|
||||||
|
"filter": "ip_mask",
|
||||||
|
"ipv4_cidr": 24,
|
||||||
|
"ipv6_cidr": 32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"format": "filter",
|
||||||
|
"wrap": {
|
||||||
|
"format": "console"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"http.log.access.log0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"logs": {
|
||||||
|
"default_logger_name": "log0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,34 @@
|
|||||||
|
|
||||||
@matcher3 not method PUT
|
@matcher3 not method PUT
|
||||||
respond @matcher3 "not put"
|
respond @matcher3 "not put"
|
||||||
|
|
||||||
|
@matcher4 vars "{http.request.uri}" "/vars-matcher"
|
||||||
|
respond @matcher4 "from vars matcher"
|
||||||
|
|
||||||
|
@matcher5 vars_regexp static "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
|
||||||
|
respond @matcher5 "from vars_regexp matcher with name"
|
||||||
|
|
||||||
|
@matcher6 vars_regexp "{http.request.uri}" `\.([a-f0-9]{6})\.(css|js)$`
|
||||||
|
respond @matcher6 "from vars_regexp matcher without name"
|
||||||
|
|
||||||
|
@matcher7 {
|
||||||
|
header Foo bar
|
||||||
|
header Foo foobar
|
||||||
|
header Bar foo
|
||||||
|
}
|
||||||
|
respond @matcher7 "header matcher merging values of the same field"
|
||||||
|
|
||||||
|
@matcher8 {
|
||||||
|
query foo=bar foo=baz bar=foo
|
||||||
|
query bar=baz
|
||||||
|
}
|
||||||
|
respond @matcher8 "query matcher merging pairs with the same keys"
|
||||||
|
|
||||||
|
@matcher9 {
|
||||||
|
header !Foo
|
||||||
|
header Bar foo
|
||||||
|
}
|
||||||
|
respond @matcher9 "header matcher with null field matcher"
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
@@ -68,6 +96,117 @@
|
|||||||
"handler": "static_response"
|
"handler": "static_response"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"vars": {
|
||||||
|
"{http.request.uri}": "/vars-matcher"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "from vars matcher",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"vars_regexp": {
|
||||||
|
"{http.request.uri}": {
|
||||||
|
"name": "static",
|
||||||
|
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "from vars_regexp matcher with name",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"vars_regexp": {
|
||||||
|
"{http.request.uri}": {
|
||||||
|
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "from vars_regexp matcher without name",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"Bar": [
|
||||||
|
"foo"
|
||||||
|
],
|
||||||
|
"Foo": [
|
||||||
|
"bar",
|
||||||
|
"foobar"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "header matcher merging values of the same field",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"query": {
|
||||||
|
"bar": [
|
||||||
|
"foo",
|
||||||
|
"baz"
|
||||||
|
],
|
||||||
|
"foo": [
|
||||||
|
"bar",
|
||||||
|
"baz"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "query matcher merging pairs with the same keys",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"Bar": [
|
||||||
|
"foo"
|
||||||
|
],
|
||||||
|
"Foo": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "header matcher with null field matcher",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
:80 {
|
||||||
|
route {
|
||||||
|
# unused matchers should not panic
|
||||||
|
# see https://github.com/caddyserver/caddy/issues/3745
|
||||||
|
@matcher1 path /path1
|
||||||
|
@matcher2 path /path2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
:80 {
|
||||||
|
metrics /metrics {
|
||||||
|
disable_openmetrics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/metrics"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"disable_openmetrics": true,
|
||||||
|
"handler": "metrics"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
:80 {
|
||||||
|
metrics /metrics
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/metrics"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "metrics"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
:8886
|
||||||
|
|
||||||
|
route {
|
||||||
|
# Add trailing slash for directory requests
|
||||||
|
@canonicalPath {
|
||||||
|
file {
|
||||||
|
try_files {path}/index.php
|
||||||
|
}
|
||||||
|
not path */
|
||||||
|
}
|
||||||
|
redir @canonicalPath {path}/ 308
|
||||||
|
|
||||||
|
# If the requested file does not exist, try index files
|
||||||
|
@indexFiles {
|
||||||
|
file {
|
||||||
|
try_files {path} {path}/index.php index.php
|
||||||
|
split_path .php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rewrite @indexFiles {http.matchers.file.relative}
|
||||||
|
|
||||||
|
# Proxy PHP files to the FastCGI responder
|
||||||
|
@phpFiles {
|
||||||
|
path *.php
|
||||||
|
}
|
||||||
|
reverse_proxy @phpFiles 127.0.0.1:9000 {
|
||||||
|
transport fastcgi {
|
||||||
|
split .php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8886"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"{http.request.uri.path}/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 308
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"file": {
|
||||||
|
"try_files": [
|
||||||
|
"{http.request.uri.path}/index.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"not": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"*/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "rewrite",
|
||||||
|
"uri": "{http.matchers.file.relative}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"file": {
|
||||||
|
"split_path": [
|
||||||
|
".php"
|
||||||
|
],
|
||||||
|
"try_files": [
|
||||||
|
"{http.request.uri.path}",
|
||||||
|
"{http.request.uri.path}/index.php",
|
||||||
|
"index.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"transport": {
|
||||||
|
"protocol": "fastcgi",
|
||||||
|
"split_path": [
|
||||||
|
".php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:9000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"*.php"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
: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 off
|
index off
|
||||||
|
dial_timeout 3s
|
||||||
|
read_timeout 10s
|
||||||
|
write_timeout 20s
|
||||||
|
|
||||||
# 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
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
@@ -39,16 +42,19 @@ php_fastcgi localhost:9000 {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"transport": {
|
"transport": {
|
||||||
|
"dial_timeout": 3000000000,
|
||||||
"env": {
|
"env": {
|
||||||
"VAR1": "value1",
|
"VAR1": "value1",
|
||||||
"VAR2": "value2"
|
"VAR2": "value2"
|
||||||
},
|
},
|
||||||
"protocol": "fastcgi",
|
"protocol": "fastcgi",
|
||||||
|
"read_timeout": 10000000000,
|
||||||
"root": "/var/www",
|
"root": "/var/www",
|
||||||
"split_path": [
|
"split_path": [
|
||||||
".php",
|
".php",
|
||||||
".php5"
|
".php5"
|
||||||
]
|
],
|
||||||
|
"write_timeout": 20000000000
|
||||||
},
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{
|
{
|
||||||
@@ -63,4 +69,4 @@ php_fastcgi localhost:9000 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +1,112 @@
|
|||||||
:8884
|
:8884
|
||||||
|
|
||||||
@api host example.com
|
@api host example.com
|
||||||
php_fastcgi @api localhost:9000
|
php_fastcgi @api localhost:9000
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"apps": {
|
"apps": {
|
||||||
"http": {
|
"http": {
|
||||||
"servers": {
|
"servers": {
|
||||||
"srv0": {
|
"srv0": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":8884"
|
":8884"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"host": [
|
"host": [
|
||||||
"example.com"
|
"example.com"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"handler": "subroute",
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "static_response",
|
"handler": "static_response",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Location": [
|
"Location": [
|
||||||
"{http.request.uri.path}/"
|
"{http.request.uri.path}/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"status_code": 308
|
"status_code": 308
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"file": {
|
"file": {
|
||||||
"try_files": [
|
"try_files": [
|
||||||
"{http.request.uri.path}/index.php"
|
"{http.request.uri.path}/index.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"not": [
|
"not": [
|
||||||
{
|
{
|
||||||
"path": [
|
"path": [
|
||||||
"*/"
|
"*/"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "rewrite",
|
"handler": "rewrite",
|
||||||
"uri": "{http.matchers.file.relative}"
|
"uri": "{http.matchers.file.relative}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"file": {
|
"file": {
|
||||||
"split_path": [
|
"split_path": [
|
||||||
".php"
|
".php"
|
||||||
],
|
],
|
||||||
"try_files": [
|
"try_files": [
|
||||||
"{http.request.uri.path}",
|
"{http.request.uri.path}",
|
||||||
"{http.request.uri.path}/index.php",
|
"{http.request.uri.path}/index.php",
|
||||||
"index.php"
|
"index.php"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
"handler": "reverse_proxy",
|
"handler": "reverse_proxy",
|
||||||
"transport": {
|
"transport": {
|
||||||
"protocol": "fastcgi",
|
"protocol": "fastcgi",
|
||||||
"split_path": [
|
"split_path": [
|
||||||
".php"
|
".php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"upstreams": [
|
"upstreams": [
|
||||||
{
|
{
|
||||||
"dial": "localhost:9000"
|
"dial": "localhost:9000"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"match": [
|
"match": [
|
||||||
{
|
{
|
||||||
"path": [
|
"path": [
|
||||||
"*.php"
|
"*.php"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,113 @@
|
|||||||
|
whoami.example.com {
|
||||||
|
reverse_proxy whoami
|
||||||
|
}
|
||||||
|
|
||||||
|
app.example.com {
|
||||||
|
reverse_proxy app:80
|
||||||
|
}
|
||||||
|
unix.example.com {
|
||||||
|
reverse_proxy unix//path/to/socket
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"whoami.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "whoami:80"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"unix.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "unix//path/to/socket"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"app.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "app:80"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
localhost
|
||||||
|
|
||||||
|
request_body {
|
||||||
|
max_size 1MB
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "request_body",
|
||||||
|
"max_size": 1000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,36 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
transport fastcgi
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"transport": {
|
||||||
|
"protocol": "fastcgi"
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy h2c://localhost:8080
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"transport": {
|
||||||
|
"protocol": "http",
|
||||||
|
"versions": [
|
||||||
|
"h2c",
|
||||||
|
"2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:8080"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
@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!"
|
||||||
|
}
|
||||||
|
|
||||||
|
@changeStatus status 500
|
||||||
|
handle_response @changeStatus 400
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
500
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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,57 @@
|
|||||||
|
: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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
|
||||||
|
https://example.com {
|
||||||
|
reverse_proxy /path http://localhost:54321 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Real-IP {remote}
|
||||||
|
header_up X-Forwarded-For {remote}
|
||||||
|
header_up X-Forwarded-Port {server_port}
|
||||||
|
header_up X-Forwarded-Proto "http"
|
||||||
|
|
||||||
|
buffer_requests
|
||||||
|
|
||||||
|
transport http {
|
||||||
|
read_buffer 10MB
|
||||||
|
write_buffer 20MB
|
||||||
|
max_response_header 30MB
|
||||||
|
dial_timeout 3s
|
||||||
|
dial_fallback_delay 5s
|
||||||
|
response_header_timeout 8s
|
||||||
|
expect_continue_timeout 9s
|
||||||
|
|
||||||
|
versions h2c 2
|
||||||
|
compression off
|
||||||
|
max_conns_per_host 5
|
||||||
|
max_idle_conns_per_host 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"buffer_requests": true,
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"headers": {
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"Host": [
|
||||||
|
"{http.request.host}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-For": [
|
||||||
|
"{http.request.remote}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-Port": [
|
||||||
|
"{server_port}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-Proto": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"X-Real-Ip": [
|
||||||
|
"{http.request.remote}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"compression": false,
|
||||||
|
"dial_fallback_delay": 5000000000,
|
||||||
|
"dial_timeout": 3000000000,
|
||||||
|
"expect_continue_timeout": 9000000000,
|
||||||
|
"max_conns_per_host": 5,
|
||||||
|
"max_idle_conns_per_host": 2,
|
||||||
|
"max_response_header_size": 30000000,
|
||||||
|
"protocol": "http",
|
||||||
|
"read_buffer_size": 10000000,
|
||||||
|
"response_header_timeout": 8000000000,
|
||||||
|
"versions": [
|
||||||
|
"h2c",
|
||||||
|
"2"
|
||||||
|
],
|
||||||
|
"write_buffer_size": 20000000
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:54321"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/path"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
:8884 {
|
||||||
|
map {host} {upstream} {
|
||||||
|
foo.example.com 1.2.3.4
|
||||||
|
default 2.3.4.5
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upstream placeholder with a port should retain the port
|
||||||
|
reverse_proxy {upstream}:80
|
||||||
|
}
|
||||||
|
|
||||||
|
:8885 {
|
||||||
|
map {host} {upstream} {
|
||||||
|
foo.example.com 1.2.3.4:8080
|
||||||
|
default 2.3.4.5:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upstream placeholder with no port should not have a port joined
|
||||||
|
reverse_proxy {upstream}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"defaults": [
|
||||||
|
"2.3.4.5"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"{upstream}"
|
||||||
|
],
|
||||||
|
"handler": "map",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"input": "foo.example.com",
|
||||||
|
"outputs": [
|
||||||
|
"1.2.3.4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "{http.request.host}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "{upstream}:80"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":8885"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"defaults": [
|
||||||
|
"2.3.4.5:8080"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"{upstream}"
|
||||||
|
],
|
||||||
|
"handler": "map",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"input": "foo.example.com",
|
||||||
|
"outputs": [
|
||||||
|
"1.2.3.4:8080"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "{http.request.host}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "{upstream}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# https://caddy.community/t/caddy-suddenly-directs-my-site-to-the-wrong-directive/11597/2
|
||||||
|
abcdef {
|
||||||
|
respond "abcdef"
|
||||||
|
}
|
||||||
|
|
||||||
|
abcdefg {
|
||||||
|
respond "abcdefg"
|
||||||
|
}
|
||||||
|
|
||||||
|
abc {
|
||||||
|
respond "abc"
|
||||||
|
}
|
||||||
|
|
||||||
|
abcde, http://abcde {
|
||||||
|
respond "abcde"
|
||||||
|
}
|
||||||
|
|
||||||
|
:443, ab {
|
||||||
|
respond "443 or ab"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"abcdefg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "abcdefg",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"abcdef"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "abcdef",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"abcde"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "abcde",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"abc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "abc",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "443 or ab",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"abcde"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "abcde",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"certificates": {
|
||||||
|
"automate": [
|
||||||
|
"ab"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
:80
|
||||||
|
|
||||||
|
respond 200
|
||||||
|
|
||||||
|
@untrusted not remote_ip 10.1.1.0/24
|
||||||
|
respond @untrusted 401
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"not": [
|
||||||
|
{
|
||||||
|
"remote_ip": {
|
||||||
|
"ranges": [
|
||||||
|
"10.1.1.0/24"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 401
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
|
||||||
|
*.tld, *.*.tld {
|
||||||
|
tls {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo.tld, www.foo.tld {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"foo.tld",
|
||||||
|
"www.foo.tld"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"*.tld",
|
||||||
|
"*.*.tld"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"foo.tld",
|
||||||
|
"www.foo.tld"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"*.*.tld",
|
||||||
|
"*.tld"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"on_demand": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# issue #3953
|
||||||
|
{
|
||||||
|
cert_issuer zerossl api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
tls {
|
||||||
|
on_demand
|
||||||
|
key_type rsa2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http://example.net {
|
||||||
|
}
|
||||||
|
|
||||||
|
:1234 {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":1234"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv2": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.net"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"api_key": "api_key",
|
||||||
|
"module": "zerossl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"key_type": "rsa2048",
|
||||||
|
"on_demand": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"api_key": "api_key",
|
||||||
|
"module": "zerossl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# https://caddy.community/t/caddyfile-having-individual-sites-differ-from-global-options/11297
|
||||||
|
{
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
|
||||||
|
a.example.com {
|
||||||
|
tls internal
|
||||||
|
}
|
||||||
|
|
||||||
|
b.example.com {
|
||||||
|
tls abc@example.com
|
||||||
|
}
|
||||||
|
|
||||||
|
c.example.com {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"a.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"b.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"c.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"b.example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"email": "abc@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "abc@example.com",
|
||||||
|
"module": "zerossl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
email my.email@example.com
|
||||||
|
}
|
||||||
|
|
||||||
|
:82 {
|
||||||
|
redir https://example.com{uri}
|
||||||
|
}
|
||||||
|
|
||||||
|
:83 {
|
||||||
|
redir https://example.com{uri}
|
||||||
|
}
|
||||||
|
|
||||||
|
:84 {
|
||||||
|
redir https://example.com{uri}
|
||||||
|
}
|
||||||
|
|
||||||
|
abc.de {
|
||||||
|
redir https://example.com{uri}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"abc.de"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"https://example.com{http.request.uri}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 302
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":82"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"https://example.com{http.request.uri}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 302
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv2": {
|
||||||
|
"listen": [
|
||||||
|
":83"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"https://example.com{http.request.uri}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 302
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv3": {
|
||||||
|
"listen": [
|
||||||
|
":84"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"headers": {
|
||||||
|
"Location": [
|
||||||
|
"https://example.com{http.request.uri}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status_code": 302
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"email": "my.email@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "my.email@example.com",
|
||||||
|
"module": "zerossl"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
a.example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
b.example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
:443 {
|
||||||
|
tls {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"a.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"b.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"a.example.com",
|
||||||
|
"b.example.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"on_demand": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# (this Caddyfile is contrived, but based on issue #4161)
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
tls {
|
||||||
|
ca https://foobar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com:8443 {
|
||||||
|
tls {
|
||||||
|
ca https://foobar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com:8444 {
|
||||||
|
tls {
|
||||||
|
ca https://foobar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com:8445 {
|
||||||
|
tls {
|
||||||
|
ca https://foobar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":8443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv2": {
|
||||||
|
"listen": [
|
||||||
|
":8444"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv3": {
|
||||||
|
"listen": [
|
||||||
|
":8445"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"ca": "https://foobar",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ localhost
|
|||||||
respond "hello from localhost"
|
respond "hello from localhost"
|
||||||
tls {
|
tls {
|
||||||
client_auth {
|
client_auth {
|
||||||
mode request
|
mode request
|
||||||
trusted_ca_cert_file ../caddy.ca.cer
|
trusted_ca_cert_file ../caddy.ca.cer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ localhost
|
|||||||
respond "hello from localhost"
|
respond "hello from localhost"
|
||||||
tls {
|
tls {
|
||||||
client_auth {
|
client_auth {
|
||||||
mode request
|
mode request
|
||||||
trusted_ca_cert MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
trusted_ca_cert MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# https://github.com/caddyserver/caddy/issues/3906
|
||||||
|
a.a {
|
||||||
|
tls internal
|
||||||
|
respond 403
|
||||||
|
}
|
||||||
|
|
||||||
|
http://b.b https://b.b:8443 {
|
||||||
|
tls internal
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"a.a"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 403
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv1": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"b.b"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"srv2": {
|
||||||
|
"listen": [
|
||||||
|
":8443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"b.b"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 404
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"a.a",
|
||||||
|
"b.b"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"module": "internal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
jsonMod "encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -32,14 +35,19 @@ func TestCaddyfileAdaptToJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// split the Caddyfile (first) and JSON (second) parts
|
// split the Caddyfile (first) and JSON (second) parts
|
||||||
|
// (append newline to Caddyfile to match formatter expectations)
|
||||||
parts := strings.Split(string(data), "----------")
|
parts := strings.Split(string(data), "----------")
|
||||||
caddyfile, json := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
caddyfile, json := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
// replace windows newlines in the json with unix newlines
|
// replace windows newlines in the json with unix newlines
|
||||||
json = winNewlines.ReplaceAllString(json, "\n")
|
json = winNewlines.ReplaceAllString(json, "\n")
|
||||||
|
|
||||||
|
// replace os-specific default path for file_server's hide field
|
||||||
|
replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile"))
|
||||||
|
json = strings.ReplaceAll(json, `"./Caddyfile"`, string(replacePath))
|
||||||
|
|
||||||
// run the test
|
// run the test
|
||||||
ok := caddytest.CompareAdapt(t, caddyfile, "caddyfile", json)
|
ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", json)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("failed to adapt %s", filename)
|
t.Errorf("failed to adapt %s", filename)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBrowse(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
file_server browse
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tester.AssertResponseCode(req, 200)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user