mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
335 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| f379bf3421 | |||
| 1896b420d8 | |||
| 1580169e2b | |||
| 95514da91b | |||
| 18ff8748e7 | |||
| 2ed1dd6afc | |||
| 8039a7127f | |||
| a8dfa9f0b7 | |||
| 33aeb1cb5c | |||
| 8bdd13b594 | |||
| 52316952a5 | |||
| 7c868afd32 | |||
| 4df8028bc3 | |||
| f1eaae9b0d | |||
| 385ea53309 | |||
| 2716e272c1 | |||
| ca34a3e1aa | |||
| 3ee6d30659 | |||
| ef40659c70 | |||
| 6e2de19d9f | |||
| 3afb1ae380 | |||
| 37c852c382 | |||
| 3d01f46efa | |||
| 3a6496c268 | |||
| 64c9f20919 | |||
| d10d8c23c4 | |||
| 3cd36fd47d | |||
| aaec7e469c | |||
| 6f78cc49d1 | |||
| 13dfffd203 | |||
| 5552dcbbc7 | |||
| 37b291f82c | |||
| d3f338ddab | |||
| 3b66865da5 | |||
| 637b0b47ee | |||
| a6521357e5 | |||
| 269a8b5fce | |||
| 1201492222 | |||
| faa5248d1f | |||
| 986d4ffe3d | |||
| a03eba6fbc | |||
| 8db80c4a88 | |||
| 4704a56a17 | |||
| 896dc6bc69 | |||
| 6f4cf7eec7 | |||
| be96cc0e65 | |||
| ef585ed810 | |||
| 4b2e22289d | |||
| f26447e2fb | |||
| 08028714b5 | |||
| 2de4950015 | |||
| d29640699e | |||
| 6a9aea04b1 | |||
| 592d199315 | |||
| 5820356cf6 | |||
| 6b3c2212a1 | |||
| 703cf7bf8b | |||
| 3e00e18adc | |||
| 6c17e4d4c8 | |||
| 388ff6bc0a | |||
| fc2ff9155c | |||
| a50f3a4cfe | |||
| fd3fafa50c | |||
| e20779e405 | |||
| fc6d62286e | |||
| e2997ac974 | |||
| 8f0b44b8a4 | |||
| 50ab4fe11e | |||
| 106d62b067 | |||
| a76222f607 | |||
| e9515425e0 | |||
| c80c34ef45 | |||
| 1ba5512015 | |||
| 55a564df6d | |||
| 8a326d4dc1 | |||
| d35719daed | |||
| c296d7e7e0 | |||
| fc1509eed4 | |||
| 9619fe224c | |||
| c0efec52d9 | |||
| a74320bf4c | |||
| 1125a236ea | |||
| 8658e189e1 | |||
| 9a22cda15d | |||
| 169ab3acda | |||
| 5f39cbef94 | |||
| 63fd264043 | |||
| 345b312e00 | |||
| 5cca9cc18e | |||
| 9ebc11d775 | |||
| 689591ef01 | |||
| 2782553231 | |||
| 4ec5522a33 | |||
| ad2956fd1d | |||
| 34a34c565d | |||
| 74d4fd3c29 | |||
| ac1f3bfaaa | |||
| f7a70266ed | |||
| fc75527eb5 | |||
| e5d04f9a96 | |||
| 91a60a8d25 | |||
| 5c9fc3a473 | |||
| 02ac1f61c4 | |||
| 59a8ada4a8 | |||
| 1889049ef3 | |||
| 68a495f144 | |||
| a2db340378 | |||
| c6a2911725 | |||
| 654f26cb91 | |||
| dd4b3efa47 | |||
| 3a969bc075 | |||
| 425f61142f | |||
| 79072828a5 | |||
| 0548b97701 | |||
| 99625ae3f6 | |||
| c4dfbb9956 | |||
| b0d9c058cc | |||
| cccfe3b4ef | |||
| f71955e89c | |||
| dd44491e13 | |||
| ac865e8910 | |||
| b7167803f2 | |||
| 97710ced7e | |||
| f878247a18 | |||
| 118cf5f240 | |||
| f9cba03d25 | |||
| baf6db5b57 | |||
| e60400a92e | |||
| e377eeff50 | |||
| 84a2f8e89e | |||
| 64be3e410c | |||
| 643dac688c | |||
| 0a624f87ff | |||
| fea8f37f9d | |||
| a808252079 | |||
| 93bcca0ccc | |||
| d39b95600a | |||
| 545fa844bb | |||
| b6e10e3cb2 |
@@ -10,5 +10,10 @@
|
||||
# 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
|
||||
|
||||
*.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
|
||||
|
||||
@@ -103,7 +103,7 @@ While we really do value your requests and implement many of them, not all featu
|
||||
|
||||
### 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/docs](https://caddyserver.com/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 +111,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?
|
||||
|
||||
@@ -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! -->
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: For behaviors which violate documentation or cause incorrect results
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
This template is for bug reports. The lack of a feature is not a bug; to request a feature, please switch templates.
|
||||
|
||||
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
|
||||
|
||||
Please do not skip relevant questions; this will slow down the debugging process and your issue may be closed.
|
||||
-->
|
||||
|
||||
## 1. Which version of Caddy are you using (`caddy -version`)?
|
||||
<!-- If there is no version information, please paste commit SHA instead. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. What are you trying to do?
|
||||
<!-- Please clearly describe what you are trying to do thoroughly enough so that a reader with no background information can repeat it. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 3. What is your Caddyfile?
|
||||
```text
|
||||
paste entire Caddyfile here - DO NOT REDACT ANYTHING (except credentials)
|
||||
```
|
||||
<!-- Changing or hiding parts of your Caddyfile only slows things down and may result in your report being closed.
|
||||
For more information, see https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
|
||||
<!-- If you are unable to post this publicly, we offer private support: https://caddyserver.com/products/support -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 4. How did you run Caddy (give the full command and describe the execution environment)?
|
||||
<!-- IMPORTANT: Please eliminate Docker, systemd, reverse proxies, upstream dependencies, caches, firewalls, and other unnecessary, external factors from your setup first. This will help prove that this is a bug in Caddy and not a misconfiguration of your environment. We may close issues that are too complex to replicate. Thank you! -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 5. Please paste any relevant HTTP request(s) here.
|
||||
<!-- Paste curl command, or full HTTP request including headers and body. You may skip this if the bug does not require HTTP requests. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 6. What did you expect to see?
|
||||
<!-- Describe your expected results as precisely as possible. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 7. What did you see instead (give full error messages and/or log)?
|
||||
<!-- Please run Caddy with the -log flag, and use the log and errors directives as needed. DO NOT REDACT INFORMATION except for credentials. See https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 8. Why is this a bug, and how do you think this should be fixed?
|
||||
<!-- Help us understand why it is a bug; it is not always obvious. You can help us get this resolved faster by thinking about the problem and describing possible solutions! -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 9. What are you doing to work around the problem in the meantime?
|
||||
<!-- This can help others who encounter the same problem, until we implement a fix. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 10. Please link to any related issues, pull requests, and/or discussion.
|
||||
<!-- This can add crucial context to your report. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## Bonus: What do you use Caddy for? Why did you choose Caddy?
|
||||
<!-- We'd like to know! -->
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
This template is for feature requests. If you are reporting a bug instead, please switch templates.
|
||||
|
||||
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
|
||||
-->
|
||||
|
||||
|
||||
## 1. What would you like to have changed?
|
||||
<!-- Fully describe the feature or enhancement you are requesting; examples can be helpful too -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. Why is this feature a useful, necessary, and/or important addition to this project?
|
||||
<!-- Please justify why this change adds value to the project, considering the added maintenance burden and complexity the change introduces -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 3. What alternatives are there, or what are you doing in the meantime to work around the lack of this feature?
|
||||
<!-- We want to get an idea of what is being done in practice, or how other projects support your feature -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 4. Please link to any relevant issues, pull requests, or other discussions.
|
||||
<!-- This adds crucial context to your feature request and can speed things up -->
|
||||
@@ -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
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Pull request
|
||||
about: Propose changes to the code
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for contributing to Caddy! Please fill this out to help us make the most of your pull request.
|
||||
|
||||
Was this change discussed in an issue first? That can help save time in case the change is not a good fit for the project. Not all pull requests get merged.
|
||||
|
||||
It is not uncommon for pull requests to go through several, iterative reviews. Please be patient with us! Every reviewer is a volunteer, and each has their own style.
|
||||
-->
|
||||
|
||||
## 1. What does this change do, exactly?
|
||||
<!-- Please be specific. Motivate the problem, and justify why this is the best solution. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 2. Please link to the relevant issues.
|
||||
<!-- This adds crucial context to your change. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 3. Which documentation changes (if any) need to be made because of this PR?
|
||||
<!-- Reviewers will often reference this first in order to know what to expect from the change. Please be specific enough so that they can paste your wording into the documentation directly. -->
|
||||
|
||||
|
||||
|
||||
|
||||
## 4. Checklist
|
||||
|
||||
- [ ] I have written tests and verified that they fail without my change
|
||||
- [ ] I have squashed any insignificant commits
|
||||
- [ ] This change has comments explaining 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
|
||||
+4
-1
@@ -13,7 +13,10 @@ access.log
|
||||
|
||||
/*.conf
|
||||
Caddyfile
|
||||
!caddyfile/
|
||||
|
||||
og_static/
|
||||
|
||||
.vscode/
|
||||
.vscode/
|
||||
|
||||
*.bat
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
language: go
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
- quic.clemente.io
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
- 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 and certain linters
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
- go get github.com/client9/misspell/cmd/misspell
|
||||
- go get github.com/gordonklaus/ineffassign
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get github.com/tsenart/deadcode
|
||||
|
||||
script:
|
||||
- gometalinter --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode --tests --vendor ./...
|
||||
- vendorcheck ./...
|
||||
- go test -race ./...
|
||||
|
||||
after_script:
|
||||
- golint ./...
|
||||
@@ -1,11 +1,10 @@
|
||||
<p align="center">
|
||||
<a href="https://caddyserver.com"><img src="https://cloud.githubusercontent.com/assets/1128849/25305033/12916fce-2731-11e7-86ec-580d4d31cb16.png" alt="Caddy" width="400"></a>
|
||||
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
|
||||
</p>
|
||||
<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://dev.azure.com/mholt-dev/Caddy/_build?definitionId=1"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/1/master.svg?label=cross-platform%20tests"></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>
|
||||
<br>
|
||||
@@ -21,10 +20,16 @@
|
||||
|
||||
---
|
||||
|
||||
Caddy is fast, easy to use, and makes you more productive.
|
||||
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).
|
||||
|
||||
<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
|
||||
|
||||
- [Features](#features)
|
||||
@@ -41,25 +46,83 @@ Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.co
|
||||
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
|
||||
- **HTTP/2** by default
|
||||
- **Virtual hosting** so multiple sites just work
|
||||
- Experimental **QUIC support** for those that like speed
|
||||
- Experimental **QUIC support** for cutting-edge transmissions
|
||||
- TLS session ticket **key rotation** for more secure connections
|
||||
- **Extensible with plugins** because a convenient web server is a helpful one
|
||||
- **Runs anywhere** with **no external dependencies** (not even libc)
|
||||
|
||||
There's way more, too! [See all features built into Caddy.](https://caddyserver.com/features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
|
||||
[See a more complete list of features built into Caddy.](https://caddyserver.com/features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
|
||||
|
||||
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 any one 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)** allows you to
|
||||
customize your build in the browser
|
||||
- **[Latest release](https://github.com/mholt/caddy/releases/latest)** for
|
||||
pre-built, vanilla binaries
|
||||
- **go get** to build from source: `go get github.com/mholt/caddy/caddy` (requires Go 1.8 or newer) - to build with proper version information (required when filing issues), `cd` to the `caddy` folder and use `go run build.go`.
|
||||
- **[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
|
||||
- **[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>
|
||||
|
||||
Then make sure the `caddy` binary is in your PATH.
|
||||
|
||||
## Build
|
||||
|
||||
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.12 or newer).
|
||||
|
||||
**To build Caddy without plugins:**
|
||||
|
||||
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
|
||||
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
|
||||
<!-- TODO: The specific version will not be required after the stable 1.0.0 release -->
|
||||
2. Run `go get github.com/mholt/caddy/caddy@v1.0.0-beta2`
|
||||
|
||||
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.
|
||||
|
||||
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
|
||||
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
|
||||
2. Create a new folder anywhere, and put this Go file into it, then import the plugins you want to include:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mholt/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 mycaddy` (the name doesn't really matter).
|
||||
4. `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:**
|
||||
|
||||
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
|
||||
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
|
||||
2. Run `git clone https://github.com/mholt/caddy.git` in any folder (doesn't have to be in GOPATH).
|
||||
|
||||
You can make changes to the source code in this repo, since it is a Go module.
|
||||
|
||||
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
|
||||
@@ -80,7 +143,7 @@ If the `caddy` binary has permission to bind to low ports and your domain name's
|
||||
caddy -host example.com
|
||||
```
|
||||
|
||||
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you!
|
||||
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
|
||||
|
||||
### Customizing your site
|
||||
|
||||
@@ -110,7 +173,7 @@ To host multiple sites and do more with the Caddyfile, please see the [Caddyfile
|
||||
|
||||
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
|
||||
|
||||
Caddy has a command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
Caddy has a nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
|
||||
|
||||
|
||||
## Running in Production
|
||||
@@ -128,13 +191,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/mholt/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).
|
||||
|
||||
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/mholt/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/mholt/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
@@ -153,6 +220,6 @@ We thank them for their services. **If you want to help keep Caddy free, please
|
||||
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from [spark](https://github.com/rif/spark), [nginx](https://github.com/nginx/nginx), lighttpd,
|
||||
[Websocketd](https://github.com/joewalnes/websocketd) and [Vagrant](https://www.vagrantup.com/), which provides a pleasant mixture of features from each of them.
|
||||
|
||||
**The name "Caddy":** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand).
|
||||
**The name "Caddy" is trademarked:** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
|
||||
|
||||
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
|
||||
@@ -1,40 +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 and certain linters
|
||||
- go get github.com/alecthomas/gometalinter
|
||||
- go get github.com/client9/misspell/cmd/misspell
|
||||
- go get github.com/gordonklaus/ineffassign
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get github.com/tsenart/deadcode
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- 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
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
|
||||
+22
-2
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
@@ -11,9 +25,15 @@ func TestAssetsPath(t *testing.T) {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
|
||||
os.Setenv("CADDYPATH", "testpath")
|
||||
err := os.Setenv("CADDYPATH", "testpath")
|
||||
if err != nil {
|
||||
t.Error("Could not set CADDYPATH")
|
||||
}
|
||||
if actual, expected := AssetsPath(), "testpath"; actual != expected {
|
||||
t.Errorf("Expected path to be %v, got: %v", expected, actual)
|
||||
}
|
||||
os.Setenv("CADDYPATH", "")
|
||||
err = os.Setenv("CADDYPATH", "")
|
||||
if err != nil {
|
||||
t.Error("Could not set CADDYPATH")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package caddy implements the Caddy server manager.
|
||||
//
|
||||
// To use this package:
|
||||
@@ -30,6 +44,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
@@ -63,8 +78,18 @@ var (
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
OnProcessExit = append(OnProcessExit, func() {
|
||||
if PidFile != "" {
|
||||
os.Remove(PidFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Instance contains the state of servers created as a result of
|
||||
// calling Start and can be used to access or control those servers.
|
||||
// It is literally an instance of a server type. Instance values
|
||||
// should NOT be copied. Use *Instance for safety.
|
||||
type Instance struct {
|
||||
// serverType is the name of the instance's server type
|
||||
serverType string
|
||||
@@ -75,18 +100,33 @@ type Instance struct {
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg *sync.WaitGroup
|
||||
|
||||
// context is the context created for this instance.
|
||||
// context is the context created for this instance,
|
||||
// used to coordinate the setting up of the server type
|
||||
context Context
|
||||
|
||||
// servers is the list of servers with their listeners.
|
||||
// servers is the list of servers with their listeners
|
||||
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-
|
||||
// collected after in-process reloads when the
|
||||
// old instances are destroyed; use StorageMu
|
||||
// to access this value safely
|
||||
Storage map[interface{}]interface{}
|
||||
StorageMu sync.RWMutex
|
||||
}
|
||||
|
||||
// Instances returns the list of instances.
|
||||
func Instances() []*Instance {
|
||||
return instances
|
||||
}
|
||||
|
||||
// Servers returns the ServerListeners in i.
|
||||
@@ -123,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)
|
||||
@@ -147,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
|
||||
}
|
||||
@@ -182,22 +241,28 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
|
||||
}
|
||||
|
||||
// create new instance; if the restart fails, it is simply discarded
|
||||
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg}
|
||||
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 := shutdownFunc()
|
||||
err = i.Stop()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
for _, shutdownFunc := range i.OnShutdown {
|
||||
err = shutdownFunc()
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
i.Stop()
|
||||
|
||||
// Execute instantiation events
|
||||
EmitEvent(InstanceStartupEvent, newInst)
|
||||
|
||||
log.Println("[INFO] Reloading complete")
|
||||
|
||||
@@ -211,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 {
|
||||
@@ -325,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.
|
||||
@@ -435,7 +469,7 @@ func (i *Instance) Caddyfile() Input {
|
||||
//
|
||||
// This function blocks until all the servers are listening.
|
||||
func Start(cdyfile Input) (*Instance, error) {
|
||||
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
|
||||
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
|
||||
err := startWithListenerFds(cdyfile, inst, nil)
|
||||
if err != nil {
|
||||
return inst, err
|
||||
@@ -444,15 +478,42 @@ 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
|
||||
}
|
||||
|
||||
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
|
||||
// save this instance in the list now so that
|
||||
// plugins can access it if need be, for example
|
||||
// the caddytls package, so it can perform cert
|
||||
// renewals while starting up; we just have to
|
||||
// remove the instance from the list later if
|
||||
// it fails
|
||||
instancesMu.Lock()
|
||||
instances = append(instances, inst)
|
||||
instancesMu.Unlock()
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
instancesMu.Lock()
|
||||
for i, otherInst := range instances {
|
||||
if otherInst == inst {
|
||||
instances = append(instances[:i], instances[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
instancesMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
if cdyfile == nil {
|
||||
cdyfile = CaddyfileInput{}
|
||||
}
|
||||
|
||||
err := ValidateAndExecuteDirectives(cdyfile, inst, false)
|
||||
err = ValidateAndExecuteDirectives(cdyfile, inst, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -465,15 +526,15 @@ 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 {
|
||||
err := firstStartupFunc()
|
||||
for _, firstStartupFunc := range inst.OnFirstStartup {
|
||||
err = firstStartupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, startupFunc := range inst.onStartup {
|
||||
err := startupFunc()
|
||||
for _, startupFunc := range inst.OnStartup {
|
||||
err = startupFunc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -484,10 +545,6 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
return err
|
||||
}
|
||||
|
||||
instancesMu.Lock()
|
||||
instances = append(instances, inst)
|
||||
instancesMu.Unlock()
|
||||
|
||||
// run any AfterStartup callbacks if this is not
|
||||
// part of a restart; then show file descriptor notice
|
||||
if restartFds == nil {
|
||||
@@ -498,6 +555,11 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
}
|
||||
if !Quiet {
|
||||
for _, srvln := range inst.servers {
|
||||
// only show FD notice if the listener is not nil.
|
||||
// This can happen when only serving UDP or TCP
|
||||
if srvln.listener == nil {
|
||||
continue
|
||||
}
|
||||
if !IsLoopback(srvln.listener.Addr().String()) {
|
||||
checkFdlimit()
|
||||
break
|
||||
@@ -521,7 +583,7 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
|
||||
func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bool) error {
|
||||
// If parsing only inst will be nil, create an instance for this function call only.
|
||||
if justValidate {
|
||||
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
|
||||
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
|
||||
}
|
||||
|
||||
stypeName := cdyfile.ServerType()
|
||||
@@ -538,22 +600,19 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
|
||||
return err
|
||||
}
|
||||
|
||||
inst.context = stype.NewContext()
|
||||
inst.context = stype.NewContext(inst)
|
||||
if inst.context == nil {
|
||||
return fmt.Errorf("server type %s produced a nil Context", stypeName)
|
||||
}
|
||||
|
||||
sblocks, err = inst.context.InspectServerBlocks(cdyfile.Path(), sblocks)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error inspecting server blocks: %v", err)
|
||||
}
|
||||
|
||||
err = executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
telemetry.Set("num_server_blocks", len(sblocks))
|
||||
|
||||
return nil
|
||||
return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
|
||||
}
|
||||
|
||||
func executeDirectives(inst *Instance, filename string,
|
||||
@@ -626,6 +685,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
|
||||
@@ -641,19 +705,26 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
|
||||
if fdIndex, ok := loadedGob.ListenerFds["tcp"+addr]; ok {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
ln, err = net.FileListener(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("making listener from file: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
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)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("making packet connection from file: %v", err)
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing copy of packet connection file: %v", err)
|
||||
}
|
||||
}
|
||||
ln = gs.WrapListener(ln)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,71 +737,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 fmt.Errorf("closing copy of listener file: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
// 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 fmt.Errorf("close copy of packet file: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -780,6 +877,7 @@ func Stop() error {
|
||||
for {
|
||||
instancesMu.Lock()
|
||||
if len(instances) == 0 {
|
||||
instancesMu.Unlock()
|
||||
break
|
||||
}
|
||||
inst := instances[0]
|
||||
@@ -795,7 +893,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,73 +0,0 @@
|
||||
// +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)
|
||||
}
|
||||
}
|
||||
+402
-65
@@ -1,46 +1,72 @@
|
||||
// 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.
|
||||
|
||||
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/xenolf/lego/acme"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/klauspost/cpuid"
|
||||
"github.com/mholt/caddy"
|
||||
// plug in the HTTP server type
|
||||
_ "github.com/mholt/caddy/caddyhttp"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
"github.com/mholt/certmagic"
|
||||
lumberjack "gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
_ "github.com/mholt/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-v01.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.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")
|
||||
|
||||
@@ -52,9 +78,12 @@ 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
|
||||
certmagic.UserAgent = appName + "/" + cleanModVersion
|
||||
|
||||
// Set up process log before anything bad happens
|
||||
switch logfile {
|
||||
@@ -65,12 +94,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
|
||||
@@ -83,9 +147,11 @@ func Run() {
|
||||
os.Exit(0)
|
||||
}
|
||||
if version {
|
||||
fmt.Printf("%s %s\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)
|
||||
}
|
||||
@@ -94,6 +160,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 {
|
||||
@@ -126,6 +195,26 @@ func Run() {
|
||||
mustLogFatalf("%v", err)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
@@ -153,10 +242,18 @@ func confLoader(serverType string) (caddy.Input, error) {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var contents []byte
|
||||
if strings.Contains(conf, "*") {
|
||||
// Let caddyfile.doImport logic handle the globbed path
|
||||
contents = []byte("import " + conf)
|
||||
} else {
|
||||
var err error
|
||||
contents, err = ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
@@ -180,30 +277,63 @@ 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/mholt/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
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
// If the percent resolves to less than a single
|
||||
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
|
||||
func setCPU(cpu string) error {
|
||||
var numCPU int
|
||||
|
||||
@@ -219,6 +349,9 @@ func setCPU(cpu string) error {
|
||||
}
|
||||
percent = float32(pctInt) / 100
|
||||
numCPU = int(float32(availCPU) * percent)
|
||||
if numCPU < 1 {
|
||||
numCPU = 1
|
||||
}
|
||||
} else {
|
||||
// Number
|
||||
num, err := strconv.Atoi(cpu)
|
||||
@@ -236,29 +369,233 @@ 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
|
||||
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
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
// 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.
|
||||
|
||||
package caddymain
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -27,6 +43,7 @@ func TestSetCPU(t *testing.T) {
|
||||
{"invalid input", currentCPU, true},
|
||||
{"invalid input%", currentCPU, true},
|
||||
{"9999", maxCPU, false}, // over available CPU
|
||||
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
|
||||
} {
|
||||
err := setCPU(test.input)
|
||||
if test.shouldErr && err == nil {
|
||||
@@ -42,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,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// By moving the application's package main logic into
|
||||
// a package other than main, it becomes much easier to
|
||||
// wrap caddy for custom builds that are go-gettable.
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
+83
-38
@@ -1,9 +1,26 @@
|
||||
// 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.
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -34,6 +51,70 @@ 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
|
||||
})
|
||||
|
||||
c.instance.Restart(CaddyfileInput{Contents: []byte(""), ServerTypeName: serverName})
|
||||
|
||||
if !reflect.DeepEqual(calls, test.expectedCalls) {
|
||||
t.Errorf("Test %d: Callbacks expected: %v, got: %v", i, test.expectedCalls, calls)
|
||||
}
|
||||
|
||||
c.instance.Stop()
|
||||
c.instance.Wait()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsLoopback(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
@@ -121,39 +202,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
|
||||
+137
-60
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
@@ -6,6 +20,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
@@ -40,6 +56,7 @@ type parser struct {
|
||||
block ServerBlock // current server block being parsed
|
||||
validDirectives []string // a directive must be valid or it's an error
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
definedSnippets map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
@@ -81,6 +98,24 @@ func (p *parser) begin() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, name := p.isSnippet(); ok {
|
||||
if p.definedSnippets == nil {
|
||||
p.definedSnippets = map[string][]Token{}
|
||||
}
|
||||
if _, found := p.definedSnippets[name]; found {
|
||||
return p.Errf("redeclaration of previously declared snippet %s", name)
|
||||
}
|
||||
// consume all tokens til matched close brace
|
||||
tokens, err := p.snippetTokens()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.definedSnippets[name] = tokens
|
||||
// empty block keys so we don't save this block as a real server.
|
||||
p.block.Keys = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.blockContents()
|
||||
}
|
||||
|
||||
@@ -207,69 +242,57 @@ func (p *parser) doImport() error {
|
||||
if p.NextArg() {
|
||||
return p.Err("Import takes only one argument (glob pattern or file)")
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return p.Errf("Failed to get absolute path of file: %s", p.Dispenser.filename)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} else {
|
||||
globPattern = importPattern
|
||||
}
|
||||
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)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// splice out the import directive and its argument (2 tokens total)
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
|
||||
// collect all the imported tokens
|
||||
var importedTokens []Token
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
|
||||
// first check snippets. That is a simple, non-recursive replacement
|
||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||
importedTokens = p.definedSnippets[importPattern]
|
||||
} else {
|
||||
// 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 err
|
||||
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
}
|
||||
var importLine int
|
||||
importDir := filepath.Dir(importFile)
|
||||
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(importDir, token.Text)
|
||||
}
|
||||
newTokens[i] = Token{
|
||||
Text: abs,
|
||||
Line: token.Line,
|
||||
File: token.File,
|
||||
}
|
||||
|
||||
var matches []string
|
||||
var globPattern string
|
||||
if !filepath.IsAbs(importPattern) {
|
||||
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
|
||||
} 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.ContainsAny(globPattern, "*?[]") {
|
||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
|
||||
// collect all the imported tokens
|
||||
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
}
|
||||
|
||||
// splice the imported tokens in the place of the import statement
|
||||
@@ -300,8 +323,12 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
|
||||
}
|
||||
|
||||
// Tack the filename onto these tokens so errors show the imported file's name
|
||||
filename := filepath.Base(importFile)
|
||||
// Tack the file path onto these tokens so errors show the imported file's name
|
||||
// (we use full, absolute path to avoid bugs: issue #1892)
|
||||
filename, err := filepath.Abs(importFile)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
|
||||
}
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].File = filename
|
||||
}
|
||||
@@ -316,7 +343,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")
|
||||
@@ -326,6 +353,7 @@ func (p *parser) directive() error {
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
telemetry.AppendUnique("directives", dir)
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
@@ -337,6 +365,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])
|
||||
@@ -396,8 +430,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 {
|
||||
@@ -414,3 +453,41 @@ type ServerBlock struct {
|
||||
Keys []string
|
||||
Tokens map[string][]Token
|
||||
}
|
||||
|
||||
func (p *parser) isSnippet() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
|
||||
return true, strings.TrimSuffix(keys[0][1:], ")")
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// read and store everything in a block for later replay.
|
||||
func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// TODO: disallow imports in snippets for simplicity at import time
|
||||
// snippet must have curlies.
|
||||
err := p.openCurlyBrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := 1
|
||||
tokens := []Token{}
|
||||
for p.Next() {
|
||||
if p.Val() == "}" {
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.Val() == "{" {
|
||||
count++
|
||||
}
|
||||
tokens = append(tokens, p.tokens[p.cursor])
|
||||
}
|
||||
// make sure we're matched up
|
||||
if count != 0 {
|
||||
return nil, p.SyntaxErr("}")
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
@@ -214,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)
|
||||
|
||||
@@ -346,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
|
||||
@@ -427,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}`)
|
||||
@@ -435,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()
|
||||
@@ -493,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 {
|
||||
@@ -500,3 +602,119 @@ func testParser(input string) parser {
|
||||
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`
|
||||
(common) {
|
||||
gzip foo
|
||||
errors stderr
|
||||
}
|
||||
http://example.com {
|
||||
import common
|
||||
}
|
||||
`)
|
||||
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) != 2 {
|
||||
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)
|
||||
}
|
||||
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
|
||||
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package basicauth implements HTTP Basic Authentication for Caddy.
|
||||
//
|
||||
// This is useful for simple protections on a website, like requiring
|
||||
@@ -37,6 +51,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 {
|
||||
@@ -49,7 +72,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 ||
|
||||
@@ -65,6 +88,10 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||
// user; this replaces the request with a wrapped instance
|
||||
r = r.WithContext(context.WithValue(r.Context(),
|
||||
httpserver.RemoteUserCtxKey, username))
|
||||
|
||||
// Provide username to be used in log by replacer
|
||||
repl := httpserver.NewReplacer(r, nil, "-")
|
||||
repl.Set("user", username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +103,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
@@ -8,6 +22,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
@@ -46,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
|
||||
@@ -75,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",
|
||||
@@ -110,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 {
|
||||
@@ -134,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'",
|
||||
@@ -157,7 +178,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
htfh, err := ioutil.TempFile("", "basicauth-")
|
||||
if err != nil {
|
||||
t.Skipf("Error creating temp file (%v), will skip htpassword test")
|
||||
t.Skip("Error creating temp file, will skip htpassword test")
|
||||
return
|
||||
}
|
||||
defer os.Remove(htfh.Name())
|
||||
@@ -180,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
|
||||
+15
-1
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package bind
|
||||
|
||||
import (
|
||||
@@ -18,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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package bind
|
||||
|
||||
import (
|
||||
@@ -18,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package browse provides middleware for listing files in a directory
|
||||
// when directory path is requested instead of a specific file.
|
||||
package browse
|
||||
@@ -172,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
|
||||
@@ -238,7 +252,7 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
for _, indexName := range staticfiles.IndexPages {
|
||||
for _, indexName := range config.Fs.IndexPages {
|
||||
if name == indexName {
|
||||
hasIndexFile = true
|
||||
break
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package browse
|
||||
|
||||
import (
|
||||
@@ -352,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 {
|
||||
|
||||
+37
-15
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package browse
|
||||
|
||||
import (
|
||||
@@ -64,8 +78,9 @@ func browseParse(c *caddy.Controller) ([]Config, error) {
|
||||
}
|
||||
|
||||
bc.Fs = staticfiles.FileServer{
|
||||
Root: http.Dir(cfg.Root),
|
||||
Hide: cfg.HiddenFiles,
|
||||
Root: http.Dir(cfg.Root),
|
||||
Hide: cfg.HiddenFiles,
|
||||
IndexPages: cfg.IndexPages,
|
||||
}
|
||||
|
||||
// Second argument would be the template file to use
|
||||
@@ -110,6 +125,7 @@ const defaultTemplate = `<!DOCTYPE html>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-rendering: optimizespeed;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -130,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 {
|
||||
@@ -226,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;
|
||||
}
|
||||
|
||||
@@ -286,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;
|
||||
}
|
||||
@@ -310,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 -->
|
||||
@@ -375,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>
|
||||
@@ -410,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>
|
||||
@@ -422,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}}
|
||||
@@ -442,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>
|
||||
@@ -484,7 +506,7 @@ footer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.textContent = d.toLocaleString();
|
||||
e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
}
|
||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||
timeList.forEach(localizeDatetime);
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package browse
|
||||
|
||||
import (
|
||||
@@ -39,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
|
||||
|
||||
+15
-1
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
@@ -31,5 +45,5 @@ import (
|
||||
_ "github.com/mholt/caddy/caddyhttp/templates"
|
||||
_ "github.com/mholt/caddy/caddyhttp/timeouts"
|
||||
_ "github.com/mholt/caddy/caddyhttp/websocket"
|
||||
_ "github.com/mholt/caddy/startupshutdown"
|
||||
_ "github.com/mholt/caddy/onevent"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
@@ -13,7 +27,7 @@ import (
|
||||
func TestStandardPlugins(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package errors implements an HTTP error handling middleware.
|
||||
package errors
|
||||
|
||||
@@ -128,7 +142,7 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Trim file path
|
||||
delim := "/caddy/"
|
||||
delim := "/github.com/mholt/caddy/"
|
||||
pkgPathPos := strings.Index(file, delim)
|
||||
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
|
||||
file = file[pkgPathPos+len(delim):]
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
@@ -109,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 {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
@@ -165,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 {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package expvar
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package extensions contains middleware for clean URLs.
|
||||
//
|
||||
// The root path of the site is passed in as well as possible extensions
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package fastcgi has middleware that acts as a FastCGI client. Requests
|
||||
// that get forwarded to FastCGI stop the middleware execution chain.
|
||||
// The most common use for this package is to serve PHP websites via php-fpm.
|
||||
@@ -6,6 +20,7 @@ package fastcgi
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,8 +33,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// Handler is a middleware type that can handle requests as a FastCGI client.
|
||||
@@ -93,7 +111,11 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
}
|
||||
|
||||
// Connect to FastCGI gateway
|
||||
network, address := parseAddress(rule.Address())
|
||||
address, err := rule.Address()
|
||||
if err != nil {
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
network, address := parseAddress(address)
|
||||
|
||||
ctx := context.Background()
|
||||
if rule.ConnectTimeout > 0 {
|
||||
@@ -129,7 +151,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
case "HEAD":
|
||||
resp, err = fcgiBackend.Head(env)
|
||||
case "GET":
|
||||
resp, err = fcgiBackend.Get(env)
|
||||
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
|
||||
case "OPTIONS":
|
||||
resp, err = fcgiBackend.Options(env)
|
||||
default:
|
||||
@@ -220,9 +242,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 {
|
||||
@@ -244,11 +263,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)
|
||||
@@ -264,6 +285,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{
|
||||
@@ -280,6 +306,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,
|
||||
@@ -304,6 +331,19 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||
// Some web apps rely on knowing HTTPS or not
|
||||
if r.TLS != nil {
|
||||
env["HTTPS"] = "on"
|
||||
// and pass the protocol details in a manner compatible with apache's mod_ssl
|
||||
// (which is why they have a SSL_ prefix and not TLS_).
|
||||
v, ok := tlsProtocolStringToMap[r.TLS.Version]
|
||||
if ok {
|
||||
env["SSL_PROTOCOL"] = v
|
||||
}
|
||||
// and pass the cipher suite in a manner compatible with apache's mod_ssl
|
||||
for k, v := range caddytls.SupportedCiphersMap {
|
||||
if v == r.TLS.CipherSuite {
|
||||
env["SSL_CIPHER"] = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add env variables from config (with support for placeholders in values)
|
||||
@@ -367,7 +407,7 @@ type Rule struct {
|
||||
type balancer interface {
|
||||
// Address picks an upstream address from the
|
||||
// underlying load balancer.
|
||||
Address() string
|
||||
Address() (string, error)
|
||||
}
|
||||
|
||||
// roundRobin is a round robin balancer for fastcgi upstreams.
|
||||
@@ -379,9 +419,34 @@ type roundRobin struct {
|
||||
addresses []string
|
||||
}
|
||||
|
||||
func (r *roundRobin) Address() string {
|
||||
func (r *roundRobin) Address() (string, error) {
|
||||
index := atomic.AddInt64(&r.index, 1) % int64(len(r.addresses))
|
||||
return r.addresses[index]
|
||||
return r.addresses[index], nil
|
||||
}
|
||||
|
||||
// srvResolver is a private interface used to abstract
|
||||
// the DNS resolver. It is mainly used to facilitate testing.
|
||||
type srvResolver interface {
|
||||
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
|
||||
}
|
||||
|
||||
// srv is a service locator for fastcgi upstreams
|
||||
type srv struct {
|
||||
resolver srvResolver
|
||||
service string
|
||||
}
|
||||
|
||||
// Address looks up the service and returns the address:port
|
||||
// from first result in resolved list.
|
||||
// No explicit balancing is required because net.LookupSRV
|
||||
// sorts the results by priority and randomizes within priority.
|
||||
func (s *srv) Address() (string, error) {
|
||||
_, addrs, err := s.resolver.LookupSRV(context.Background(), "", "", s.service)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", strings.TrimRight(addrs[0].Target, "."), addrs[0].Port), nil
|
||||
}
|
||||
|
||||
// canSplit checks if path can split into two based on rule.SplitPath.
|
||||
@@ -421,3 +486,11 @@ type LogError string
|
||||
func (l LogError) Error() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
// Map of supported protocols to Apache ssl_mod format
|
||||
// Note that these are slightly different from SupportedProtocols in caddytls/config.go's
|
||||
var tlsProtocolStringToMap = map[uint16]string{
|
||||
tls.VersionTLS10: "TLSv1",
|
||||
tls.VersionTLS11: "TLSv1.1",
|
||||
tls.VersionTLS12: "TLSv1.2",
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
@@ -7,6 +21,7 @@ import (
|
||||
"net/http/fcgi"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -70,11 +85,15 @@ func TestRuleParseAddress(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, entry := range getClientTestTable {
|
||||
if actualnetwork, _ := parseAddress(entry.rule.Address()); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", entry.rule.Address(), actualnetwork, entry.expectednetwork)
|
||||
addr, err := entry.rule.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
|
||||
}
|
||||
if _, actualaddress := parseAddress(entry.rule.Address()); actualaddress != entry.expectedaddress {
|
||||
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", entry.rule.Address(), actualaddress, entry.expectedaddress)
|
||||
if actualnetwork, _ := parseAddress(addr); actualnetwork != entry.expectednetwork {
|
||||
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", addr, actualnetwork, entry.expectednetwork)
|
||||
}
|
||||
if _, actualaddress := parseAddress(addr); actualaddress != entry.expectedaddress {
|
||||
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", addr, actualaddress, entry.expectedaddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +239,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) {
|
||||
@@ -351,7 +385,10 @@ func TestBalancer(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
b := address(test...)
|
||||
for _, host := range test {
|
||||
a := b.Address()
|
||||
a, err := b.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
|
||||
}
|
||||
if a != host {
|
||||
t.Errorf("Test %d: expected %s, found %s", i, host, a)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
|
||||
// (which is forked from https://code.google.com/p/go-fastcgi-client/)
|
||||
|
||||
@@ -202,7 +216,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()
|
||||
}
|
||||
@@ -446,12 +460,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
|
||||
}
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
func (c *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) {
|
||||
func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "GET"
|
||||
p["CONTENT_LENGTH"] = "0"
|
||||
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
||||
|
||||
return c.Request(p, nil)
|
||||
return c.Request(p, body)
|
||||
}
|
||||
|
||||
// Head issues a HEAD request to the fcgi responder.
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// NOTE: These tests were adapted from the original
|
||||
// repository from which this package was forked.
|
||||
// The tests are slow (~10s) and in dire need of rewriting.
|
||||
@@ -30,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 (
|
||||
@@ -126,7 +140,8 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
}
|
||||
resp, err = fcgi.PostForm(fcgiParams, values)
|
||||
} else {
|
||||
resp, err = fcgi.Get(fcgiParams)
|
||||
rd := bytes.NewReader(data)
|
||||
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
// 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.
|
||||
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
var defaultTimeout = 60 * time.Second
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
|
||||
ServerType: "http",
|
||||
@@ -59,11 +78,20 @@ 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]}
|
||||
|
||||
srvUpstream := false
|
||||
if strings.HasPrefix(upstreams[0], "srv://") {
|
||||
srvUpstream = true
|
||||
}
|
||||
|
||||
if len(args) == 3 {
|
||||
if err := fastcgiPreset(args[2], &rule); err != nil {
|
||||
return rules, err
|
||||
@@ -98,6 +126,10 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
rule.IndexFiles = args
|
||||
|
||||
case "upstream":
|
||||
if srvUpstream {
|
||||
return rules, c.Err("additional upstreams are not supported with SRV upstream")
|
||||
}
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
if len(args) != 1 {
|
||||
@@ -147,13 +179,32 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
|
||||
}
|
||||
}
|
||||
|
||||
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
|
||||
if srvUpstream {
|
||||
balancer, err := parseSRV(upstreams[0])
|
||||
if err != nil {
|
||||
return rules, c.Err("malformed service locator string: " + err.Error())
|
||||
}
|
||||
rule.balancer = balancer
|
||||
} else {
|
||||
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func parseSRV(locator string) (*srv, error) {
|
||||
if locator[6:] == "" {
|
||||
return nil, fmt.Errorf("%s does not include the host", locator)
|
||||
}
|
||||
|
||||
return &srv{
|
||||
service: locator[6:],
|
||||
resolver: &net.Resolver{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *Rule) error {
|
||||
|
||||
+146
-13
@@ -1,8 +1,25 @@
|
||||
// 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.
|
||||
|
||||
package fastcgi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
@@ -29,10 +46,26 @@ func TestSetup(t *testing.T) {
|
||||
if myHandler.Rules[0].Path != "/" {
|
||||
t.Errorf("Expected / as the Path")
|
||||
}
|
||||
if myHandler.Rules[0].Address() != "127.0.0.1:9000" {
|
||||
addr, err := myHandler.Rules[0].Address()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -44,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
|
||||
@@ -71,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 {
|
||||
@@ -92,9 +138,19 @@ func TestFastcgiParse(t *testing.T) {
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Address() != test.expectedFastcgiConfig[j].Address() {
|
||||
actualAddr, err := actualFastcgiConfig.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d unexpected error in trying to retrieve %dth actual address: %s", i, j, err.Error())
|
||||
}
|
||||
|
||||
expectedAddr, err := test.expectedFastcgiConfig[j].Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d unexpected error in trying to retrieve %dth expected address: %s", i, j, err.Error())
|
||||
}
|
||||
|
||||
if actualAddr != expectedAddr {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Address(), actualFastcgiConfig.Address())
|
||||
i, j, expectedAddr, actualAddr)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
@@ -116,7 +172,84 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFastCGIResolveSRV(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
locator string
|
||||
target string
|
||||
port uint16
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
`fastcgi / srv://fpm.tcp.service.consul {
|
||||
upstream yolo
|
||||
}`,
|
||||
"fpm.tcp.service.consul",
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`fastcgi / srv://fpm.tcp.service.consul`,
|
||||
"fpm.tcp.service.consul",
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
for _, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
resolver, ok := (actualFastcgiConfig.balancer).(*srv)
|
||||
if !ok {
|
||||
t.Errorf("Test %d upstream balancer is not srv", i)
|
||||
}
|
||||
resolver.resolver = buildTestResolver(test.target, test.port)
|
||||
|
||||
addr, err := actualFastcgiConfig.Address()
|
||||
if err != nil {
|
||||
t.Errorf("Test %d failed to retrieve upstream address. %s", i, err.Error())
|
||||
}
|
||||
|
||||
expectedAddr := fmt.Sprintf("%s:%d", test.target, test.port)
|
||||
if addr != expectedAddr {
|
||||
t.Errorf("Test %d expected upstream address to be %s, got %s", i, expectedAddr, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildTestResolver(target string, port uint16) srvResolver {
|
||||
return &testSRVResolver{target, port}
|
||||
}
|
||||
|
||||
type testSRVResolver struct {
|
||||
target string
|
||||
port uint16
|
||||
}
|
||||
|
||||
func (r *testSRVResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
|
||||
return "", []*net.SRV{
|
||||
{Target: r.target,
|
||||
Port: r.port,
|
||||
Priority: 1,
|
||||
Weight: 1}}, nil
|
||||
}
|
||||
|
||||
+44
-10
@@ -1,8 +1,23 @@
|
||||
// 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.
|
||||
|
||||
// Package gzip provides a middleware layer that performs
|
||||
// gzip compression on the response.
|
||||
package gzip
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -51,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/mholt/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
|
||||
@@ -89,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
|
||||
@@ -121,9 +147,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)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
@@ -16,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"}
|
||||
|
||||
// DefaultExtFilter creates an ExtFilter with default extensions.
|
||||
func DefaultExtFilter() ExtFilter {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
@@ -68,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
@@ -33,7 +47,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))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package gzip
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package header provides middleware that appends headers to
|
||||
// requests based on a set of configuration rules that define
|
||||
// which routes receive which headers.
|
||||
@@ -27,10 +41,6 @@ func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
for _, rule := range h.Rules {
|
||||
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
|
||||
for name := range rule.Headers {
|
||||
if name == "Caddy-Sponsors" || name == "-Caddy-Sponsors" {
|
||||
// see EULA
|
||||
continue
|
||||
}
|
||||
|
||||
// One can either delete a header, add multiple values to a header, or simply
|
||||
// set a header.
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package header
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package header
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package header
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -26,6 +40,7 @@ func SetupIfMatcher(controller *caddy.Controller) (RequestMatcher, error) {
|
||||
return matcher, err
|
||||
}
|
||||
matcher.ifs = append(matcher.ifs, ifc)
|
||||
matcher.Enabled = true
|
||||
case "if_op":
|
||||
if !c.NextArg() {
|
||||
return matcher, c.ArgErr()
|
||||
@@ -141,8 +156,9 @@ func (i ifCond) True(r *http.Request) bool {
|
||||
|
||||
// IfMatcher is a RequestMatcher for 'if' conditions.
|
||||
type IfMatcher struct {
|
||||
ifs []ifCond // list of If
|
||||
isOr bool // if true, conditions are 'or' instead of 'and'
|
||||
Enabled bool // if true, matcher has been configured; otherwise it's no-op
|
||||
ifs []ifCond // list of If
|
||||
isOr bool // if true, conditions are 'or' instead of 'and'
|
||||
}
|
||||
|
||||
// Match satisfies RequestMatcher interface.
|
||||
@@ -175,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -7,13 +21,14 @@ import (
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func activateHTTPS(cctx caddy.Context) error {
|
||||
operatorPresent := !caddy.Started()
|
||||
|
||||
if !caddy.Quiet && operatorPresent {
|
||||
fmt.Print("Activating privacy features...")
|
||||
fmt.Print("Activating privacy features... ")
|
||||
}
|
||||
|
||||
ctx := cctx.(*httpContext)
|
||||
@@ -23,10 +38,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
|
||||
}
|
||||
@@ -48,14 +66,19 @@ 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(operatorPresent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !caddy.Quiet && operatorPresent {
|
||||
fmt.Println(" done.")
|
||||
fmt.Println("done.")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -81,13 +104,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.Addr.Host) {
|
||||
_, err := cfg.TLS.CacheManagedCertificate(cfg.Addr.Host)
|
||||
if loadCertificates && certmagic.HostQualifies(cfg.TLS.Hostname) {
|
||||
_, err := cfg.TLS.Manager.CacheManagedCertificate(cfg.TLS.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,7 +123,7 @@ 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
|
||||
}
|
||||
@@ -146,23 +170,37 @@ func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort str
|
||||
// to listen on HTTPPort. The TLS field of cfg must not be nil.
|
||||
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
redirPort := cfg.Addr.Port
|
||||
if redirPort == DefaultHTTPSPort {
|
||||
redirPort = "" // default port is redundant
|
||||
if redirPort == 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
|
||||
// so that it is no longer DefaultHTTPSPort, we shouldn't
|
||||
// append it to the URL in the Location because changing
|
||||
// the HTTPS port is assumed to be an internal-only change
|
||||
// (in other words, we assume port forwarding is going on);
|
||||
// but redirects go back to a presumably-external client.
|
||||
// (If redirect clients are also internal, that is more
|
||||
// advanced, and the user should configure HTTP->HTTPS
|
||||
// redirects themselves.)
|
||||
redirPort = ""
|
||||
}
|
||||
|
||||
redirMiddleware := func(next Handler) Handler {
|
||||
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Construct the URL to which to redirect. Note that the Host in a request might
|
||||
// contain a port, but we just need the hostname; we'll set the port if needed.
|
||||
// Construct the URL to which to redirect. Note that the Host in a
|
||||
// request might contain a port, but we just need the hostname from
|
||||
// it; and we'll set the port if needed.
|
||||
toURL := "https://"
|
||||
requestHost, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
requestHost = r.Host // Host did not contain a port; great
|
||||
requestHost = r.Host // Host did not contain a port, so use the whole value
|
||||
}
|
||||
if redirPort == "" {
|
||||
toURL += requestHost
|
||||
} else {
|
||||
toURL += net.JoinHostPort(requestHost, redirPort)
|
||||
}
|
||||
|
||||
toURL += r.URL.RequestURI()
|
||||
|
||||
w.Header().Set("Connection", "close")
|
||||
@@ -170,14 +208,16 @@ func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
host := cfg.Addr.Host
|
||||
port := 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -8,6 +22,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
@@ -39,7 +54,7 @@ func TestRedirPlaintextHost(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Host: "foohost",
|
||||
Port: "443", // since this is the default HTTPS port, should not be included in Location value
|
||||
Port: HTTPSPort, // since this is the 'default' HTTPS port, should not be included in Location value
|
||||
},
|
||||
{
|
||||
Host: "*.example.com",
|
||||
@@ -161,7 +176,7 @@ 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!
|
||||
}
|
||||
|
||||
@@ -182,18 +197,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
|
||||
|
||||
@@ -210,3 +225,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{}}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
gsyslog "github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
@@ -23,9 +38,13 @@ var remoteSyslogPrefixes = map[string]string{
|
||||
type Logger struct {
|
||||
Output string
|
||||
*log.Logger
|
||||
Roller *LogRoller
|
||||
writer io.Writer
|
||||
fileMu *sync.RWMutex
|
||||
Roller *LogRoller
|
||||
writer io.Writer
|
||||
fileMu *sync.RWMutex
|
||||
V4ipMask net.IPMask
|
||||
V6ipMask net.IPMask
|
||||
IPMaskExists bool
|
||||
Exceptions []string
|
||||
}
|
||||
|
||||
// NewTestLogger creates logger suitable for testing purposes
|
||||
@@ -50,6 +69,33 @@ func (l Logger) Printf(format string, args ...interface{}) {
|
||||
l.fileMu.RUnlock()
|
||||
}
|
||||
|
||||
func (l Logger) MaskIP(ip string) string {
|
||||
var reqIP net.IP
|
||||
// If unable to parse, simply return IP as provided.
|
||||
reqIP = net.ParseIP(ip)
|
||||
if reqIP == nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
if reqIP.To4() != nil {
|
||||
return reqIP.Mask(l.V4ipMask).String()
|
||||
} else {
|
||||
return reqIP.Mask(l.V6ipMask).String()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ShouldLog returns true if the path is not exempted from
|
||||
// being logged (i.e. it is not found in l.Exceptions).
|
||||
func (l Logger) ShouldLog(path string) bool {
|
||||
for _, exc := range l.Exceptions {
|
||||
if Path(path).Matches(exc) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Attach binds logger Start and Close functions to
|
||||
// controller's OnStartup and OnShutdown hooks.
|
||||
func (l *Logger) Attach(controller *caddy.Controller) {
|
||||
@@ -116,7 +162,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()
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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 linux darwin
|
||||
|
||||
package httpserver
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -144,7 +158,7 @@ func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
|
||||
|
||||
// CaseSensitivePath determines if paths should be case sensitive.
|
||||
// This is configurable via CASE_SENSITIVE_PATH environment variable.
|
||||
var CaseSensitivePath = true
|
||||
var CaseSensitivePath = false
|
||||
|
||||
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
|
||||
@@ -153,10 +167,10 @@ const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
|
||||
// This could have been in init, but init cannot be called from tests.
|
||||
func initCaseSettings() {
|
||||
switch os.Getenv(caseSensitivePathEnv) {
|
||||
case "0", "false":
|
||||
CaseSensitivePath = false
|
||||
default:
|
||||
case "1", "true":
|
||||
CaseSensitivePath = true
|
||||
default:
|
||||
CaseSensitivePath = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +214,9 @@ func SameNext(next1, next2 Handler) bool {
|
||||
|
||||
// Context key constants.
|
||||
const (
|
||||
// ReplacerCtxKey is the context key for a per-request replacer.
|
||||
ReplacerCtxKey caddy.CtxKey = "replacer"
|
||||
|
||||
// RemoteUserCtxKey is the key for the remote user of the request, if any (basicauth).
|
||||
RemoteUserCtxKey caddy.CtxKey = "remote_user"
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -45,7 +59,7 @@ func TestPathCaseSensitiveEnv(t *testing.T) {
|
||||
{"0", false},
|
||||
{"false", false},
|
||||
{"true", true},
|
||||
{"", true},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -10,6 +24,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/mholt/caddy/telemetry"
|
||||
)
|
||||
|
||||
// tlsHandler is a http.Handler that will inject a value
|
||||
@@ -35,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
|
||||
@@ -45,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") ||
|
||||
@@ -83,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 {
|
||||
@@ -181,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
|
||||
}
|
||||
@@ -201,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
|
||||
@@ -217,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 {
|
||||
@@ -230,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:]
|
||||
|
||||
@@ -258,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:
|
||||
@@ -271,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:
|
||||
@@ -286,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:]
|
||||
@@ -338,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
|
||||
}
|
||||
@@ -372,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
|
||||
}
|
||||
|
||||
@@ -423,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
|
||||
@@ -464,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
|
||||
}
|
||||
|
||||
@@ -495,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
|
||||
@@ -518,7 +550,7 @@ func (info rawHelloInfo) looksLikeEdge() bool {
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.cipherSuites) {
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -544,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
|
||||
}
|
||||
|
||||
@@ -585,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
|
||||
}
|
||||
@@ -605,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) {
|
||||
@@ -622,7 +654,7 @@ func (info rawHelloInfo) looksLikeTor() bool {
|
||||
}
|
||||
}
|
||||
|
||||
if hasGreaseCiphers(info.cipherSuites) {
|
||||
if hasGreaseCiphers(info.CipherSuites) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -649,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -18,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},
|
||||
},
|
||||
},
|
||||
} {
|
||||
@@ -186,10 +204,17 @@ func TestHeuristicFunctionsAndHandler(t *testing.T) {
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// I think this was iOS 11 beta
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.28 (KHTML, like Gecko) Version/11.0 Mobile/15A5318g Safari/604.1",
|
||||
helloHex: `010000e10303be294e11847ba01301e0bb6129f4a0d66344602141a8f0a1ab0750a1db145755000028c02cc02bc024c023cca9c00ac009c030c02fc028c027cca8c014c013009d009c003d003c0035002f01000090ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000000d00140012040308040401050308050501080606010201000500050100000000337400000012000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e31000b00020100000a00080006001d00170018`,
|
||||
interception: false,
|
||||
},
|
||||
{
|
||||
// iOS 11 stable
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
|
||||
helloHex: `010000dc030327fafb16708fcbe489fda332260d32b1a22bea6672a72b5e61d7b9963df1b10d000028c02cc02bc024c023c00ac009cca9c030c02fc028c027c014c013cca8009d009c003d003c0035002f0100008bff010001000000000f000d00000a6d69746d2e776174636800170000000d00140012040308040401050308050501080606010201000500050100000000337400000012000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e31000b00020100000a00080006001d00170018`,
|
||||
interception: false,
|
||||
},
|
||||
},
|
||||
"Tor": {
|
||||
{
|
||||
@@ -310,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
|
||||
@@ -346,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import "testing"
|
||||
|
||||
+216
-32
@@ -1,6 +1,21 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -8,12 +23,16 @@ 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/mholt/caddy/telemetry"
|
||||
"github.com/mholt/certmagic"
|
||||
)
|
||||
|
||||
const serverType = "http"
|
||||
@@ -50,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
|
||||
@@ -76,11 +101,13 @@ func hideCaddyfile(cctx caddy.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newContext() caddy.Context {
|
||||
return &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
func newContext(inst *caddy.Instance) caddy.Context {
|
||||
return &httpContext{instance: inst, keysToSiteConfigs: make(map[string]*SiteConfig)}
|
||||
}
|
||||
|
||||
type httpContext struct {
|
||||
instance *caddy.Instance
|
||||
|
||||
// keysToSiteConfigs maps an address at the top of a
|
||||
// server block (a "key") to its SiteConfig. Not all
|
||||
// SiteConfigs will be represented here, only ones
|
||||
@@ -100,18 +127,22 @@ func (h *httpContext) saveConfig(key string, cfg *SiteConfig) {
|
||||
// executing directives and otherwise prepares the directives to
|
||||
// be parsed and executed.
|
||||
func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
|
||||
siteAddrs := make(map[string]string)
|
||||
|
||||
// For each address in each server block, make a new config
|
||||
for _, sb := range serverBlocks {
|
||||
for _, key := range sb.Keys {
|
||||
key = strings.ToLower(key)
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site address: %s", key)
|
||||
}
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return serverBlocks, err
|
||||
}
|
||||
|
||||
addr = addr.Normalize()
|
||||
key = addr.Key()
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site key: %s", key)
|
||||
}
|
||||
|
||||
// Fill in address components from command line so that middleware
|
||||
// have access to the correct information during setup
|
||||
if addr.Host == "" && Host != DefaultHost {
|
||||
@@ -121,26 +152,59 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
addr.Port = Port
|
||||
}
|
||||
|
||||
// Make sure the adjusted site address is distinct
|
||||
addrCopy := addr // make copy so we don't disturb the original, carefully-parsed address struct
|
||||
if addrCopy.Port == "" && Port == DefaultPort {
|
||||
addrCopy.Port = Port
|
||||
}
|
||||
addrStr := addrCopy.String()
|
||||
if otherSiteKey, dup := siteAddrs[addrStr]; dup {
|
||||
err := fmt.Errorf("duplicate site address: %s", addrStr)
|
||||
if (addrCopy.Host == Host && Host != DefaultHost) ||
|
||||
(addrCopy.Port == Port && Port != DefaultPort) {
|
||||
err = fmt.Errorf("site defined as %s is a duplicate of %s because of modified "+
|
||||
"default host and/or port values (usually via -host or -port flags)", key, otherSiteKey)
|
||||
}
|
||||
return serverBlocks, err
|
||||
}
|
||||
siteAddrs[addrStr] = key
|
||||
|
||||
// If default HTTP or HTTPS ports have been customized,
|
||||
// make sure the ACME challenge ports match
|
||||
var altHTTPPort, altTLSSNIPort string
|
||||
var altHTTPPort, altTLSALPNPort int
|
||||
if HTTPPort != DefaultHTTPPort {
|
||||
altHTTPPort = HTTPPort
|
||||
portInt, err := strconv.Atoi(HTTPPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
altHTTPPort = portInt
|
||||
}
|
||||
if HTTPSPort != DefaultHTTPSPort {
|
||||
altTLSSNIPort = HTTPSPort
|
||||
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, err := caddytls.NewConfig(h.instance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating new caddytls configuration: %v", err)
|
||||
}
|
||||
caddytlsConfig.Hostname = addr.Host
|
||||
caddytlsConfig.Manager.AltHTTPPort = altHTTPPort
|
||||
caddytlsConfig.Manager.AltTLSALPNPort = altTLSALPNPort
|
||||
|
||||
// Save the config to our master list, and key it for lookups
|
||||
cfg := &SiteConfig{
|
||||
Addr: addr,
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{
|
||||
Hostname: addr.Host,
|
||||
AltHTTPPort: altHTTPPort,
|
||||
AltTLSSNIPort: altTLSSNIPort,
|
||||
},
|
||||
Addr: addr,
|
||||
Root: Root,
|
||||
TLS: caddytlsConfig,
|
||||
originCaddyfile: sourceFile,
|
||||
IndexPages: staticfiles.DefaultIndexPages,
|
||||
}
|
||||
h.saveConfig(key, cfg)
|
||||
}
|
||||
@@ -164,9 +228,41 @@ 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)
|
||||
// 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
|
||||
}
|
||||
@@ -181,12 +277,23 @@ 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// we must map (group) each config to a bind address
|
||||
@@ -205,22 +312,48 @@ 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
|
||||
}
|
||||
|
||||
// normalizedKey returns "normalized" key representation:
|
||||
// scheme and host names are lowered, everything else stays the same
|
||||
func normalizedKey(key string) string {
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return key
|
||||
}
|
||||
return addr.Normalize().Key()
|
||||
}
|
||||
|
||||
// GetConfig gets the SiteConfig that corresponds to c.
|
||||
// If none exist (should only happen in tests), then a
|
||||
// new, empty one will be created.
|
||||
func GetConfig(c *caddy.Controller) *SiteConfig {
|
||||
ctx := c.Context().(*httpContext)
|
||||
key := strings.ToLower(c.Key)
|
||||
key := normalizedKey(c.Key)
|
||||
if cfg, ok := ctx.keysToSiteConfigs[key]; ok {
|
||||
return cfg
|
||||
}
|
||||
// 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)}
|
||||
cfg := &SiteConfig{
|
||||
Root: Root,
|
||||
TLS: &caddytls.Config{Manager: certmagic.NewDefault()},
|
||||
IndexPages: staticfiles.DefaultIndexPages,
|
||||
}
|
||||
ctx.saveConfig(key, cfg)
|
||||
return cfg
|
||||
}
|
||||
@@ -275,6 +408,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
|
||||
}
|
||||
@@ -296,11 +431,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
|
||||
@@ -317,6 +453,50 @@ func (a Address) VHost() string {
|
||||
return a.Original
|
||||
}
|
||||
|
||||
// Normalize normalizes URL: turn scheme and host names into lower case
|
||||
func (a Address) Normalize() Address {
|
||||
path := a.Path
|
||||
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(host),
|
||||
Port: a.Port,
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// Key is similar to String, just replaces scheme and host values with modified values.
|
||||
// Unlike String it doesn't add anything default (scheme, port, etc)
|
||||
func (a Address) Key() string {
|
||||
res := ""
|
||||
if a.Scheme != "" {
|
||||
res += a.Scheme + "://"
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if a.Path != "" {
|
||||
res += a.Path
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// standardizeAddress parses an address string into a structured format with separate
|
||||
// scheme, host, port, and path portions, as well as the original input string.
|
||||
func standardizeAddress(str string) (Address, error) {
|
||||
@@ -441,8 +621,10 @@ var directives = []string{
|
||||
"tls",
|
||||
|
||||
// services/utilities, or other directives that don't necessarily inject handlers
|
||||
"startup",
|
||||
"shutdown",
|
||||
"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
|
||||
@@ -456,25 +638,27 @@ 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
|
||||
"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
|
||||
"extauth", // github.com/BTBurke/caddy-extauth
|
||||
"jwt", // github.com/BTBurke/caddy-jwt
|
||||
"jsonp", // github.com/pschlump/caddy-jsonp
|
||||
"upload", // blitznote.com/src/caddy.upload
|
||||
@@ -490,18 +674,18 @@ var directives = []string{
|
||||
"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 (
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sort"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
@@ -123,7 +141,7 @@ func TestAddressString(t *testing.T) {
|
||||
func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
|
||||
Port = "9999"
|
||||
filename := "Testfile"
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
input := strings.NewReader(`localhost`)
|
||||
sblocks, err := caddyfile.Parse(filename, input, nil)
|
||||
if err != nil {
|
||||
@@ -133,15 +151,45 @@ func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Didn't expect an error, but got: %v", err)
|
||||
}
|
||||
addr := ctx.keysToSiteConfigs["localhost"].Addr
|
||||
localhostKey := "localhost"
|
||||
item, ok := ctx.keysToSiteConfigs[localhostKey]
|
||||
if !ok {
|
||||
availableKeys := make(sort.StringSlice, len(ctx.keysToSiteConfigs))
|
||||
i := 0
|
||||
for key := range ctx.keysToSiteConfigs {
|
||||
availableKeys[i] = fmt.Sprintf("'%s'", key)
|
||||
i++
|
||||
}
|
||||
availableKeys.Sort()
|
||||
t.Errorf("`%s` not found within registered keys, only these are available: %s", localhostKey, strings.Join(availableKeys, ", "))
|
||||
return
|
||||
}
|
||||
addr := item.Addr
|
||||
if addr.Port != Port {
|
||||
t.Errorf("Expected the port on the address to be set, but got: %#v", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// See discussion on PR #2015
|
||||
func TestInspectServerBlocksWithAdjustedAddress(t *testing.T) {
|
||||
Port = DefaultPort
|
||||
Host = "example.com"
|
||||
filename := "Testfile"
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
input := strings.NewReader("example.com {\n}\n:2015 {\n}")
|
||||
sblocks, err := caddyfile.Parse(filename, input, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error setting up test, got: %v", err)
|
||||
}
|
||||
_, err = ctx.InspectServerBlocks(filename, sblocks)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected an error because site definitions should overlap, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
|
||||
filename := "Testfile"
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
input := strings.NewReader("localhost {\n}\nLOCALHOST {\n}")
|
||||
sblocks, err := caddyfile.Parse(filename, input, nil)
|
||||
if err != nil {
|
||||
@@ -153,6 +201,64 @@ func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyNormalization(t *testing.T) {
|
||||
originalCaseSensitivePath := CaseSensitivePath
|
||||
defer func() {
|
||||
CaseSensitivePath = originalCaseSensitivePath
|
||||
}()
|
||||
CaseSensitivePath = true
|
||||
|
||||
caseSensitiveData := []struct {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A/ABCDEF",
|
||||
res: "a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Port",
|
||||
res: "a:2015/Port",
|
||||
},
|
||||
}
|
||||
for _, item := range caseSensitiveData {
|
||||
v := normalizedKey(item.orig)
|
||||
if v != item.res {
|
||||
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to true must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
|
||||
}
|
||||
}
|
||||
|
||||
CaseSensitivePath = false
|
||||
caseInsensitiveData := []struct {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/abcdef",
|
||||
},
|
||||
{
|
||||
orig: "A/ABCDEF",
|
||||
res: "a/abcdef",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Port",
|
||||
res: "a:2015/port",
|
||||
},
|
||||
}
|
||||
for _, item := range caseInsensitiveData {
|
||||
v := normalizedKey(item.orig)
|
||||
if v != item.res {
|
||||
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to false must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
// case insensitivity for key
|
||||
con := caddy.NewTestController("http", "")
|
||||
@@ -170,6 +276,14 @@ func TestGetConfig(t *testing.T) {
|
||||
if cfg == cfg3 {
|
||||
t.Errorf("Expected different configs using when key is different; got %p and %p", cfg, cfg3)
|
||||
}
|
||||
|
||||
con.Key = "foo/foobar"
|
||||
cfg4 := GetConfig(con)
|
||||
con.Key = "foo/Foobar"
|
||||
cfg5 := GetConfig(con)
|
||||
if cfg4 == cfg5 {
|
||||
t.Errorf("Expected different cases in path to differentiate keys in general")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectivesList(t *testing.T) {
|
||||
@@ -193,7 +307,7 @@ func TestDirectivesList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextSaveConfig(t *testing.T) {
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
ctx.saveConfig("foo", new(SiteConfig))
|
||||
if _, ok := ctx.keysToSiteConfigs["foo"]; !ok {
|
||||
t.Error("Expected config to be saved, but it wasn't")
|
||||
@@ -212,7 +326,7 @@ func TestContextSaveConfig(t *testing.T) {
|
||||
|
||||
// Test to make sure we are correctly hiding the Caddyfile
|
||||
func TestHideCaddyfile(t *testing.T) {
|
||||
ctx := newContext().(*httpContext)
|
||||
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
|
||||
ctx.saveConfig("test", &SiteConfig{
|
||||
Root: Root,
|
||||
originCaddyfile: "Testfile",
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -101,6 +115,7 @@ type ResponseBuffer struct {
|
||||
shouldBuffer func(status int, header http.Header) bool
|
||||
stream bool
|
||||
rw http.ResponseWriter
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
// NewResponseBuffer returns a new ResponseBuffer that will
|
||||
@@ -138,6 +153,11 @@ func (rb *ResponseBuffer) Header() http.Header {
|
||||
// upcoming body should be buffered, and then writes
|
||||
// the header to the response.
|
||||
func (rb *ResponseBuffer) WriteHeader(status int) {
|
||||
if rb.wroteHeader {
|
||||
return
|
||||
}
|
||||
rb.wroteHeader = true
|
||||
|
||||
rb.status = status
|
||||
rb.stream = !rb.shouldBuffer(status, rb.header)
|
||||
if rb.stream {
|
||||
@@ -149,6 +169,10 @@ func (rb *ResponseBuffer) WriteHeader(status int) {
|
||||
// Write writes buf to rb.Buffer if buffering, otherwise
|
||||
// to the ResponseWriter directly if streaming.
|
||||
func (rb *ResponseBuffer) Write(buf []byte) (int, error) {
|
||||
if !rb.wroteHeader {
|
||||
rb.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
if rb.stream {
|
||||
return rb.ResponseWriterWrapper.Write(buf)
|
||||
}
|
||||
@@ -176,6 +200,10 @@ func (rb *ResponseBuffer) CopyHeader() {
|
||||
// from ~8,200 to ~9,600 on templated files by ensuring that this type
|
||||
// implements io.ReaderFrom.
|
||||
func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) {
|
||||
if !rb.wroteHeader {
|
||||
rb.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
if rb.stream {
|
||||
// first see if we can avoid any allocations at all
|
||||
if wt, ok := src.(io.WriterTo); ok {
|
||||
@@ -189,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)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -15,6 +33,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
// requestReplacer is a strings.Replacer which is used to
|
||||
@@ -88,20 +107,30 @@ func (lw *limitWriter) String() string {
|
||||
// emptyValue should be the string that is used in place
|
||||
// of empty string (can still be empty string).
|
||||
func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Replacer {
|
||||
rb := newLimitWriter(MaxLogBodySize)
|
||||
if r.Body != nil {
|
||||
r.Body = struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
|
||||
repl := &replacer{
|
||||
request: r,
|
||||
responseRecorder: rr,
|
||||
emptyValue: emptyValue,
|
||||
}
|
||||
return &replacer{
|
||||
request: r,
|
||||
requestBody: rb,
|
||||
responseRecorder: rr,
|
||||
customReplacements: make(map[string]string),
|
||||
emptyValue: emptyValue,
|
||||
|
||||
// extract customReplacements from a request replacer when present.
|
||||
if existing, ok := r.Context().Value(ReplacerCtxKey).(*replacer); ok {
|
||||
repl.requestBody = existing.requestBody
|
||||
repl.customReplacements = existing.customReplacements
|
||||
} else {
|
||||
// if there is no existing replacer, build one from scratch.
|
||||
rb := newLimitWriter(MaxLogBodySize)
|
||||
if r.Body != nil {
|
||||
r.Body = struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
|
||||
}
|
||||
repl.requestBody = rb
|
||||
repl.customReplacements = make(map[string]string)
|
||||
}
|
||||
|
||||
return repl
|
||||
}
|
||||
|
||||
func canLogRequest(r *http.Request) bool {
|
||||
@@ -116,6 +145,14 @@ func canLogRequest(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// unescapeBraces finds escaped braces in s and returns
|
||||
// a string with those braces unescaped.
|
||||
func unescapeBraces(s string) string {
|
||||
s = strings.Replace(s, "\\{", "{", -1)
|
||||
s = strings.Replace(s, "\\}", "}", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
// Replace performs a replacement of values on s and returns
|
||||
// the string with the replaced values.
|
||||
func (r *replacer) Replace(s string) string {
|
||||
@@ -125,32 +162,59 @@ func (r *replacer) Replace(s string) string {
|
||||
}
|
||||
|
||||
result := ""
|
||||
Placeholders: // process each placeholder in sequence
|
||||
for {
|
||||
idxStart := strings.Index(s, "{")
|
||||
if idxStart == -1 {
|
||||
// no placeholder anymore
|
||||
break
|
||||
}
|
||||
idxEnd := strings.Index(s[idxStart:], "}")
|
||||
if idxEnd == -1 {
|
||||
// unpaired placeholder
|
||||
break
|
||||
}
|
||||
idxEnd += idxStart
|
||||
var idxStart, idxEnd int
|
||||
|
||||
// get a replacement
|
||||
placeholder := s[idxStart : idxEnd+1]
|
||||
idxOffset := 0
|
||||
for { // find first unescaped opening brace
|
||||
searchSpace := s[idxOffset:]
|
||||
idxStart = strings.Index(searchSpace, "{")
|
||||
if idxStart == -1 {
|
||||
// no more placeholders
|
||||
break Placeholders
|
||||
}
|
||||
if idxStart == 0 || searchSpace[idxStart-1] != '\\' {
|
||||
// preceding character is not an escape
|
||||
idxStart += idxOffset
|
||||
break
|
||||
}
|
||||
// the brace we found was escaped
|
||||
// search the rest of the string next
|
||||
idxOffset += idxStart + 1
|
||||
}
|
||||
|
||||
idxOffset = 0
|
||||
for { // find first unescaped closing brace
|
||||
searchSpace := s[idxStart+idxOffset:]
|
||||
idxEnd = strings.Index(searchSpace, "}")
|
||||
if idxEnd == -1 {
|
||||
// unpaired placeholder
|
||||
break Placeholders
|
||||
}
|
||||
if idxEnd == 0 || searchSpace[idxEnd-1] != '\\' {
|
||||
// preceding character is not an escape
|
||||
idxEnd += idxOffset + idxStart
|
||||
break
|
||||
}
|
||||
// the brace we found was escaped
|
||||
// search the rest of the string next
|
||||
idxOffset += idxEnd + 1
|
||||
}
|
||||
|
||||
// get a replacement for the unescaped placeholder
|
||||
placeholder := unescapeBraces(s[idxStart : idxEnd+1])
|
||||
replacement := r.getSubstitution(placeholder)
|
||||
|
||||
// append prefix + replacement
|
||||
result += s[:idxStart] + replacement
|
||||
// append unescaped prefix + replacement
|
||||
result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement
|
||||
|
||||
// strip out scanned parts
|
||||
s = s[idxEnd+1:]
|
||||
}
|
||||
|
||||
// append unscanned parts
|
||||
return result + s
|
||||
return result + unescapeBraces(s)
|
||||
}
|
||||
|
||||
func roundDuration(d time.Duration) time.Duration {
|
||||
@@ -183,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
|
||||
@@ -200,6 +273,16 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// search response headers then
|
||||
if r.responseRecorder != nil && key[1] == '<' {
|
||||
want := key[2 : len(key)-1]
|
||||
for key, values := range r.responseRecorder.Header() {
|
||||
// Header placeholders (case-insensitive)
|
||||
if strings.EqualFold(key, want) {
|
||||
return strings.Join(values, ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
// next check for cookies
|
||||
if key[1] == '~' {
|
||||
name := key[2 : len(key)-1]
|
||||
@@ -285,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
|
||||
@@ -341,14 +428,120 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
elapsedDuration := time.Since(r.responseRecorder.start)
|
||||
return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10)
|
||||
case "{tls_protocol}":
|
||||
if r.request.TLS != nil {
|
||||
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 r.emptyValue // because not using a secure channel
|
||||
case "{tls_cipher}":
|
||||
if r.request.TLS != nil {
|
||||
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 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") {
|
||||
nStr := key[6 : len(key)-1] // get the integer N in "{labelN}"
|
||||
n, err := strconv.Atoi(nStr)
|
||||
if err != nil || n < 1 {
|
||||
return r.emptyValue
|
||||
}
|
||||
labels := strings.Split(r.request.Host, ".")
|
||||
if n > len(labels) {
|
||||
return r.emptyValue
|
||||
}
|
||||
return labels[n-1]
|
||||
}
|
||||
}
|
||||
|
||||
return r.emptyValue
|
||||
}
|
||||
|
||||
//convertToMilliseconds returns the number of milliseconds in the given duration
|
||||
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.
|
||||
@@ -358,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"
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
// 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.
|
||||
|
||||
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/mholt/caddy/caddytls"
|
||||
)
|
||||
|
||||
func TestNewReplacer(t *testing.T) {
|
||||
@@ -39,7 +62,7 @@ func TestReplace(t *testing.T) {
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
request, err := http.NewRequest("POST", "http://localhost.local/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
@@ -53,6 +76,9 @@ func TestReplace(t *testing.T) {
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some response headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to determine hostname: %v", err)
|
||||
@@ -60,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
|
||||
@@ -70,18 +97,23 @@ func TestReplace(t *testing.T) {
|
||||
expect string
|
||||
}{
|
||||
{"This hostname is {hostname}", "This hostname is " + hostname},
|
||||
{"This host is {host}.", "This host is localhost."},
|
||||
{"This host is {host}.", "This host is localhost.local."},
|
||||
{"This request method is {method}.", "This request method is POST."},
|
||||
{"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 request is {request}.", "The request is POST /?foo=bar HTTP/1.1\\r\\nHost: localhost\\r\\n" +
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is CustomResponseHeader."},
|
||||
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
|
||||
{"The request is {request}.", "The request is POST /?foo=bar HTTP/1.1\\r\\nHost: localhost.local\\r\\n" +
|
||||
"Cookie: foo=bar; taste=delicious\\r\\nCustom: foobarbaz\\r\\nCustomadd: caddy\\r\\n" +
|
||||
"Shorterval: 1\\r\\n\\r\\n."},
|
||||
{"The cUsToM header is {>cUsToM}...", "The cUsToM header is foobarbaz..."},
|
||||
{"The cUsToM response header is {<CuSTom}.", "The cUsToM response header is CustomResponseHeader."},
|
||||
{"The Non-Existent header is {>Non-Existent}.", "The Non-Existent header is -."},
|
||||
{"Bad {host placeholder...", "Bad {host placeholder..."},
|
||||
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
|
||||
@@ -92,6 +124,10 @@ func TestReplace(t *testing.T) {
|
||||
{"Query string is {query}", "Query string is foo=bar"},
|
||||
{"Query string value for foo is {?foo}", "Query string value for foo is bar"},
|
||||
{"Missing query string argument is {?missing}", "Missing query string argument is "},
|
||||
{"{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 {
|
||||
@@ -124,6 +160,234 @@ 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)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
repl.Replace("This hostname is {hostname}")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplaceEscaped(b *testing.B) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
request.Header.Set("ShorterVal", "1")
|
||||
repl := NewReplacer(request, recordRequest, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
repl.Replace("\\{ 'hostname': '{hostname}' \\}")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseRecorderNil(t *testing.T) {
|
||||
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
repl := NewReplacer(request, nil, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
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() {
|
||||
now = old
|
||||
}()
|
||||
testCases := []struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is -."},
|
||||
}
|
||||
|
||||
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 TestSet(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
@@ -209,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,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -6,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
|
||||
@@ -52,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 {
|
||||
@@ -65,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
|
||||
@@ -82,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:
|
||||
@@ -113,6 +131,7 @@ const (
|
||||
// defaultRotateKeep is 10 files.
|
||||
defaultRotateKeep = 10
|
||||
|
||||
directiveRotateDisable = "rotate_disable"
|
||||
directiveRotateSize = "rotate_size"
|
||||
directiveRotateAge = "rotate_age"
|
||||
directiveRotateKeep = "rotate_keep"
|
||||
@@ -121,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)
|
||||
|
||||
+115
-43
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
// Package httpserver implements an HTTP server on top of Caddy.
|
||||
package httpserver
|
||||
|
||||
@@ -15,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/mholt/caddy/telemetry"
|
||||
)
|
||||
|
||||
// Server is the HTTP server implementation.
|
||||
type Server struct {
|
||||
Server *http.Server
|
||||
quicServer *h2quic.Server
|
||||
listener net.Listener
|
||||
listenerMu sync.Mutex
|
||||
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
|
||||
@@ -128,7 +140,7 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
|
||||
// Compile custom middleware for every site (enables virtual hosting)
|
||||
for _, site := range group {
|
||||
stack := Handler(staticfiles.FileServer{Root: http.Dir(site.Root), Hide: site.HiddenFiles})
|
||||
stack := Handler(staticfiles.FileServer{Root: http.Dir(site.Root), Hide: site.HiddenFiles, IndexPages: site.IndexPages})
|
||||
for i := len(site.middleware) - 1; i >= 0; i-- {
|
||||
stack = site.middleware[i](stack)
|
||||
}
|
||||
@@ -259,16 +271,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,
|
||||
@@ -285,10 +307,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
|
||||
@@ -304,11 +322,17 @@ func (s *Server) Serve(ln net.Listener) error {
|
||||
s.tlsGovChan = caddytls.RotateSessionTicketKeys(s.Server.TLSConfig)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if s.quicServer != nil {
|
||||
s.quicServer.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
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.
|
||||
@@ -331,6 +355,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
|
||||
@@ -342,9 +376,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c := context.WithValue(r.Context(), OriginalURLCtxKey, urlCopy)
|
||||
r = r.WithContext(c)
|
||||
|
||||
// Setup a replacer for the request that keeps track of placeholder
|
||||
// values across plugins.
|
||||
replacer := NewReplacer(r, nil, "")
|
||||
c = context.WithValue(r.Context(), ReplacerCtxKey, replacer)
|
||||
r = r.WithContext(c)
|
||||
|
||||
w.Header().Set("Server", caddy.AppName)
|
||||
sponsors := "Minio, Uptime Robot, and Sourcegraph"
|
||||
w.Header().Set("Caddy-Sponsors", "This free web server is made possible by its sponsors: "+sponsors)
|
||||
|
||||
status, _ := s.serveHTTP(w, r)
|
||||
|
||||
@@ -370,24 +408,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", caddytls.DefaultHTTPAlternatePort) {
|
||||
// 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 s.proxyHTTPChallenge(vhost, w, r) {
|
||||
// because the HTTP challenge might be disabled by its config
|
||||
if vhost.TLS.Manager.HandleHTTPChallenge(w, r) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -395,31 +435,45 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
// the URL path, so a request to example.com/foo/blog on the site
|
||||
// defined as example.com/foo appears as /blog instead of /foo/blog.
|
||||
if pathPrefix != "/" {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, pathPrefix)
|
||||
if !strings.HasPrefix(r.URL.Path, "/") {
|
||||
r.URL.Path = "/" + r.URL.Path
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// proxyHTTPChallenge solves the ACME HTTP challenge if r is the HTTP
|
||||
// request for the challenge. If it is, and if the request has been
|
||||
// fulfilled (response written), true is returned; false otherwise.
|
||||
// If you don't have a vhost, just call the challenge handler directly.
|
||||
func (s *Server) proxyHTTPChallenge(vhost *SiteConfig, w http.ResponseWriter, r *http.Request) bool {
|
||||
if vhost.Addr.Port != caddytls.HTTPChallengePort {
|
||||
return false
|
||||
func trimPathPrefix(u *url.URL, prefix string) *url.URL {
|
||||
// We need to use URL.EscapedPath() when trimming the pathPrefix as
|
||||
// URL.Path is ambiguous about / or %2f - see docs. See #1927
|
||||
trimmedPath := strings.TrimPrefix(u.EscapedPath(), prefix)
|
||||
if !strings.HasPrefix(trimmedPath, "/") {
|
||||
trimmedPath = "/" + trimmedPath
|
||||
}
|
||||
if vhost.TLS != nil && vhost.TLS.Manual {
|
||||
return false
|
||||
// After trimming path reconstruct uri string with Query before parsing
|
||||
trimmedURI := trimmedPath
|
||||
if u.RawQuery != "" || u.ForceQuery == true {
|
||||
trimmedURI = trimmedPath + "?" + u.RawQuery
|
||||
}
|
||||
altPort := caddytls.DefaultHTTPAlternatePort
|
||||
if vhost.TLS != nil && vhost.TLS.AltHTTPPort != "" {
|
||||
altPort = vhost.TLS.AltHTTPPort
|
||||
if u.Fragment != "" {
|
||||
trimmedURI = trimmedURI + "#" + u.Fragment
|
||||
}
|
||||
return caddytls.HTTPChallengeHandler(w, r, vhost.ListenHost, altPort)
|
||||
trimmedURL, err := url.Parse(trimmedURI)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Unable to parse trimmed URL %s: %v", trimmedURI, err)
|
||||
return u
|
||||
}
|
||||
return trimmedURL
|
||||
}
|
||||
|
||||
// Address returns the address s was assigned to listen on.
|
||||
@@ -449,16 +503,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -112,6 +127,114 @@ func TestMakeHTTPServerWithTimeouts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimPathPrefix(t *testing.T) {
|
||||
for i, pt := range []struct {
|
||||
url string
|
||||
prefix string
|
||||
expected string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
url: "/my/path",
|
||||
prefix: "/my",
|
||||
expected: "/path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/%2f/path",
|
||||
prefix: "/my",
|
||||
expected: "/%2f/path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/path",
|
||||
prefix: "/my/",
|
||||
expected: "/path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my///path",
|
||||
prefix: "/my",
|
||||
expected: "/path",
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
url: "/my///path",
|
||||
prefix: "/my",
|
||||
expected: "///path",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/path///slash",
|
||||
prefix: "/my",
|
||||
expected: "/path///slash",
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
url: "/my/%2f/path/%2f",
|
||||
prefix: "/my",
|
||||
expected: "/%2f/path/%2f",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/%20/path",
|
||||
prefix: "/my",
|
||||
expected: "/%20/path",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/path",
|
||||
prefix: "",
|
||||
expected: "/path",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/path/my/",
|
||||
prefix: "/my",
|
||||
expected: "/path/my/",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "",
|
||||
prefix: "/my",
|
||||
expected: "/",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/apath",
|
||||
prefix: "",
|
||||
expected: "/apath",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/path/page.php?akey=value",
|
||||
prefix: "/my",
|
||||
expected: "/path/page.php?akey=value",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/path/page?key=value#fragment",
|
||||
prefix: "/my",
|
||||
expected: "/path/page?key=value#fragment",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/path/page#fragment",
|
||||
prefix: "/my",
|
||||
expected: "/path/page#fragment",
|
||||
shouldFail: false,
|
||||
}, {
|
||||
url: "/my/apath?",
|
||||
prefix: "/my",
|
||||
expected: "/apath?",
|
||||
shouldFail: false,
|
||||
},
|
||||
} {
|
||||
|
||||
u, _ := url.Parse(pt.url)
|
||||
if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.String() != want {
|
||||
if !pt.shouldFail {
|
||||
|
||||
t.Errorf("Test %d: Expected='%s', but was '%s' ", i, want, got.String())
|
||||
}
|
||||
} else if pt.shouldFail {
|
||||
t.Errorf("SHOULDFAIL Test %d: Expected='%s', and was '%s' but should fail", i, want, got.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeHTTPServerWithHeaderLimit(t *testing.T) {
|
||||
for name, c := range map[string]struct {
|
||||
group []*SiteConfig
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -12,6 +26,9 @@ type SiteConfig struct {
|
||||
// The address of the site
|
||||
Addr Address
|
||||
|
||||
// The list of viable index page names of the site
|
||||
IndexPages []string
|
||||
|
||||
// The hostname to bind listener to;
|
||||
// defaults to Addr.Host
|
||||
ListenHost string
|
||||
@@ -19,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
|
||||
|
||||
@@ -59,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -17,6 +31,7 @@ import (
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
@@ -434,6 +449,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{} {
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@@ -86,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)
|
||||
@@ -147,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)
|
||||
@@ -263,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 {
|
||||
@@ -908,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
@@ -18,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// 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.
|
||||
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
// 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.
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -15,6 +29,8 @@ func init() {
|
||||
func setupIndex(c *caddy.Controller) error {
|
||||
var index []string
|
||||
|
||||
cfg := httpserver.GetConfig(c)
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
@@ -26,7 +42,7 @@ func setupIndex(c *caddy.Controller) error {
|
||||
index = append(index, in)
|
||||
}
|
||||
|
||||
staticfiles.IndexPages = index
|
||||
cfg.IndexPages = index
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
// 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.
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
)
|
||||
|
||||
@@ -17,7 +32,7 @@ func TestIndexIncompleteParams(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
c := caddy.NewTestController("", "index a.html b.html c.html")
|
||||
c := caddy.NewTestController("http", "index a.html b.html c.html")
|
||||
|
||||
err := setupIndex(c)
|
||||
if err != nil {
|
||||
@@ -26,14 +41,85 @@ func TestIndex(t *testing.T) {
|
||||
|
||||
expectedIndex := []string{"a.html", "b.html", "c.html"}
|
||||
|
||||
if len(staticfiles.IndexPages) != 3 {
|
||||
t.Errorf("Expected 3 values, got %v", len(staticfiles.IndexPages))
|
||||
siteConfig := httpserver.GetConfig(c)
|
||||
|
||||
if len(siteConfig.IndexPages) != len(expectedIndex) {
|
||||
t.Errorf("Expected 3 values, got %v", len(siteConfig.IndexPages))
|
||||
}
|
||||
|
||||
// Ensure ordering is correct
|
||||
for i, actual := range staticfiles.IndexPages {
|
||||
for i, actual := range siteConfig.IndexPages {
|
||||
if actual != expectedIndex[i] {
|
||||
t.Errorf("Expected value in position %d to be %v, got %v", i, expectedIndex[i], actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiSiteIndexWithEitherHasDefault(t *testing.T) {
|
||||
// TestIndex already covers the correctness of the directive
|
||||
// when used on a single controller, so no need to verify test setupIndex again.
|
||||
// This sets the stage for the actual verification.
|
||||
customIndex := caddy.NewTestController("http", "index a.html b.html")
|
||||
|
||||
// setupIndex against customIdx should not pollute the
|
||||
// index list for other controllers.
|
||||
err := setupIndex(customIndex)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
// Represents a virtual host with no index directive.
|
||||
defaultIndex := caddy.NewTestController("http", "")
|
||||
|
||||
// Not calling setupIndex because it guards against lack of arguments,
|
||||
// and we need to ensure the site gets the default set of index pages.
|
||||
|
||||
siteConfig := httpserver.GetConfig(defaultIndex)
|
||||
|
||||
// In case the index directive is not used, the virtual host
|
||||
// should receive staticfiles.DefaultIndexPages slice. The length, as checked here,
|
||||
// and the values, as checked in the upcoming loop, should match.
|
||||
if len(siteConfig.IndexPages) != len(staticfiles.DefaultIndexPages) {
|
||||
t.Errorf("Expected %d values, got %d", len(staticfiles.DefaultIndexPages), len(siteConfig.IndexPages))
|
||||
}
|
||||
|
||||
// Ensure values match the expected default index pages
|
||||
for i, actual := range siteConfig.IndexPages {
|
||||
if actual != staticfiles.DefaultIndexPages[i] {
|
||||
t.Errorf("Expected value in position %d to be %v, got %v", i, staticfiles.DefaultIndexPages[i], actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerSiteIndexPageIsolation(t *testing.T) {
|
||||
firstIndex := "first.html"
|
||||
secondIndex := "second.html"
|
||||
|
||||
// Create two sites with different index page configurations
|
||||
firstSite := caddy.NewTestController("http", "index first.html")
|
||||
err := setupIndex(firstSite)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
secondSite := caddy.NewTestController("http", "index second.html")
|
||||
err = setupIndex(secondSite)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
firstSiteConfig := httpserver.GetConfig(firstSite)
|
||||
if firstSiteConfig.IndexPages[0] != firstIndex {
|
||||
t.Errorf("Expected index for first site as %s, received %s", firstIndex, firstSiteConfig.IndexPages[0])
|
||||
}
|
||||
|
||||
secondSiteConfig := httpserver.GetConfig(secondSite)
|
||||
if secondSiteConfig.IndexPages[0] != secondIndex {
|
||||
t.Errorf("Expected index for second site as %s, received %s", secondIndex, secondSiteConfig.IndexPages[0])
|
||||
}
|
||||
|
||||
// They should have different index pages, as per the provided config.
|
||||
if firstSiteConfig.IndexPages[0] == secondSiteConfig.IndexPages[0] {
|
||||
t.Errorf("Expected different index pages for both sites, got %s for first and %s for second", firstSiteConfig.IndexPages[0], secondSiteConfig.IndexPages[0])
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user