mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
277 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11ae1aa6b8 | |||
| 3c8837163d | |||
| b6ca782c99 | |||
| a976629174 | |||
| cd66b62083 | |||
| 4b68de8418 | |||
| 008415f206 | |||
| f0eae39cb2 | |||
| 7fa90f08ae | |||
| 5ec503386c | |||
| 6f9a39525a | |||
| cf611796c6 | |||
| aadda6e34e | |||
| 6c4cb5006a | |||
| 12107f035c | |||
| 832df649c1 | |||
| cc63eca0c8 | |||
| aa94f2b802 | |||
| 3f9a431100 | |||
| 8c860641b9 | |||
| 99914d2204 | |||
| 24b2e02ee5 | |||
| be2fdb6af6 | |||
| 16b296c97e | |||
| 11eee95222 | |||
| 1de4a99ec3 | |||
| 96579b97f6 | |||
| 8cc2f770fa | |||
| a23f707268 | |||
| ed4c2775e4 | |||
| bff2469d9d | |||
| a08ab0c007 | |||
| 28e1f7c562 | |||
| 914f39d784 | |||
| 0ba427a6f4 | |||
| 7fab1b15c8 | |||
| 3856ad03b0 | |||
| d411b7d087 | |||
| 580f7677ad | |||
| 120811e7f7 | |||
| 43458bda46 | |||
| a9ccaa1ae5 | |||
| f6ee100bae | |||
| f5720fecd6 | |||
| 0b2e054839 | |||
| 6f01928512 | |||
| 6115a462c7 | |||
| 5f9cba0f19 | |||
| 05b3938556 | |||
| 62b4553f7d | |||
| ad20323b52 | |||
| 721c100bb0 | |||
| 6720bdfb55 | |||
| 0c626fbc2e | |||
| af82141808 | |||
| d11b648137 | |||
| 14a8ffedd8 | |||
| b5906135c7 | |||
| 4bad5c79be | |||
| 81430e4aff | |||
| c238b72d5d | |||
| a2ed91bc45 | |||
| 15fecbc161 | |||
| c32a0f5f71 | |||
| 0c3d90ed21 | |||
| fb31669261 | |||
| 917d9bc9da | |||
| fd6e4516dc | |||
| 86205efcfe | |||
| 701e77514f | |||
| 018105eec9 | |||
| bf6ec2bbfd | |||
| 13d0454f71 | |||
| db2741c6e0 | |||
| 605787f671 | |||
| 657780bcdf | |||
| 9d767e768a | |||
| 15268e8cdb | |||
| 04789a2446 | |||
| e28ee90c2a | |||
| 3841517ce1 | |||
| bea48b80ce | |||
| 9f525af210 | |||
| c00b3a520c | |||
| f6e6a6be04 | |||
| bc5df3b383 | |||
| 1a0292b830 | |||
| e6a3e5e1f3 | |||
| 397d67876c | |||
| 47b78714b8 | |||
| fda7350a43 | |||
| 80dfb8b2a7 | |||
| 98f160e39c | |||
| 4f8020a94c | |||
| b295aab2d8 | |||
| 448edcca8e | |||
| 72d0debde6 | |||
| 9037d3ab85 | |||
| 8a511989a0 | |||
| 44e3a97a67 | |||
| c0190a3460 | |||
| 396d8e989f | |||
| 33b00dc8b1 | |||
| eb9857137a | |||
| c1d6c928e3 | |||
| 118f666706 | |||
| e9641c5c7e | |||
| 495656f72b | |||
| c70d4a4cf6 | |||
| 39c5d6b964 | |||
| 0c69e9ed7f | |||
| 0a95b5d359 | |||
| 6246d4c3ca | |||
| 4de9d64c0c | |||
| 1867ded14c | |||
| 22db8bcf3d | |||
| 59e7a8864a | |||
| 7d737427a9 | |||
| eac939e9a7 | |||
| 2ea544e9a0 | |||
| 87b645386f | |||
| e3ba9ffff2 | |||
| e0efb027da | |||
| 9e4a29191c | |||
| fa10b0275f | |||
| 4f8ff09551 | |||
| f2491580e0 | |||
| 8369a12115 | |||
| 97e1f14dd3 | |||
| 930ca1cc1b | |||
| 23627bbf54 | |||
| 2fc615b405 | |||
| a36c7c7e87 | |||
| fdec3c68f0 | |||
| 0ecc5c46bf | |||
| a947f70c56 | |||
| c259381541 | |||
| 7f546e529e | |||
| a7aeb979be | |||
| 771dcf3d40 | |||
| f3a4f46d78 | |||
| 78455c7cb9 | |||
| 01f2b85826 | |||
| 7fe9e13fbf | |||
| f92a3aa0e5 | |||
| 917534e35e | |||
| 8ab447e615 | |||
| 0d8384a9b4 | |||
| e14328b71b | |||
| f5aaa471de | |||
| 0b83014ff8 | |||
| 0684cf8611 | |||
| 1570bc5d03 | |||
| 8811853f6d | |||
| b7028b139f | |||
| 620f9687c8 | |||
| 2c43616781 | |||
| d1171af679 | |||
| 598de9e6d9 | |||
| 393bc2992e | |||
| 33f2b16a1b | |||
| f03ad80701 | |||
| a68b01080c | |||
| e0f1a02c37 | |||
| 2358102c07 | |||
| 1533652b78 | |||
| c7562e46a4 | |||
| 8f583dcf36 | |||
| 09188981c4 | |||
| ae5f013a48 | |||
| b7091650f8 | |||
| 3a810c6502 | |||
| 764c9ec956 | |||
| ce0988f48a | |||
| 1c92557c8b | |||
| 8f7a1d6a25 | |||
| 1b085efa47 | |||
| d9e6e7ffa5 | |||
| 05d0b213a9 | |||
| 6f580c6aa3 | |||
| 1d9a094315 | |||
| f6e50890b3 | |||
| 22dfb140d0 | |||
| 15455e5a7e | |||
| f46da403d8 | |||
| 4f5df39bdd | |||
| 1f8d1df4ec | |||
| dd83687447 | |||
| 3ce3f3a96a | |||
| 86060ef9b4 | |||
| d3e3fc533f | |||
| 03b10f9c8e | |||
| f7757da7ed | |||
| 13f9c34d16 | |||
| 13a54dbdda | |||
| 7ed7a95524 | |||
| d47b041923 | |||
| dfbc2e81e3 | |||
| 9edc16e4d6 | |||
| 73273c5bf8 | |||
| 93c5256318 | |||
| 3ccad1814e | |||
| 35269572d7 | |||
| a457b35750 | |||
| 5e5f9b0563 | |||
| 16722e4d99 | |||
| 89c20f9a55 | |||
| d3b731e925 | |||
| 3e0695ee31 | |||
| 9239f3cbcc | |||
| b7a7fd4651 | |||
| 06b067b02c | |||
| dfb5aa6dc6 | |||
| f56696f478 | |||
| fcbb90a9af | |||
| be84b74d01 | |||
| bb5b01c911 | |||
| 3ca6bc4a66 | |||
| 053373a385 | |||
| e263566673 | |||
| 6965075825 | |||
| e54dfa49c3 | |||
| accaa378f0 | |||
| 60a0208e8d | |||
| 2aaaa368bb | |||
| 4829cc6aaf | |||
| 553acf93e2 | |||
| f058419042 | |||
| 13268db536 | |||
| 1f7b5abc80 | |||
| c667f81866 | |||
| b321c00a8f | |||
| 9160789b42 | |||
| df7cdc3fae | |||
| 86fd2f22fb | |||
| 148a6f4430 | |||
| b05006663f | |||
| 5f1f8e4ee6 | |||
| ef48e17e79 | |||
| fe03c1aefa | |||
| 078770a5a6 | |||
| 294f6957f0 | |||
| fe664c00ff | |||
| 518edd3cd4 | |||
| b019501b8b | |||
| 2922d09bef | |||
| 97487e6f0d | |||
| 694d2c9b2e | |||
| a674c0051a | |||
| 98de336a21 | |||
| 9fe2ef417c | |||
| 88edca65d3 | |||
| 64c18a7c6c | |||
| d2fc045219 | |||
| 917a604094 | |||
| b33b24fc9e | |||
| 4d9ee000c8 | |||
| 2966db7b78 | |||
| 38e65e28d4 | |||
| 73b61af58d | |||
| 858e96f21c | |||
| 8039a7127f | |||
| 33aeb1cb5c | |||
| 8bdd13b594 | |||
| 52316952a5 | |||
| 7c868afd32 | |||
| 4df8028bc3 | |||
| 385ea53309 | |||
| a6521357e5 | |||
| 269a8b5fce | |||
| 5820356cf6 | |||
| 6b3c2212a1 | |||
| 703cf7bf8b | |||
| 3e00e18adc | |||
| 6c17e4d4c8 | |||
| 388ff6bc0a | |||
| 8f0b44b8a4 |
@@ -9,6 +9,13 @@
|
||||
|
||||
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
|
||||
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
|
||||
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
|
||||
.git* text eol=auto core.whitespace whitespace=trailing-space
|
||||
|
||||
+40
-15
@@ -23,15 +23,15 @@ Other menu items:
|
||||
|
||||
### Contributing code
|
||||
|
||||
You can have a direct impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/mholt/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/mholt/caddy/-/search).
|
||||
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).
|
||||
|
||||
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. :wink: 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:
|
||||
|
||||
- If your change is more than just a minor alteration, **open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that changes are in-line with the project's goals and the best interests of its users. 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.
|
||||
|
||||
- **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 DON'T do.](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)
|
||||
|
||||
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
|
||||
|
||||
@@ -52,17 +52,17 @@ We often grant [collaborator status](#collaborator-instructions) to contributors
|
||||
|
||||
Contributing to Go projects on GitHub is fun and easy. We recommend the following workflow:
|
||||
|
||||
1. [Fork this repo](https://github.com/mholt/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 (mholt/caddy.git) repo on your computer, get it with `go get github.com/mholt/caddy/caddy`.
|
||||
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/caddy`.
|
||||
|
||||
3. Tell git that it can push the mholt/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/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/you/caddy.git`
|
||||
|
||||
4. Make your changes in the mholt/caddy.git repo on your computer.
|
||||
4. Make your changes in the caddyserver/caddy.git repo on your computer.
|
||||
|
||||
5. Push your changes to your fork: `git push myfork`
|
||||
|
||||
6. [Create a pull request](https://github.com/mholt/caddy/pull/new/master) to merge your changes into mholt/caddy @ master. (Click "compare across forks" and change the head fork.)
|
||||
6. [Create a pull request](https://github.com/caddyserver/caddy/pull/new/master) to merge your changes into caddyserver/caddy @ master. (Click "compare across forks" and change the head fork.)
|
||||
|
||||
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
|
||||
|
||||
@@ -71,7 +71,7 @@ This workflow is nice because you don't have to change import paths. You can get
|
||||
|
||||
Caddy can do more with plugins! Anyone can write a plugin. Plugins are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, change how the Caddyfile is loaded, and even implement new server types (e.g. HTTP, DNS). When it's ready, you can submit your plugin to the Caddy website so others can download it.
|
||||
|
||||
[Learn how to write and submit a plugin](https://github.com/mholt/caddy/wiki) on the wiki. You should also share and discuss your plugin idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for plugins.
|
||||
[Learn how to write and submit a plugin](https://github.com/caddyserver/caddy/wiki) on the wiki. You should also share and discuss your plugin idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for plugins.
|
||||
|
||||
|
||||
### Getting help using Caddy
|
||||
@@ -83,7 +83,7 @@ Many people on the forums could benefit from your experience and expertise, too.
|
||||
|
||||
### Reporting bugs
|
||||
|
||||
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/mholt/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/mholt/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy, not plugins.)
|
||||
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.
|
||||
|
||||
@@ -93,17 +93,42 @@ We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.o
|
||||
|
||||
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else!
|
||||
|
||||
#### Bug reporting requirements
|
||||
|
||||
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).
|
||||
- A bug is undesired or surprising behavior which violates documentation or the spec.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
|
||||
- Sometimes the best solution is a documentation change.
|
||||
- Usually the developers have the best domain knowledge for inventing a solution, but reporters may have ideas or preferences for how they would like the software to work.
|
||||
- Security, correctness, and project goals/vision all take priority over a user's preferences.
|
||||
- It's simply good business to yield a solution that satisfies the users, and it's even better business to leave them impressed.
|
||||
|
||||
Thus, at the very least, the reporter is expected to:
|
||||
|
||||
1. Convince the reader that it's a bug (if it's not obvious).
|
||||
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.
|
||||
|
||||
|
||||
|
||||
### Suggesting features
|
||||
|
||||
First, [search to see if your feature has already been requested](https://github.com/mholt/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. You don't have to follow the bug template for feature requests. 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 plugins](https://github.com/mholt/caddy/wiki), though, 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 plugins](https://github.com/caddyserver/caddy/wiki), 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.
|
||||
|
||||
|
||||
### Improving documentation
|
||||
|
||||
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs). If you would like to make a fix to the docs, feel free to contribute at the [caddyserver/website](https://github.com/caddyserver/website) repository!
|
||||
Caddy's documentation is available at [https://caddyserver.com/v1/docs](https://caddyserver.com/v1/docs). If you would like to make a fix to the docs, please submit an issue here describing the change to make.
|
||||
|
||||
Note that plugin documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual plugin authors, and you will have to contact them to change their documentation.
|
||||
|
||||
@@ -111,7 +136,7 @@ Note that plugin documentation is not hosted by the Caddy website, other than ba
|
||||
|
||||
## Collaborator Instructions
|
||||
|
||||
Collabators 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:
|
||||
- Can the change be made more elegant?
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,32 +0,0 @@
|
||||
<!--
|
||||
Are you asking for help with using Caddy? Please use our forum instead: https://caddy.community. If you are filing a bug report, please take a few minutes to carefully answer the following questions. If your issue is not a bug report, you do not need to use this template. Thanks!)
|
||||
-->
|
||||
|
||||
### 1. What version of Caddy are you using (`caddy -version`)?
|
||||
|
||||
|
||||
### 2. What are you trying to do?
|
||||
|
||||
|
||||
### 3. What is your entire Caddyfile?
|
||||
```text
|
||||
(paste Caddyfile here)
|
||||
```
|
||||
|
||||
### 4. How did you run Caddy (give the full command and describe the execution environment)?
|
||||
|
||||
|
||||
### 5. Please paste any relevant HTTP request(s) here.
|
||||
|
||||
<!-- Paste curl command, or full HTTP request including headers and body, here. -->
|
||||
|
||||
|
||||
### 6. What did you expect to see?
|
||||
|
||||
|
||||
### 7. What did you see instead (give full error messages and/or log)?
|
||||
|
||||
|
||||
### 8. How can someone who is starting from scratch reproduce the bug as minimally as possible?
|
||||
|
||||
<!-- Please strip away any extra infrastructure such as containers, reverse proxies, upstream apps, caches, dependencies, etc, to prove this is a bug in Caddy and not an external misconfiguration. Your chances of getting this bug fixed go way up the easier it is to replicate. Thank you! -->
|
||||
@@ -1,19 +0,0 @@
|
||||
<!--
|
||||
Thank you for contributing to Caddy! Please fill this out to help us make the most of your pull request.
|
||||
-->
|
||||
|
||||
### 1. What does this change do, exactly?
|
||||
|
||||
|
||||
### 2. Please link to the relevant issues.
|
||||
|
||||
|
||||
### 3. Which documentation changes (if any) need to be made because of this PR?
|
||||
|
||||
|
||||
### 4. Checklist
|
||||
|
||||
- [ ] I have written tests and verified that they fail without my change
|
||||
- [ ] I have squashed any insignificant commits
|
||||
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code
|
||||
- [ ] I am willing to help maintain this change if there are issues with it later
|
||||
+2
-1
@@ -13,9 +13,10 @@ access.log
|
||||
|
||||
/*.conf
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
og_static/
|
||||
|
||||
.vscode/
|
||||
|
||||
*.bat
|
||||
*.bat
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
language: go
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
- quic.clemente.io
|
||||
|
||||
go:
|
||||
- 1.x
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
|
||||
before_install:
|
||||
# Decrypts a script that installs an authenticated cookie
|
||||
# for git to use when cloning from googlesource.com.
|
||||
# Bypasses "bandwidth limit exceeded" errors.
|
||||
# See github.com/golang/go/issues/12933
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_3df18f9af81d_key -iv $encrypted_3df18f9af81d_iv -in dist/gitcookie.sh.enc -out dist/gitcookie.sh -d; fi
|
||||
|
||||
install:
|
||||
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi
|
||||
- go get -t ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get github.com/FiloSottile/vendorcheck
|
||||
# Install gometalinter
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
|
||||
script:
|
||||
- gometalinter --install
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --tests --vendor ./...
|
||||
- vendorcheck ./...
|
||||
- go test -race ./...
|
||||
|
||||
after_script:
|
||||
- golint ./...
|
||||
@@ -4,14 +4,13 @@
|
||||
<h3 align="center">Every Site on HTTPS <!-- Serve Confidently --></h3>
|
||||
<p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</p>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/mholt/caddy"><img src="https://img.shields.io/travis/mholt/caddy.svg?label=linux+build"></a>
|
||||
<a href="https://ci.appveyor.com/project/mholt/caddy"><img src="https://img.shields.io/appveyor/ci/mholt/caddy.svg?label=windows+build"></a>
|
||||
<a href="https://godoc.org/github.com/mholt/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
||||
<a href="https://goreportcard.com/report/mholt/caddy"><img src="https://goreportcard.com/badge/github.com/mholt/caddy"></a>
|
||||
<a href="https://dev.azure.com/mholt-dev/Caddy/_build?definitionId=5"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/5/master.svg?label=cross-platform%20tests"></a>
|
||||
<a href="https://godoc.org/github.com/caddyserver/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
|
||||
<a href="https://goreportcard.com/report/caddyserver/caddy"><img src="https://goreportcard.com/badge/github.com/caddyserver/caddy"></a>
|
||||
<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://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
|
||||
<a href="https://sourcegraph.com/github.com/mholt/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/mholt/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>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com/download">Download</a> ·
|
||||
@@ -23,7 +22,13 @@
|
||||
|
||||
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
|
||||
|
||||
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android).
|
||||
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
|
||||
|
||||
<p align="center">
|
||||
<b>Thanks to our special sponsor:</b>
|
||||
<br><br>
|
||||
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
|
||||
</p>
|
||||
|
||||
## Menu
|
||||
|
||||
@@ -51,24 +56,66 @@ Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.co
|
||||
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
|
||||
|
||||
|
||||
<p align="center">
|
||||
<b>Powered by</b>
|
||||
<br>
|
||||
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
||||
</p>
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
Caddy binaries have no dependencies and are available for every platform. Get Caddy either of these ways:
|
||||
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
|
||||
|
||||
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
|
||||
- **[Latest release](https://github.com/mholt/caddy/releases/latest)** for pre-built, vanilla binaries
|
||||
- **[Latest release](https://github.com/caddyserver/caddy/releases/latest)** for pre-built, vanilla binaries
|
||||
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
|
||||
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.9 or newer). Follow these instruction for fast building:
|
||||
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
|
||||
|
||||
- Get the source with `go get github.com/mholt/caddy/caddy` and then run `go get github.com/caddyserver/builds`
|
||||
- Now `cd $GOPATH/src/github.com/mholt/caddy/caddy` and run `go run build.go`
|
||||
**To build Caddy without plugins:**
|
||||
|
||||
Then make sure the `caddy` binary is in your PATH.
|
||||
- Run `go get github.com/caddyserver/caddy/caddy`
|
||||
|
||||
To build for other platforms, use build.go with the `--goos` and `--goarch` flags.
|
||||
Caddy will be installed to your `$GOPATH/bin` folder.
|
||||
|
||||
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
|
||||
|
||||
**To build Caddy with plugins (and with version information):**
|
||||
|
||||
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
|
||||
- Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
// plug in plugins here, for example:
|
||||
// _ "import/path/here"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// optional: disable telemetry
|
||||
// caddymain.EnableTelemetry = false
|
||||
caddymain.Run()
|
||||
}
|
||||
```
|
||||
3. `go mod init caddy`
|
||||
4. Run `go get github.com/caddyserver/caddy`
|
||||
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
|
||||
|
||||
**To install Caddy's source code for development:**
|
||||
|
||||
- Run `git clone https://github.com/caddyserver/caddy.git` in any folder (doesn't have to be in GOPATH).
|
||||
|
||||
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
|
||||
|
||||
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
|
||||
|
||||
|
||||
## Quick Start
|
||||
@@ -128,7 +175,7 @@ Caddy is production-ready if you find it to be a good fit for your site and work
|
||||
|
||||
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
|
||||
|
||||
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/mholt/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
|
||||
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/caddyserver/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
|
||||
|
||||
How you choose to run Caddy is up to you. Many users are satisfied with `nohup caddy &`. Others use `screen`. Users who need Caddy to come back up after reboots either do so in the script that caused the reboot, add a command to an init script, or configure a service with their OS.
|
||||
|
||||
@@ -137,13 +184,17 @@ If you have questions or concerns about Caddy' underlying crypto implementations
|
||||
|
||||
## Contributing
|
||||
|
||||
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy/-/search)!
|
||||
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/)!
|
||||
|
||||
Please see our [contributing guidelines](https://github.com/mholt/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/mholt/caddy/wiki).
|
||||
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
|
||||
|
||||
We use GitHub issues and pull requests only for discussing bug reports and the development of specific changes. We welcome all other topics on the [forum](https://caddy.community)!
|
||||
|
||||
If you want to contribute to the documentation, please submit pull requests to [caddyserver/website](https://github.com/caddyserver/website).
|
||||
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/caddy/issues/new) describing the change that should be made.
|
||||
|
||||
### Good First Issue
|
||||
|
||||
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
@@ -154,7 +205,7 @@ Thanks for making Caddy -- and the Web -- better!
|
||||
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
|
||||
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
|
||||
|
||||
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://caddyserver.com/pricing)!**
|
||||
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://github.com/sponsors/mholt)!**
|
||||
|
||||
|
||||
## About the Project
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
version: "{build}"
|
||||
|
||||
hosts:
|
||||
quic.clemente.io: 127.0.0.1
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.9.windows-amd64.zip
|
||||
- 7z x go1.9.windows-amd64.zip -y -oC:\ > NUL
|
||||
- set PATH=%GOPATH%\bin;%PATH%
|
||||
- go version
|
||||
- go env
|
||||
- go get -t ./...
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get github.com/FiloSottile/vendorcheck
|
||||
# Install gometalinter
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- gometalinter --install
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --tests --vendor ./...
|
||||
- vendorcheck ./...
|
||||
- go test -race ./...
|
||||
|
||||
after_test:
|
||||
- golint ./...
|
||||
|
||||
deploy: off
|
||||
@@ -0,0 +1,88 @@
|
||||
# Mutilated beyond recognition from the example at:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
|
||||
|
||||
trigger:
|
||||
- master
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
linux:
|
||||
imageName: ubuntu-16.04
|
||||
gorootDir: /usr/local
|
||||
mac:
|
||||
imageName: macos-10.13
|
||||
gorootDir: /usr/local
|
||||
windows:
|
||||
imageName: windows-2019
|
||||
gorootDir: C:\
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
variables:
|
||||
GOROOT: $(gorootDir)/go
|
||||
GOPATH: $(system.defaultWorkingDirectory)/gopath
|
||||
GOBIN: $(GOPATH)/bin
|
||||
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
|
||||
# TODO: modules should be the default in Go 1.13, so this won't be needed
|
||||
GO111MODULE: on
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
latestGo=$(curl "https://golang.org/VERSION?m=text")
|
||||
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
|
||||
echo "Latest Go version: $latestGo"
|
||||
displayName: "Get latest Go version"
|
||||
|
||||
- bash: |
|
||||
sudo rm -f $(which go)
|
||||
echo '##vso[task.prependpath]$(GOBIN)'
|
||||
echo '##vso[task.prependpath]$(GOROOT)/bin'
|
||||
mkdir -p '$(modulePath)'
|
||||
shopt -s extglob
|
||||
shopt -s dotglob
|
||||
mv !(gopath) '$(modulePath)'
|
||||
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
|
||||
|
||||
# Install Go (this varies by platform)
|
||||
|
||||
- bash: |
|
||||
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
|
||||
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
|
||||
condition: eq( variables['Agent.OS'], 'Linux' )
|
||||
displayName: Install Go on Linux
|
||||
|
||||
- bash: |
|
||||
wget "https://dl.google.com/go/$(LATEST_GO).darwin-amd64.tar.gz"
|
||||
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).darwin-amd64.tar.gz"
|
||||
condition: eq( variables['Agent.OS'], 'Darwin' )
|
||||
displayName: Install Go on macOS
|
||||
|
||||
- powershell: |
|
||||
Write-Host "Downloading Go... (please be patient, I am very slow)"
|
||||
(New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/$(LATEST_GO).windows-amd64.zip", "$(LATEST_GO).windows-amd64.zip")
|
||||
Write-Host "Extracting Go... (I'm slow too)"
|
||||
Expand-Archive "$(LATEST_GO).windows-amd64.zip" -DestinationPath "$(gorootDir)"
|
||||
condition: eq( variables['Agent.OS'], 'Windows_NT' )
|
||||
displayName: Install Go on Windows
|
||||
|
||||
# TODO: When this issue is fixed, replace with installer script:
|
||||
# https://github.com/golangci/golangci-lint/issues/472
|
||||
- script: go get -v github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
displayName: Install golangci-lint
|
||||
|
||||
- bash: |
|
||||
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
|
||||
displayName: Print Go version and environment
|
||||
|
||||
- script: |
|
||||
go get -v -t -d ./...
|
||||
golangci-lint run -E gofmt -E goimports -E misspell
|
||||
go test -race ./...
|
||||
workingDirectory: '$(modulePath)'
|
||||
displayName: Run tests
|
||||
@@ -20,7 +20,7 @@
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile.
|
||||
// Pass in the name of the server type (like "http").
|
||||
// Make sure the server type's package is imported
|
||||
// (import _ "github.com/mholt/caddy/caddyhttp").
|
||||
// (import _ "github.com/caddyserver/caddy/caddyhttp").
|
||||
// 3. Call caddy.Start() to start Caddy. You get back
|
||||
// an Instance, on which you can call Restart() to
|
||||
// restart it or Stop() to stop it.
|
||||
@@ -43,7 +43,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
@@ -107,11 +108,12 @@ type Instance struct {
|
||||
servers []ServerListener
|
||||
|
||||
// these callbacks execute when certain events occur
|
||||
onFirstStartup []func() error // starting, not as part of a restart
|
||||
onStartup []func() error // starting, even as part of a restart
|
||||
onRestart []func() error // before restart commences
|
||||
onShutdown []func() error // stopping, even as part of a restart
|
||||
onFinalShutdown []func() error // stopping, not as part of a restart
|
||||
OnFirstStartup []func() error // starting, not as part of a restart
|
||||
OnStartup []func() error // starting, even as part of a restart
|
||||
OnRestart []func() error // before restart commences
|
||||
OnRestartFailed []func() error // if restart failed
|
||||
OnShutdown []func() error // stopping, even as part of a restart
|
||||
OnFinalShutdown []func() error // stopping, not as part of a restart
|
||||
|
||||
// storing values on an instance is preferable to
|
||||
// global state because these will get garbage-
|
||||
@@ -122,6 +124,7 @@ type Instance struct {
|
||||
StorageMu sync.RWMutex
|
||||
}
|
||||
|
||||
// Instances returns the list of instances.
|
||||
func Instances() []*Instance {
|
||||
return instances
|
||||
}
|
||||
@@ -160,13 +163,13 @@ func (i *Instance) Stop() error {
|
||||
// the rest. All the non-nil errors will be returned.
|
||||
func (i *Instance) ShutdownCallbacks() []error {
|
||||
var errs []error
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
for _, shutdownFunc := range i.OnShutdown {
|
||||
err := shutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, finalShutdownFunc := range i.onFinalShutdown {
|
||||
for _, finalShutdownFunc := range i.OnFinalShutdown {
|
||||
err := finalShutdownFunc()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -184,9 +187,28 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
i.wg.Add(1)
|
||||
defer i.wg.Done()
|
||||
|
||||
var err error
|
||||
// if something went wrong on restart then run onRestartFailed callbacks
|
||||
defer func() {
|
||||
r := recover()
|
||||
if err != nil || r != nil {
|
||||
for _, fn := range i.OnRestartFailed {
|
||||
if err := fn(); err != nil {
|
||||
log.Printf("[ERROR] Restart failed callback returned error: %v", err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Restart failed: %v", err)
|
||||
}
|
||||
if r != nil {
|
||||
log.Printf("[PANIC] Restart: %v", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// run restart callbacks
|
||||
for _, fn := range i.onRestart {
|
||||
err := fn()
|
||||
for _, fn := range i.OnRestart {
|
||||
err = fn()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -222,22 +244,22 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg, Storage: make(map[interface{}]interface{})}
|
||||
|
||||
// attempt to start new instance
|
||||
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
err = startWithListenerFds(newCaddyfile, newInst, restartFds)
|
||||
if err != nil {
|
||||
return i, err
|
||||
return i, fmt.Errorf("starting with listener file descriptors: %v", err)
|
||||
}
|
||||
|
||||
// success! stop the old instance
|
||||
for _, shutdownFunc := range i.onShutdown {
|
||||
err = i.Stop()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
for _, shutdownFunc := range i.OnShutdown {
|
||||
err = shutdownFunc()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
err = i.Stop()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
// Execute instantiation events
|
||||
EmitEvent(InstanceStartupEvent, newInst)
|
||||
@@ -254,42 +276,6 @@ func (i *Instance) SaveServer(s Server, ln net.Listener) {
|
||||
i.servers = append(i.servers, ServerListener{server: s, listener: ln})
|
||||
}
|
||||
|
||||
// HasListenerWithAddress returns whether this package is
|
||||
// tracking a server using a listener with the address
|
||||
// addr.
|
||||
func HasListenerWithAddress(addr string) bool {
|
||||
instancesMu.Lock()
|
||||
defer instancesMu.Unlock()
|
||||
for _, inst := range instances {
|
||||
for _, sln := range inst.servers {
|
||||
if listenerAddrEqual(sln.listener, addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// listenerAddrEqual compares a listener's address with
|
||||
// addr. Extra care is taken to match addresses with an
|
||||
// empty hostname portion, as listeners tend to report
|
||||
// [::]:80, for example, when the matching address that
|
||||
// created the listener might be simply :80.
|
||||
func listenerAddrEqual(ln net.Listener, addr string) bool {
|
||||
lnAddr := ln.Addr().String()
|
||||
hostname, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return lnAddr == addr
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("::", port) {
|
||||
return true
|
||||
}
|
||||
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
|
||||
return true
|
||||
}
|
||||
return hostname != "" && lnAddr == addr
|
||||
}
|
||||
|
||||
// TCPServer is a type that can listen and serve connections.
|
||||
// A TCPServer must associate with exactly zero or one net.Listeners.
|
||||
type TCPServer interface {
|
||||
@@ -368,6 +354,11 @@ type GracefulServer interface {
|
||||
// address; you must store the address the
|
||||
// server is to serve on some other way.
|
||||
Address() string
|
||||
|
||||
// WrapListener wraps a listener with the
|
||||
// listener middlewares configured for this
|
||||
// server, if any.
|
||||
WrapListener(net.Listener) net.Listener
|
||||
}
|
||||
|
||||
// Listener is a net.Listener with an underlying file descriptor.
|
||||
@@ -487,6 +478,10 @@ func Start(cdyfile Input) (*Instance, error) {
|
||||
if pidErr := writePidFile(); pidErr != nil {
|
||||
log.Printf("[ERROR] Could not write pidfile: %v", pidErr)
|
||||
}
|
||||
|
||||
// Execute instantiation events
|
||||
EmitEvent(InstanceStartupEvent, inst)
|
||||
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
@@ -531,14 +526,14 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
// run startup callbacks
|
||||
if !IsUpgrade() && restartFds == nil {
|
||||
// first startup means not a restart or upgrade
|
||||
for _, firstStartupFunc := range inst.onFirstStartup {
|
||||
for _, firstStartupFunc := range inst.OnFirstStartup {
|
||||
err = firstStartupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, startupFunc := range inst.onStartup {
|
||||
for _, startupFunc := range inst.OnStartup {
|
||||
err = startupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -605,6 +600,12 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sb := range sblocks {
|
||||
for dir := range sb.Tokens {
|
||||
telemetry.AppendUnique("directives", dir)
|
||||
}
|
||||
}
|
||||
|
||||
inst.context = stype.NewContext(inst)
|
||||
if inst.context == nil {
|
||||
return fmt.Errorf("server type %s produced a nil Context", stypeName)
|
||||
@@ -615,6 +616,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
|
||||
return fmt.Errorf("error inspecting server blocks: %v", err)
|
||||
}
|
||||
|
||||
telemetry.Set("num_server_blocks", len(sblocks))
|
||||
|
||||
return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
|
||||
}
|
||||
|
||||
@@ -688,6 +691,11 @@ func executeDirectives(inst *Instance, filename string,
|
||||
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
|
||||
errChan := make(chan error, len(serverList))
|
||||
|
||||
// used for signaling to error logging goroutine to terminate
|
||||
stopChan := make(chan struct{})
|
||||
// used to track termination of servers
|
||||
stopWg := &sync.WaitGroup{}
|
||||
|
||||
for _, s := range serverList {
|
||||
var (
|
||||
ln net.Listener
|
||||
@@ -704,24 +712,25 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
|
||||
file := os.NewFile(fdIndex, "")
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("making listener from file: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("closing copy of listener file: %v", err)
|
||||
}
|
||||
}
|
||||
if fdIndex, ok := loadedGob.ListenerFds["udp"+addr]; ok {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
pc, err = net.FilePacketConn(file)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("making packet connection from file: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("closing copy of packet connection file: %v", err)
|
||||
}
|
||||
}
|
||||
ln = gs.WrapListener(ln)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,77 +743,97 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
|
||||
if old.listener != nil {
|
||||
file, err := old.listener.File()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting old listener file: %v", err)
|
||||
}
|
||||
ln, err = net.FileListener(file)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting file listener: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("closing copy of listener file: %v", err)
|
||||
}
|
||||
}
|
||||
// packetconn
|
||||
if old.packet != nil {
|
||||
file, err := old.packet.File()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting old packet file: %v", err)
|
||||
}
|
||||
pc, err = net.FilePacketConn(file)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("getting file packet connection: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("close copy of packet file: %v", err)
|
||||
}
|
||||
}
|
||||
ln = gs.WrapListener(ln)
|
||||
}
|
||||
}
|
||||
|
||||
if ln == nil {
|
||||
ln, err = s.Listen()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Listen: %v", err)
|
||||
}
|
||||
}
|
||||
if pc == nil {
|
||||
pc, err = s.ListenPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ListenPacket: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
|
||||
}
|
||||
|
||||
for _, s := range inst.servers {
|
||||
inst.wg.Add(2)
|
||||
go func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
|
||||
defer inst.wg.Done()
|
||||
stopWg.Add(2)
|
||||
func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
|
||||
go func() {
|
||||
defer func() {
|
||||
inst.wg.Done()
|
||||
stopWg.Done()
|
||||
}()
|
||||
errChan <- s.Serve(ln)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
errChan <- s.Serve(ln)
|
||||
defer inst.wg.Done()
|
||||
defer func() {
|
||||
inst.wg.Done()
|
||||
stopWg.Done()
|
||||
}()
|
||||
errChan <- s.ServePacket(pc)
|
||||
}()
|
||||
errChan <- s.ServePacket(pc)
|
||||
}(s, ln, pc, inst)
|
||||
|
||||
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
|
||||
}(s.server, s.listener, s.packet, inst)
|
||||
}
|
||||
|
||||
// Log errors that may be returned from Serve() calls,
|
||||
// these errors should only be occurring in the server loop.
|
||||
go func() {
|
||||
for err := range errChan {
|
||||
if err == nil {
|
||||
continue
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener; see https://github.com/golang/go/issues/4373
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "use of closed network connection") {
|
||||
// this error is normal when closing the listener
|
||||
continue
|
||||
}
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stopWg.Wait()
|
||||
stopChan <- struct{}{}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -854,10 +883,15 @@ func Stop() error {
|
||||
for {
|
||||
instancesMu.Lock()
|
||||
if len(instances) == 0 {
|
||||
instancesMu.Unlock()
|
||||
break
|
||||
}
|
||||
inst := instances[0]
|
||||
instancesMu.Unlock()
|
||||
// Increase the instance waitgroup so that the last wait() call in
|
||||
// caddymain/run.go blocks until this server instance has shut down
|
||||
inst.wg.Add(1)
|
||||
defer inst.wg.Done()
|
||||
if err := inst.Stop(); err != nil {
|
||||
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
|
||||
}
|
||||
@@ -869,7 +903,7 @@ func Stop() error {
|
||||
// explicitly like a common local hostname. addr must only
|
||||
// be a host or a host:port combination.
|
||||
func IsLoopback(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
host, _, err := net.SplitHostPort(strings.ToLower(addr))
|
||||
if err != nil {
|
||||
host = addr // happens if the addr is just a hostname
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
//
|
||||
// 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 dev
|
||||
|
||||
// build.go automates proper versioning of caddy binaries.
|
||||
// Use it like: go run build.go
|
||||
// You can customize the build with the -goos, -goarch, and
|
||||
// -goarm CLI options: go run build.go -goos=windows
|
||||
//
|
||||
// To get proper version information, this program must be
|
||||
// run from the directory of this file, and the source code
|
||||
// must be a working git repository, since it needs to know
|
||||
// if the source is in a clean state.
|
||||
//
|
||||
// This program is NOT required to build Caddy from source
|
||||
// since it is go-gettable. (You can run plain `go build`
|
||||
// in this directory to get a binary.) However, issues filed
|
||||
// without version information will likely be closed.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/caddyserver/builds"
|
||||
)
|
||||
|
||||
var goos, goarch, goarm string
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&goos, "goos", "", "GOOS for which to build")
|
||||
flag.StringVar(&goarch, "goarch", "", "GOARCH for which to build")
|
||||
flag.StringVar(&goarm, "goarm", "", "GOARM for which to build")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
gopath := os.Getenv("GOPATH")
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ldflags, err := builds.MakeLdFlags(filepath.Join(pwd, ".."))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
args := []string{"build", "-ldflags", ldflags}
|
||||
args = append(args, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
|
||||
args = append(args, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Env = os.Environ()
|
||||
for _, env := range []string{
|
||||
"CGO_ENABLED=0",
|
||||
"GOOS=" + goos,
|
||||
"GOARCH=" + goarch,
|
||||
"GOARM=" + goarm,
|
||||
} {
|
||||
cmd.Env = append(cmd.Env, env)
|
||||
}
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
+383
-65
@@ -15,46 +15,59 @@
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
"github.com/google/uuid"
|
||||
"github.com/klauspost/cpuid"
|
||||
"github.com/mholt/certmagic"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
"github.com/xenolf/lego/acmev2"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
// plug in the HTTP server type
|
||||
_ "github.com/mholt/caddy/caddyhttp"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
|
||||
// This is where other plugins get plugged in (imported)
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.TrapSignals()
|
||||
setVersion()
|
||||
|
||||
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v02.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
|
||||
flag.BoolVar(&caddytls.DisableHTTPChallenge, "disable-http-challenge", caddytls.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
|
||||
flag.BoolVar(&caddytls.DisableTLSSNIChallenge, "disable-tls-sni-challenge", caddytls.DisableTLSSNIChallenge, "Disable the ACME TLS-SNI challenge")
|
||||
flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
|
||||
flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
|
||||
flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", certmagic.Default.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
|
||||
flag.BoolVar(&certmagic.Default.DisableHTTPChallenge, "disable-http-challenge", certmagic.Default.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
|
||||
flag.BoolVar(&certmagic.Default.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.Default.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
|
||||
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
|
||||
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
|
||||
flag.StringVar(&envFile, "envfile", "", "Path to file with environment variables to load in KEY=VALUE format")
|
||||
flag.BoolVar(&fromJSON, "json-to-caddyfile", false, "From JSON stdin to Caddyfile stdout")
|
||||
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
|
||||
flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
|
||||
flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout")
|
||||
flag.StringVar(&certmagic.Default.Email, "email", "", "Default ACME CA account email address")
|
||||
flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout")
|
||||
flag.StringVar(&logfile, "log", "", "Process log file")
|
||||
flag.BoolVar(&logTimestamps, "log-timestamps", true, "Enable timestamps for the process log")
|
||||
flag.IntVar(&logRollMB, "log-roll-mb", 100, "Roll process log when it reaches this many megabytes (0 to disable rolling)")
|
||||
flag.BoolVar(&logRollCompress, "log-roll-compress", true, "Gzip-compress rolled process log files")
|
||||
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
|
||||
flag.StringVar(&serverType, "type", "http", "Type of server to run")
|
||||
flag.BoolVar(&toJSON, "caddyfile-to-json", false, "From Caddyfile stdin to JSON stdout")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
flag.BoolVar(&validate, "validate", false, "Parse the Caddyfile but do not start the server")
|
||||
|
||||
@@ -66,9 +79,18 @@ func init() {
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
|
||||
module := getBuildModule()
|
||||
cleanModVersion := strings.TrimPrefix(module.Version, "v")
|
||||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = appVersion
|
||||
acme.UserAgent = appName + "/" + appVersion
|
||||
caddy.AppVersion = module.Version
|
||||
caddy.OnProcessExit = append(caddy.OnProcessExit, certmagic.CleanUpOwnLocks)
|
||||
certmagic.UserAgent = appName + "/" + cleanModVersion
|
||||
|
||||
if !logTimestamps {
|
||||
// Disable timestamps for logging
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
// Set up process log before anything bad happens
|
||||
switch logfile {
|
||||
@@ -79,12 +101,47 @@ func Run() {
|
||||
case "":
|
||||
log.SetOutput(ioutil.Discard)
|
||||
default:
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: 100,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
})
|
||||
if logRollMB > 0 {
|
||||
log.SetOutput(&lumberjack.Logger{
|
||||
Filename: logfile,
|
||||
MaxSize: logRollMB,
|
||||
MaxAge: 14,
|
||||
MaxBackups: 10,
|
||||
Compress: logRollCompress,
|
||||
})
|
||||
} else {
|
||||
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
// don't close file; log should be writeable for duration of process
|
||||
log.SetOutput(f)
|
||||
}
|
||||
}
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if err := LoadEnvFromFile(envFile); err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
if printEnv {
|
||||
for _, v := range os.Environ() {
|
||||
fmt.Println(v)
|
||||
}
|
||||
}
|
||||
|
||||
// initialize telemetry client
|
||||
if EnableTelemetry {
|
||||
err := initTelemetry()
|
||||
if err != nil {
|
||||
mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
|
||||
}
|
||||
} else if disabledMetrics != "" {
|
||||
mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
|
||||
}
|
||||
|
||||
// Check for one-time actions
|
||||
@@ -97,9 +154,11 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
if version {
|
||||
fmt.Printf("%s %s (unofficial)\n", appName, appVersion)
|
||||
if devBuild && gitShortStat != "" {
|
||||
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
|
||||
if module.Sum != "" {
|
||||
// a build with a known version will also have a checksum
|
||||
fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
|
||||
} else {
|
||||
fmt.Println(module.Version)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -108,6 +167,9 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check if we just need to do a Caddyfile Convert and exit
|
||||
checkJSONCaddyfile()
|
||||
|
||||
// Set CPU cap
|
||||
err := setCPU(cpu)
|
||||
if err != nil {
|
||||
@@ -134,14 +196,34 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Log Caddy version before start
|
||||
log.Printf("[INFO] Caddy version: %s", module.Version)
|
||||
|
||||
// Start your engines
|
||||
instance, err := caddy.Start(caddyfileinput)
|
||||
if err != nil {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// Execute instantiation events
|
||||
caddy.EmitEvent(caddy.InstanceStartupEvent, instance)
|
||||
// Begin telemetry (these are no-ops if telemetry disabled)
|
||||
telemetry.Set("caddy_version", module.Version)
|
||||
telemetry.Set("num_listeners", len(instance.Servers()))
|
||||
telemetry.Set("server_type", serverType)
|
||||
telemetry.Set("os", runtime.GOOS)
|
||||
telemetry.Set("arch", runtime.GOARCH)
|
||||
telemetry.Set("cpu", struct {
|
||||
BrandName string `json:"brand_name,omitempty"`
|
||||
NumLogical int `json:"num_logical,omitempty"`
|
||||
AESNI bool `json:"aes_ni,omitempty"`
|
||||
}{
|
||||
BrandName: cpuid.CPU.BrandName,
|
||||
NumLogical: runtime.NumCPU(),
|
||||
AESNI: cpuid.CPU.AesNi(),
|
||||
})
|
||||
if containerized := detectContainer(); containerized {
|
||||
telemetry.Set("container", containerized)
|
||||
}
|
||||
telemetry.StartEmitting()
|
||||
|
||||
// Twiddle your thumbs
|
||||
instance.Wait()
|
||||
@@ -205,25 +287,56 @@ func defaultLoader(serverType string) (caddy.Input, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// setVersion figures out the version information
|
||||
// based on variables set by -ldflags.
|
||||
func setVersion() {
|
||||
// A development build is one that's not at a tag or has uncommitted changes
|
||||
devBuild = gitTag == "" || gitShortStat != ""
|
||||
|
||||
if buildDate != "" {
|
||||
buildDate = " " + buildDate
|
||||
}
|
||||
|
||||
// Only set the appVersion if -ldflags was used
|
||||
if gitNearestTag != "" || gitTag != "" {
|
||||
if devBuild && gitNearestTag != "" {
|
||||
appVersion = fmt.Sprintf("%s (+%s%s)",
|
||||
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
|
||||
} else if gitTag != "" {
|
||||
appVersion = strings.TrimPrefix(gitTag, "v")
|
||||
// getBuildModule returns the build info of Caddy
|
||||
// from debug.BuildInfo (requires Go modules). If
|
||||
// no version information is available, a non-nil
|
||||
// value will still be returned, but with an
|
||||
// unknown version.
|
||||
func getBuildModule() *debug.Module {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
// The recommended way to build Caddy involves
|
||||
// creating a separate main module, which
|
||||
// preserves caddy a read-only dependency
|
||||
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
||||
for _, mod := range bi.Deps {
|
||||
if mod.Path == "github.com/caddyserver/caddy" {
|
||||
return mod
|
||||
}
|
||||
}
|
||||
}
|
||||
return &debug.Module{Version: "unknown"}
|
||||
}
|
||||
|
||||
func checkJSONCaddyfile() {
|
||||
if fromJSON {
|
||||
jsonBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
caddyfileBytes, err := caddyfile.FromJSON(jsonBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Converting from JSON failed: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Println(string(caddyfileBytes))
|
||||
os.Exit(0)
|
||||
}
|
||||
if toJSON {
|
||||
caddyfileBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
jsonBytes, err := caddyfile.ToJSON(caddyfileBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Converting to JSON failed: %v", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
@@ -266,29 +379,234 @@ func setCPU(cpu string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectContainer attempts to determine whether the process is
|
||||
// being run inside a container. References:
|
||||
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
||||
// https://stackoverflow.com/a/20012536/1048862
|
||||
// https://gist.github.com/anantkamath/623ce7f5432680749e087cf8cfba9b69
|
||||
func detectContainer() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
|
||||
file, err := os.Open("/proc/1/cgroup")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
i := 0
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
i++
|
||||
if i > 1000 {
|
||||
return false
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(parts[2], "docker") ||
|
||||
strings.Contains(parts[2], "lxc") ||
|
||||
strings.Contains(parts[2], "moby") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// initTelemetry initializes the telemetry engine.
|
||||
func initTelemetry() error {
|
||||
uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid")
|
||||
if customUUIDFile := os.Getenv("CADDY_UUID_FILE"); customUUIDFile != "" {
|
||||
uuidFilename = customUUIDFile
|
||||
}
|
||||
|
||||
newUUID := func() uuid.UUID {
|
||||
id := uuid.New()
|
||||
err := os.MkdirAll(caddy.AssetsPath(), 0700)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Persisting instance UUID: %v", err)
|
||||
return id
|
||||
}
|
||||
err = ioutil.WriteFile(uuidFilename, []byte(id.String()), 0600) // human-readable as a string
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Persisting instance UUID: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
var id uuid.UUID
|
||||
|
||||
// load UUID from storage, or create one if we don't have one
|
||||
if uuidFile, err := os.Open(uuidFilename); os.IsNotExist(err) {
|
||||
// no UUID exists yet; create a new one and persist it
|
||||
id = newUUID()
|
||||
} else if err != nil {
|
||||
log.Printf("[ERROR] Loading persistent UUID: %v", err)
|
||||
id = newUUID()
|
||||
} else {
|
||||
defer uuidFile.Close()
|
||||
uuidBytes, err := ioutil.ReadAll(uuidFile)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Reading persistent UUID: %v", err)
|
||||
id = newUUID()
|
||||
} else {
|
||||
id, err = uuid.ParseBytes(uuidBytes)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Parsing UUID: %v", err)
|
||||
id = newUUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse and check the list of disabled metrics
|
||||
var disabledMetricsSlice []string
|
||||
if len(disabledMetrics) > 0 {
|
||||
if len(disabledMetrics) > 1024 {
|
||||
// mitigate disk space exhaustion at the collection endpoint
|
||||
return fmt.Errorf("too many metrics to disable")
|
||||
}
|
||||
disabledMetricsSlice = splitTrim(disabledMetrics, ",")
|
||||
for _, metric := range disabledMetricsSlice {
|
||||
if metric == "instance_id" || metric == "timestamp" || metric == "disabled_metrics" {
|
||||
return fmt.Errorf("instance_id, timestamp, and disabled_metrics cannot be disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initialize telemetry
|
||||
telemetry.Init(id, disabledMetricsSlice)
|
||||
|
||||
// if any metrics were disabled, report which ones (so we know how representative the data is)
|
||||
if len(disabledMetricsSlice) > 0 {
|
||||
telemetry.Set("disabled_metrics", disabledMetricsSlice)
|
||||
log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Split string s into all substrings separated by sep and returns a slice of
|
||||
// the substrings between those separators.
|
||||
//
|
||||
// If s does not contain sep and sep is not empty, Split returns a
|
||||
// slice of length 1 whose only element is s.
|
||||
//
|
||||
// If sep is empty, Split splits after each UTF-8 sequence. If both s
|
||||
// and sep are empty, Split returns an empty slice.
|
||||
//
|
||||
// Each item that in result is trim space and not empty string
|
||||
func splitTrim(s string, sep string) []string {
|
||||
splitItems := strings.Split(s, sep)
|
||||
trimItems := make([]string, 0, len(splitItems))
|
||||
for _, item := range splitItems {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
trimItems = append(trimItems, item)
|
||||
}
|
||||
}
|
||||
return trimItems
|
||||
}
|
||||
|
||||
// LoadEnvFromFile loads additional envs if file provided and exists
|
||||
// Envs in file should be in KEY=VALUE format
|
||||
func LoadEnvFromFile(envFile string) error {
|
||||
if envFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(envFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
envMap, err := ParseEnvFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range envMap {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseEnvFile implements parse logic for environment files
|
||||
func ParseEnvFile(envInput io.Reader) (map[string]string, error) {
|
||||
envMap := make(map[string]string)
|
||||
|
||||
scanner := bufio.NewScanner(envInput)
|
||||
var line string
|
||||
lineNumber := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
lineNumber++
|
||||
|
||||
// skip lines starting with comment
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip empty line
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("Can't parse line %d; line should be in KEY=VALUE format", lineNumber)
|
||||
}
|
||||
|
||||
if strings.Contains(fields[0], " ") {
|
||||
return nil, fmt.Errorf("Can't parse line %d; KEY contains whitespace", lineNumber)
|
||||
}
|
||||
|
||||
key := fields[0]
|
||||
val := fields[1]
|
||||
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("Can't parse line %d; KEY can't be empty string", lineNumber)
|
||||
}
|
||||
envMap[key] = val
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return envMap, nil
|
||||
}
|
||||
|
||||
const appName = "Caddy"
|
||||
|
||||
// Flags that control program flow or startup
|
||||
var (
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
logfile string
|
||||
revoke string
|
||||
version bool
|
||||
plugins bool
|
||||
validate bool
|
||||
serverType string
|
||||
conf string
|
||||
cpu string
|
||||
envFile string
|
||||
fromJSON bool
|
||||
logfile string
|
||||
logTimestamps bool
|
||||
logRollMB int
|
||||
logRollCompress bool
|
||||
revoke string
|
||||
toJSON bool
|
||||
version bool
|
||||
plugins bool
|
||||
printEnv bool
|
||||
validate bool
|
||||
disabledMetrics string
|
||||
)
|
||||
|
||||
// Build information obtained with the help of -ldflags
|
||||
var (
|
||||
appVersion = "(untracked dev build)" // inferred at startup
|
||||
devBuild = true // inferred at startup
|
||||
|
||||
buildDate string // date -u
|
||||
gitTag string // git describe --exact-match HEAD 2> /dev/null
|
||||
gitNearestTag string // git describe --abbrev=0 --tags HEAD
|
||||
gitCommit string // git rev-parse HEAD
|
||||
gitShortStat string // git diff-index --shortstat
|
||||
gitFilesModified string // git diff-index --name-only HEAD
|
||||
)
|
||||
// EnableTelemetry defines whether telemetry is enabled in Run.
|
||||
var EnableTelemetry = true
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -57,3 +59,60 @@ func TestSetCPU(t *testing.T) {
|
||||
runtime.GOMAXPROCS(currentCPU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitTrim(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
output []string
|
||||
sep string
|
||||
}{
|
||||
{"os,arch,cpu,caddy_version", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os,arch,cpu,caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os,,, arch, cpu, caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{", , os, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
{"os, ,, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
|
||||
} {
|
||||
got := splitTrim(test.input, test.sep)
|
||||
if len(got) != len(test.output) {
|
||||
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
|
||||
continue
|
||||
}
|
||||
for j, item := range test.output {
|
||||
if item != got[j] {
|
||||
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{"parsing KEY=VALUE", "PORT=4096", map[string]string{"PORT": "4096"}, false},
|
||||
{"empty KEY", "=4096", nil, true},
|
||||
{"one value", "test", nil, true},
|
||||
{"comments skipped", "#TEST=1\nPORT=8888", map[string]string{"PORT": "8888"}, false},
|
||||
{"empty line", "\nPORT=7777", map[string]string{"PORT": "7777"}, false},
|
||||
{"comments with space skipped", " #TEST=1", map[string]string{}, false},
|
||||
{"KEY with space", "PORT =8888", nil, true},
|
||||
{"only spaces", " ", map[string]string{}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(tt.input)
|
||||
got, err := ParseEnvFile(reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseEnvFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseEnvFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/mholt/caddy/caddy/caddymain"
|
||||
import "github.com/caddyserver/caddy/caddy/caddymain"
|
||||
|
||||
var run = caddymain.Run // replaced for tests
|
||||
|
||||
|
||||
+77
-38
@@ -15,9 +15,13 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -48,6 +52,77 @@ func TestCaddyStartStop(t *testing.T) {
|
||||
}
|
||||
*/
|
||||
|
||||
// CallbackTestContext implements Context interface
|
||||
type CallbackTestContext struct {
|
||||
// If MakeServersFail is set to true then MakeServers returns an error
|
||||
MakeServersFail bool
|
||||
}
|
||||
|
||||
func (h *CallbackTestContext) InspectServerBlocks(name string, sblock []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
return sblock, nil
|
||||
}
|
||||
func (h *CallbackTestContext) MakeServers() ([]Server, error) {
|
||||
if h.MakeServersFail {
|
||||
return make([]Server, 0), fmt.Errorf("MakeServers failed")
|
||||
}
|
||||
return make([]Server, 0), nil
|
||||
}
|
||||
|
||||
func TestCaddyRestartCallbacks(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
restartFail bool
|
||||
expectedCalls []string
|
||||
}{
|
||||
{false, []string{"OnRestart", "OnShutdown"}},
|
||||
{true, []string{"OnRestart", "OnRestartFailed"}},
|
||||
} {
|
||||
serverName := fmt.Sprintf("%v", i)
|
||||
// RegisterServerType to make successful restart possible
|
||||
RegisterServerType(serverName, ServerType{
|
||||
Directives: func() []string { return []string{} },
|
||||
// If MakeServersFail is true then the restart will fail due to context failure
|
||||
NewContext: func(inst *Instance) Context { return &CallbackTestContext{MakeServersFail: test.restartFail} },
|
||||
})
|
||||
c := NewTestController(serverName, "")
|
||||
c.instance = &Instance{
|
||||
serverType: serverName,
|
||||
wg: new(sync.WaitGroup),
|
||||
}
|
||||
|
||||
// Register callbacks which save the calls order
|
||||
calls := make([]string, 0)
|
||||
c.OnRestart(func() error {
|
||||
calls = append(calls, "OnRestart")
|
||||
return nil
|
||||
})
|
||||
c.OnRestartFailed(func() error {
|
||||
calls = append(calls, "OnRestartFailed")
|
||||
return nil
|
||||
})
|
||||
c.OnShutdown(func() error {
|
||||
calls = append(calls, "OnShutdown")
|
||||
return nil
|
||||
})
|
||||
|
||||
_, err := c.instance.Restart(CaddyfileInput{Contents: []byte(""), ServerTypeName: serverName})
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Restart failed: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(calls, test.expectedCalls) {
|
||||
t.Errorf("Test %d: Callbacks expected: %v, got: %v", i, test.expectedCalls, calls)
|
||||
}
|
||||
|
||||
err = c.instance.Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Stop failed: %v", err)
|
||||
}
|
||||
|
||||
c.instance.Wait()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsLoopback(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
@@ -135,39 +210,3 @@ func TestIsInternal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerAddrEqual(t *testing.T) {
|
||||
ln1, err := net.Listen("tcp", "[::]:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln1.Close()
|
||||
ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln2.Close()
|
||||
ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
for i, test := range []struct {
|
||||
ln net.Listener
|
||||
addr string
|
||||
expect bool
|
||||
}{
|
||||
{ln1, ":" + ln2port, false},
|
||||
{ln1, "0.0.0.0:" + ln2port, false},
|
||||
{ln1, "0.0.0.0", false},
|
||||
{ln1, ":" + ln1port, true},
|
||||
{ln1, "0.0.0.0:" + ln1port, true},
|
||||
{ln2, ":" + ln2port, false},
|
||||
{ln2, "127.0.0.1:" + ln1port, false},
|
||||
{ln2, "127.0.0.1", false},
|
||||
{ln2, "127.0.0.1:" + ln2port, true},
|
||||
} {
|
||||
if got, want := listenerAddrEqual(test.ln, test.addr), test.expect; got != want {
|
||||
t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -158,7 +159,9 @@ func TestLexer(t *testing.T) {
|
||||
|
||||
func tokenize(input string) (tokens []Token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
if err := l.load(strings.NewReader(input)); err != nil {
|
||||
log.Printf("[ERROR] load failed: %v", err)
|
||||
}
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
|
||||
+25
-32
@@ -249,9 +249,10 @@ func (p *parser) doImport() error {
|
||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||
importedTokens = p.definedSnippets[importPattern]
|
||||
} else {
|
||||
// make path relative to Caddyfile rather than current working directory (issue #867)
|
||||
// and then use glob to get list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.filename)
|
||||
// make path relative to the file of the _token_ being processed rather
|
||||
// than current working directory (issue #867) and then use glob to get
|
||||
// list of matching filenames
|
||||
absFile, err := filepath.Abs(p.Dispenser.File())
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
}
|
||||
@@ -263,14 +264,19 @@ func (p *parser) doImport() error {
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
|
||||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
|
||||
// See issue #2096 - a pattern with many glob expansions can hang for too long
|
||||
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
|
||||
}
|
||||
matches, err = filepath.Glob(globPattern)
|
||||
|
||||
if err != nil {
|
||||
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.Contains(globPattern, "*") {
|
||||
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
|
||||
if strings.ContainsAny(globPattern, "*?[]") {
|
||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
@@ -283,30 +289,6 @@ func (p *parser) doImport() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var importLine int
|
||||
for i, token := range newTokens {
|
||||
if token.Text == "import" {
|
||||
importLine = token.Line
|
||||
continue
|
||||
}
|
||||
if token.Line == importLine {
|
||||
var abs string
|
||||
if filepath.IsAbs(token.Text) {
|
||||
abs = token.Text
|
||||
} else if !filepath.IsAbs(importFile) {
|
||||
abs = filepath.Join(filepath.Dir(absFile), token.Text)
|
||||
} else {
|
||||
abs = filepath.Join(filepath.Dir(importFile), token.Text)
|
||||
}
|
||||
newTokens[i] = Token{
|
||||
Text: abs,
|
||||
Line: token.Line,
|
||||
File: token.File,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
}
|
||||
@@ -359,7 +341,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := p.Val()
|
||||
dir := replaceEnvVars(p.Val())
|
||||
nesting := 0
|
||||
|
||||
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
|
||||
@@ -380,6 +362,12 @@ func (p *parser) directive() error {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
} else if p.Val() == "import" && p.isNewLine() {
|
||||
if err := p.doImport(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
@@ -439,8 +427,13 @@ func replaceEnvVars(s string) string {
|
||||
func replaceEnvReferences(s, refStart, refEnd string) string {
|
||||
index := strings.Index(s, refStart)
|
||||
for index != -1 {
|
||||
endIndex := strings.Index(s, refEnd)
|
||||
if endIndex != -1 {
|
||||
endIndex := strings.Index(s[index:], refEnd)
|
||||
if endIndex == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
endIndex += index
|
||||
if endIndex > index+len(refStart) {
|
||||
ref := s[index : endIndex+len(refEnd)]
|
||||
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
|
||||
} else {
|
||||
|
||||
+177
-9
@@ -228,6 +228,17 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
{`""`, false, []string{}, map[string]int{}},
|
||||
|
||||
{``, false, []string{}, map[string]int{}},
|
||||
|
||||
// test cases found by fuzzing!
|
||||
{`import }{$"`, true, []string{}, map[string]int{}},
|
||||
{`import /*/*.txt`, true, []string{}, map[string]int{}},
|
||||
{`import /???/?*?o`, true, []string{}, map[string]int{}},
|
||||
{`import /??`, true, []string{}, map[string]int{}},
|
||||
{`import /[a-z]`, true, []string{}, map[string]int{}},
|
||||
{`import {$}`, true, []string{}, map[string]int{}},
|
||||
{`import {%}`, true, []string{}, map[string]int{}},
|
||||
{`import {$$}`, true, []string{}, map[string]int{}},
|
||||
{`import {%%}`, true, []string{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
@@ -360,6 +371,68 @@ func TestRecursiveImport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectiveImport(t *testing.T) {
|
||||
testParseOne := func(input string) (ServerBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
isExpected := func(got ServerBlock) bool {
|
||||
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
|
||||
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens) != 2 {
|
||||
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
|
||||
return false
|
||||
}
|
||||
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["proxy"]) != 8 {
|
||||
t.Errorf("got unexpect tokens: %v", got.Tokens)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
directiveFile, err := filepath.Abs("testdata/directive_import_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
|
||||
prop2 2`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(directiveFile)
|
||||
|
||||
// import from existing file
|
||||
result, err := testParseOne(`localhost
|
||||
dir1
|
||||
proxy {
|
||||
import testdata/directive_import_test
|
||||
transparent
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !isExpected(result) {
|
||||
t.Error("directive import failed")
|
||||
}
|
||||
|
||||
// import from nonexistent file
|
||||
_, err = testParseOne(`localhost
|
||||
dir1
|
||||
proxy {
|
||||
import testdata/nonexistent_file
|
||||
transparent
|
||||
}`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when importing a nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
@@ -441,6 +514,7 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
os.Setenv("PORT", "8080")
|
||||
os.Setenv("ADDRESS", "servername.com")
|
||||
os.Setenv("FOOBAR", "foobar")
|
||||
os.Setenv("PARTIAL_DIR", "r1")
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p := testParser(`{$ADDRESS}`)
|
||||
@@ -449,6 +523,13 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// basic test; unix-style env vars
|
||||
p = testParser(`di{$PARTIAL_DIR}`)
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Keys[0], "dir1"; expected != actual {
|
||||
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// multiple vars per token
|
||||
p = testParser(`{$ADDRESS}:{$PORT}`)
|
||||
blocks, _ = p.parseAll()
|
||||
@@ -507,6 +588,13 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// after end token
|
||||
p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
|
||||
blocks, _ = p.parseAll()
|
||||
if actual, expected := blocks[0].Tokens["answer"][1].Text, "{{ .Name }} foobar"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
@@ -516,15 +604,15 @@ func testParser(input string) parser {
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`(common) {
|
||||
gzip foo
|
||||
errors stderr
|
||||
|
||||
}
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
errors stderr
|
||||
}
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -550,3 +638,83 @@ func TestSnippets(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
||||
file, err := ioutil.TempFile("", t.Name())
|
||||
if err != nil {
|
||||
panic(err) // get a stack trace so we know where this was called from.
|
||||
}
|
||||
if _, err := file.WriteString(str); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return file.Name()
|
||||
}
|
||||
|
||||
func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
|
||||
fileName := writeStringToTempFileOrDie(t, `
|
||||
http://example.com {
|
||||
# This isn't an import directive, it's just an arg with value 'import'
|
||||
basicauth / import password
|
||||
}
|
||||
`)
|
||||
// Parse the root file that imports the other one.
|
||||
p := testParser(`import ` + fileName)
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
auth := blocks[0].Tokens["basicauth"]
|
||||
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
|
||||
if line != "basicauth / import password" {
|
||||
// Previously, it would be changed to:
|
||||
// basicauth / import /path/to/test/dir/password
|
||||
// referencing a file that (probably) doesn't exist and changing the
|
||||
// password!
|
||||
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippetAcrossMultipleFiles(t *testing.T) {
|
||||
// Make the derived Caddyfile that expects (common) to be defined.
|
||||
fileName := writeStringToTempFileOrDie(t, `
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
|
||||
// Parse the root file that defines (common) and then imports the other one.
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
}
|
||||
import ` + fileName + `
|
||||
`)
|
||||
|
||||
blocks, err := p.parseAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
t.Log(b.Keys)
|
||||
t.Log(b.Tokens)
|
||||
}
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
|
||||
}
|
||||
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
|
||||
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
if len(blocks[0].Tokens) != 1 {
|
||||
t.Fatalf("Server block should have tokens from import")
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,15 @@ import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// BasicAuth is middleware to protect resources with a username and password.
|
||||
@@ -51,6 +52,15 @@ type BasicAuth struct {
|
||||
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
var protected, isAuthenticated bool
|
||||
var realm string
|
||||
var username string
|
||||
var password string
|
||||
var ok bool
|
||||
|
||||
// do not check for basic auth on OPTIONS call
|
||||
if r.Method == http.MethodOptions {
|
||||
// Pass-through when no paths match
|
||||
return a.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
for _, rule := range a.Rules {
|
||||
for _, res := range rule.Resources {
|
||||
@@ -63,7 +73,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
realm = rule.Realm
|
||||
|
||||
// parse auth header
|
||||
username, password, ok := r.BasicAuth()
|
||||
username, password, ok = r.BasicAuth()
|
||||
|
||||
// check credentials
|
||||
if !ok ||
|
||||
@@ -94,7 +104,13 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
realm = "Restricted"
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
|
||||
return http.StatusUnauthorized, nil
|
||||
|
||||
// Get a replacer so we can provide basic info for the authentication error.
|
||||
repl := httpserver.NewReplacer(r, nil, "-")
|
||||
repl.Set("user", username)
|
||||
errstr := repl.Replace("BasicAuth: user \"{user}\" was not found or password was incorrect. {remote} {host} {uri} {proto}")
|
||||
err := fmt.Errorf("%s", errstr)
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
|
||||
// Pass-through when no paths match
|
||||
@@ -178,11 +194,15 @@ func PlainMatcher(passw string) PasswordMatcher {
|
||||
// compare hashes of equal length instead of actual password
|
||||
// to avoid leaking password length
|
||||
passwHash := sha1.New()
|
||||
passwHash.Write([]byte(passw))
|
||||
if _, err := passwHash.Write([]byte(passw)); err != nil {
|
||||
log.Printf("[ERROR] unable to write password hash: %v", err)
|
||||
}
|
||||
passwSum := passwHash.Sum(nil)
|
||||
return func(pw string) bool {
|
||||
pwHash := sha1.New()
|
||||
pwHash.Write([]byte(pw))
|
||||
if _, err := pwHash.Write([]byte(pw)); err != nil {
|
||||
log.Printf("[ERROR] unable to write password hash: %v", err)
|
||||
}
|
||||
pwSum := pwHash.Sum(nil)
|
||||
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
@@ -60,17 +61,18 @@ func TestBasicAuth(t *testing.T) {
|
||||
result int
|
||||
user string
|
||||
password string
|
||||
haserror bool
|
||||
}
|
||||
|
||||
tests := []testType{
|
||||
{"/testing", http.StatusOK, "okuser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "baduser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "badpass"},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS"},
|
||||
{"/testing", http.StatusUnauthorized, "", "okpass"},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", ""},
|
||||
{"/testing", http.StatusUnauthorized, "", ""},
|
||||
{"/testing", http.StatusOK, "okuser", "okpass", false},
|
||||
{"/testing", http.StatusUnauthorized, "baduser", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "badpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS", true},
|
||||
{"/testing", http.StatusUnauthorized, "", "okpass", true},
|
||||
{"/testing", http.StatusUnauthorized, "okuser", "", true},
|
||||
{"/testing", http.StatusUnauthorized, "", "", true},
|
||||
}
|
||||
|
||||
var test testType
|
||||
@@ -89,7 +91,9 @@ func TestBasicAuth(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
|
||||
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
|
||||
}
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected status code %d but was %d",
|
||||
@@ -106,7 +110,7 @@ func TestBasicAuth(t *testing.T) {
|
||||
}
|
||||
} else {
|
||||
if req.Header.Get("Authorization") == "" {
|
||||
// see issue #1508: https://github.com/mholt/caddy/issues/1508
|
||||
// see issue #1508: https://github.com/caddyserver/caddy/issues/1508
|
||||
t.Errorf("Test %d: Expected Authorization header to be retained after successful auth, but was empty", i)
|
||||
}
|
||||
}
|
||||
@@ -124,16 +128,17 @@ func TestMultipleOverlappingRules(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
haserror bool
|
||||
}{
|
||||
{"/t", http.StatusOK, "t:p1"},
|
||||
{"/t/t", http.StatusOK, "t:p1"},
|
||||
{"/t/t", http.StatusOK, "t1:p2"},
|
||||
{"/a", http.StatusOK, "t1:p2"},
|
||||
{"/t/t", http.StatusUnauthorized, "t1:p3"},
|
||||
{"/t", http.StatusUnauthorized, "t1:p2"},
|
||||
{"/t", http.StatusOK, "t:p1", false},
|
||||
{"/t/t", http.StatusOK, "t:p1", false},
|
||||
{"/t/t", http.StatusOK, "t1:p2", false},
|
||||
{"/a", http.StatusOK, "t1:p2", false},
|
||||
{"/t/t", http.StatusUnauthorized, "t1:p3", true},
|
||||
{"/t", http.StatusUnauthorized, "t1:p2", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
@@ -148,7 +153,9 @@ func TestMultipleOverlappingRules(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
}
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
|
||||
@@ -194,3 +201,30 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionsMethod(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: httpserver.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "username", Password: PlainMatcher("password"), Resources: []string{"/testing"}},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
// add basic auth with invalid username
|
||||
// and password to make sure basic auth is ignored
|
||||
req.SetBasicAuth("invaliduser", "invalidpassword")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not ServeHTTP: %v", err)
|
||||
}
|
||||
if result != http.StatusOK {
|
||||
t.Errorf("Expected status code %d but was %d", http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ package basicauth
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package bind
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -32,7 +32,7 @@ func setupBind(c *caddy.Controller) error {
|
||||
if !c.Args(&config.ListenHost) {
|
||||
return c.ArgErr()
|
||||
}
|
||||
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
config.TLS.Manager.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ package bind
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetupBind(t *testing.T) {
|
||||
@@ -32,7 +32,7 @@ func TestSetupBind(t *testing.T) {
|
||||
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want {
|
||||
if got, want := cfg.TLS.Manager.ListenHost, "1.2.3.4"; got != want {
|
||||
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
+19
-19
@@ -29,9 +29,9 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -59,34 +59,34 @@ type Config struct {
|
||||
|
||||
// A Listing is the context used to fill out a template.
|
||||
type Listing struct {
|
||||
// The name of the directory (the last element of the path)
|
||||
// The name of the directory (the last element of the path).
|
||||
Name string
|
||||
|
||||
// The full path of the request
|
||||
// The full path of the request.
|
||||
Path string
|
||||
|
||||
// Whether the parent directory is browsable
|
||||
// Whether the parent directory is browse-able.
|
||||
CanGoUp bool
|
||||
|
||||
// The items (files and folders) in the path
|
||||
// The items (files and folders) in the path.
|
||||
Items []FileInfo
|
||||
|
||||
// The number of directories in the listing
|
||||
// The number of directories in the listing.
|
||||
NumDirs int
|
||||
|
||||
// The number of files (items that aren't directories) in the listing
|
||||
// The number of files (items that aren't directories) in the listing.
|
||||
NumFiles int
|
||||
|
||||
// Which sorting order is used
|
||||
// Which sorting order is used.
|
||||
Sort string
|
||||
|
||||
// And which order
|
||||
// And which order.
|
||||
Order string
|
||||
|
||||
// If ≠0 then Items have been limited to that many elements
|
||||
// If ≠0 then Items have been limited to that many elements.
|
||||
ItemsLimitedTo int
|
||||
|
||||
// Optional custom variables for use in browse templates
|
||||
// Optional custom variables for use in browse templates.
|
||||
User interface{}
|
||||
|
||||
httpserver.Context
|
||||
@@ -186,7 +186,7 @@ func (l bySize) Less(i, j int) bool {
|
||||
iSize, jSize := l.Items[i].Size, l.Items[j].Size
|
||||
|
||||
// Directory sizes depend on the filesystem implementation,
|
||||
// which is opaque to a visitor, and should indeed does not change if the operator choses to change the fs.
|
||||
// which is opaque to a visitor, and should indeed does not change if the operator chooses to change the fs.
|
||||
// For a consistent user experience directories are pulled to the front…
|
||||
if l.Items[i].IsDir {
|
||||
iSize = directoryOffset
|
||||
@@ -244,7 +244,7 @@ func (l Listing) applySort() {
|
||||
|
||||
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config *Config) (Listing, bool) {
|
||||
var (
|
||||
fileinfos []FileInfo
|
||||
fileInfos []FileInfo
|
||||
dirCount, fileCount int
|
||||
hasIndexFile bool
|
||||
)
|
||||
@@ -272,14 +272,14 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
|
||||
continue
|
||||
}
|
||||
|
||||
url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
|
||||
fileinfos = append(fileinfos, FileInfo{
|
||||
fileInfos = append(fileInfos, FileInfo{
|
||||
IsDir: isDir,
|
||||
IsSymlink: isSymlink(f),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: url.String(),
|
||||
URL: u.String(),
|
||||
ModTime: f.ModTime().UTC(),
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
@@ -289,7 +289,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
|
||||
Name: path.Base(urlPath),
|
||||
Path: urlPath,
|
||||
CanGoUp: canGoUp,
|
||||
Items: fileinfos,
|
||||
Items: fileInfos,
|
||||
NumDirs: dirCount,
|
||||
NumFiles: fileCount,
|
||||
}, hasIndexFile
|
||||
@@ -504,7 +504,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
|
||||
|
||||
}
|
||||
|
||||
buf.WriteTo(w)
|
||||
_, _ = buf.WriteTo(w)
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
const testDirPrefix = "caddy_browse_test"
|
||||
@@ -366,25 +366,25 @@ func TestBrowseJson(t *testing.T) {
|
||||
}
|
||||
|
||||
actualJSONResponse := rec.Body.String()
|
||||
copyOflisting := listing
|
||||
copyOfListing := listing
|
||||
if test.SortBy == "" {
|
||||
copyOflisting.Sort = "name"
|
||||
copyOfListing.Sort = "name"
|
||||
} else {
|
||||
copyOflisting.Sort = test.SortBy
|
||||
copyOfListing.Sort = test.SortBy
|
||||
}
|
||||
if test.OrderBy == "" {
|
||||
copyOflisting.Order = "asc"
|
||||
copyOfListing.Order = "asc"
|
||||
} else {
|
||||
copyOflisting.Order = test.OrderBy
|
||||
copyOfListing.Order = test.OrderBy
|
||||
}
|
||||
|
||||
copyOflisting.applySort()
|
||||
copyOfListing.applySort()
|
||||
|
||||
limit := test.Limit
|
||||
if limit <= len(copyOflisting.Items) && limit > 0 {
|
||||
marsh, err = json.Marshal(copyOflisting.Items[:limit])
|
||||
if limit <= len(copyOfListing.Items) && limit > 0 {
|
||||
marsh, err = json.Marshal(copyOfListing.Items[:limit])
|
||||
} else { // if the 'limit' query is empty, or has the wrong value, list everything
|
||||
marsh, err = json.Marshal(copyOflisting.Items)
|
||||
marsh, err = json.Marshal(copyOfListing.Items)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
+22
-15
@@ -20,9 +20,9 @@ import (
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -125,6 +125,7 @@ const defaultTemplate = `<!DOCTYPE html>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -145,12 +146,12 @@ header,
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 5%;
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 5%;
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
header {
|
||||
@@ -241,20 +242,20 @@ td {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 100%;
|
||||
td:nth-child(2) {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
td:nth-child(3) {
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:first-child svg {
|
||||
td:nth-child(2) svg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -301,12 +302,12 @@ footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
td:nth-child(2) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
th:nth-child(3),
|
||||
td:nth-child(3) {
|
||||
padding-right: 5%;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -325,7 +326,7 @@ footer {
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body onload='filter()'>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
|
||||
<defs>
|
||||
<!-- Folder -->
|
||||
@@ -390,6 +391,7 @@ footer {
|
||||
<table aria-describedby="summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
||||
<a href="?sort=namedirfirst&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
|
||||
@@ -425,11 +427,13 @@ footer {
|
||||
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a>
|
||||
{{- end}}
|
||||
</th>
|
||||
<th class="hideable"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- if .CanGoUp}}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="..">
|
||||
<span class="goup">Go up</span>
|
||||
@@ -437,10 +441,12 @@ footer {
|
||||
</td>
|
||||
<td>—</td>
|
||||
<td class="hideable">—</td>
|
||||
<td class="hideable"></td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
{{- range .Items}}
|
||||
<tr class="file">
|
||||
<td></td>
|
||||
<td>
|
||||
<a href="{{html .URL}}">
|
||||
{{- if .IsDir}}
|
||||
@@ -457,6 +463,7 @@ footer {
|
||||
<td data-order="{{.Size}}">{{.HumanSize}}</td>
|
||||
{{- end}}
|
||||
<td class="hideable"><time datetime="{{.HumanModTime "2006-01-02T15:04:05Z"}}">{{.HumanModTime "01/02/2006 03:04:05 PM -07:00"}}</time></td>
|
||||
<td class="hideable"></td>
|
||||
</tr>
|
||||
{{- end}}
|
||||
</tbody>
|
||||
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -53,7 +53,7 @@ func TestSetup(t *testing.T) {
|
||||
// test case #1 tests instantiation of Config with default values
|
||||
{"browse /", []string{"/"}, false},
|
||||
|
||||
// test case #2 tests detectaction of custom template
|
||||
// test case #2 tests detection of custom template
|
||||
{"browse . " + tempTemplatePath, []string{"."}, false},
|
||||
|
||||
// test case #3 tests detection of non-existent template
|
||||
|
||||
+28
-28
@@ -16,34 +16,34 @@ package caddyhttp
|
||||
|
||||
import (
|
||||
// plug in the server
|
||||
_ "github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
|
||||
// plug in the standard directives
|
||||
_ "github.com/mholt/caddy/caddyhttp/basicauth"
|
||||
_ "github.com/mholt/caddy/caddyhttp/bind"
|
||||
_ "github.com/mholt/caddy/caddyhttp/browse"
|
||||
_ "github.com/mholt/caddy/caddyhttp/errors"
|
||||
_ "github.com/mholt/caddy/caddyhttp/expvar"
|
||||
_ "github.com/mholt/caddy/caddyhttp/extensions"
|
||||
_ "github.com/mholt/caddy/caddyhttp/fastcgi"
|
||||
_ "github.com/mholt/caddy/caddyhttp/gzip"
|
||||
_ "github.com/mholt/caddy/caddyhttp/header"
|
||||
_ "github.com/mholt/caddy/caddyhttp/index"
|
||||
_ "github.com/mholt/caddy/caddyhttp/internalsrv"
|
||||
_ "github.com/mholt/caddy/caddyhttp/limits"
|
||||
_ "github.com/mholt/caddy/caddyhttp/log"
|
||||
_ "github.com/mholt/caddy/caddyhttp/markdown"
|
||||
_ "github.com/mholt/caddy/caddyhttp/mime"
|
||||
_ "github.com/mholt/caddy/caddyhttp/pprof"
|
||||
_ "github.com/mholt/caddy/caddyhttp/proxy"
|
||||
_ "github.com/mholt/caddy/caddyhttp/push"
|
||||
_ "github.com/mholt/caddy/caddyhttp/redirect"
|
||||
_ "github.com/mholt/caddy/caddyhttp/requestid"
|
||||
_ "github.com/mholt/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/mholt/caddy/caddyhttp/root"
|
||||
_ "github.com/mholt/caddy/caddyhttp/status"
|
||||
_ "github.com/mholt/caddy/caddyhttp/templates"
|
||||
_ "github.com/mholt/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/mholt/caddy/caddyhttp/websocket"
|
||||
_ "github.com/mholt/caddy/onevent"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/basicauth"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/bind"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/browse"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/errors"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/expvar"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/extensions"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/fastcgi"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/gzip"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/header"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/index"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/internalsrv"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/limits"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/log"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/markdown"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/mime"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/pprof"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/proxy"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/push"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/requestid"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/rewrite"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/root"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/status"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/templates"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/caddyserver/caddy/caddyhttp/websocket"
|
||||
_ "github.com/caddyserver/caddy/onevent"
|
||||
)
|
||||
|
||||
@@ -18,16 +18,16 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
// TODO: this test could be improved; the purpose is to
|
||||
// ensure that the standard plugins are in fact plugged in
|
||||
// and registered properly; this is a quick/naive way to do it.
|
||||
func TestStandardPlugins(t *testing.T) {
|
||||
numStandardPlugins := 31 // importing caddyhttp plugs in this many plugins
|
||||
numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins
|
||||
s := caddy.DescribePlugins()
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
|
||||
if got, want := strings.Count(s, "\n"), numStandardPlugins+4; got != want {
|
||||
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,9 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -50,7 +49,7 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
||||
status, err := h.Next.ServeHTTP(w, r)
|
||||
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
|
||||
errMsg := fmt.Sprintf("[ERROR %d %s] %v", status, r.URL.Path, err)
|
||||
if h.Debug {
|
||||
// Write error to response instead of to log
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
@@ -79,8 +78,7 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
|
||||
errorPage, err := os.Open(pagePath)
|
||||
if err != nil {
|
||||
// An additional error handling an error... <insert grumpy cat here>
|
||||
h.Log.Printf("%s [NOTICE %d %s] could not load error page: %v",
|
||||
time.Now().Format(timeFormat), code, r.URL.String(), err)
|
||||
h.Log.Printf("[NOTICE %d %s] could not load error page: %v", code, r.URL.String(), err)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
return
|
||||
}
|
||||
@@ -93,8 +91,7 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
|
||||
|
||||
if err != nil {
|
||||
// Epic fail... sigh.
|
||||
h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v",
|
||||
time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err)
|
||||
h.Log.Printf("[NOTICE %d %s] could not respond with %s: %v", code, r.URL.String(), pagePath, err)
|
||||
httpserver.DefaultErrorFunc(w, r, code)
|
||||
}
|
||||
|
||||
@@ -142,13 +139,13 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Trim file path
|
||||
delim := "/caddy/"
|
||||
delim := "/github.com/caddyserver/caddy/"
|
||||
pkgPathPos := strings.Index(file, delim)
|
||||
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
|
||||
file = file[pkgPathPos+len(delim):]
|
||||
}
|
||||
|
||||
panicMsg := fmt.Sprintf("%s [PANIC %s] %s:%d - %v", time.Now().Format(timeFormat), r.URL.String(), file, line, rec)
|
||||
panicMsg := fmt.Sprintf("[PANIC %s] %s:%d - %v", r.URL.String(), file, line, rec)
|
||||
if h.Debug {
|
||||
// Write error and stack trace to the response rather than to a log
|
||||
var stackBuf [4096]byte
|
||||
@@ -160,5 +157,3 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
h.errorPage(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
const timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
@@ -153,7 +153,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
|
||||
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") {
|
||||
if !strings.Contains(body, "[PANIC /]") {
|
||||
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, panicMsg) {
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new errors middleware instance.
|
||||
@@ -123,6 +123,10 @@ func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
return handler, c.Errf("Only 1 Argument expected for errors directive")
|
||||
}
|
||||
|
||||
// Configuration may be in a block
|
||||
err := optionalBlock()
|
||||
if err != nil {
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -179,6 +179,11 @@ func TestErrorsParse(t *testing.T) {
|
||||
* generic_error.html
|
||||
* generic_error.html
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
{`errors /path error.txt {
|
||||
404
|
||||
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
|
||||
{`errors /path error.txt`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// ExpVar is a simple struct to hold expvar's configuration
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestExpVar(t *testing.T) {
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -17,8 +17,8 @@ package expvar
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Ext can assume an extension from clean URLs.
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -17,8 +17,8 @@ package extensions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -35,9 +35,9 @@ import (
|
||||
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// Handler is a middleware type that can handle requests as a FastCGI client.
|
||||
@@ -85,6 +85,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
// but we also want to be flexible for the script we proxy to.
|
||||
|
||||
fpath := r.URL.Path
|
||||
// We trim those characters because they are served as plain text if appended after .php on Windows
|
||||
fpath = strings.TrimRight(fpath, " .")
|
||||
|
||||
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
|
||||
fpath = idx
|
||||
@@ -102,7 +104,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// These criteria work well in this order for PHP sites
|
||||
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
|
||||
// We lower path and Ext as on Windows, the system is case insensitive, so .PHP is served as .php
|
||||
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(strings.ToLower(fpath), strings.ToLower(rule.Ext)) {
|
||||
|
||||
// Create environment for CGI script
|
||||
env, err := h.buildEnv(r, rule, fpath)
|
||||
@@ -242,9 +245,6 @@ func (h Handler) exists(path string) bool {
|
||||
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
|
||||
var env map[string]string
|
||||
|
||||
// Get absolute path of requested resource
|
||||
absPath := filepath.Join(rule.Root, fpath)
|
||||
|
||||
// Separate remote IP and port; more lenient than net.SplitHostPort
|
||||
var ip, port string
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
|
||||
@@ -266,11 +266,13 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
docURI := fpath[:splitPos+len(rule.SplitPath)]
|
||||
pathInfo := fpath[splitPos+len(rule.SplitPath):]
|
||||
scriptName := fpath
|
||||
scriptFilename := absPath
|
||||
|
||||
// Strip PATH_INFO from SCRIPT_NAME
|
||||
scriptName = strings.TrimSuffix(scriptName, pathInfo)
|
||||
|
||||
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
|
||||
scriptFilename := filepath.Join(rule.Root, scriptName)
|
||||
|
||||
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
|
||||
// have difficulty discovering its URL.
|
||||
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
|
||||
@@ -286,6 +288,11 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
// Retrieve name of remote user that was set by some downstream middleware such as basicauth.
|
||||
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
|
||||
|
||||
requestScheme := "http"
|
||||
if r.TLS != nil {
|
||||
requestScheme = "https"
|
||||
}
|
||||
|
||||
// Some variables are unused but cleared explicitly to prevent
|
||||
// the parent environment from interfering.
|
||||
env = map[string]string{
|
||||
@@ -302,6 +309,7 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
"REMOTE_IDENT": "", // Not used
|
||||
"REMOTE_USER": remoteUser,
|
||||
"REQUEST_METHOD": r.Method,
|
||||
"REQUEST_SCHEME": requestScheme,
|
||||
"SERVER_NAME": h.ServerName,
|
||||
"SERVER_PORT": h.ServerPort,
|
||||
"SERVER_PROTOCOL": r.Proto,
|
||||
|
||||
@@ -16,18 +16,20 @@ package fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/fcgi"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
@@ -38,11 +40,20 @@ func TestServeHTTP(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create listener for test: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
w.Write([]byte(body))
|
||||
}))
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", bodyLenStr)
|
||||
_, err := w.Write([]byte(body))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to write header: %v", err)
|
||||
}
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
@@ -146,7 +157,7 @@ func TestBuildEnv(t *testing.T) {
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
}
|
||||
url, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
|
||||
u, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
|
||||
if err != nil {
|
||||
t.Error("Unexpected error:", err.Error())
|
||||
}
|
||||
@@ -154,7 +165,7 @@ func TestBuildEnv(t *testing.T) {
|
||||
var newReq = func() *http.Request {
|
||||
r := http.Request{
|
||||
Method: "GET",
|
||||
URL: url,
|
||||
URL: u,
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
@@ -238,6 +249,21 @@ func TestBuildEnv(t *testing.T) {
|
||||
envExpected = newEnv()
|
||||
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 7. Test SCRIPT_NAME,SCRIPT_FILENAME do not include PATH_INFO
|
||||
fpath = "/fgci_test.php/extra/paths"
|
||||
r = newReq()
|
||||
envExpected = newEnv()
|
||||
envExpected["PATH_INFO"] = "/extra/paths"
|
||||
envExpected["SCRIPT_NAME"] = "/fgci_test.php"
|
||||
envExpected["SCRIPT_FILENAME"] = filepath.FromSlash("/fgci_test.php")
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
|
||||
// 8. Test REQUEST_SCHEME in env
|
||||
r = newReq()
|
||||
envExpected = newEnv()
|
||||
envExpected["REQUEST_SCHEME"] = "http"
|
||||
testBuildEnv(r, rule, fpath, envExpected)
|
||||
}
|
||||
|
||||
func TestReadTimeout(t *testing.T) {
|
||||
@@ -258,7 +284,7 @@ func TestReadTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
@@ -277,11 +303,16 @@ func TestReadTimeout(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
wg.Add(1)
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(test.sleep)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
wg.Done()
|
||||
}))
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(test.sleep)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
wg.Done()
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := handler.ServeHTTP(w, r)
|
||||
if test.shouldErr {
|
||||
@@ -318,7 +349,7 @@ func TestSendTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
defer func() { _ = listener.Close() }()
|
||||
|
||||
handler := Handler{
|
||||
Next: nil,
|
||||
@@ -336,9 +367,14 @@ func TestSendTimeout(t *testing.T) {
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
go func() {
|
||||
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] unable to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := handler.ServeHTTP(w, r)
|
||||
if test.shouldErr {
|
||||
|
||||
@@ -175,15 +175,13 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||
// FCGIClient implements a FastCGI client, which is a standard for
|
||||
// interfacing external applications with Web servers.
|
||||
type FCGIClient struct {
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
keepAlive bool
|
||||
reqID uint16
|
||||
readTimeout time.Duration
|
||||
sendTimeout time.Duration
|
||||
mutex sync.Mutex
|
||||
rwc io.ReadWriteCloser
|
||||
h header
|
||||
buf bytes.Buffer
|
||||
stderr bytes.Buffer
|
||||
keepAlive bool
|
||||
reqID uint16
|
||||
}
|
||||
|
||||
// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
|
||||
@@ -216,7 +214,7 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) {
|
||||
return DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
// Close closes fcgi connnection
|
||||
// Close closes fcgi connection
|
||||
func (c *FCGIClient) Close() {
|
||||
c.rwc.Close()
|
||||
}
|
||||
@@ -397,7 +395,7 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
|
||||
|
||||
body := newWriter(c, Stdin)
|
||||
if req != nil {
|
||||
io.Copy(body, req)
|
||||
_, _ = io.Copy(body, req)
|
||||
}
|
||||
body.Close()
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import (
|
||||
// test fcgi protocol includes:
|
||||
// Get, Post, Post in multipart/form-data, and Post with files
|
||||
// each key should be the md5 of the value or the file uploaded
|
||||
// sepicify remote fcgi responer ip:port to test with php
|
||||
// specify remote fcgi responder ip:port to test with php
|
||||
// test failed if the remote fcgi(script) failed md5 verification
|
||||
// and output "FAILED" in response
|
||||
const (
|
||||
@@ -59,7 +59,9 @@ type FastCGIServer struct{}
|
||||
|
||||
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
|
||||
req.ParseMultipartForm(100000000)
|
||||
if err := req.ParseMultipartForm(100000000); err != nil {
|
||||
log.Printf("[ERROR] failed to parse: %v", err)
|
||||
}
|
||||
|
||||
stat := "PASSED"
|
||||
fmt.Fprintln(resp, "-")
|
||||
@@ -68,15 +70,15 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
length := 0
|
||||
for k0, v0 := range req.Form {
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0[0])
|
||||
md5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
_, _ = io.WriteString(h, v0[0])
|
||||
_md5 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
length += len(k0)
|
||||
length += len(v0[0])
|
||||
|
||||
// echo error when key != md5(val)
|
||||
if md5 != k0 {
|
||||
fmt.Fprintln(resp, "server:err ", md5, k0)
|
||||
// echo error when key != _md5(val)
|
||||
if _md5 != k0 {
|
||||
fmt.Fprintln(resp, "server:err ", _md5, k0)
|
||||
stat = "FAILED"
|
||||
}
|
||||
}
|
||||
@@ -197,8 +199,12 @@ func generateRandFile(size int) (p string, m string) {
|
||||
for i := 0; i < size/16; i++ {
|
||||
buf := make([]byte, 16)
|
||||
binary.PutVarint(buf, rand.Int63())
|
||||
fo.Write(buf)
|
||||
h.Write(buf)
|
||||
if _, err := fo.Write(buf); err != nil {
|
||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||
}
|
||||
if _, err := h.Write(buf); err != nil {
|
||||
log.Printf("[ERROR] failed to write buffer: %v\n", err)
|
||||
}
|
||||
}
|
||||
m = fmt.Sprintf("%x", h.Sum(nil))
|
||||
return
|
||||
@@ -214,12 +220,13 @@ func DisabledTest(t *testing.T) {
|
||||
go func() {
|
||||
listener, err := net.Listen("tcp", ipPort)
|
||||
if err != nil {
|
||||
// handle error
|
||||
log.Println("listener creation failed: ", err)
|
||||
}
|
||||
|
||||
srv := new(FastCGIServer)
|
||||
fcgi.Serve(listener, srv)
|
||||
if err := fcgi.Serve(listener, srv); err != nil {
|
||||
log.Print("[ERROR] failed to start server: ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -244,7 +251,7 @@ func DisabledTest(t *testing.T) {
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 256)
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0)
|
||||
_, _ = io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
data += k0 + "=" + url.QueryEscape(v0) + "&"
|
||||
}
|
||||
@@ -261,7 +268,7 @@ func DisabledTest(t *testing.T) {
|
||||
for i := 0x00; i < 0xff; i++ {
|
||||
v0 := strings.Repeat(string(i), 4096)
|
||||
h := md5.New()
|
||||
io.WriteString(h, v0)
|
||||
_, _ = io.WriteString(h, v0)
|
||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||
p1[k0] = v0
|
||||
}
|
||||
@@ -285,6 +292,10 @@ func DisabledTest(t *testing.T) {
|
||||
delete(f0, "m0")
|
||||
sendFcgi(1, fcgiParams, nil, nil, f0)
|
||||
|
||||
os.Remove(path0)
|
||||
os.Remove(path1)
|
||||
if err := os.Remove(path0); err != nil {
|
||||
log.Println("[ERROR] failed to remove path: ", err)
|
||||
}
|
||||
if err := os.Remove(path1); err != nil {
|
||||
log.Println("[ERROR] failed to remove path: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
var defaultTimeout = 60 * time.Second
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
@@ -76,8 +78,11 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
|
||||
rule := Rule{
|
||||
Root: absRoot,
|
||||
Path: args[0],
|
||||
Root: absRoot,
|
||||
Path: args[0],
|
||||
ConnectTimeout: defaultTimeout,
|
||||
ReadTimeout: defaultTimeout,
|
||||
SendTimeout: defaultTimeout,
|
||||
}
|
||||
|
||||
upstreams := []string{args[1]}
|
||||
|
||||
@@ -19,9 +19,10 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -53,6 +54,18 @@ func TestSetup(t *testing.T) {
|
||||
if addr != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].ConnectTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].ReadTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].SendTimeout != 60*time.Second {
|
||||
t.Errorf("Expected default value of 60 seconds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
@@ -64,21 +77,23 @@ func TestFastcgiParse(t *testing.T) {
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []Rule{{
|
||||
Path: "/blog",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
Path: "/blog",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
Path: "/",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
@@ -91,6 +106,17 @@ func TestFastcgiParse(t *testing.T) {
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
IgnoredSubPaths: []string{"/admin", "/user"},
|
||||
SendTimeout: 60 * time.Second,
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
send_timeout 30s
|
||||
}`,
|
||||
false, []Rule{{
|
||||
Path: "/",
|
||||
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
|
||||
Ext: "",
|
||||
IndexFiles: []string{},
|
||||
SendTimeout: 30 * time.Second,
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
@@ -146,6 +172,11 @@ func TestFastcgiParse(t *testing.T) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IgnoredSubPaths to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SendTimeout != test.expectedFastcgiConfig[j].SendTimeout {
|
||||
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SendTimeout, actualFastcgiConfig.SendTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+45
-13
@@ -17,12 +17,13 @@
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -65,21 +66,31 @@ outer:
|
||||
}
|
||||
}
|
||||
|
||||
// gzipWriter modifies underlying writer at init,
|
||||
// use a discard writer instead to leave ResponseWriter in
|
||||
// original form.
|
||||
gzipWriter := getWriter(c.Level)
|
||||
defer putWriter(c.Level, gzipWriter)
|
||||
// In order to avoid unused memory allocation, gzip.putWriter only be called when gzip compression happened.
|
||||
// see https://github.com/caddyserver/caddy/issues/2395
|
||||
gz := &gzipResponseWriter{
|
||||
Writer: gzipWriter,
|
||||
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
|
||||
newWriter: func() io.Writer {
|
||||
// gzipWriter modifies underlying writer at init,
|
||||
// use a discard writer instead to leave ResponseWriter in
|
||||
// original form.
|
||||
return getWriter(c.Level)
|
||||
},
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if gzWriter, ok := gz.internalWriter.(*gzip.Writer); ok {
|
||||
putWriter(c.Level, gzWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
var rw http.ResponseWriter
|
||||
// if no response filter is used
|
||||
if len(c.ResponseFilters) == 0 {
|
||||
// replace discard writer with ResponseWriter
|
||||
gzipWriter.Reset(w)
|
||||
if gzWriter, ok := gz.Writer().(*gzip.Writer); ok {
|
||||
gzWriter.Reset(w)
|
||||
}
|
||||
rw = gz
|
||||
} else {
|
||||
// wrap gzip writer with ResponseFilterWriter
|
||||
@@ -103,12 +114,13 @@ outer:
|
||||
return g.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// gzipResponeWriter wraps the underlying Write method
|
||||
// gzipResponseWriter wraps the underlying Write method
|
||||
// with a gzip.Writer to compress the output.
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
internalWriter io.Writer
|
||||
*httpserver.ResponseWriterWrapper
|
||||
statusCodeWritten bool
|
||||
newWriter func() io.Writer
|
||||
}
|
||||
|
||||
// WriteHeader wraps the underlying WriteHeader method to prevent
|
||||
@@ -118,7 +130,19 @@ type gzipResponseWriter struct {
|
||||
func (w *gzipResponseWriter) WriteHeader(code int) {
|
||||
w.Header().Del("Content-Length")
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
varyList, exist := w.Header()["Vary"]
|
||||
shouldAddVary := true
|
||||
if exist {
|
||||
for _, vary := range varyList {
|
||||
if vary == "Accept-Encoding" {
|
||||
shouldAddVary = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldAddVary {
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
}
|
||||
originalEtag := w.Header().Get("ETag")
|
||||
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
|
||||
w.Header().Set("ETag", "W/"+originalEtag)
|
||||
@@ -135,9 +159,17 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
if !w.statusCodeWritten {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
n, err := w.Writer.Write(b)
|
||||
n, err := w.Writer().Write(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
//Writer use a lazy way to initialize Writer
|
||||
func (w *gzipResponseWriter) Writer() io.Writer {
|
||||
if w.internalWriter == nil {
|
||||
w.internalWriter = w.newWriter()
|
||||
}
|
||||
return w.internalWriter
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var _ httpserver.HTTPInterfaces = (*gzipResponseWriter)(nil)
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestGzipHandler(t *testing.T) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// RequestFilter determines if a request should be gzipped.
|
||||
@@ -30,7 +30,7 @@ type RequestFilter interface {
|
||||
|
||||
// defaultExtensions is the list of default extensions for which to enable gzipping.
|
||||
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp"}
|
||||
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp", ".m3u", ".m3u8", ".wasm"}
|
||||
|
||||
// DefaultExtFilter creates an ExtFilter with default extensions.
|
||||
func DefaultExtFilter() ExtFilter {
|
||||
|
||||
@@ -82,7 +82,7 @@ func (r *ResponseFilterWriter) WriteHeader(code int) {
|
||||
|
||||
if r.shouldCompress {
|
||||
// replace discard writer with ResponseWriter
|
||||
if gzWriter, ok := r.gzipResponseWriter.Writer.(*gzip.Writer); ok {
|
||||
if gzWriter, ok := r.gzipResponseWriter.Writer().(*gzip.Writer); ok {
|
||||
gzWriter.Reset(r.ResponseWriter)
|
||||
}
|
||||
// use gzip WriteHeader to include and delete
|
||||
|
||||
@@ -17,11 +17,12 @@ package gzip
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestLengthFilter(t *testing.T) {
|
||||
@@ -47,7 +48,7 @@ func TestLengthFilter(t *testing.T) {
|
||||
for j, filter := range filters {
|
||||
r := httptest.NewRecorder()
|
||||
r.Header().Set("Content-Length", fmt.Sprint(ts.length))
|
||||
wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), &httpserver.ResponseWriterWrapper{ResponseWriter: r}, false})
|
||||
wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), &httpserver.ResponseWriterWrapper{ResponseWriter: r}, false, nil})
|
||||
if filter.ShouldCompress(wWriter) != ts.shouldCompress[j] {
|
||||
t.Errorf("Test %v: Expected %v found %v", i, ts.shouldCompress[j], filter.ShouldCompress(r))
|
||||
}
|
||||
@@ -77,7 +78,9 @@ func TestResponseFilterWriter(t *testing.T) {
|
||||
for i, ts := range tests {
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(ts.body)))
|
||||
w.Write([]byte(ts.body))
|
||||
if _, err := w.Write([]byte(ts.body)); err != nil {
|
||||
log.Println("[ERROR] failed to write response: ", err)
|
||||
}
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
@@ -86,7 +89,9 @@ func TestResponseFilterWriter(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
if _, err := server.ServeHTTP(w, r); err != nil {
|
||||
log.Println("[ERROR] unable to serve a gzipped response: ", err)
|
||||
}
|
||||
|
||||
resp := w.Body.String()
|
||||
|
||||
@@ -109,7 +114,9 @@ func TestResponseGzippedOutput(t *testing.T) {
|
||||
|
||||
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Write([]byte("gzipped"))
|
||||
if _, err := w.Write([]byte("gzipped")); err != nil {
|
||||
log.Println("[ERROR] failed to write response: ", err)
|
||||
}
|
||||
return 200, nil
|
||||
})
|
||||
|
||||
@@ -117,7 +124,9 @@ func TestResponseGzippedOutput(t *testing.T) {
|
||||
r.Header.Set("Accept-Encoding", "gzip")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
server.ServeHTTP(w, r)
|
||||
if _, err := server.ServeHTTP(w, r); err != nil {
|
||||
log.Println("[ERROR] unable to serve a gzipped response: ", err)
|
||||
}
|
||||
resp := w.Body.String()
|
||||
|
||||
if resp != "gzipped" {
|
||||
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup configures a new gzip middleware instance.
|
||||
|
||||
@@ -17,8 +17,8 @@ package gzip
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Headers is middleware that adds headers to the responses
|
||||
|
||||
@@ -16,6 +16,7 @@ package header
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestHeader(t *testing.T) {
|
||||
@@ -69,7 +70,9 @@ func TestHeader(t *testing.T) {
|
||||
// preset header
|
||||
rec.Header().Set("Server", "Caddy")
|
||||
|
||||
he.ServeHTTP(rec, req)
|
||||
if _, err := he.ServeHTTP(rec, req); err != nil {
|
||||
log.Println("[ERROR] ServeHTTP failed: ", err)
|
||||
}
|
||||
|
||||
if got := rec.Header().Get(test.name); got != test.value {
|
||||
t.Errorf("Test %d: Expected %s header to be %q but was %q",
|
||||
@@ -81,7 +84,9 @@ func TestHeader(t *testing.T) {
|
||||
func TestMultipleHeaders(t *testing.T) {
|
||||
he := Headers{
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprint(w, "This is a test")
|
||||
if _, err := fmt.Fprint(w, "This is a test"); err != nil {
|
||||
log.Println("[ERROR] Fprint failed: ", err)
|
||||
}
|
||||
return 0, nil
|
||||
}),
|
||||
Rules: []Rule{
|
||||
@@ -97,7 +102,9 @@ func TestMultipleHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
he.ServeHTTP(rec, req)
|
||||
if _, err := he.ServeHTTP(rec, req); err != nil {
|
||||
log.Println("[ERROR] ServeHTTP failed: ", err)
|
||||
}
|
||||
|
||||
desiredHeaders := []string{"</css/main.css>; rel=preload", "</images/image.png>; rel=preload"}
|
||||
actualHeaders := rec.HeaderMap[http.CanonicalHeaderKey("Link")]
|
||||
|
||||
@@ -17,8 +17,8 @@ package header
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
// SetupIfMatcher parses `if` or `if_op` in the current dispenser block.
|
||||
@@ -191,7 +191,7 @@ func (m IfMatcher) Or(r *http.Request) bool {
|
||||
}
|
||||
|
||||
// IfMatcherKeyword checks if the next value in the dispenser is a keyword for 'if' config block.
|
||||
// If true, remaining arguments in the dispinser are cleard to keep the dispenser valid for use.
|
||||
// If true, remaining arguments in the dispenser are cleared to keep the dispenser valid for use.
|
||||
func IfMatcherKeyword(c *caddy.Controller) bool {
|
||||
if c.Val() == "if" || c.Val() == "if_op" {
|
||||
// clear remaining args
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func TestConditions(t *testing.T) {
|
||||
|
||||
@@ -18,9 +18,11 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func activateHTTPS(cctx caddy.Context) error {
|
||||
@@ -37,10 +39,13 @@ func activateHTTPS(cctx caddy.Context) error {
|
||||
|
||||
// place certificates and keys on disk
|
||||
for _, c := range ctx.siteConfigs {
|
||||
if c.TLS.OnDemand {
|
||||
if !c.TLS.Managed {
|
||||
continue
|
||||
}
|
||||
if c.TLS.Manager.OnDemand != nil {
|
||||
continue // obtain these certificates on-demand instead
|
||||
}
|
||||
err := c.TLS.ObtainCert(c.TLS.Hostname, operatorPresent)
|
||||
err := c.TLS.Manager.ObtainCert(c.TLS.Hostname, operatorPresent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,9 +67,14 @@ func activateHTTPS(cctx caddy.Context) error {
|
||||
// on the ports we'd need to do ACME before we finish starting; parent process
|
||||
// already running renewal ticker, so renewal won't be missed anyway.)
|
||||
if !caddy.IsUpgrade() {
|
||||
err = caddytls.RenewManagedCertificates(true)
|
||||
if err != nil {
|
||||
return err
|
||||
ctx.instance.StorageMu.RLock()
|
||||
certCache, ok := ctx.instance.Storage[caddytls.CertCacheInstStorageKey].(*certmagic.Cache)
|
||||
ctx.instance.StorageMu.RUnlock()
|
||||
if ok && certCache != nil {
|
||||
err = certCache.RenewManagedCertificates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,13 +105,14 @@ func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
|
||||
// value will always be nil.
|
||||
func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
for _, cfg := range configs {
|
||||
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed || cfg.TLS.OnDemand {
|
||||
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed ||
|
||||
cfg.TLS.Manager == nil || cfg.TLS.Manager.OnDemand != nil {
|
||||
continue
|
||||
}
|
||||
cfg.TLS.Enabled = true
|
||||
cfg.Addr.Scheme = "https"
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.TLS.Hostname) {
|
||||
_, err := cfg.TLS.CacheManagedCertificate(cfg.TLS.Hostname)
|
||||
if loadCertificates && certmagic.HostQualifies(cfg.TLS.Hostname) {
|
||||
_, err := cfg.TLS.Manager.CacheManagedCertificate(cfg.TLS.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,9 +124,9 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
// Set default port of 443 if not explicitly set
|
||||
if cfg.Addr.Port == "" &&
|
||||
cfg.TLS.Enabled &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
|
||||
(!cfg.TLS.Manual || cfg.TLS.Manager.OnDemand != nil) &&
|
||||
cfg.Addr.Host != "localhost" {
|
||||
cfg.Addr.Port = HTTPSPort
|
||||
cfg.Addr.Port = strconv.Itoa(certmagic.HTTPSPort)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -128,10 +139,12 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
// only set up redirects for configs that qualify. It returns the updated list of
|
||||
// all configs.
|
||||
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
for i, cfg := range allConfigs {
|
||||
if cfg.TLS.Managed &&
|
||||
!hostHasOtherPort(allConfigs, i, HTTPPort) &&
|
||||
(cfg.Addr.Port == HTTPSPort || !hostHasOtherPort(allConfigs, i, HTTPSPort)) {
|
||||
!hostHasOtherPort(allConfigs, i, httpPort) &&
|
||||
(cfg.Addr.Port == httpsPort || !hostHasOtherPort(allConfigs, i, httpsPort)) {
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
|
||||
}
|
||||
}
|
||||
@@ -157,10 +170,10 @@ func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort str
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on HTTPPort. The TLS field of cfg must not be nil.
|
||||
// to listen on certmagic.HTTPPort. The TLS field of cfg must not be nil.
|
||||
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
redirPort := cfg.Addr.Port
|
||||
if redirPort == HTTPSPort {
|
||||
if redirPort == strconv.Itoa(certmagic.HTTPSPort) {
|
||||
// By default, HTTPSPort should be DefaultHTTPSPort,
|
||||
// which of course doesn't need to be explicitly stated
|
||||
// in the Location header. Even if HTTPSPort is changed
|
||||
@@ -200,14 +213,14 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
}
|
||||
|
||||
host := cfg.Addr.Host
|
||||
port := HTTPPort
|
||||
port := strconv.Itoa(certmagic.HTTPPort)
|
||||
addr := net.JoinHostPort(host, port)
|
||||
|
||||
return &SiteConfig{
|
||||
Addr: Address{Original: addr, Host: host, Port: port},
|
||||
ListenHost: cfg.ListenHost,
|
||||
middleware: []Middleware{redirMiddleware},
|
||||
TLS: &caddytls.Config{AltHTTPPort: cfg.TLS.AltHTTPPort, AltTLSSNIPort: cfg.TLS.AltTLSSNIPort},
|
||||
TLS: &caddytls.Config{Manager: cfg.TLS.Manager},
|
||||
Timeouts: cfg.Timeouts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,15 @@ package httpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
@@ -53,7 +56,7 @@ func TestRedirPlaintextHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Host: "foohost",
|
||||
Port: HTTPSPort, // since this is the 'default' HTTPS port, should not be included in Location value
|
||||
Port: strconv.Itoa(certmagic.HTTPSPort), // since this is the 'default' HTTPS port, should not be included in Location value
|
||||
},
|
||||
{
|
||||
Host: "*.example.com",
|
||||
@@ -81,7 +84,7 @@ func TestRedirPlaintextHost(t *testing.T) {
|
||||
if actual, expected := cfg.ListenHost, testcase.ListenHost; actual != expected {
|
||||
t.Errorf("Test %d: Expected redir config to have bindhost %s but got %s", i, expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Addr.Port, HTTPPort; actual != expected {
|
||||
if actual, expected := cfg.Addr.Port, strconv.Itoa(certmagic.HTTPPort); actual != expected {
|
||||
t.Errorf("Test %d: Expected redir config to have port '%s' but got '%s'", i, expected, actual)
|
||||
}
|
||||
|
||||
@@ -175,11 +178,13 @@ func TestMakePlaintextRedirects(t *testing.T) {
|
||||
|
||||
func TestEnableAutoHTTPS(t *testing.T) {
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true, Manager: &certmagic.Config{}}},
|
||||
{}, // not managed - no changes!
|
||||
}
|
||||
|
||||
enableAutoHTTPS(configs, false)
|
||||
if err := enableAutoHTTPS(configs, false); err != nil {
|
||||
log.Println("[ERROR] enableAutoHTTPS failed: ", err)
|
||||
}
|
||||
|
||||
if !configs[0].TLS.Enabled {
|
||||
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
|
||||
@@ -196,18 +201,18 @@ func TestEnableAutoHTTPS(t *testing.T) {
|
||||
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
|
||||
configs := []*SiteConfig{
|
||||
{Addr: Address{Host: ""}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: ""}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "localhost"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "123.44.3.21"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com"}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)},
|
||||
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com", Manager: &certmagic.Config{}}},
|
||||
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "80"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: newManagedConfig()},
|
||||
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: newManagedConfig()},
|
||||
}
|
||||
expectedManagedCount := 4
|
||||
|
||||
@@ -224,3 +229,7 @@ func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
|
||||
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
|
||||
}
|
||||
}
|
||||
|
||||
func newManagedConfig() *caddytls.Config {
|
||||
return &caddytls.Config{Manager: &certmagic.Config{}}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
gsyslog "github.com/hashicorp/go-syslog"
|
||||
)
|
||||
|
||||
var remoteSyslogPrefixes = map[string]string{
|
||||
@@ -79,10 +79,8 @@ func (l Logger) MaskIP(ip string) string {
|
||||
|
||||
if reqIP.To4() != nil {
|
||||
return reqIP.Mask(l.V4ipMask).String()
|
||||
} else {
|
||||
return reqIP.Mask(l.V6ipMask).String()
|
||||
}
|
||||
|
||||
return reqIP.Mask(l.V6ipMask).String()
|
||||
}
|
||||
|
||||
// ShouldLog returns true if the path is not exempted from
|
||||
@@ -162,7 +160,7 @@ selectwriter:
|
||||
return err
|
||||
}
|
||||
|
||||
if l.Roller != nil {
|
||||
if l.Roller != nil && !l.Roller.Disabled {
|
||||
file.Close()
|
||||
l.Roller.Filename = l.Output
|
||||
l.writer = l.Roller.GetLogWriter()
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -179,9 +180,13 @@ func bootServer(location string, ch chan format.LogParts) (*syslog.Server, error
|
||||
|
||||
switch address.network {
|
||||
case "tcp":
|
||||
server.ListenTCP(address.address)
|
||||
if err := server.ListenTCP(address.address); err != nil {
|
||||
log.Println("[ERROR] server failed to listen on TCP address: ", err)
|
||||
}
|
||||
case "udp":
|
||||
server.ListenUDP(address.address)
|
||||
if err := server.ListenUDP(address.address); err != nil {
|
||||
log.Println("[ERROR] server failed to listen on UDP address: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
server.SetHandler(syslog.NewChannelHandler(ch))
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -117,6 +117,10 @@ func (c ConfigSelector) Select(r *http.Request) (config HandlerConfig) {
|
||||
// path separator, just like URLs. IndexFle handles path manipulation
|
||||
// internally for systems that use different path separators.
|
||||
func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) {
|
||||
if len(fpath) == 0 {
|
||||
// https://caddy.community/t/panic-runtime-error-index-out-of-range/5781
|
||||
fpath = "/"
|
||||
}
|
||||
if fpath[len(fpath)-1] != '/' || root == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
)
|
||||
|
||||
// tlsHandler is a http.Handler that will inject a value
|
||||
@@ -49,6 +52,9 @@ type tlsHandler struct {
|
||||
// Halderman, et. al. in "The Security Impact of HTTPS Interception" (NDSS '17):
|
||||
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
|
||||
func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: one request per connection, we should report UA in connection with
|
||||
// handshake (reported in caddytls package) and our MITM assessment
|
||||
|
||||
if h.listener == nil {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
@@ -59,11 +65,16 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.listener.helloInfosMu.RUnlock()
|
||||
|
||||
ua := r.Header.Get("User-Agent")
|
||||
uaHash := telemetry.FastHash([]byte(ua))
|
||||
|
||||
// report this request's UA in connection with this ClientHello
|
||||
go telemetry.AppendUnique("tls_client_hello_ua:"+caddytls.ClientHelloInfo(info).Key(), uaHash)
|
||||
|
||||
var checked, mitm bool
|
||||
if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values)
|
||||
r.Header.Get("X-FCCKV2") != "" || // Fortinet
|
||||
info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat
|
||||
// TODO: Move the heartbeat check into each "looksLike" function...
|
||||
checked = true
|
||||
mitm = true
|
||||
} else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") ||
|
||||
@@ -97,6 +108,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if checked {
|
||||
r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm))
|
||||
if mitm {
|
||||
go telemetry.AppendUnique("http_mitm", "likely")
|
||||
} else {
|
||||
go telemetry.AppendUnique("http_mitm", "unlikely")
|
||||
}
|
||||
} else {
|
||||
go telemetry.AppendUnique("http_mitm", "unknown")
|
||||
}
|
||||
|
||||
if mitm && h.closeOnMITM {
|
||||
@@ -195,6 +213,11 @@ func (c *clientHelloConn) Read(b []byte) (n int, err error) {
|
||||
c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed
|
||||
c.listener.helloInfosMu.Unlock()
|
||||
|
||||
// report this ClientHello to telemetry
|
||||
chKey := caddytls.ClientHelloInfo(rawParsed).Key()
|
||||
go telemetry.SetNested("tls_client_hello", chKey, rawParsed)
|
||||
go telemetry.AppendUnique("tls_client_hello_count", chKey)
|
||||
|
||||
c.readHello = true
|
||||
return
|
||||
}
|
||||
@@ -215,6 +238,7 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
if len(data) < 42 {
|
||||
return
|
||||
}
|
||||
info.Version = uint16(data[4])<<8 | uint16(data[5])
|
||||
sessionIDLen := int(data[38])
|
||||
if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
|
||||
return
|
||||
@@ -231,9 +255,9 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
}
|
||||
numCipherSuites := cipherSuiteLen / 2
|
||||
// read in the cipher suites
|
||||
info.cipherSuites = make([]uint16, numCipherSuites)
|
||||
info.CipherSuites = make([]uint16, numCipherSuites)
|
||||
for i := 0; i < numCipherSuites; i++ {
|
||||
info.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])
|
||||
info.CipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])
|
||||
}
|
||||
data = data[2+cipherSuiteLen:]
|
||||
if len(data) < 1 {
|
||||
@@ -244,7 +268,7 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
if len(data) < 1+compressionMethodsLen {
|
||||
return
|
||||
}
|
||||
info.compressionMethods = data[1 : 1+compressionMethodsLen]
|
||||
info.CompressionMethods = data[1 : 1+compressionMethodsLen]
|
||||
|
||||
data = data[1+compressionMethodsLen:]
|
||||
|
||||
@@ -272,7 +296,7 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
}
|
||||
|
||||
// record that the client advertised support for this extension
|
||||
info.extensions = append(info.extensions, extension)
|
||||
info.Extensions = append(info.Extensions, extension)
|
||||
|
||||
switch extension {
|
||||
case extensionSupportedCurves:
|
||||
@@ -285,10 +309,10 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
return
|
||||
}
|
||||
numCurves := l / 2
|
||||
info.curves = make([]tls.CurveID, numCurves)
|
||||
info.Curves = make([]tls.CurveID, numCurves)
|
||||
d := data[2:]
|
||||
for i := 0; i < numCurves; i++ {
|
||||
info.curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1])
|
||||
info.Curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1])
|
||||
d = d[2:]
|
||||
}
|
||||
case extensionSupportedPoints:
|
||||
@@ -300,8 +324,8 @@ func parseRawClientHello(data []byte) (info rawHelloInfo) {
|
||||
if length != l+1 {
|
||||
return
|
||||
}
|
||||
info.points = make([]uint8, l)
|
||||
copy(info.points, data[1:])
|
||||
info.Points = make([]uint8, l)
|
||||
copy(info.Points, data[1:])
|
||||
}
|
||||
|
||||
data = data[length:]
|
||||
@@ -352,18 +376,12 @@ func (l *tlsHelloListener) Accept() (net.Conn, error) {
|
||||
// by Durumeric, Halderman, et. al. in
|
||||
// "The Security Impact of HTTPS Interception":
|
||||
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
|
||||
type rawHelloInfo struct {
|
||||
cipherSuites []uint16
|
||||
extensions []uint16
|
||||
compressionMethods []byte
|
||||
curves []tls.CurveID
|
||||
points []uint8
|
||||
}
|
||||
type rawHelloInfo caddytls.ClientHelloInfo
|
||||
|
||||
// advertisesHeartbeatSupport returns true if info indicates
|
||||
// that the client supports the Heartbeat extension.
|
||||
func (info rawHelloInfo) advertisesHeartbeatSupport() bool {
|
||||
for _, ext := range info.extensions {
|
||||
for _, ext := range info.Extensions {
|
||||
if ext == extensionHeartbeat {
|
||||
return true
|
||||
}
|
||||
@@ -386,31 +404,31 @@ func (info rawHelloInfo) looksLikeFirefox() bool {
|
||||
// Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13.
|
||||
// Note: Firefox on Fedora (or RedHat) doesn't include ECC suites because of patent liability.
|
||||
requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We check for both presence of curves and their ordering.
|
||||
requiredCurves := []tls.CurveID{29, 23, 24, 25}
|
||||
if len(info.curves) < len(requiredCurves) {
|
||||
if len(info.Curves) < len(requiredCurves) {
|
||||
return false
|
||||
}
|
||||
for i := range requiredCurves {
|
||||
if info.curves[i] != requiredCurves[i] {
|
||||
if info.Curves[i] != requiredCurves[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(info.curves) > len(requiredCurves) {
|
||||
if len(info.Curves) > len(requiredCurves) {
|
||||
// newer Firefox (55 Nightly?) may have additional curves at end of list
|
||||
allowedCurves := []tls.CurveID{256, 257}
|
||||
for i := range allowedCurves {
|
||||
if info.curves[len(requiredCurves)+i] != allowedCurves[i] {
|
||||
if info.Curves[len(requiredCurves)+i] != allowedCurves[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.cipherSuites) {
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -437,7 +455,7 @@ func (info rawHelloInfo) looksLikeFirefox() bool {
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
|
||||
}
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false)
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false)
|
||||
}
|
||||
|
||||
// looksLikeChrome returns true if info looks like a handshake
|
||||
@@ -478,20 +496,20 @@ func (info rawHelloInfo) looksLikeChrome() bool {
|
||||
TLS_DHE_RSA_WITH_AES_128_CBC_SHA: {}, // 0x33
|
||||
TLS_DHE_RSA_WITH_AES_256_CBC_SHA: {}, // 0x39
|
||||
}
|
||||
for _, ext := range info.cipherSuites {
|
||||
for _, ext := range info.CipherSuites {
|
||||
if _, ok := chromeCipherExclusions[ext]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome does not include curve 25 (CurveP521) (as of Chrome 56, Feb. 2017).
|
||||
for _, curve := range info.curves {
|
||||
for _, curve := range info.Curves {
|
||||
if curve == 25 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if !hasGreaseCiphers(info.cipherSuites) {
|
||||
if !hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -509,19 +527,19 @@ func (info rawHelloInfo) looksLikeEdge() bool {
|
||||
// More specifically, the OCSP status request extension appears
|
||||
// *directly* before the other two extensions, which occur in that
|
||||
// order. (I contacted the authors for clarification and verified it.)
|
||||
for i, ext := range info.extensions {
|
||||
for i, ext := range info.Extensions {
|
||||
if ext == extensionOCSPStatusRequest {
|
||||
if len(info.extensions) <= i+2 {
|
||||
if len(info.Extensions) <= i+2 {
|
||||
return false
|
||||
}
|
||||
if info.extensions[i+1] != extensionSupportedCurves ||
|
||||
info.extensions[i+2] != extensionSupportedPoints {
|
||||
if info.Extensions[i+1] != extensionSupportedCurves ||
|
||||
info.Extensions[i+2] != extensionSupportedPoints {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cs := range info.cipherSuites {
|
||||
for _, cs := range info.CipherSuites {
|
||||
// As of Feb. 2017, Edge does not have 0xff, but Avast adds it
|
||||
if cs == scsvRenegotiation {
|
||||
return false
|
||||
@@ -532,7 +550,7 @@ func (info rawHelloInfo) looksLikeEdge() bool {
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.cipherSuites) {
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -558,23 +576,23 @@ func (info rawHelloInfo) looksLikeSafari() bool {
|
||||
|
||||
// We check for the presence and order of the extensions.
|
||||
requiredExtensionsOrder := []uint16{10, 11, 13, 13172, 16, 5, 18, 23}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
|
||||
// Safari on iOS 11 (beta) uses different set/ordering of extensions
|
||||
requiredExtensionsOrderiOS11 := []uint16{65281, 0, 23, 13, 5, 13172, 18, 16, 11, 10}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrderiOS11, info.extensions, true) {
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrderiOS11, info.Extensions, true) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// For these versions of Safari, expect TLS_EMPTY_RENEGOTIATION_INFO_SCSV first.
|
||||
if len(info.cipherSuites) < 1 {
|
||||
if len(info.CipherSuites) < 1 {
|
||||
return false
|
||||
}
|
||||
if info.cipherSuites[0] != scsvRenegotiation {
|
||||
if info.CipherSuites[0] != scsvRenegotiation {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.cipherSuites) {
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -599,19 +617,19 @@ func (info rawHelloInfo) looksLikeSafari() bool {
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
|
||||
}
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, true)
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, true)
|
||||
}
|
||||
|
||||
// looksLikeTor returns true if the info looks like a ClientHello from Tor browser
|
||||
// (based on Firefox).
|
||||
func (info rawHelloInfo) looksLikeTor() bool {
|
||||
requiredExtensionsOrder := []uint16{10, 11, 16, 5, 13}
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.extensions, true) {
|
||||
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check for session tickets support; Tor doesn't support them to prevent tracking
|
||||
for _, ext := range info.extensions {
|
||||
for _, ext := range info.Extensions {
|
||||
if ext == 35 {
|
||||
return false
|
||||
}
|
||||
@@ -619,12 +637,12 @@ func (info rawHelloInfo) looksLikeTor() bool {
|
||||
|
||||
// We check for both presence of curves and their ordering, including
|
||||
// an optional curve at the beginning (for Tor based on Firefox 52)
|
||||
infoCurves := info.curves
|
||||
if len(info.curves) == 4 {
|
||||
if info.curves[0] != 29 {
|
||||
infoCurves := info.Curves
|
||||
if len(info.Curves) == 4 {
|
||||
if info.Curves[0] != 29 {
|
||||
return false
|
||||
}
|
||||
infoCurves = info.curves[1:]
|
||||
infoCurves = info.Curves[1:]
|
||||
}
|
||||
requiredCurves := []tls.CurveID{23, 24, 25}
|
||||
if len(infoCurves) < len(requiredCurves) {
|
||||
@@ -636,7 +654,7 @@ func (info rawHelloInfo) looksLikeTor() bool {
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.cipherSuites) {
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -663,7 +681,7 @@ func (info rawHelloInfo) looksLikeTor() bool {
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
|
||||
}
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.cipherSuites, false)
|
||||
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false)
|
||||
}
|
||||
|
||||
// assertPresenceAndOrdering will return true if candidateList contains
|
||||
|
||||
@@ -32,44 +32,48 @@ func TestParseClientHello(t *testing.T) {
|
||||
// curl 7.51.0 (x86_64-apple-darwin16.0) libcurl/7.51.0 SecureTransport zlib/1.2.8
|
||||
inputHex: `010000a6030358a28c73a71bdfc1f09dee13fecdc58805dcce42ac44254df548f14645f7dc2c00004400ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009f009e006b0067003900330016009d009c003d003c0035002f000a00af00ae008d008c008b01000039000a00080006001700180019000b00020100000d00120010040102010501060104030203050306030005000501000000000012000000170000`,
|
||||
expected: rawHelloInfo{
|
||||
cipherSuites: []uint16{255, 49196, 49195, 49188, 49187, 49162, 49161, 49160, 49200, 49199, 49192, 49191, 49172, 49171, 49170, 159, 158, 107, 103, 57, 51, 22, 157, 156, 61, 60, 53, 47, 10, 175, 174, 141, 140, 139},
|
||||
extensions: []uint16{10, 11, 13, 5, 18, 23},
|
||||
compressionMethods: []byte{0},
|
||||
curves: []tls.CurveID{23, 24, 25},
|
||||
points: []uint8{0},
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{255, 49196, 49195, 49188, 49187, 49162, 49161, 49160, 49200, 49199, 49192, 49191, 49172, 49171, 49170, 159, 158, 107, 103, 57, 51, 22, 157, 156, 61, 60, 53, 47, 10, 175, 174, 141, 140, 139},
|
||||
Extensions: []uint16{10, 11, 13, 5, 18, 23},
|
||||
CompressionMethods: []byte{0},
|
||||
Curves: []tls.CurveID{23, 24, 25},
|
||||
Points: []uint8{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Chrome 56
|
||||
inputHex: `010000c003031dae75222dae1433a5a283ddcde8ddabaefbf16d84f250eee6fdff48cdfff8a00000201a1ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010000777a7a0000ff010001000000000e000c0000096c6f63616c686f73740017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a0008aaaa001d001700182a2a000100`,
|
||||
expected: rawHelloInfo{
|
||||
cipherSuites: []uint16{6682, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49171, 49172, 156, 157, 47, 53, 10},
|
||||
extensions: []uint16{31354, 65281, 0, 23, 35, 13, 5, 18, 16, 30032, 11, 10, 10794},
|
||||
compressionMethods: []byte{0},
|
||||
curves: []tls.CurveID{43690, 29, 23, 24},
|
||||
points: []uint8{0},
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{6682, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49171, 49172, 156, 157, 47, 53, 10},
|
||||
Extensions: []uint16{31354, 65281, 0, 23, 35, 13, 5, 18, 16, 30032, 11, 10, 10794},
|
||||
CompressionMethods: []byte{0},
|
||||
Curves: []tls.CurveID{43690, 29, 23, 24},
|
||||
Points: []uint8{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Firefox 51
|
||||
inputHex: `010000bd030375f9022fc3a6562467f3540d68013b2d0b961979de6129e944efe0b35531323500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a010000760000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000ff030000000d0020001e040305030603020308040805080604010501060102010402050206020202`,
|
||||
expected: rawHelloInfo{
|
||||
cipherSuites: []uint16{49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 51, 57, 47, 53, 10},
|
||||
extensions: []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 65283, 13},
|
||||
compressionMethods: []byte{0},
|
||||
curves: []tls.CurveID{29, 23, 24, 25},
|
||||
points: []uint8{0},
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 51, 57, 47, 53, 10},
|
||||
Extensions: []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 65283, 13},
|
||||
CompressionMethods: []byte{0},
|
||||
Curves: []tls.CurveID{29, 23, 24, 25},
|
||||
Points: []uint8{0},
|
||||
},
|
||||
},
|
||||
{
|
||||
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016)
|
||||
inputHex: `0100012b03035d385236b8ca7b7946fa0336f164e76bf821ed90e8de26d97cc677671b6f36380000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc00200050004c012c008001600130010000dc00dc003000a00ff0201000055000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101`,
|
||||
expected: rawHelloInfo{
|
||||
cipherSuites: []uint16{49200, 49196, 49192, 49188, 49172, 49162, 165, 163, 161, 159, 107, 106, 105, 104, 57, 56, 55, 54, 136, 135, 134, 133, 49202, 49198, 49194, 49190, 49167, 49157, 157, 61, 53, 132, 49199, 49195, 49191, 49187, 49171, 49161, 164, 162, 160, 158, 103, 64, 63, 62, 51, 50, 49, 48, 154, 153, 152, 151, 69, 68, 67, 66, 49201, 49197, 49193, 49189, 49166, 49156, 156, 60, 47, 150, 65, 7, 49169, 49159, 49164, 49154, 5, 4, 49170, 49160, 22, 19, 16, 13, 49165, 49155, 10, 255},
|
||||
extensions: []uint16{11, 10, 35, 13, 15},
|
||||
compressionMethods: []byte{1, 0},
|
||||
curves: []tls.CurveID{23, 25, 28, 27, 24, 26, 22, 14, 13, 11, 12, 9, 10},
|
||||
points: []uint8{0, 1, 2},
|
||||
Version: 0x303,
|
||||
CipherSuites: []uint16{49200, 49196, 49192, 49188, 49172, 49162, 165, 163, 161, 159, 107, 106, 105, 104, 57, 56, 55, 54, 136, 135, 134, 133, 49202, 49198, 49194, 49190, 49167, 49157, 157, 61, 53, 132, 49199, 49195, 49191, 49187, 49171, 49161, 164, 162, 160, 158, 103, 64, 63, 62, 51, 50, 49, 48, 154, 153, 152, 151, 69, 68, 67, 66, 49201, 49197, 49193, 49189, 49166, 49156, 156, 60, 47, 150, 65, 7, 49169, 49159, 49164, 49154, 5, 4, 49170, 49160, 22, 19, 16, 13, 49165, 49155, 10, 255},
|
||||
Extensions: []uint16{11, 10, 35, 13, 15},
|
||||
CompressionMethods: []byte{1, 0},
|
||||
Curves: []tls.CurveID{23, 25, 28, 27, 24, 26, 22, 14, 13, 11, 12, 9, 10},
|
||||
Points: []uint8{0, 1, 2},
|
||||
},
|
||||
},
|
||||
} {
|
||||
@@ -331,15 +335,15 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||
// in other words, if one returns true, the others
|
||||
// should return false, with as little logic as possible,
|
||||
// but with enough logic to force TLS proxies to do a
|
||||
// good job preserving characterstics of the handshake.
|
||||
// good job preserving characteristics of the handshake.
|
||||
if (isChrome && (isFirefox || isSafari || isEdge || isTor)) ||
|
||||
(isFirefox && (isChrome || isSafari || isEdge || isTor)) ||
|
||||
(isSafari && (isChrome || isFirefox || isEdge || isTor)) ||
|
||||
(isEdge && (isChrome || isFirefox || isSafari || isTor)) ||
|
||||
(isTor && (isChrome || isFirefox || isSafari || isEdge)) {
|
||||
t.Errorf("[%s] Test %d: Multiple fingerprinting functions matched: "+
|
||||
"Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n\tparsed hello hex: %#x\n",
|
||||
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed, parsed)
|
||||
"Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n",
|
||||
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed)
|
||||
}
|
||||
|
||||
// test the handler and detection results
|
||||
@@ -367,8 +371,8 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||
if got != want {
|
||||
t.Errorf("[%s] Test %d: Expected MITM=%v but got %v (type assertion OK (checked)=%v)",
|
||||
client, i, want, got, checked)
|
||||
t.Errorf("[%s] Test %d: Looks like Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n\tparsed hello hex: %#x\n",
|
||||
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed, parsed)
|
||||
t.Errorf("[%s] Test %d: Looks like Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n",
|
||||
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+164
-63
@@ -15,6 +15,7 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -22,20 +23,23 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
const serverType = "http"
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&HTTPPort, "http-port", HTTPPort, "Default port to use for HTTP")
|
||||
flag.StringVar(&HTTPSPort, "https-port", HTTPSPort, "Default port to use for HTTPS")
|
||||
flag.IntVar(&certmagic.HTTPPort, "http-port", certmagic.HTTPPort, "Default port to use for HTTP")
|
||||
flag.IntVar(&certmagic.HTTPSPort, "https-port", certmagic.HTTPSPort, "Default port to use for HTTPS")
|
||||
flag.StringVar(&Host, "host", DefaultHost, "Default host")
|
||||
flag.StringVar(&Port, "port", DefaultPort, "Default port")
|
||||
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site")
|
||||
@@ -65,6 +69,12 @@ func init() {
|
||||
caddy.RegisterParsingCallback(serverType, "root", hideCaddyfile)
|
||||
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
|
||||
caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS })
|
||||
|
||||
// disable the caddytls package reporting ClientHellos
|
||||
// to telemetry, since our MITM detector does this but
|
||||
// with more information than the standard lib provides
|
||||
// (as of May 2018)
|
||||
caddytls.ClientHelloTelemetry = false
|
||||
}
|
||||
|
||||
// hideCaddyfile hides the source/origin Caddyfile if it is within the
|
||||
@@ -118,6 +128,8 @@ func (h *httpContext) saveConfig(key string, cfg *SiteConfig) {
|
||||
// be parsed and executed.
|
||||
func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
siteAddrs := make(map[string]string)
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
// For each address in each server block, make a new config
|
||||
for _, sb := range serverBlocks {
|
||||
@@ -161,21 +173,32 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
|
||||
// If default HTTP or HTTPS ports have been customized,
|
||||
// make sure the ACME challenge ports match
|
||||
var altHTTPPort, altTLSSNIPort string
|
||||
if HTTPPort != DefaultHTTPPort {
|
||||
altHTTPPort = HTTPPort
|
||||
var altHTTPPort, altTLSALPNPort int
|
||||
if httpPort != DefaultHTTPPort {
|
||||
portInt, err := strconv.Atoi(httpPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
altHTTPPort = portInt
|
||||
}
|
||||
if HTTPSPort != DefaultHTTPSPort {
|
||||
altTLSSNIPort = HTTPSPort
|
||||
if httpsPort != DefaultHTTPSPort {
|
||||
portInt, err := strconv.Atoi(httpsPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
altTLSALPNPort = portInt
|
||||
}
|
||||
|
||||
// Make our caddytls.Config, which has a pointer to the
|
||||
// instance's certificate cache and enough information
|
||||
// to use automatic HTTPS when the time comes
|
||||
caddytlsConfig := caddytls.NewConfig(h.instance)
|
||||
caddytlsConfig, err := caddytls.NewConfig(h.instance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating new caddytls configuration: %v", err)
|
||||
}
|
||||
caddytlsConfig.Hostname = addr.Host
|
||||
caddytlsConfig.AltHTTPPort = altHTTPPort
|
||||
caddytlsConfig.AltTLSSNIPort = altTLSSNIPort
|
||||
caddytlsConfig.Manager.AltHTTPPort = altHTTPPort
|
||||
caddytlsConfig.Manager.AltTLSALPNPort = altTLSALPNPort
|
||||
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &SiteConfig{
|
||||
@@ -207,13 +230,48 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
// MakeServers uses the newly-created siteConfigs to
|
||||
// create and return a list of server instances.
|
||||
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
// make sure TLS is disabled for explicitly-HTTP sites
|
||||
// (necessary when HTTP address shares a block containing tls)
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
// make a rough estimate as to whether we're in a "production
|
||||
// environment/system" - start by assuming that most production
|
||||
// servers will set their default CA endpoint to a public,
|
||||
// trusted CA (obviously not a perfect heuristic)
|
||||
var looksLikeProductionCA bool
|
||||
for _, publicCAEndpoint := range caddytls.KnownACMECAs {
|
||||
if strings.Contains(certmagic.Default.CA, publicCAEndpoint) {
|
||||
looksLikeProductionCA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate each site configuration and make sure that:
|
||||
// 1) TLS is disabled for explicitly-HTTP sites (necessary
|
||||
// when an HTTP address shares a block containing tls)
|
||||
// 2) if QUIC is enabled, TLS ClientAuth is not, because
|
||||
// currently, QUIC does not support ClientAuth (TODO:
|
||||
// revisit this when our QUIC implementation supports it)
|
||||
// 3) if TLS ClientAuth is used, StrictHostMatching is on
|
||||
var atLeastOneSiteLooksLikeProduction bool
|
||||
for _, cfg := range h.siteConfigs {
|
||||
// see if all the addresses (both sites and
|
||||
// listeners) are loopback to help us determine
|
||||
// if this is a "production" instance or not
|
||||
if !atLeastOneSiteLooksLikeProduction {
|
||||
if !caddy.IsLoopback(cfg.Addr.Host) &&
|
||||
!caddy.IsLoopback(cfg.ListenHost) &&
|
||||
(caddytls.QualifiesForManagedTLS(cfg) ||
|
||||
certmagic.HostQualifies(cfg.Addr.Host)) {
|
||||
atLeastOneSiteLooksLikeProduction = true
|
||||
}
|
||||
}
|
||||
|
||||
// make sure TLS is disabled for explicitly-HTTP sites
|
||||
// (necessary when HTTP address shares a block containing tls)
|
||||
if !cfg.TLS.Enabled {
|
||||
continue
|
||||
}
|
||||
if cfg.Addr.Port == HTTPPort || cfg.Addr.Scheme == "http" {
|
||||
if cfg.Addr.Port == httpPort || cfg.Addr.Scheme == "http" {
|
||||
cfg.TLS.Enabled = false
|
||||
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
|
||||
} else if cfg.Addr.Scheme == "" {
|
||||
@@ -224,11 +282,22 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
// is incorrect for this site.
|
||||
cfg.Addr.Scheme = "https"
|
||||
}
|
||||
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.OnDemand) {
|
||||
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.Manager.OnDemand != nil) {
|
||||
// this is vital, otherwise the function call below that
|
||||
// sets the listener address will use the default port
|
||||
// instead of 443 because it doesn't know about TLS.
|
||||
cfg.Addr.Port = HTTPSPort
|
||||
cfg.Addr.Port = httpsPort
|
||||
}
|
||||
if cfg.TLS.ClientAuth != tls.NoClientCert {
|
||||
if QUIC {
|
||||
return nil, fmt.Errorf("cannot enable TLS client authentication with QUIC, because QUIC does not yet support it")
|
||||
}
|
||||
// this must be enabled so that a client cannot connect
|
||||
// using SNI for another site on this listener that
|
||||
// does NOT require ClientAuth, and then send HTTP
|
||||
// requests with the Host header of this site which DOES
|
||||
// require client auth, thus bypassing it...
|
||||
cfg.StrictHostMatching = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +317,18 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
servers = append(servers, s)
|
||||
}
|
||||
|
||||
// NOTE: This value is only a "good guess". Quite often, development
|
||||
// environments will use internal DNS or a local hosts file to serve
|
||||
// real-looking domains in local development. We can't easily tell
|
||||
// which without doing a DNS lookup, so this guess is definitely naive,
|
||||
// and if we ever want a better guess, we will have to do DNS lookups.
|
||||
deploymentGuess := "dev"
|
||||
if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction {
|
||||
deploymentGuess = "prod"
|
||||
}
|
||||
telemetry.Set("http_deployment_guess", deploymentGuess)
|
||||
telemetry.Set("http_num_sites", len(h.siteConfigs))
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
@@ -273,7 +354,11 @@ func GetConfig(c *caddy.Controller) *SiteConfig {
|
||||
// we should only get here during tests because directive
|
||||
// actions typically skip the server blocks where we make
|
||||
// the configs
|
||||
cfg := &SiteConfig{Root: Root, TLS: new(caddytls.Config), IndexPages: staticfiles.DefaultIndexPages}
|
||||
cfg := &SiteConfig{
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{Manager: certmagic.NewDefault()},
|
||||
IndexPages: staticfiles.DefaultIndexPages,
|
||||
}
|
||||
ctx.saveConfig(key, cfg)
|
||||
return cfg
|
||||
}
|
||||
@@ -328,6 +413,8 @@ func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConf
|
||||
// parts of an address. The component parts may be
|
||||
// updated to the correct values as setup proceeds,
|
||||
// but the original value should never be changed.
|
||||
//
|
||||
// The Host field must be in a normalized form.
|
||||
type Address struct {
|
||||
Original, Scheme, Host, Port, Path string
|
||||
}
|
||||
@@ -339,7 +426,7 @@ func (a Address) String() string {
|
||||
}
|
||||
scheme := a.Scheme
|
||||
if scheme == "" {
|
||||
if a.Port == HTTPSPort {
|
||||
if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
@@ -349,11 +436,12 @@ func (a Address) String() string {
|
||||
if s != "" {
|
||||
s += "://"
|
||||
}
|
||||
s += a.Host
|
||||
if a.Port != "" &&
|
||||
((scheme == "https" && a.Port != DefaultHTTPSPort) ||
|
||||
(scheme == "http" && a.Port != DefaultHTTPPort)) {
|
||||
s += ":" + a.Port
|
||||
s += net.JoinHostPort(a.Host, a.Port)
|
||||
} else {
|
||||
s += a.Host
|
||||
}
|
||||
if a.Path != "" {
|
||||
s += a.Path
|
||||
@@ -376,10 +464,17 @@ func (a Address) Normalize() Address {
|
||||
if !CaseSensitivePath {
|
||||
path = strings.ToLower(path)
|
||||
}
|
||||
|
||||
// ensure host is normalized if it's an IP address
|
||||
host := a.Host
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
host = ip.String()
|
||||
}
|
||||
|
||||
return Address{
|
||||
Original: a.Original,
|
||||
Scheme: strings.ToLower(a.Scheme),
|
||||
Host: strings.ToLower(a.Host),
|
||||
Host: strings.ToLower(host),
|
||||
Port: a.Port,
|
||||
Path: path,
|
||||
}
|
||||
@@ -395,11 +490,10 @@ func (a Address) Key() string {
|
||||
if a.Host != "" {
|
||||
res += a.Host
|
||||
}
|
||||
if a.Port != "" {
|
||||
if strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
||||
// insert port only if the original has its own explicit port
|
||||
res += ":" + a.Port
|
||||
}
|
||||
// insert port only if the original has its own explicit port
|
||||
if a.Port != "" && len(a.Original) >= len(res) &&
|
||||
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
||||
res += ":" + a.Port
|
||||
}
|
||||
if a.Path != "" {
|
||||
res += a.Path
|
||||
@@ -412,6 +506,18 @@ func (a Address) Key() string {
|
||||
func standardizeAddress(str string) (Address, error) {
|
||||
input := str
|
||||
|
||||
httpPort := strconv.Itoa(certmagic.HTTPPort)
|
||||
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
|
||||
|
||||
// As of Go 1.12.8 (Aug 2019), ports that are service names such
|
||||
// as ":http" and ":https" are no longer parsed as they were
|
||||
// before, which is a breaking change for us. Attempt to smooth
|
||||
// this over for now by replacing those strings with their port
|
||||
// equivalents. See
|
||||
// https://github.com/golang/go/commit/3226f2d492963d361af9dfc6714ef141ba606713
|
||||
str = strings.Replace(str, ":https", ":"+httpsPort, 1)
|
||||
str = strings.Replace(str, ":http", ":"+httpPort, 1)
|
||||
|
||||
// Split input into components (prepend with // to assert host by default)
|
||||
if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
|
||||
str = "//" + str
|
||||
@@ -433,32 +539,28 @@ func standardizeAddress(str string) (Address, error) {
|
||||
// see if we can set port based off scheme
|
||||
if port == "" {
|
||||
if u.Scheme == "http" {
|
||||
port = HTTPPort
|
||||
port = httpPort
|
||||
} else if u.Scheme == "https" {
|
||||
port = HTTPSPort
|
||||
port = httpsPort
|
||||
}
|
||||
}
|
||||
|
||||
// repeated or conflicting scheme is confusing, so error
|
||||
if u.Scheme != "" && (port == "http" || port == "https") {
|
||||
return Address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
|
||||
}
|
||||
|
||||
// error if scheme and port combination violate convention
|
||||
if (u.Scheme == "http" && port == HTTPSPort) || (u.Scheme == "https" && port == HTTPPort) {
|
||||
if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) {
|
||||
return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
|
||||
}
|
||||
|
||||
// standardize http and https ports to their respective port numbers
|
||||
if port == "http" {
|
||||
u.Scheme = "http"
|
||||
port = HTTPPort
|
||||
} else if port == "https" {
|
||||
u.Scheme = "https"
|
||||
port = HTTPSPort
|
||||
// (this behavior changed in Go 1.12.8)
|
||||
if u.Scheme == "" {
|
||||
if port == httpPort {
|
||||
u.Scheme = "http"
|
||||
} else if port == httpsPort {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
}
|
||||
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
|
||||
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, nil
|
||||
}
|
||||
|
||||
// RegisterDevDirective splices name into the list of directives
|
||||
@@ -534,6 +636,7 @@ var directives = []string{
|
||||
"startup", // TODO: Deprecate this directive
|
||||
"shutdown", // TODO: Deprecate this directive
|
||||
"on",
|
||||
"supervisor", // github.com/lucaslorentz/caddy-supervisor
|
||||
"request_id",
|
||||
"realip", // github.com/captncraig/caddy-realip
|
||||
"git", // github.com/abiosoft/caddy-git
|
||||
@@ -547,30 +650,33 @@ var directives = []string{
|
||||
"cache", // github.com/nicolasazrak/caddy-cache
|
||||
"rewrite",
|
||||
"ext",
|
||||
"minify", // github.com/hacdias/caddy-minify
|
||||
"gzip",
|
||||
"header",
|
||||
"geoip", // github.com/kodnaplakal/caddy-geoip
|
||||
"errors",
|
||||
"authz", // github.com/casbin/caddy-authz
|
||||
"filter", // github.com/echocat/caddy-filter
|
||||
"minify", // github.com/hacdias/caddy-minify
|
||||
"ipfilter", // github.com/pyed/ipfilter
|
||||
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
|
||||
"search", // github.com/pedronasser/caddy-search
|
||||
"recaptcha", // github.com/defund/caddy-recaptcha
|
||||
"expires", // github.com/epicagency/caddy-expires
|
||||
"forwardproxy", // github.com/caddyserver/forwardproxy
|
||||
"basicauth",
|
||||
"redir",
|
||||
"status",
|
||||
"cors", // github.com/captncraig/cors/caddy
|
||||
"nobots", // github.com/Xumeiquer/nobots
|
||||
"cors", // github.com/captncraig/cors/caddy
|
||||
"s3browser", // github.com/techknowlogick/caddy-s3browser
|
||||
"nobots", // github.com/Xumeiquer/nobots
|
||||
"mime",
|
||||
"login", // github.com/tarent/loginsrv/caddy
|
||||
"reauth", // github.com/freman/caddy-reauth
|
||||
"jwt", // github.com/BTBurke/caddy-jwt
|
||||
"jsonp", // github.com/pschlump/caddy-jsonp
|
||||
"upload", // blitznote.com/src/caddy.upload
|
||||
"multipass", // github.com/namsral/multipass/caddy
|
||||
"login", // github.com/tarent/loginsrv/caddy
|
||||
"reauth", // github.com/freman/caddy-reauth
|
||||
"extauth", // github.com/BTBurke/caddy-extauth
|
||||
"jwt", // github.com/BTBurke/caddy-jwt
|
||||
"permission", // github.com/dhaavi/caddy-permission
|
||||
"jsonp", // github.com/pschlump/caddy-jsonp
|
||||
"upload", // blitznote.com/src/caddy.upload
|
||||
"multipass", // github.com/namsral/multipass/caddy
|
||||
"internal",
|
||||
"pprof",
|
||||
"expvar",
|
||||
@@ -579,21 +685,22 @@ var directives = []string{
|
||||
"prometheus", // github.com/miekg/caddy-prometheus
|
||||
"templates",
|
||||
"proxy",
|
||||
"pubsub", // github.com/jung-kurt/caddy-pubsub
|
||||
"fastcgi",
|
||||
"cgi", // github.com/jung-kurt/caddy-cgi
|
||||
"websocket",
|
||||
"filemanager", // github.com/hacdias/filemanager/caddy/filemanager
|
||||
"filebrowser", // github.com/filebrowser/caddy
|
||||
"webdav", // github.com/hacdias/caddy-webdav
|
||||
"markdown",
|
||||
"browse",
|
||||
"jekyll", // github.com/hacdias/filemanager/caddy/jekyll
|
||||
"hugo", // github.com/hacdias/filemanager/caddy/hugo
|
||||
"mailout", // github.com/SchumacherFM/mailout
|
||||
"awses", // github.com/miquella/caddy-awses
|
||||
"awslambda", // github.com/coopernurse/caddy-awslambda
|
||||
"grpc", // github.com/pieterlouw/caddy-grpc
|
||||
"gopkg", // github.com/zikes/gopkg
|
||||
"restic", // github.com/restic/caddy
|
||||
"wkd", // github.com/emersion/caddy-wkd
|
||||
"dyndns", // github.com/linkonoid/caddy-dyndns
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -629,10 +736,4 @@ var (
|
||||
|
||||
// QUIC indicates whether QUIC is enabled or not.
|
||||
QUIC bool
|
||||
|
||||
// HTTPPort is the port to use for HTTP.
|
||||
HTTPPort = DefaultHTTPPort
|
||||
|
||||
// HTTPSPort is the port to use for HTTPS.
|
||||
HTTPSPort = DefaultHTTPSPort
|
||||
)
|
||||
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyfile"
|
||||
)
|
||||
|
||||
func TestStandardizeAddress(t *testing.T) {
|
||||
@@ -43,12 +43,12 @@ func TestStandardizeAddress(t *testing.T) {
|
||||
{`:`, "", "", "", "", false},
|
||||
{`localhost:http`, "http", "localhost", "80", "", false},
|
||||
{`localhost:https`, "https", "localhost", "443", "", false},
|
||||
{`:http`, "http", "", "80", "", false},
|
||||
{`:https`, "https", "", "443", "", false},
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "", "", "", "", true}, // repeated scheme
|
||||
{`http://localhost:443`, "", "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
{`:http`, "http", "", "80", "", false}, // as of Go 1.12.8, service name in port is no longer supported
|
||||
{`:https`, "https", "", "443", "", false}, // as of Go 1.12.8, service name in port is no longer supported
|
||||
{`http://localhost:https`, "", "", "", "", true}, // conflict
|
||||
{`http://localhost:http`, "http", "localhost", "80", "", false}, // repeated scheme -- test adjusted for Go 1.12.8 (expect no error)
|
||||
{`http://localhost:443`, "", "", "", "", true}, // not conventional
|
||||
{`https://localhost:80`, "", "", "", "", true}, // not conventional
|
||||
{`http://localhost`, "http", "localhost", "80", "", false},
|
||||
{`https://localhost`, "https", "localhost", "443", "", false},
|
||||
{`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false},
|
||||
@@ -58,8 +58,8 @@ func TestStandardizeAddress(t *testing.T) {
|
||||
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false},
|
||||
{`http://[::1]:1234`, "http", "::1", "1234", "", false},
|
||||
{``, "", "", "", "", false},
|
||||
{`::1`, "", "::1", "", "", true},
|
||||
{`localhost::`, "", "localhost::", "", "", true},
|
||||
{`::1`, "", "::1", "", "", false}, // test adjusted for Go 1.12.8 (expect no error)
|
||||
{`localhost::`, "", "localhost::", "", "", false}, // test adjusted for Go 1.12.8 (expect no error)
|
||||
{`#$%@`, "", "", "", "", true},
|
||||
{`host/path`, "", "host", "", "/path", false},
|
||||
{`http://host/`, "http", "host", "80", "/", false},
|
||||
@@ -67,7 +67,7 @@ func TestStandardizeAddress(t *testing.T) {
|
||||
{`:1234/asdf`, "", "", "1234", "/asdf", false},
|
||||
{`http://host/path`, "http", "host", "80", "/path", false},
|
||||
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
|
||||
{`host:80/path`, "", "host", "80", "/path", false},
|
||||
{`host:80/path`, "http", "host", "80", "/path", false}, // test adjusted for Go 1.12.8 (expect "http" scheme)
|
||||
{`host:https/path`, "https", "host", "443", "/path", false},
|
||||
{`/path`, "", "", "", "/path", false},
|
||||
} {
|
||||
@@ -212,6 +212,10 @@ func TestKeyNormalization(t *testing.T) {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "http://host:1234/path",
|
||||
res: "http://host:1234/path",
|
||||
},
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/ABCDEF",
|
||||
@@ -221,8 +225,20 @@ func TestKeyNormalization(t *testing.T) {
|
||||
res: "a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Port",
|
||||
res: "a:2015/Port",
|
||||
orig: "A:2015/Path",
|
||||
res: "a:2015/Path",
|
||||
},
|
||||
{
|
||||
orig: ":80",
|
||||
res: "http://",
|
||||
},
|
||||
{
|
||||
orig: ":443",
|
||||
res: "https://",
|
||||
},
|
||||
{
|
||||
orig: ":1234",
|
||||
res: ":1234",
|
||||
},
|
||||
}
|
||||
for _, item := range caseSensitiveData {
|
||||
|
||||
@@ -217,7 +217,7 @@ func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) {
|
||||
// https://go-review.googlesource.com/c/22134#message-ff351762308fe05f6b72a487d6842e3988916486
|
||||
buf := respBufPool.Get().([]byte)
|
||||
n, err := io.CopyBuffer(rb.ResponseWriterWrapper, src, buf)
|
||||
respBufPool.Put(buf) // defer'ing this slowed down benchmarks a smidgin, I think
|
||||
respBufPool.Put(buf) // deferring this slowed down benchmarks a smidgin, I think
|
||||
return n, err
|
||||
}
|
||||
return rb.Buffer.ReadFrom(src)
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestWrite(t *testing.T) {
|
||||
responseTestString := "test"
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
buf := []byte(responseTestString)
|
||||
recordRequest.Write(buf)
|
||||
_, _ = recordRequest.Write(buf)
|
||||
if recordRequest.size != len(buf) {
|
||||
t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -28,8 +32,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// requestReplacer is a strings.Replacer which is used to
|
||||
@@ -243,6 +247,15 @@ func round(d, r time.Duration) time.Duration {
|
||||
return d
|
||||
}
|
||||
|
||||
// getPeerCert returns peer certificate
|
||||
func (r *replacer) getPeerCert() *x509.Certificate {
|
||||
if r.request.TLS != nil && len(r.request.TLS.PeerCertificates) > 0 {
|
||||
return r.request.TLS.PeerCertificates[0]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSubstitution retrieves value from corresponding key
|
||||
func (r *replacer) getSubstitution(key string) string {
|
||||
// search custom replacements first
|
||||
@@ -355,10 +368,14 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
return url.QueryEscape(r.request.URL.RequestURI())
|
||||
case "{when}":
|
||||
return now().Format(timeFormat)
|
||||
case "{when_iso_local}":
|
||||
return now().Format(timeFormatISO)
|
||||
case "{when_iso}":
|
||||
return now().UTC().Format(timeFormatISOUTC)
|
||||
case "{when_unix}":
|
||||
return strconv.FormatInt(now().Unix(), 10)
|
||||
case "{when_unix_ms}":
|
||||
return strconv.FormatInt(nanoToMilliseconds(now().UnixNano()), 10)
|
||||
case "{file}":
|
||||
_, file := path.Split(r.request.URL.Path)
|
||||
return file
|
||||
@@ -413,24 +430,92 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10)
|
||||
case "{tls_protocol}":
|
||||
if r.request.TLS != nil {
|
||||
for k, v := range caddytls.SupportedProtocols {
|
||||
if v == r.request.TLS.Version {
|
||||
return k
|
||||
}
|
||||
if name, err := caddytls.GetSupportedProtocolName(r.request.TLS.Version); err == nil {
|
||||
return name
|
||||
} else {
|
||||
return "tls" // this should never happen, but guard in case
|
||||
}
|
||||
return "tls" // this should never happen, but guard in case
|
||||
}
|
||||
return r.emptyValue // because not using a secure channel
|
||||
case "{tls_cipher}":
|
||||
if r.request.TLS != nil {
|
||||
for k, v := range caddytls.SupportedCiphersMap {
|
||||
if v == r.request.TLS.CipherSuite {
|
||||
return k
|
||||
}
|
||||
if name, err := caddytls.GetSupportedCipherName(r.request.TLS.CipherSuite); err == nil {
|
||||
return name
|
||||
} else {
|
||||
return "UNKNOWN" // this should never happen, but guard in case
|
||||
}
|
||||
return "UNKNOWN" // this should never happen, but guard in case
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_escaped_cert}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
pemBlock := pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
return url.QueryEscape(string(pem.EncodeToMemory(&pemBlock)))
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_fingerprint}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw))
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_i_dn}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.Issuer.String()
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_raw_cert}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return string(cert.Raw)
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_s_dn}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.Subject.String()
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_serial}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return fmt.Sprintf("%x", cert.SerialNumber)
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_v_end}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.NotAfter.In(time.UTC).Format("Jan 02 15:04:05 2006 MST")
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_v_remain}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
now := time.Now().In(time.UTC)
|
||||
days := int64(cert.NotAfter.Sub(now).Seconds() / 86400)
|
||||
return strconv.FormatInt(days, 10)
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{tls_client_v_start}":
|
||||
cert := r.getPeerCert()
|
||||
if cert != nil {
|
||||
return cert.NotBefore.Format("Jan 02 15:04:05 2006 MST")
|
||||
}
|
||||
return r.emptyValue
|
||||
case "{server_port}":
|
||||
_, port, err := net.SplitHostPort(r.request.Host)
|
||||
if err != nil {
|
||||
if r.request.TLS != nil {
|
||||
return "443"
|
||||
} else {
|
||||
return "80"
|
||||
}
|
||||
}
|
||||
return port
|
||||
default:
|
||||
// {labelN}
|
||||
if strings.HasPrefix(key, "{label") {
|
||||
@@ -450,9 +535,13 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
return r.emptyValue
|
||||
}
|
||||
|
||||
func nanoToMilliseconds(d int64) int64 {
|
||||
return d / 1e6
|
||||
}
|
||||
|
||||
// convertToMilliseconds returns the number of milliseconds in the given duration
|
||||
func convertToMilliseconds(d time.Duration) int64 {
|
||||
return d.Nanoseconds() / 1e6
|
||||
return nanoToMilliseconds(d.Nanoseconds())
|
||||
}
|
||||
|
||||
// Set sets key to value in the r.customReplacements map.
|
||||
@@ -462,6 +551,7 @@ func (r *replacer) Set(key, value string) {
|
||||
|
||||
const (
|
||||
timeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||
timeFormatISO = "2006-01-02T15:04:05" // ISO 8601 with timezone to be assumed as local
|
||||
timeFormatISOUTC = "2006-01-02T15:04:05Z" // ISO 8601 with timezone to be assumed as UTC
|
||||
headerContentType = "Content-Type"
|
||||
contentTypeJSON = "application/json"
|
||||
|
||||
@@ -16,12 +16,21 @@ package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
func TestNewReplacer(t *testing.T) {
|
||||
@@ -67,7 +76,7 @@ func TestReplace(t *testing.T) {
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some respons headers
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
@@ -77,7 +86,8 @@ func TestReplace(t *testing.T) {
|
||||
|
||||
old := now
|
||||
now = func() time.Time {
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 99999999, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
defer func() {
|
||||
now = old
|
||||
@@ -92,7 +102,9 @@ func TestReplace(t *testing.T) {
|
||||
{"The response status is {status}.", "The response status is 200."},
|
||||
{"{when}", "02/Jan/2006:15:04:05 +0000"},
|
||||
{"{when_iso}", "2006-01-02T15:04:12Z"},
|
||||
{"{when_iso_local}", "2006-01-02T15:04:05"},
|
||||
{"{when_unix}", "1136214252"},
|
||||
{"{when_unix_ms}", "1136214252099"},
|
||||
{"The Custom header is {>Custom}.", "The Custom header is foobarbaz."},
|
||||
{"The CustomAdd header is {>CustomAdd}.", "The CustomAdd header is caddy."},
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is CustomResponseHeader."},
|
||||
@@ -115,6 +127,7 @@ func TestReplace(t *testing.T) {
|
||||
{"{label1} {label2} {label3} {label4}", "localhost local - -"},
|
||||
{"Label with missing number is {label} or {labelQQ}", "Label with missing number is - or -"},
|
||||
{"\\{ 'hostname': '{hostname}' \\}", "{ 'hostname': '" + hostname + "' }"},
|
||||
{"{server_port}", "80"},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
@@ -147,6 +160,130 @@ func TestReplace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomServerPort(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost.local:8000/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
|
||||
testCase := struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
template: "{server_port}",
|
||||
expect: "8000",
|
||||
}
|
||||
|
||||
if expected, actual := testCase.expect, repl.Replace(testCase.template); expected != actual {
|
||||
t.Errorf("for template '%s', expected '%s', got '%s'", testCase.template, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTlsReplace(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
|
||||
clientCertText := []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
|
||||
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
|
||||
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
|
||||
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
|
||||
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
|
||||
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
|
||||
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
|
||||
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
|
||||
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
|
||||
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
block, _ := pem.Decode(clientCertText)
|
||||
if block == nil {
|
||||
t.Fatalf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode PEM certificate: %v", err)
|
||||
}
|
||||
|
||||
request := &http.Request{
|
||||
Method: "GET",
|
||||
Host: "foo.com",
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Path: "/path/",
|
||||
Host: "foo.com",
|
||||
},
|
||||
Header: http.Header{},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
RemoteAddr: "192.0.2.1:1234",
|
||||
RequestURI: "https://foo.com/path/",
|
||||
TLS: &tls.ConnectionState{
|
||||
Version: tls.VersionTLS12,
|
||||
HandshakeComplete: true,
|
||||
ServerName: "foo.com",
|
||||
CipherSuite: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
PeerCertificates: []*x509.Certificate{cert},
|
||||
},
|
||||
}
|
||||
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
|
||||
now := time.Now().In(time.UTC)
|
||||
days := int64(cert.NotAfter.Sub(now).Seconds() / 86400)
|
||||
pemBlock := pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
|
||||
protocol, _ := caddytls.GetSupportedProtocolName(request.TLS.Version)
|
||||
cipher, _ := caddytls.GetSupportedCipherName(request.TLS.CipherSuite)
|
||||
cEscapedCert := url.QueryEscape(string(pem.EncodeToMemory(&pemBlock)))
|
||||
cFingerprint := fmt.Sprintf("%x", sha256.Sum256(cert.Raw))
|
||||
cIDn := cert.Issuer.String()
|
||||
cRawCert := string(cert.Raw)
|
||||
cSDn := cert.Subject.String()
|
||||
cSerial := fmt.Sprintf("%x", cert.SerialNumber)
|
||||
cVEnd := cert.NotAfter.In(time.UTC).Format("Jan 02 15:04:05 2006 MST")
|
||||
cVRemain := strconv.FormatInt(days, 10)
|
||||
cVStart := cert.NotBefore.Format("Jan 02 15:04:05 2006 MST")
|
||||
|
||||
testCases := []struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
{"{tls_protocol}", protocol},
|
||||
{"{tls_cipher}", cipher},
|
||||
{"{tls_client_escaped_cert}", cEscapedCert},
|
||||
{"{tls_client_fingerprint}", cFingerprint},
|
||||
{"{tls_client_i_dn}", cIDn},
|
||||
{"{tls_client_raw_cert}", cRawCert},
|
||||
{"{tls_client_s_dn}", cSDn},
|
||||
{"{tls_client_serial}", cSerial},
|
||||
{"{tls_client_v_end}", cVEnd},
|
||||
{"{tls_client_v_remain}", cVRemain},
|
||||
{"{tls_client_v_start}", cVStart},
|
||||
{"{server_port}", "443"},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
if expected, actual := c.expect, repl.Replace(c.template); expected != actual {
|
||||
t.Errorf("for template '%s', expected '%s', got '%s'", c.template, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplace(b *testing.B) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
@@ -166,10 +303,11 @@ func BenchmarkReplace(b *testing.B) {
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some respons headers
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
now = func() time.Time {
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
|
||||
@@ -198,10 +336,11 @@ func BenchmarkReplaceEscaped(b *testing.B) {
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some respons headers
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
now = func() time.Time {
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
|
||||
@@ -228,6 +367,7 @@ func TestResponseRecorderNil(t *testing.T) {
|
||||
|
||||
old := now
|
||||
now = func() time.Time {
|
||||
// Note that the `-7` is seconds, not hours.
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
defer func() {
|
||||
@@ -333,7 +473,7 @@ func TestRound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMillisecondConverstion(t *testing.T) {
|
||||
func TestMillisecondConversion(t *testing.T) {
|
||||
var testCases = map[time.Duration]int64{
|
||||
2 * time.Second: 2000,
|
||||
9039492 * time.Nanosecond: 9,
|
||||
|
||||
@@ -20,11 +20,12 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
// LogRoller implements a type that provides a rolling logger.
|
||||
type LogRoller struct {
|
||||
Disabled bool
|
||||
Filename string
|
||||
MaxSize int
|
||||
MaxAge int
|
||||
@@ -66,10 +67,11 @@ func IsLogRollerSubdirective(subdir string) bool {
|
||||
return subdir == directiveRotateSize ||
|
||||
subdir == directiveRotateAge ||
|
||||
subdir == directiveRotateKeep ||
|
||||
subdir == directiveRotateCompress
|
||||
subdir == directiveRotateCompress ||
|
||||
subdir == directiveRotateDisable
|
||||
}
|
||||
|
||||
var invalidRollerParameterErr = errors.New("invalid roller parameter")
|
||||
var errInvalidRollParameter = errors.New("invalid roller parameter")
|
||||
|
||||
// ParseRoller parses roller contents out of c.
|
||||
func ParseRoller(l *LogRoller, what string, where ...string) error {
|
||||
@@ -79,16 +81,16 @@ func ParseRoller(l *LogRoller, what string, where ...string) error {
|
||||
|
||||
// rotate_compress doesn't accept any parameters.
|
||||
// others only accept one parameter
|
||||
if (what == directiveRotateCompress && len(where) != 0) ||
|
||||
(what != directiveRotateCompress && len(where) != 1) {
|
||||
return invalidRollerParameterErr
|
||||
if ((what == directiveRotateCompress || what == directiveRotateDisable) && len(where) != 0) ||
|
||||
((what != directiveRotateCompress && what != directiveRotateDisable) && len(where) != 1) {
|
||||
return errInvalidRollParameter
|
||||
}
|
||||
|
||||
var (
|
||||
value int
|
||||
err error
|
||||
)
|
||||
if what != directiveRotateCompress {
|
||||
if what != directiveRotateCompress && what != directiveRotateDisable {
|
||||
value, err = strconv.Atoi(where[0])
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -96,6 +98,8 @@ func ParseRoller(l *LogRoller, what string, where ...string) error {
|
||||
}
|
||||
|
||||
switch what {
|
||||
case directiveRotateDisable:
|
||||
l.Disabled = true
|
||||
case directiveRotateSize:
|
||||
l.MaxSize = value
|
||||
case directiveRotateAge:
|
||||
@@ -127,6 +131,7 @@ const (
|
||||
// defaultRotateKeep is 10 files.
|
||||
defaultRotateKeep = 10
|
||||
|
||||
directiveRotateDisable = "rotate_disable"
|
||||
directiveRotateSize = "rotate_size"
|
||||
directiveRotateAge = "rotate_age"
|
||||
directiveRotateKeep = "rotate_keep"
|
||||
@@ -135,4 +140,4 @@ const (
|
||||
|
||||
// lumberjacks maps log filenames to the logger
|
||||
// that is being used to keep them rolled/maintained.
|
||||
var lumberjacks = make(map[string]*lumberjack.Logger)
|
||||
var lumberjacks = make(map[string]io.Writer)
|
||||
|
||||
+102
-36
@@ -29,21 +29,19 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/h2quic"
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/telemetry"
|
||||
"github.com/lucas-clemente/quic-go/http3"
|
||||
)
|
||||
|
||||
// Server is the HTTP server implementation.
|
||||
type Server struct {
|
||||
Server *http.Server
|
||||
quicServer *h2quic.Server
|
||||
listener net.Listener
|
||||
listenerMu sync.Mutex
|
||||
quicServer *http3.Server
|
||||
sites []*SiteConfig
|
||||
connTimeout time.Duration // max time to wait for a connection before force stop
|
||||
tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine
|
||||
@@ -106,7 +104,7 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
if s.Server.TLSConfig != nil {
|
||||
// enable QUIC if desired (requires HTTP/2)
|
||||
if HTTP2 && QUIC {
|
||||
s.quicServer = &h2quic.Server{Server: s.Server}
|
||||
s.quicServer = &http3.Server{Server: s.Server}
|
||||
s.Server.Handler = s.wrapWithSvcHeaders(s.Server.Handler)
|
||||
}
|
||||
|
||||
@@ -236,7 +234,9 @@ func makeHTTPServerWithTimeouts(addr string, group []*SiteConfig) *http.Server {
|
||||
|
||||
func (s *Server) wrapWithSvcHeaders(previousHandler http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.quicServer.SetQuicHeaders(w.Header())
|
||||
if err := s.quicServer.SetQuicHeaders(w.Header()); err != nil {
|
||||
log.Println("[Error] failed to set proper headers for QUIC: ", err)
|
||||
}
|
||||
previousHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func (s *Server) wrapWithSvcHeaders(previousHandler http.Handler) http.HandlerFu
|
||||
// used to serve requests.
|
||||
func (s *Server) Listen() (net.Listener, error) {
|
||||
if s.Server == nil {
|
||||
return nil, fmt.Errorf("Server field is nil")
|
||||
return nil, fmt.Errorf("server field is nil")
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", s.Server.Addr)
|
||||
@@ -273,16 +273,26 @@ func (s *Server) Listen() (net.Listener, error) {
|
||||
ln = tcpKeepAliveListener{TCPListener: tcpLn}
|
||||
}
|
||||
|
||||
cln := s.WrapListener(ln)
|
||||
|
||||
// Very important to return a concrete caddy.Listener
|
||||
// implementation for graceful restarts.
|
||||
return cln.(caddy.Listener), nil
|
||||
}
|
||||
|
||||
// WrapListener wraps ln in the listener middlewares configured
|
||||
// for this server.
|
||||
func (s *Server) WrapListener(ln net.Listener) net.Listener {
|
||||
if ln == nil {
|
||||
return nil
|
||||
}
|
||||
cln := ln.(caddy.Listener)
|
||||
for _, site := range s.sites {
|
||||
for _, m := range site.listenerMiddleware {
|
||||
cln = m(cln)
|
||||
}
|
||||
}
|
||||
|
||||
// Very important to return a concrete caddy.Listener
|
||||
// implementation for graceful restarts.
|
||||
return cln.(caddy.Listener), nil
|
||||
return cln
|
||||
}
|
||||
|
||||
// ListenPacket creates udp connection for QUIC if it is enabled,
|
||||
@@ -299,10 +309,6 @@ func (s *Server) ListenPacket() (net.PacketConn, error) {
|
||||
|
||||
// Serve serves requests on ln. It blocks until ln is closed.
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
s.listenerMu.Lock()
|
||||
s.listener = ln
|
||||
s.listenerMu.Unlock()
|
||||
|
||||
if s.Server.TLSConfig != nil {
|
||||
// Create TLS listener - note that we do not replace s.listener
|
||||
// with this TLS listener; tls.listener is unexported and does
|
||||
@@ -318,11 +324,19 @@ func (s *Server) Serve(ln net.Listener) error {
|
||||
s.tlsGovChan = caddytls.RotateSessionTicketKeys(s.Server.TLSConfig)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if s.quicServer != nil {
|
||||
if err := s.quicServer.Close(); err != nil {
|
||||
log.Println("[ERROR] failed to close QUIC server: ", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := s.Server.Serve(ln)
|
||||
if s.quicServer != nil {
|
||||
s.quicServer.Close()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServePacket serves QUIC requests on pc until it is closed.
|
||||
@@ -345,6 +359,16 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}()
|
||||
|
||||
// record the User-Agent string (with a cap on its length to mitigate attacks)
|
||||
ua := r.Header.Get("User-Agent")
|
||||
if len(ua) > 512 {
|
||||
ua = ua[:512]
|
||||
}
|
||||
uaHash := telemetry.FastHash([]byte(ua)) // this is a normalized field
|
||||
go telemetry.SetNested("http_user_agent", uaHash, ua)
|
||||
go telemetry.AppendUnique("http_user_agent_count", uaHash)
|
||||
go telemetry.Increment("http_request_count")
|
||||
|
||||
// copy the original, unchanged URL into the context
|
||||
// so it can be referenced by middlewares
|
||||
urlCopy := *r.URL
|
||||
@@ -388,24 +412,26 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
|
||||
if vhost == nil {
|
||||
// check for ACME challenge even if vhost is nil;
|
||||
// could be a new host coming online soon
|
||||
if caddytls.HTTPChallengeHandler(w, r, "localhost") {
|
||||
// could be a new host coming online soon - choose any
|
||||
// vhost's cert manager configuration, I guess
|
||||
if len(s.sites) > 0 && s.sites[0].TLS.Manager.HandleHTTPChallenge(w, r) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// otherwise, log the error and write a message to the client
|
||||
remoteHost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteHost = r.RemoteAddr
|
||||
}
|
||||
WriteSiteNotFound(w, r) // don't add headers outside of this function
|
||||
WriteSiteNotFound(w, r) // don't add headers outside of this function (http.forwardproxy)
|
||||
log.Printf("[INFO] %s - No such site at %s (Remote: %s, Referer: %s)",
|
||||
hostname, s.Server.Addr, remoteHost, r.Header.Get("Referer"))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// we still check for ACME challenge if the vhost exists,
|
||||
// because we must apply its HTTP challenge config settings
|
||||
if caddytls.HTTPChallengeHandler(w, r, vhost.ListenHost) {
|
||||
// because the HTTP challenge might be disabled by its config
|
||||
if vhost.TLS.Manager.HandleHTTPChallenge(w, r) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -416,6 +442,18 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
r.URL = trimPathPrefix(r.URL, pathPrefix)
|
||||
}
|
||||
|
||||
// enforce strict host matching, which ensures that the SNI
|
||||
// value (if any), matches the Host header; essential for
|
||||
// sites that rely on TLS ClientAuth sharing a port with
|
||||
// sites that do not - if mismatched, close the connection
|
||||
if vhost.StrictHostMatching && r.TLS != nil &&
|
||||
strings.ToLower(r.TLS.ServerName) != strings.ToLower(hostname) {
|
||||
r.Close = true
|
||||
log.Printf("[ERROR] %s - strict host matching: SNI (%s) and HTTP Host (%s) values differ",
|
||||
vhost.Addr, r.TLS.ServerName, hostname)
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return vhost.middlewareChain.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -469,16 +507,34 @@ func (s *Server) Stop() error {
|
||||
// OnStartupComplete lists the sites served by this server
|
||||
// and any relevant information, assuming caddy.Quiet == false.
|
||||
func (s *Server) OnStartupComplete() {
|
||||
if caddy.Quiet {
|
||||
return
|
||||
if !caddy.Quiet {
|
||||
firstSite := s.sites[0]
|
||||
scheme := "HTTP"
|
||||
if firstSite.TLS.Enabled {
|
||||
scheme = "HTTPS"
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
fmt.Printf("Serving %s on port "+firstSite.Port()+" \n", scheme)
|
||||
s.outputSiteInfo(false)
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
// Print out process log without header comment
|
||||
s.outputSiteInfo(true)
|
||||
}
|
||||
|
||||
func (s *Server) outputSiteInfo(isProcessLog bool) {
|
||||
for _, site := range s.sites {
|
||||
output := site.Addr.String()
|
||||
if caddy.IsLoopback(s.Address()) && !caddy.IsLoopback(site.Addr.Host) {
|
||||
output += " (only accessible on this machine)"
|
||||
}
|
||||
fmt.Println(output)
|
||||
log.Println(output)
|
||||
if isProcessLog {
|
||||
log.Printf("[INFO] Serving %s \n", output)
|
||||
} else {
|
||||
fmt.Println(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,13 +554,21 @@ type tcpKeepAliveListener struct {
|
||||
}
|
||||
|
||||
// Accept accepts the connection with a keep-alive enabled.
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
if err = tc.SetKeepAlive(true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// OpenBSD has no user-settable per-socket TCP keepalive
|
||||
// https://github.com/caddyserver/caddy/pull/2787
|
||||
if runtime.GOOS != "openbsd" {
|
||||
if err = tc.SetKeepAlivePeriod(3 * time.Minute); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
@@ -542,7 +606,9 @@ func WriteTextResponse(w http.ResponseWriter, status int, body string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(body))
|
||||
if _, err := w.Write([]byte(body)); err != nil {
|
||||
log.Println("[Error] failed to write body: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SafePath joins siteRoot and reqPath and converts it to a path that can
|
||||
|
||||
@@ -17,7 +17,7 @@ package httpserver
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
)
|
||||
|
||||
// SiteConfig contains information about a site
|
||||
@@ -36,6 +36,16 @@ type SiteConfig struct {
|
||||
// TLS configuration
|
||||
TLS *caddytls.Config
|
||||
|
||||
// If true, the Host header in the HTTP request must
|
||||
// match the SNI value in the TLS handshake (if any).
|
||||
// This should be enabled whenever a site relies on
|
||||
// TLS client authentication, for example; or any time
|
||||
// you want to enforce that THIS site's TLS config
|
||||
// is used and not the TLS config of any other site
|
||||
// on the same listener. TODO: Check how relevant this
|
||||
// is with TLS 1.3.
|
||||
StrictHostMatching bool
|
||||
|
||||
// Uncompiled middleware stack
|
||||
middleware []Middleware
|
||||
|
||||
@@ -76,7 +86,7 @@ type SiteConfig struct {
|
||||
}
|
||||
|
||||
// Timeouts specify various timeouts for a server to use.
|
||||
// If the assocated bool field is true, then the duration
|
||||
// If the associated bool field is true, then the duration
|
||||
// value should be treated literally (i.e. a zero-value
|
||||
// duration would mean "no timeout"). If false, the duration
|
||||
// was left unset, so a zero-value duration would mean to
|
||||
|
||||
@@ -19,11 +19,13 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
mathrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
@@ -31,6 +33,8 @@ import (
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/caddyserver/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
@@ -176,7 +180,7 @@ func (c Context) Port() (string, error) {
|
||||
if err != nil {
|
||||
if !strings.Contains(c.Req.Host, ":") {
|
||||
// common with sites served on the default port 80
|
||||
return HTTPPort, nil
|
||||
return strconv.Itoa(certmagic.HTTPPort), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -419,7 +423,9 @@ func (c Context) RandomString(minLen, maxLen int) string {
|
||||
// secureRandomBytes returns a number of bytes using crypto/rand.
|
||||
secureRandomBytes := func(numBytes int) []byte {
|
||||
randomBytes := make([]byte, numBytes)
|
||||
rand.Read(randomBytes)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
log.Println("[ERROR] failed to read bytes: ", err)
|
||||
}
|
||||
return randomBytes
|
||||
}
|
||||
|
||||
@@ -448,6 +454,15 @@ func (c Context) AddLink(link string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Returns either TLS protocol version if TLS used or empty string otherwise
|
||||
func (c Context) TLSVersion() (ret string) {
|
||||
if c.Req.TLS != nil {
|
||||
// Safe to ignore an error
|
||||
ret, _ = caddytls.GetSupportedProtocolName(c.Req.TLS.Version)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// buffer pool for .Include context actions
|
||||
var includeBufs = sync.Pool{
|
||||
New: func() interface{} {
|
||||
|
||||
@@ -16,6 +16,7 @@ package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -100,7 +101,7 @@ func TestInclude(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// WriteFile truncates the contentt
|
||||
// WriteFile truncates the content
|
||||
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
|
||||
@@ -161,7 +162,7 @@ func TestMarkdown(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
testPrefix := getTestPrefix(i)
|
||||
|
||||
// WriteFile truncates the contentt
|
||||
// WriteFile truncates the content
|
||||
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
|
||||
@@ -277,7 +278,7 @@ func TestHostname(t *testing.T) {
|
||||
// // Test 3 - ipv6 without port and brackets
|
||||
// {"2001:4860:4860::8888", "google-public-dns-a.google.com."},
|
||||
// Test 4 - no hostname available
|
||||
{"1.1.1.1", "1.1.1.1"},
|
||||
{"0.0.0.0", "0.0.0.0"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
@@ -922,3 +923,40 @@ func TestAddLink(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTlsVersion(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
tlsState *tls.ConnectionState
|
||||
expectedResult string
|
||||
}{
|
||||
{
|
||||
&tls.ConnectionState{Version: tls.VersionTLS10},
|
||||
"tls1.0",
|
||||
},
|
||||
{
|
||||
&tls.ConnectionState{Version: tls.VersionTLS11},
|
||||
"tls1.1",
|
||||
},
|
||||
{
|
||||
&tls.ConnectionState{Version: tls.VersionTLS12},
|
||||
"tls1.2",
|
||||
},
|
||||
// TLS not used
|
||||
{
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
// Unsupported version
|
||||
{
|
||||
&tls.ConnectionState{Version: 0x0399},
|
||||
"",
|
||||
},
|
||||
} {
|
||||
context := getContextOrFail(t)
|
||||
context.Req.TLS = test.tlsState
|
||||
result := context.TLSVersion()
|
||||
if result != test.expectedResult {
|
||||
t.Errorf("Expected %s got %s", test.expectedResult, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,12 @@ type vhostTrie struct {
|
||||
|
||||
// newVHostTrie returns a new vhostTrie.
|
||||
func newVHostTrie() *vhostTrie {
|
||||
return &vhostTrie{edges: make(map[string]*vhostTrie), fallbackHosts: []string{"0.0.0.0", ""}}
|
||||
// TODO: fallbackHosts doesn't discriminate between network interfaces;
|
||||
// i.e. if there is a host "0.0.0.0", it could match a request coming
|
||||
// in to "[::1]" (and vice-versa) even though the IP versions differ.
|
||||
// This might be OK, or maybe it's not desirable. The 'bind' directive
|
||||
// can be used to restrict what interface a listener binds to.
|
||||
return &vhostTrie{edges: make(map[string]*vhostTrie), fallbackHosts: []string{"0.0.0.0", "[::]", ""}}
|
||||
}
|
||||
|
||||
// Insert adds stack to t keyed by key. The key should be
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -103,7 +104,9 @@ func populateTestTrie(trie *vhostTrie, keys []string) {
|
||||
func(key string) {
|
||||
site := &SiteConfig{
|
||||
middlewareChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
w.Write([]byte(key))
|
||||
if _, err := w.Write([]byte(key)); err != nil {
|
||||
log.Println("[ERROR] failed to write bytes: ", err)
|
||||
}
|
||||
return 0, nil
|
||||
}),
|
||||
}
|
||||
@@ -139,7 +142,9 @@ func assertTestTrie(t *testing.T, trie *vhostTrie, tests []vhostTrieTest, hasWil
|
||||
|
||||
// And it must be the correct value
|
||||
resp := httptest.NewRecorder()
|
||||
site.middlewareChain.ServeHTTP(resp, nil)
|
||||
if _, err := site.middlewareChain.ServeHTTP(resp, nil); err != nil {
|
||||
log.Println("[ERROR] failed to serve HTTP: ", err)
|
||||
}
|
||||
actualHandlerKey := resp.Body.String()
|
||||
if actualHandlerKey != test.expectedKey {
|
||||
t.Errorf("Test %d: Expected match '%s' but matched '%s'",
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -17,9 +17,9 @@ package index
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
func TestIndexIncompleteParams(t *testing.T) {
|
||||
|
||||
@@ -23,7 +23,7 @@ package internalsrv
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Internal middleware protects internal locations from external requests -
|
||||
|
||||
@@ -16,13 +16,14 @@ package internalsrv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -126,7 +127,9 @@ func internalTestHandlerFunc(w http.ResponseWriter, r *http.Request) (int, error
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", contentTypeOctetStream)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(internalProtectedData)))
|
||||
w.Write([]byte(internalProtectedData))
|
||||
if _, err := w.Write([]byte(internalProtectedData)); err != nil {
|
||||
log.Println("[ERROR] failed to write bytes: ", err)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package internalsrv
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -33,7 +33,12 @@ func setup(c *caddy.Controller) error {
|
||||
return err
|
||||
}
|
||||
|
||||
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
// Append Internal paths to Caddy config HiddenFiles to ensure
|
||||
// files do not appear in Browse
|
||||
config := httpserver.GetConfig(c)
|
||||
config.HiddenFiles = append(config.HiddenFiles, paths...)
|
||||
|
||||
config.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
|
||||
return Internal{Next: next, Paths: paths}
|
||||
})
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ package internalsrv
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// Limit is a middleware to control request body size
|
||||
|
||||
@@ -16,12 +16,13 @@ package limits
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestBodySizeLimit(t *testing.T) {
|
||||
@@ -39,7 +40,9 @@ func TestBodySizeLimit(t *testing.T) {
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "/", strings.NewReader(expectContent+expectContent))
|
||||
l.ServeHTTP(httptest.NewRecorder(), r)
|
||||
if _, err := l.ServeHTTP(httptest.NewRecorder(), r); err != nil {
|
||||
log.Println("[ERROR] failed to serve HTTP: ", err)
|
||||
}
|
||||
if got := string(gotContent); got != expectContent {
|
||||
t.Errorf("expected content[%s], got[%s]", expectContent, got)
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -17,12 +17,13 @@ package log
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
type erroringMiddleware struct{}
|
||||
@@ -89,7 +90,9 @@ func TestLogRequestBody(t *testing.T) {
|
||||
}},
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// drain up body
|
||||
ioutil.ReadAll(r.Body)
|
||||
if _, err := ioutil.ReadAll(r.Body); err != nil {
|
||||
log.Println("[ERROR] failed to read request body: ", err)
|
||||
}
|
||||
return 0, nil
|
||||
}),
|
||||
}
|
||||
@@ -153,7 +156,9 @@ func TestMultiEntries(t *testing.T) {
|
||||
}},
|
||||
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// drain up body
|
||||
ioutil.ReadAll(r.Body)
|
||||
if _, err := ioutil.ReadAll(r.Body); err != nil {
|
||||
log.Println("[ERROR] failed to read request body: ", err)
|
||||
}
|
||||
return 0, nil
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
// setup sets up the logging middleware.
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -336,7 +336,7 @@ func TestLogParse(t *testing.T) {
|
||||
{`log access.log { rotate_size }`, true, nil},
|
||||
{`log access.log { ipmask }`, true, nil},
|
||||
{`log access.log { invalid_option 1 }`, true, nil},
|
||||
{`log / acccess.log "{remote} - [{when}] "{method} {port}" {scheme} {mitm} "`, true, nil},
|
||||
{`log / access.log "{remote} - [{when}] "{method} {port}" {scheme} {mitm} "`, true, nil},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("http", test.inputLogRules)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
@@ -142,7 +143,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
case err == nil: // nop
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
case os.IsNotExist(err):
|
||||
return http.StatusNotFound, nil
|
||||
default: // did we run out of FD?
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -168,7 +169,9 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(html)))
|
||||
httpserver.SetLastModifiedHeader(w, lastModTime)
|
||||
if r.Method == http.MethodGet {
|
||||
w.Write(html)
|
||||
if _, err := w.Write(html); err != nil {
|
||||
log.Println("[ERROR] failed to write html response: ", err)
|
||||
}
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func (n *NoneParser) Type() string {
|
||||
return "None"
|
||||
}
|
||||
|
||||
// Init prepases and parses the metadata and markdown file
|
||||
// Init preparses and parses the metadata and markdown file
|
||||
func (n *NoneParser) Init(b *bytes.Buffer) bool {
|
||||
m := make(map[string]interface{})
|
||||
n.metadata = NewMetadata(m)
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/markdown/metadata"
|
||||
"github.com/mholt/caddy/caddyhttp/markdown/summary"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/markdown/metadata"
|
||||
"github.com/caddyserver/caddy/caddyhttp/markdown/summary"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/caddyserver/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestConfig_Markdown(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user