mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8bb4a665a | |||
| 3a1e0dbf47 | |||
| 77a77c0219 | |||
| db62942d63 | |||
| dadd4b59b0 | |||
| d230b33007 | |||
| 0d13173071 | |||
| c3a82f53d5 | |||
| 30b6d1f47a | |||
| bc15b4b0e7 | |||
| e2535233bb | |||
| 00234c8ac2 | |||
| 6512832f9f | |||
| 3e3bb00265 | |||
| e4ce40f8ff | |||
| afca242111 | |||
| 7d229665ed | |||
| 22d8edb984 | |||
| 734acc776a | |||
| b4f1a71397 | |||
| d06d0e79f8 | |||
| a58f240d3e | |||
| 4b75f3e2f0 | |||
| b8dbecb841 | |||
| 134b805644 | |||
| c9b5e7f77b | |||
| 79cbe7bfd0 | |||
| 55b4c12e04 | |||
| 2196c92c0e | |||
| c2327161f7 | |||
| c5fffb4ac2 | |||
| dc4d147388 | |||
| 93c99f6734 | |||
| 4e9fbee1e2 | |||
| a9c7e94a38 | |||
| 3d616e8c6d | |||
| b82e22b459 | |||
| bf6a1b7538 | |||
| c7d6c4cbb9 | |||
| d0b608af31 | |||
| d9b1d46325 | |||
| c8f2834b51 | |||
| ab0455922a | |||
| c50094fc9d | |||
| d058dee11d | |||
| 09ba9e994e | |||
| be82cc7aca | |||
| 2bb8550a4c | |||
| a72acd21b0 | |||
| a6199cf814 | |||
| ceef70dbc5 | |||
| f5e104944e | |||
| 6b385a36f9 | |||
| 9b7cdfa2f2 | |||
| 78e381b29f | |||
| de490c7cad | |||
| bbad6931e3 | |||
| 5bd96a6ac2 | |||
| ac14b64e08 | |||
| 15c95e9d5b | |||
| bc447e307f | |||
| 87a1f228b4 | |||
| acbee94708 | |||
| 7ea5b2a818 | |||
| 186fdba916 | |||
| 7778912d4e | |||
| c921e08296 | |||
| ddbb234d91 | |||
| 0de51593a6 | |||
| 26d633baf8 | |||
| ff137d17d0 | |||
| 57a708d189 | |||
| 32aad90938 | |||
| 40b54434f3 | |||
| 1d0425b26f | |||
| 7557d1d922 | |||
| ff74a0aa09 | |||
| 599c81d753 | |||
| 741b0502ee | |||
| 7ca5921a87 | |||
| da4a759bad | |||
| 042abeb431 | |||
| eb891d4683 | |||
| 44e5e9e43f | |||
| bf380d00ab | |||
| 94035c1797 | |||
| b3f7ce34b4 | |||
| a79b4055e5 | |||
| 5a07156894 | |||
| bcb7a19cd3 | |||
| 6e6ce2be6b | |||
| 1b7ff5d76c | |||
| 93a7a45e7e | |||
| 1a7a78a1f2 | |||
| 1feb65952a | |||
| 66de438a98 | |||
| 850e1605df | |||
| af1ac9cd2e | |||
| 64a3218f5c | |||
| c634bbe9cc | |||
| 4b9849c792 | |||
| 80d7a356b3 | |||
| b4bfa29be2 | |||
| 6cadb60fa2 | |||
| 2e46c2ac1d | |||
| 249adc1c87 | |||
| e9dde23024 | |||
| 3fe2c73dd0 | |||
| 5333c3528b | |||
| 180ae0cc48 | |||
| a1c41210d3 | |||
| ecac03cdcb | |||
| c04d24cafa | |||
| 81ee34e962 | |||
| 78b5356f2b | |||
| 6f9b6ad78e | |||
| 4906b9357a | |||
| e90d751732 | |||
| dce81e85d5 | |||
| a1b417c832 | |||
| 5bf0adad87 | |||
| 8e5aafa5cd | |||
| c133153447 | |||
| ec14ccdd40 | |||
| f55b123d63 | |||
| 0eb0b60f47 | |||
| 5e5af50e64 | |||
| 9ee68c1bd5 | |||
| 789efa5dee | |||
| 8887adb027 | |||
| bcac2beee7 | |||
| 1e10f6f725 | |||
| c8b5a81607 | |||
| eead337324 | |||
| 7d5047c1f1 | |||
| 7f364c777a | |||
| b47af6ef04 | |||
| e81369e220 | |||
| e7457b43e4 | |||
| f376a38b25 | |||
| 749e55c738 | |||
| 24fda7514d | |||
| 3385856966 | |||
| f73f55dba7 | |||
| 012d235314 | |||
| 997e41deae | |||
| 0ffb2229b0 | |||
| a21d5a001f | |||
| a2119c09e9 | |||
| 062657d0d8 | |||
| b092061591 | |||
| 64f8b557b1 | |||
| 95c035060f | |||
| c4790d7f9d | |||
| 837cdc566d | |||
| be5f77e84d | |||
| cbb045a121 | |||
| c48fadc4a7 | |||
| 059fc32f00 | |||
| e2d964ea30 | |||
| 501da21f20 | |||
| 3336faf254 | |||
| 16f752125f | |||
| 0a5f7a677f | |||
| d3a0259944 | |||
| 5fda9610f9 | |||
| 3f2c3ecf85 | |||
| 907e2d8d3a | |||
| 33c70f418f | |||
| 2ebfda1ae9 | |||
| 2392478bd3 | |||
| a437206643 | |||
| a779e1b383 | |||
| 46ab93be51 | |||
| e0fc46a911 | |||
| 9f6393c64c | |||
| 105dac8c2a | |||
| 4ebf100f09 | |||
| f43fd6f388 | |||
| 84b906a248 | |||
| 403732c433 | |||
| f6d5ec2fd6 | |||
| 19a55d6aeb | |||
| bfbc459c0a | |||
| f70a7578fa | |||
| 51f125bd44 | |||
| d74913f871 | |||
| ce5a45db45 | |||
| e0a6a1efff | |||
| c1cd192ee7 | |||
| a056fcd7ba | |||
| 9e333c39da | |||
| 8a974a4f8f | |||
| 6bc87ea2ff | |||
| 1b1e625c20 | |||
| a10910f398 | |||
| ab32440b21 | |||
| e6c29ce081 | |||
| 68c5c71659 | |||
| 569ecdbd02 | |||
| c131339c5c | |||
| b6f51254ea | |||
| 124ba1ba71 | |||
| 1c6c7714a3 | |||
| 46d99aba85 | |||
| 9e16e80f3c | |||
| d882211080 | |||
| 42e140b1b2 | |||
| 4245ceb67d | |||
| 0bdb8aa82d | |||
| 191dc86f9e | |||
| 81e5318021 | |||
| b3d35a4995 | |||
| 2de7e14e1c | |||
| 885a9aaf48 | |||
| 69c914483d | |||
| 9d4ed3a323 | |||
| fbd6560976 | |||
| 238914d70b | |||
| e8ae80adca | |||
| 32c284b54a | |||
| 7c68809f4e | |||
| 6d25261c22 | |||
| 8848df9c5d | |||
| 89aa3a5ef3 | |||
| 05656a60b3 | |||
| 1e92258dd6 | |||
| 76913b19ff | |||
| 4c2da18841 | |||
| f9b54454a1 | |||
| 658772ff24 | |||
| 323ffd2076 | |||
| 2a8109468c | |||
| 94b712009a | |||
| 7b500e74b4 | |||
| ecd5eeab38 | |||
| b4cef492cc | |||
| e3c369d452 | |||
| c052162203 | |||
| 7f26a6b3e5 | |||
| b82db994f3 | |||
| aef8d4decc | |||
| 37718560c1 | |||
| 2aefe15686 | |||
| dbe164d98a | |||
| bc22102478 | |||
| f5db41ce1d | |||
| 77764714ad | |||
| 61642b766b | |||
| 3cf443f0fe | |||
| d4b2f1bcee | |||
| a17c3b568d | |||
| 74f5d66c48 | |||
| efe84497d7 | |||
| e4a22de9d1 | |||
| e6f6d3a476 | |||
| ef7f15f3a4 | |||
| 6e0e3e1537 | |||
| 53ececda21 | |||
| 637fd8f67b | |||
| 956f01163d | |||
| ff6ca577ec | |||
| 9017557169 | |||
| 3a1e81dbf6 | |||
| a8d45277ca | |||
| 1e218e1d2e | |||
| 4d0474e3b8 | |||
| d789596bc0 | |||
| 96bb365929 | |||
| 00e12aa918 | |||
| 2250920e1d | |||
| 42b7134ffa | |||
| 3903642aa7 | |||
| 03b5debd95 | |||
| 3f6283b385 | |||
| 45fb7202ac | |||
| 66783eb4d9 | |||
| 1455d6bb69 | |||
| 3401f91dbe |
@@ -0,0 +1,5 @@
|
||||
[*]
|
||||
end_of_line = lf
|
||||
|
||||
[caddytest/integration/caddyfile_adapt/*.txt]
|
||||
indent_style = tab
|
||||
+37
-5
@@ -2,9 +2,6 @@
|
||||
|
||||
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
||||
|
||||
Some security problems are more the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please report only vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing or BGP hijacks a vulnerability in the Caddy web server).
|
||||
|
||||
Please note that we consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
@@ -14,11 +11,46 @@ Please note that we consider publicly-registered domain names to be public infor
|
||||
| 1.x | :x: |
|
||||
| < 1.x | :x: |
|
||||
|
||||
|
||||
## Acceptable Scope
|
||||
|
||||
A security report must demonstrate a security bug in the source code from this repository.
|
||||
|
||||
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
|
||||
|
||||
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that.
|
||||
|
||||
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
|
||||
|
||||
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
|
||||
|
||||
Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please email Matt Holt (the author) directly: matt [at] lightcodelabs [dot com].
|
||||
We get a lot of difficult reports that turn out to be invalid. Clear, obvious reports tend to be the most credible (but are also rare).
|
||||
|
||||
We'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, resources permitting. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. Thank you for understanding.
|
||||
First please ensure your report falls within the accepted scope of security bugs (above).
|
||||
|
||||
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
||||
|
||||
- Most minimal possible config (without redactions!)
|
||||
- Command(s)
|
||||
- Precise HTTP requests (`curl -v` and its output please)
|
||||
- Full log output (please enable debug mode)
|
||||
- Specific minimal steps to reproduce the issue from scratch
|
||||
- A working patch
|
||||
|
||||
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
|
||||
|
||||
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
|
||||
|
||||
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
|
||||
|
||||
When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com.
|
||||
|
||||
Please don't encrypt the email body. It only makes the process more complicated.
|
||||
|
||||
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -17,12 +19,20 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
go: [ '1.15', '1.16' ]
|
||||
go: [ '1.17', '1.18' ]
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.17'
|
||||
GO_SEMVER: '~1.17.9'
|
||||
|
||||
- go: '1.18'
|
||||
GO_SEMVER: '~1.18.1'
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
CADDY_BIN_PATH: ./cmd/caddy/caddy
|
||||
SUCCESS: 0
|
||||
@@ -39,12 +49,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# These tools would be useful if we later decide to reinvestigate
|
||||
# publishing test/coverage reports to some tool for easier consumption
|
||||
@@ -128,7 +139,7 @@ jobs:
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Checkout code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Run Tests
|
||||
run: |
|
||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||
@@ -153,7 +164,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
@@ -4,9 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
|
||||
jobs:
|
||||
cross-build-test:
|
||||
@@ -14,14 +16,22 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
||||
go: [ '1.15', '1.16' ]
|
||||
go: [ '1.18' ]
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.18'
|
||||
GO_SEMVER: '~1.18.1'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
@@ -43,7 +53,7 @@ jobs:
|
||||
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
|
||||
|
||||
- name: Checkout code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Build
|
||||
env:
|
||||
|
||||
@@ -4,9 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 2.*
|
||||
|
||||
jobs:
|
||||
# From https://github.com/golangci/golangci-lint-action
|
||||
@@ -14,10 +16,15 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
version: v1.31
|
||||
go-version: '~1.17.9'
|
||||
check-latest: true
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.44
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
@@ -11,22 +11,30 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
go: [ '1.16' ]
|
||||
go: [ '1.18' ]
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.18'
|
||||
GO_SEMVER: '~1.18.1'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: ${{ matrix.GO_SEMVER }}
|
||||
check-latest: true
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Force fetch upstream tags -- because 65 minutes
|
||||
# tl;dr: actions/checkout@v2 runs this line:
|
||||
# tl;dr: actions/checkout@v3 runs this line:
|
||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||
# git fetch --prune --unshallow
|
||||
@@ -99,7 +107,7 @@ jobs:
|
||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||
|
||||
# Only publish on non-special tags (e.g. non-beta)
|
||||
# We will continue to push to Gemfury for the forseeable future, although
|
||||
# We will continue to push to Gemfury for the foreseeable future, although
|
||||
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
|
||||
# See https://gemfury.com/caddy/deb:caddy
|
||||
- name: Publish .deb to Gemfury
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,io/ioutil:^Read.*,go.uber.org/zap/zapcore:^Add.*
|
||||
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
||||
ignoretests: true
|
||||
|
||||
linters:
|
||||
|
||||
+7
-8
@@ -6,8 +6,7 @@ before:
|
||||
# subsequently causes gorleaser to refuse running.
|
||||
- mkdir -p caddy-build
|
||||
- cp cmd/caddy/main.go caddy-build/main.go
|
||||
- cp ./go.mod caddy-build/go.mod
|
||||
- sed -i.bkp 's|github.com/caddyserver/caddy/v2|caddy|g' ./caddy-build/go.mod
|
||||
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
||||
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
||||
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
|
||||
@@ -36,9 +35,9 @@ builds:
|
||||
- s390x
|
||||
- ppc64le
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
@@ -56,7 +55,7 @@ builds:
|
||||
goarch: s390x
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: 5
|
||||
goarm: "5"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
@@ -75,11 +74,11 @@ nfpms:
|
||||
- id: default
|
||||
package_name: caddy
|
||||
|
||||
vendor: Light Code Labs
|
||||
vendor: Dyanim
|
||||
homepage: https://caddyserver.com
|
||||
maintainer: Matthew Holt <mholt@users.noreply.github.com>
|
||||
description: |
|
||||
Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
|
||||
Caddy - Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
|
||||
license: Apache 2.0
|
||||
|
||||
formats:
|
||||
|
||||
@@ -75,7 +75,7 @@ For other install options, see https://caddyserver.com/docs/install.
|
||||
|
||||
Requirements:
|
||||
|
||||
- [Go 1.15 or newer](https://golang.org/dl/)
|
||||
- [Go 1.17 or newer](https://golang.org/dl/)
|
||||
|
||||
### For development
|
||||
|
||||
@@ -176,6 +176,8 @@ Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only
|
||||
|
||||
## About
|
||||
|
||||
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
|
||||
|
||||
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
|
||||
|
||||
- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
@@ -39,9 +38,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/notify"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// AdminConfig configures Caddy's API endpoint, which is used
|
||||
@@ -91,6 +92,10 @@ type AdminConfig struct {
|
||||
//
|
||||
// EXPERIMENTAL: This feature is subject to change.
|
||||
Remote *RemoteAdmin `json:"remote,omitempty"`
|
||||
|
||||
// Holds onto the routers so that we can later provision them
|
||||
// if they require provisioning.
|
||||
routers []AdminRouter
|
||||
}
|
||||
|
||||
// ConfigSettings configures the management of configuration.
|
||||
@@ -100,14 +105,26 @@ type ConfigSettings struct {
|
||||
// are not persisted; only configs that are pushed to Caddy get persisted.
|
||||
Persist *bool `json:"persist,omitempty"`
|
||||
|
||||
// Loads a configuration to use. This is helpful if your configs are
|
||||
// managed elsewhere, and you want Caddy to pull its config dynamically
|
||||
// Loads a new configuration. This is helpful if your configs are
|
||||
// managed elsewhere and you want Caddy to pull its config dynamically
|
||||
// when it starts. The pulled config completely replaces the current
|
||||
// one, just like any other config load. It is an error if a pulled
|
||||
// config is configured to pull another config.
|
||||
// config is configured to pull another config without a load_delay,
|
||||
// as this creates a tight loop.
|
||||
//
|
||||
// EXPERIMENTAL: Subject to change.
|
||||
LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"`
|
||||
|
||||
// The duration after which to load config. If set, config will be pulled
|
||||
// from the config loader after this duration. A delay is required if a
|
||||
// dynamically-loaded config is configured to load yet another config. To
|
||||
// load configs on a regular interval, ensure this value is set the same
|
||||
// on all loaded configs; it can also be variable if needed, and to stop
|
||||
// the loop, simply remove dynamic config loading from the next-loaded
|
||||
// config.
|
||||
//
|
||||
// EXPERIMENTAL: Subject to change.
|
||||
LoadDelay Duration `json:"load_delay,omitempty"`
|
||||
}
|
||||
|
||||
// IdentityConfig configures management of this server's identity. An identity
|
||||
@@ -177,7 +194,7 @@ type AdminPermissions struct {
|
||||
|
||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||
func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
|
||||
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) adminHandler {
|
||||
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||
|
||||
// secure the local or remote endpoint respectively
|
||||
@@ -186,6 +203,7 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
|
||||
} else {
|
||||
muxWrap.enforceHost = !addr.isWildcardInterface()
|
||||
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
|
||||
muxWrap.enforceOrigin = admin.EnforceOrigin
|
||||
}
|
||||
|
||||
addRouteWithMetrics := func(pattern string, handlerLabel string, h http.Handler) {
|
||||
@@ -236,17 +254,39 @@ func (admin AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admin
|
||||
for _, route := range router.Routes() {
|
||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||
}
|
||||
admin.routers = append(admin.routers, router)
|
||||
}
|
||||
|
||||
return muxWrap
|
||||
}
|
||||
|
||||
// provisionAdminRouters provisions all the router modules
|
||||
// in the admin.api namespace that need provisioning.
|
||||
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
|
||||
for _, router := range admin.routers {
|
||||
provisioner, ok := router.(Provisioner)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
err := provisioner.Provision(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// We no longer need the routers once provisioned, allow for GC
|
||||
admin.routers = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// allowedOrigins returns a list of origins that are allowed.
|
||||
// If admin.Origins is nil (null), the provided listen address
|
||||
// will be used as the default origin. If admin.Origins is
|
||||
// empty, no origins will be allowed, effectively bricking the
|
||||
// endpoint for non-unix-socket endpoints, but whatever.
|
||||
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
|
||||
func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
||||
uniqueOrigins := make(map[string]struct{})
|
||||
for _, o := range admin.Origins {
|
||||
uniqueOrigins[o] = struct{}{}
|
||||
@@ -270,8 +310,23 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []string {
|
||||
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
|
||||
}
|
||||
}
|
||||
allowed := make([]string, 0, len(uniqueOrigins))
|
||||
for origin := range uniqueOrigins {
|
||||
allowed := make([]*url.URL, 0, len(uniqueOrigins))
|
||||
for originStr := range uniqueOrigins {
|
||||
var origin *url.URL
|
||||
if strings.Contains(originStr, "://") {
|
||||
var err error
|
||||
origin, err = url.Parse(originStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
origin.Path = ""
|
||||
origin.RawPath = ""
|
||||
origin.Fragment = ""
|
||||
origin.RawFragment = ""
|
||||
origin.RawQuery = ""
|
||||
} else {
|
||||
origin = &url.URL{Host: originStr}
|
||||
}
|
||||
allowed = append(allowed, origin)
|
||||
}
|
||||
return allowed
|
||||
@@ -303,31 +358,33 @@ func replaceLocalAdminServer(cfg *Config) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// always get a valid admin config
|
||||
adminConfig := DefaultAdminConfig
|
||||
if cfg != nil && cfg.Admin != nil {
|
||||
adminConfig = cfg.Admin
|
||||
// set a default if admin wasn't otherwise configured
|
||||
if cfg.Admin == nil {
|
||||
cfg.Admin = &AdminConfig{
|
||||
Listen: DefaultAdminListen,
|
||||
}
|
||||
}
|
||||
|
||||
// if new admin endpoint is to be disabled, we're done
|
||||
if adminConfig.Disabled {
|
||||
if cfg.Admin.Disabled {
|
||||
Log().Named("admin").Warn("admin endpoint disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// extract a singular listener address
|
||||
addr, err := parseAdminListenAddr(adminConfig.Listen, DefaultAdminListen)
|
||||
addr, err := parseAdminListenAddr(cfg.Admin.Listen, DefaultAdminListen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handler := adminConfig.newAdminHandler(addr, false)
|
||||
handler := cfg.Admin.newAdminHandler(addr, false)
|
||||
|
||||
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverMu.Lock()
|
||||
localAdminServer = &http.Server{
|
||||
Addr: addr.String(), // for logging purposes only
|
||||
Handler: handler,
|
||||
@@ -336,18 +393,22 @@ func replaceLocalAdminServer(cfg *Config) error {
|
||||
IdleTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 1024 * 64,
|
||||
}
|
||||
serverMu.Unlock()
|
||||
|
||||
adminLogger := Log().Named("admin")
|
||||
go func() {
|
||||
if err := localAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverMu.Lock()
|
||||
server := localAdminServer
|
||||
serverMu.Unlock()
|
||||
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
adminLogger.Info("admin endpoint started",
|
||||
zap.String("address", addr.String()),
|
||||
zap.Bool("enforce_origin", adminConfig.EnforceOrigin),
|
||||
zap.Strings("origins", handler.allowedOrigins))
|
||||
zap.Bool("enforce_origin", cfg.Admin.EnforceOrigin),
|
||||
zap.Array("origins", loggableURLArray(handler.allowedOrigins)))
|
||||
|
||||
if !handler.enforceHost {
|
||||
adminLogger.Warn("admin endpoint on open interface; host checking disabled",
|
||||
@@ -363,11 +424,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldIdentityCertCache := identityCertCache
|
||||
if oldIdentityCertCache != nil {
|
||||
defer oldIdentityCertCache.Stop()
|
||||
}
|
||||
|
||||
// set default issuers; this is pretty hacky because we can't
|
||||
// import the caddytls package -- but it works
|
||||
if cfg.Admin.Identity.IssuersRaw == nil {
|
||||
@@ -388,8 +444,13 @@ func manageIdentity(ctx Context, cfg *Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// we'll make a new cache when we make the CertMagic config, so stop any previous cache
|
||||
if identityCertCache != nil {
|
||||
identityCertCache.Stop()
|
||||
}
|
||||
|
||||
logger := Log().Named("admin.identity")
|
||||
cmCfg := cfg.Admin.Identity.certmagicConfig(logger)
|
||||
cmCfg := cfg.Admin.Identity.certmagicConfig(logger, true)
|
||||
|
||||
// issuers have circular dependencies with the configs because,
|
||||
// as explained in the caddytls package, they need access to the
|
||||
@@ -455,7 +516,10 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
}
|
||||
|
||||
// create TLS config that will enforce mutual authentication
|
||||
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger)
|
||||
if identityCertCache == nil {
|
||||
return fmt.Errorf("cannot enable remote admin without a certificate cache; configure identity management to initialize a certificate cache")
|
||||
}
|
||||
cmCfg := cfg.Admin.Identity.certmagicConfig(remoteLogger, false)
|
||||
tlsConfig := cmCfg.TLSConfig()
|
||||
tlsConfig.NextProtos = nil // this server does not solve ACME challenges
|
||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
@@ -467,6 +531,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
serverMu.Lock()
|
||||
// create secure HTTP server
|
||||
remoteAdminServer = &http.Server{
|
||||
Addr: addr.String(), // for logging purposes only
|
||||
@@ -478,6 +543,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
MaxHeaderBytes: 1024 * 64,
|
||||
ErrorLog: serverLogger,
|
||||
}
|
||||
serverMu.Unlock()
|
||||
|
||||
// start listener
|
||||
ln, err := Listen(addr.Network, addr.JoinHostPort(0))
|
||||
@@ -487,7 +553,10 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
ln = tls.NewListener(ln, tlsConfig)
|
||||
|
||||
go func() {
|
||||
if err := remoteAdminServer.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverMu.Lock()
|
||||
server := remoteAdminServer
|
||||
serverMu.Unlock()
|
||||
if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
remoteLogger.Error("admin remote server shutdown for unknown reason", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
@@ -498,7 +567,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Config {
|
||||
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
|
||||
if ident == nil {
|
||||
// user might not have configured identity; that's OK, we can still make a
|
||||
// certmagic config, although it'll be mostly useless for remote management
|
||||
@@ -509,7 +578,7 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger) *certmagic.Conf
|
||||
Logger: logger,
|
||||
Issuers: ident.issuers,
|
||||
}
|
||||
if identityCertCache == nil {
|
||||
if makeCache {
|
||||
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
|
||||
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
|
||||
return cmCfg, nil
|
||||
@@ -532,7 +601,7 @@ func (ctx Context) IdentityCredentials(logger *zap.Logger) ([]tls.Certificate, e
|
||||
if logger == nil {
|
||||
logger = Log()
|
||||
}
|
||||
magic := ident.certmagicConfig(logger)
|
||||
magic := ident.certmagicConfig(logger, false)
|
||||
return magic.ClientCredentials(ctx, ident.Identifiers)
|
||||
}
|
||||
|
||||
@@ -631,10 +700,10 @@ type AdminRoute struct {
|
||||
type adminHandler struct {
|
||||
mux *http.ServeMux
|
||||
|
||||
// security for local/plaintext) endpoint, on by default
|
||||
// security for local/plaintext endpoint
|
||||
enforceOrigin bool
|
||||
enforceHost bool
|
||||
allowedOrigins []string
|
||||
allowedOrigins []*url.URL
|
||||
|
||||
// security for remote/encrypted endpoint
|
||||
remoteControl *RemoteAdmin
|
||||
@@ -643,11 +712,17 @@ type adminHandler struct {
|
||||
// ServeHTTP is the external entry point for API requests.
|
||||
// It will only be called once per request.
|
||||
func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ip, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
ip = r.RemoteAddr
|
||||
port = ""
|
||||
}
|
||||
log := Log().Named("admin.api").With(
|
||||
zap.String("method", r.Method),
|
||||
zap.String("host", r.Host),
|
||||
zap.String("uri", r.RequestURI),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.String("remote_ip", ip),
|
||||
zap.String("remote_port", port),
|
||||
zap.Reflect("headers", r.Header),
|
||||
)
|
||||
if r.TLS != nil {
|
||||
@@ -716,6 +791,10 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err == errInternalRedir {
|
||||
h.serveHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
apiErr, ok := err.(APIError)
|
||||
if !ok {
|
||||
@@ -750,8 +829,8 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
|
||||
// rebinding attacks.
|
||||
func (h adminHandler) checkHost(r *http.Request) error {
|
||||
var allowed bool
|
||||
for _, allowedHost := range h.allowedOrigins {
|
||||
if r.Host == allowedHost {
|
||||
for _, allowedOrigin := range h.allowedOrigins {
|
||||
if r.Host == allowedOrigin.Host {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
@@ -770,43 +849,45 @@ func (h adminHandler) checkHost(r *http.Request) error {
|
||||
// sites from issuing requests to our listener. It
|
||||
// returns the origin that was obtained from r.
|
||||
func (h adminHandler) checkOrigin(r *http.Request) (string, error) {
|
||||
origin := h.getOriginHost(r)
|
||||
if origin == "" {
|
||||
return origin, APIError{
|
||||
originStr, origin := h.getOrigin(r)
|
||||
if origin == nil {
|
||||
return "", APIError{
|
||||
HTTPStatus: http.StatusForbidden,
|
||||
Err: fmt.Errorf("missing required Origin header"),
|
||||
Err: fmt.Errorf("required Origin header is missing or invalid"),
|
||||
}
|
||||
}
|
||||
if !h.originAllowed(origin) {
|
||||
return origin, APIError{
|
||||
return "", APIError{
|
||||
HTTPStatus: http.StatusForbidden,
|
||||
Err: fmt.Errorf("client is not allowed to access from origin %s", origin),
|
||||
Err: fmt.Errorf("client is not allowed to access from origin '%s'", originStr),
|
||||
}
|
||||
}
|
||||
return origin, nil
|
||||
return origin.String(), nil
|
||||
}
|
||||
|
||||
func (h adminHandler) getOriginHost(r *http.Request) string {
|
||||
func (h adminHandler) getOrigin(r *http.Request) (string, *url.URL) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
origin = r.Header.Get("Referer")
|
||||
}
|
||||
originURL, err := url.Parse(origin)
|
||||
if err == nil && originURL.Host != "" {
|
||||
origin = originURL.Host
|
||||
if err != nil {
|
||||
return origin, nil
|
||||
}
|
||||
return origin
|
||||
originURL.Path = ""
|
||||
originURL.RawPath = ""
|
||||
originURL.Fragment = ""
|
||||
originURL.RawFragment = ""
|
||||
originURL.RawQuery = ""
|
||||
return origin, originURL
|
||||
}
|
||||
|
||||
func (h adminHandler) originAllowed(origin string) bool {
|
||||
func (h adminHandler) originAllowed(origin *url.URL) bool {
|
||||
for _, allowedOrigin := range h.allowedOrigins {
|
||||
originCopy := origin
|
||||
if !strings.Contains(allowedOrigin, "://") {
|
||||
// no scheme specified, so allow both
|
||||
originCopy = strings.TrimPrefix(originCopy, "http://")
|
||||
originCopy = strings.TrimPrefix(originCopy, "https://")
|
||||
if allowedOrigin.Scheme != "" && origin.Scheme != allowedOrigin.Scheme {
|
||||
continue
|
||||
}
|
||||
if originCopy == allowedOrigin {
|
||||
if origin.Host == allowedOrigin.Host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -857,7 +938,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, errSameConfig) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -876,10 +957,16 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
parts := strings.Split(idPath, "/")
|
||||
if len(parts) < 3 || parts[2] == "" {
|
||||
return fmt.Errorf("request path is missing object ID")
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("request path is missing object ID"),
|
||||
}
|
||||
}
|
||||
if parts[0] != "" || parts[1] != "id" {
|
||||
return fmt.Errorf("malformed object path")
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("malformed object path"),
|
||||
}
|
||||
}
|
||||
id := parts[2]
|
||||
|
||||
@@ -888,14 +975,17 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||
expanded, ok := rawCfgIndex[id]
|
||||
defer currentCfgMu.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown object ID '%s'", id)
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusNotFound,
|
||||
Err: fmt.Errorf("unknown object ID '%s'", id),
|
||||
}
|
||||
}
|
||||
|
||||
// piece the full URL path back together
|
||||
parts = append([]string{expanded}, parts[3:]...)
|
||||
r.URL.Path = path.Join(parts...)
|
||||
|
||||
return nil
|
||||
return errInternalRedir
|
||||
}
|
||||
|
||||
func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||
@@ -905,7 +995,12 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||
Err: fmt.Errorf("method not allowed"),
|
||||
}
|
||||
}
|
||||
exitProcess(Log().Named("admin.api"))
|
||||
|
||||
if err := notify.NotifyStopping(); err != nil {
|
||||
Log().Error("unable to notify stopping to service manager", zap.Error(err))
|
||||
}
|
||||
|
||||
exitProcess(context.Background(), Log().Named("admin.api"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1155,6 +1250,18 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
|
||||
return x509.ParseCertificate(derBytes)
|
||||
}
|
||||
|
||||
type loggableURLArray []*url.URL
|
||||
|
||||
func (ua loggableURLArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
|
||||
if ua == nil {
|
||||
return nil
|
||||
}
|
||||
for _, u := range ua {
|
||||
enc.AppendString(u.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultAdminListen is the address for the local admin
|
||||
// listener, if none is specified at startup.
|
||||
@@ -1164,19 +1271,13 @@ var (
|
||||
// (TLS-authenticated) admin listener, if enabled and not
|
||||
// specified otherwise.
|
||||
DefaultRemoteAdminListen = ":2021"
|
||||
|
||||
// DefaultAdminConfig is the default configuration
|
||||
// for the local administration endpoint.
|
||||
DefaultAdminConfig = &AdminConfig{
|
||||
Listen: DefaultAdminListen,
|
||||
}
|
||||
)
|
||||
|
||||
// PIDFile writes a pidfile to the file at filename. It
|
||||
// will get deleted before the process gracefully exits.
|
||||
func PIDFile(filename string) error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
err := ioutil.WriteFile(filename, pid, 0600)
|
||||
err := os.WriteFile(filename, pid, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1193,6 +1294,13 @@ var idRegexp = regexp.MustCompile(`(?m),?\s*"` + idKey + `"\s*:\s*(-?[0-9]+(\.[0
|
||||
// pidfile is the name of the pidfile, if any.
|
||||
var pidfile string
|
||||
|
||||
// errInternalRedir indicates an internal redirect
|
||||
// and is useful when admin API handlers rewrite
|
||||
// the request; in that case, authentication and
|
||||
// authorization needs to happen again for the
|
||||
// rewritten request.
|
||||
var errInternalRedir = fmt.Errorf("internal redirect; re-authorization required")
|
||||
|
||||
const (
|
||||
rawConfigKey = "config"
|
||||
idKey = "@id"
|
||||
@@ -1206,6 +1314,7 @@ var bufPool = sync.Pool{
|
||||
|
||||
// keep a reference to admin endpoint singletons while they're active
|
||||
var (
|
||||
serverMu sync.Mutex
|
||||
localAdminServer, remoteAdminServer *http.Server
|
||||
identityCertCache *certmagic.Cache
|
||||
)
|
||||
|
||||
+36
-18
@@ -17,9 +17,28 @@ package caddy
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testCfg = []byte(`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"myserver": {
|
||||
"listen": ["tcp/localhost:8080-8084"],
|
||||
"read_timeout": "30s"
|
||||
},
|
||||
"yourserver": {
|
||||
"listen": ["127.0.0.1:5000"],
|
||||
"read_header_timeout": "15s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||
// each test is performed in sequence, so
|
||||
// each change builds on the previous ones;
|
||||
@@ -108,25 +127,24 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadConcurrent exercises Load under concurrent conditions
|
||||
// and is most useful under test with `-race` enabled.
|
||||
func TestLoadConcurrent(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
_ = Load(testCfg, true)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
cfg := []byte(`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"myserver": {
|
||||
"listen": ["tcp/localhost:8080-8084"],
|
||||
"read_timeout": "30s"
|
||||
},
|
||||
"yourserver": {
|
||||
"listen": ["127.0.0.1:5000"],
|
||||
"read_header_timeout": "15s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
Load(cfg, true)
|
||||
Load(testCfg, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/notify"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@@ -100,14 +101,29 @@ func Run(cfg *Config) error {
|
||||
// if it is different from the current config or
|
||||
// forceReload is true.
|
||||
func Load(cfgJSON []byte, forceReload bool) error {
|
||||
return changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
||||
if err := notify.NotifyReloading(); err != nil {
|
||||
Log().Error("unable to notify reloading to service manager", zap.Error(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := notify.NotifyReadiness(); err != nil {
|
||||
Log().Error("unable to notify readiness to service manager", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
||||
if errors.Is(err, errSameConfig) {
|
||||
err = nil // not really an error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// changeConfig changes the current config (rawCfg) according to the
|
||||
// method, traversed via the given path, and uses the given input as
|
||||
// the new value (if applicable; i.e. "DELETE" doesn't have an input).
|
||||
// If the resulting config is the same as the previous, no reload will
|
||||
// occur unless forceReload is true. This function is safe for
|
||||
// occur unless forceReload is true. If the config is unchanged and not
|
||||
// forcefully reloaded, then errConfigUnchanged This function is safe for
|
||||
// concurrent use.
|
||||
func changeConfig(method, path string, input []byte, forceReload bool) error {
|
||||
switch method {
|
||||
@@ -138,8 +154,8 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
||||
|
||||
// if nothing changed, no need to do a whole reload unless the client forces it
|
||||
if !forceReload && bytes.Equal(rawCfgJSON, newCfg) {
|
||||
Log().Named("admin.api").Info("config is unchanged")
|
||||
return nil
|
||||
Log().Info("config is unchanged")
|
||||
return errSameConfig
|
||||
}
|
||||
|
||||
// find any IDs in this config and index them
|
||||
@@ -257,8 +273,9 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||
newCfg != nil &&
|
||||
newCfg.Admin != nil &&
|
||||
newCfg.Admin.Config != nil &&
|
||||
newCfg.Admin.Config.LoadRaw != nil {
|
||||
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs")
|
||||
newCfg.Admin.Config.LoadRaw != nil &&
|
||||
newCfg.Admin.Config.LoadDelay <= 0 {
|
||||
return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_delay")
|
||||
}
|
||||
|
||||
// run the new config and start all its apps
|
||||
@@ -288,7 +305,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||
zap.String("dir", dir),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
||||
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
||||
if err == nil {
|
||||
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
||||
} else {
|
||||
@@ -416,6 +433,13 @@ func run(newCfg *Config, start bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Provision any admin routers which may need to access
|
||||
// some of the other apps at runtime
|
||||
err = newCfg.Admin.provisionAdminRouters(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start
|
||||
err = func() error {
|
||||
var started []string
|
||||
@@ -469,30 +493,74 @@ func finishSettingUp(ctx Context, cfg *Config) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading config loader module: %s", err)
|
||||
}
|
||||
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
|
||||
|
||||
logger := Log().Named("config_loader").With(
|
||||
zap.String("module", val.(Module).CaddyModule().ID.Name()),
|
||||
zap.Int("load_delay", int(cfg.Admin.Config.LoadDelay)))
|
||||
|
||||
runLoadedConfig := func(config []byte) error {
|
||||
logger.Info("applying dynamically-loaded config")
|
||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false)
|
||||
if errors.Is(err, errSameConfig) {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("failed to run dynamically-loaded config", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
logger.Info("successfully applied dynamically-loaded config")
|
||||
return nil
|
||||
}
|
||||
|
||||
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
|
||||
go func() {
|
||||
Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()))
|
||||
currentCfgMu.Lock()
|
||||
err := unsyncedDecodeAndRun(loadedConfig, false)
|
||||
currentCfgMu.Unlock()
|
||||
if err == nil {
|
||||
Log().Info("dynamically-loaded config applied successfully")
|
||||
} else {
|
||||
Log().Error("running dynamically-loaded config failed", zap.Error(err))
|
||||
if cfg.Admin.Config.LoadDelay > 0 {
|
||||
go func() {
|
||||
// the loop is here to iterate ONLY if there is an error, a no-op config load,
|
||||
// or an unchanged config; in which case we simply wait the delay and try again
|
||||
for {
|
||||
timer := time.NewTimer(time.Duration(cfg.Admin.Config.LoadDelay))
|
||||
select {
|
||||
case <-timer.C:
|
||||
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||
if err != nil {
|
||||
logger.Error("failed loading dynamic config; will retry", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if loadedConfig == nil {
|
||||
logger.Info("dynamically-loaded config was nil; will retry")
|
||||
continue
|
||||
}
|
||||
err = runLoadedConfig(loadedConfig)
|
||||
if errors.Is(err, errSameConfig) {
|
||||
logger.Info("dynamically-loaded config was unchanged; will retry")
|
||||
continue
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
logger.Info("stopping dynamic config loading")
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
// if no LoadDelay is provided, will load config synchronously
|
||||
loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading dynamic config from %T: %v", val, err)
|
||||
}
|
||||
}()
|
||||
// do this in a goroutine so current config can finish being loaded; otherwise deadlock
|
||||
go func() { _ = runLoadedConfig(loadedConfig) }()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigLoader is a type that can load a Caddy config. The
|
||||
// returned config must be valid Caddy JSON.
|
||||
// ConfigLoader is a type that can load a Caddy config. If
|
||||
// the return value is non-nil, it must be valid Caddy JSON;
|
||||
// if nil or with non-nil error, it is considered to be a
|
||||
// no-op load and may be retried later.
|
||||
type ConfigLoader interface {
|
||||
LoadConfig(Context) ([]byte, error)
|
||||
}
|
||||
@@ -553,7 +621,7 @@ func Validate(cfg *Config) error {
|
||||
// PID file, and shuts down admin endpoint(s) in a goroutine.
|
||||
// Errors are logged along the way, and an appropriate exit
|
||||
// code is emitted.
|
||||
func exitProcess(logger *zap.Logger) {
|
||||
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||
if logger == nil {
|
||||
logger = Log()
|
||||
}
|
||||
@@ -568,7 +636,7 @@ func exitProcess(logger *zap.Logger) {
|
||||
}
|
||||
|
||||
// clean up certmagic locks
|
||||
certmagic.CleanUpOwnLocks(logger)
|
||||
certmagic.CleanUpOwnLocks(ctx, logger)
|
||||
|
||||
// remove pidfile
|
||||
if pidfile != "" {
|
||||
@@ -669,13 +737,13 @@ func ParseDuration(s string) (time.Duration, error) {
|
||||
// have its own unique ID.
|
||||
func InstanceID() (uuid.UUID, error) {
|
||||
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
|
||||
uuidFileBytes, err := ioutil.ReadFile(uuidFilePath)
|
||||
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
err = ioutil.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
|
||||
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
|
||||
return uuid, err
|
||||
} else if err != nil {
|
||||
return [16]byte{}, err
|
||||
@@ -744,5 +812,10 @@ var (
|
||||
rawCfgIndex map[string]string
|
||||
)
|
||||
|
||||
// errSameConfig is returned if the new config is the same
|
||||
// as the old one. This isn't usually an actual, actionable
|
||||
// error; it's mostly a sentinel value.
|
||||
var errSameConfig = errors.New("config is unchanged")
|
||||
|
||||
// ImportPath is the package import path for Caddy core.
|
||||
const ImportPath = "github.com/caddyserver/caddy/v2"
|
||||
|
||||
@@ -67,14 +67,17 @@ func (a Adapter) Adapt(body []byte, options map[string]interface{}) ([]byte, []c
|
||||
// is any different from the input; empty warning and false otherwise.
|
||||
// TODO: also perform this check on imported files
|
||||
func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
|
||||
formatted := Format(body)
|
||||
if bytes.Equal(formatted, body) {
|
||||
// replace windows-style newlines to normalize comparison
|
||||
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
|
||||
|
||||
formatted := Format(normalizedBody)
|
||||
if bytes.Equal(formatted, normalizedBody) {
|
||||
return caddyconfig.Warning{}, false
|
||||
}
|
||||
|
||||
// find where the difference is
|
||||
line := 1
|
||||
for i, ch := range body {
|
||||
for i, ch := range normalizedBody {
|
||||
if i >= len(formatted) || ch != formatted[i] {
|
||||
break
|
||||
}
|
||||
@@ -85,7 +88,7 @@ func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bo
|
||||
return caddyconfig.Warning{
|
||||
File: filename,
|
||||
Line: line,
|
||||
Message: "input is not formatted with 'caddy fmt'",
|
||||
Message: "Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies",
|
||||
}, true
|
||||
}
|
||||
|
||||
|
||||
Executable → Regular
+126
-5
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -201,6 +202,43 @@ func (d *Dispenser) Val() string {
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// ValRaw gets the raw text of the current token (including quotes).
|
||||
// If there is no token loaded, it returns empty string.
|
||||
func (d *Dispenser) ValRaw() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
quote := d.tokens[d.cursor].wasQuoted
|
||||
if quote > 0 {
|
||||
return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal
|
||||
}
|
||||
return d.tokens[d.cursor].Text
|
||||
}
|
||||
|
||||
// ScalarVal gets value of the current token, converted to the closest
|
||||
// scalar type. If there is no token loaded, it returns nil.
|
||||
func (d *Dispenser) ScalarVal() interface{} {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return nil
|
||||
}
|
||||
quote := d.tokens[d.cursor].wasQuoted
|
||||
text := d.tokens[d.cursor].Text
|
||||
|
||||
if quote > 0 {
|
||||
return text // string literal
|
||||
}
|
||||
if num, err := strconv.Atoi(text); err == nil {
|
||||
return num
|
||||
}
|
||||
if num, err := strconv.ParseFloat(text, 64); err == nil {
|
||||
return num
|
||||
}
|
||||
if bool, err := strconv.ParseBool(text); err == nil {
|
||||
return bool
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token.
|
||||
// If there is no token loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
@@ -249,6 +287,19 @@ func (d *Dispenser) AllArgs(targets ...*string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CountRemainingArgs counts the amount of remaining arguments
|
||||
// (tokens on the same line) without consuming the tokens.
|
||||
func (d *Dispenser) CountRemainingArgs() int {
|
||||
count := 0
|
||||
for d.NextArg() {
|
||||
count++
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
d.Prev()
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
@@ -261,6 +312,18 @@ func (d *Dispenser) RemainingArgs() []string {
|
||||
return args
|
||||
}
|
||||
|
||||
// RemainingArgsRaw loads any more arguments (tokens on the same line,
|
||||
// retaining quotes) into a slice and returns them. Open curly brace
|
||||
// tokens also indicate the end of arguments, and the curly brace is
|
||||
// not included in the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgsRaw() []string {
|
||||
var args []string
|
||||
for d.NextArg() {
|
||||
args = append(args, d.ValRaw())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// NewFromNextSegment returns a new dispenser with a copy of
|
||||
// the tokens from the current token until the end of the
|
||||
// "directive" whether that be to the end of the line or
|
||||
@@ -345,13 +408,17 @@ func (d *Dispenser) EOFErr() error {
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
return d.Errf(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
return d.WrapErr(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// WrapErr takes an existing error and adds the Caddyfile file and line number.
|
||||
func (d *Dispenser) WrapErr(err error) error {
|
||||
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
|
||||
}
|
||||
|
||||
// Delete deletes the current token and returns the updated slice
|
||||
@@ -391,6 +458,60 @@ func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
|
||||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
|
||||
|
||||
prev := d.tokens[d.cursor-1]
|
||||
curr := d.tokens[d.cursor]
|
||||
|
||||
// If the previous token is from a different file,
|
||||
// we can assume it's from a different line
|
||||
if prev.File != curr.File {
|
||||
return true
|
||||
}
|
||||
|
||||
// The previous token may contain line breaks if
|
||||
// it was quoted and spanned multiple lines. e.g:
|
||||
//
|
||||
// dir "foo
|
||||
// bar
|
||||
// baz"
|
||||
prevLineBreaks := d.numLineBreaks(d.cursor - 1)
|
||||
|
||||
// If the previous token (incl line breaks) ends
|
||||
// on a line earlier than the current token,
|
||||
// then the current token is on a new line
|
||||
return prev.Line+prevLineBreaks < curr.Line
|
||||
}
|
||||
|
||||
// isNextOnNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the next token. It handles imported
|
||||
// tokens correctly. If there isn't a next token, it returns true.
|
||||
func (d *Dispenser) isNextOnNewLine() bool {
|
||||
if d.cursor < 0 {
|
||||
return false
|
||||
}
|
||||
if d.cursor >= len(d.tokens)-1 {
|
||||
return true
|
||||
}
|
||||
|
||||
curr := d.tokens[d.cursor]
|
||||
next := d.tokens[d.cursor+1]
|
||||
|
||||
// If the next token is from a different file,
|
||||
// we can assume it's from a different line
|
||||
if curr.File != next.File {
|
||||
return true
|
||||
}
|
||||
|
||||
// The current token may contain line breaks if
|
||||
// it was quoted and spanned multiple lines. e.g:
|
||||
//
|
||||
// dir "foo
|
||||
// bar
|
||||
// baz"
|
||||
currLineBreaks := d.numLineBreaks(d.cursor)
|
||||
|
||||
// If the current token (incl line breaks) ends
|
||||
// on a line earlier than the next token,
|
||||
// then the next token is on a new line
|
||||
return curr.Line+currLineBreaks < next.Line
|
||||
}
|
||||
|
||||
Executable → Regular
+7
@@ -15,6 +15,7 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -303,4 +304,10 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
if !strings.Contains(err.Error(), "foobar") {
|
||||
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
||||
}
|
||||
|
||||
var ErrBarIsFull = errors.New("bar is full")
|
||||
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
|
||||
if !errors.Is(bookingError, ErrBarIsFull) {
|
||||
t.Errorf("Errf(): should be able to unwrap the error chain")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ func Format(input []byte) []byte {
|
||||
if comment {
|
||||
if ch == '\n' {
|
||||
comment = false
|
||||
space = true
|
||||
nextLine()
|
||||
continue
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package caddyfile
|
||||
|
||||
import "bytes"
|
||||
|
||||
func FuzzFormat(input []byte) int {
|
||||
formatted := Format(input)
|
||||
if bytes.Equal(formatted, Format(formatted)) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -179,6 +179,11 @@ d {
|
||||
{$F}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "env var placeholders with port",
|
||||
input: `:{$PORT}`,
|
||||
expect: `:{$PORT}`,
|
||||
},
|
||||
{
|
||||
description: "comments",
|
||||
input: `#a "\n"
|
||||
@@ -321,6 +326,44 @@ baz`,
|
||||
foo
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: "matthewpi/vscode-caddyfile-support#13",
|
||||
input: `{
|
||||
email {$ACMEEMAIL}
|
||||
#debug
|
||||
}
|
||||
|
||||
block {
|
||||
}
|
||||
`,
|
||||
expect: `{
|
||||
email {$ACMEEMAIL}
|
||||
#debug
|
||||
}
|
||||
|
||||
block {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "matthewpi/vscode-caddyfile-support#13 - bad formatting",
|
||||
input: `{
|
||||
email {$ACMEEMAIL}
|
||||
#debug
|
||||
}
|
||||
|
||||
block {
|
||||
}
|
||||
`,
|
||||
expect: `{
|
||||
email {$ACMEEMAIL}
|
||||
#debug
|
||||
}
|
||||
|
||||
block {
|
||||
}
|
||||
`,
|
||||
},
|
||||
} {
|
||||
// the formatter should output a trailing newline,
|
||||
// even if the tests aren't written to expect that
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type adjacency map[string][]string
|
||||
|
||||
type importGraph struct {
|
||||
nodes map[string]bool
|
||||
edges adjacency
|
||||
}
|
||||
|
||||
func (i *importGraph) addNode(name string) {
|
||||
if i.nodes == nil {
|
||||
i.nodes = make(map[string]bool)
|
||||
}
|
||||
if _, exists := i.nodes[name]; exists {
|
||||
return
|
||||
}
|
||||
i.nodes[name] = true
|
||||
}
|
||||
func (i *importGraph) addNodes(names []string) {
|
||||
for _, name := range names {
|
||||
i.addNode(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *importGraph) removeNode(name string) {
|
||||
delete(i.nodes, name)
|
||||
}
|
||||
func (i *importGraph) removeNodes(names []string) {
|
||||
for _, name := range names {
|
||||
i.removeNode(name)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *importGraph) addEdge(from, to string) error {
|
||||
if !i.exists(from) || !i.exists(to) {
|
||||
return fmt.Errorf("one of the nodes does not exist")
|
||||
}
|
||||
|
||||
if i.willCycle(to, from) {
|
||||
return fmt.Errorf("a cycle of imports exists between %s and %s", from, to)
|
||||
}
|
||||
|
||||
if i.areConnected(from, to) {
|
||||
// if connected, there's nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
if i.nodes == nil {
|
||||
i.nodes = make(map[string]bool)
|
||||
}
|
||||
if i.edges == nil {
|
||||
i.edges = make(adjacency)
|
||||
}
|
||||
|
||||
i.edges[from] = append(i.edges[from], to)
|
||||
return nil
|
||||
}
|
||||
func (i *importGraph) addEdges(from string, tos []string) error {
|
||||
for _, to := range tos {
|
||||
err := i.addEdge(from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importGraph) areConnected(from, to string) bool {
|
||||
al, ok := i.edges[from]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, v := range al {
|
||||
if v == to {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *importGraph) willCycle(from, to string) bool {
|
||||
collector := make(map[string]bool)
|
||||
|
||||
var visit func(string)
|
||||
visit = func(start string) {
|
||||
if !collector[start] {
|
||||
collector[start] = true
|
||||
for _, v := range i.edges[start] {
|
||||
visit(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range i.edges[from] {
|
||||
visit(v)
|
||||
}
|
||||
for k := range collector {
|
||||
if to == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (i *importGraph) exists(key string) bool {
|
||||
_, exists := i.nodes[key]
|
||||
return exists
|
||||
}
|
||||
Executable → Regular
+13
-9
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -35,9 +35,12 @@ type (
|
||||
|
||||
// Token represents a single parsable unit.
|
||||
Token struct {
|
||||
File string
|
||||
Line int
|
||||
Text string
|
||||
File string
|
||||
Line int
|
||||
Text string
|
||||
wasQuoted rune // enclosing quote character, if any
|
||||
inSnippet bool
|
||||
snippetName string
|
||||
}
|
||||
)
|
||||
|
||||
@@ -76,8 +79,9 @@ func (l *lexer) next() bool {
|
||||
var val []rune
|
||||
var comment, quoted, btQuoted, escaped bool
|
||||
|
||||
makeToken := func() bool {
|
||||
makeToken := func(quoted rune) bool {
|
||||
l.token.Text = string(val)
|
||||
l.token.wasQuoted = quoted
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -85,7 +89,7 @@ func (l *lexer) next() bool {
|
||||
ch, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if len(val) > 0 {
|
||||
return makeToken()
|
||||
return makeToken(0)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return false
|
||||
@@ -108,10 +112,10 @@ func (l *lexer) next() bool {
|
||||
escaped = false
|
||||
} else {
|
||||
if quoted && ch == '"' {
|
||||
return makeToken()
|
||||
return makeToken('"')
|
||||
}
|
||||
if btQuoted && ch == '`' {
|
||||
return makeToken()
|
||||
return makeToken('`')
|
||||
}
|
||||
}
|
||||
if ch == '\n' {
|
||||
@@ -137,7 +141,7 @@ func (l *lexer) next() bool {
|
||||
comment = false
|
||||
}
|
||||
if len(val) > 0 {
|
||||
return makeToken()
|
||||
return makeToken(0)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package caddyfile
|
||||
|
||||
func FuzzTokenize(input []byte) int {
|
||||
tokens, err := Tokenize(input, "Caddyfile")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
Executable → Regular
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
Executable → Regular
+92
-26
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,14 +16,15 @@ package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
@@ -36,15 +37,43 @@ import (
|
||||
// Environment variables in {$ENVIRONMENT_VARIABLE} notation
|
||||
// will be replaced before parsing begins.
|
||||
func Parse(filename string, input []byte) ([]ServerBlock, error) {
|
||||
tokens, err := allTokens(filename, input)
|
||||
// unfortunately, we must copy the input because parsing must
|
||||
// remain a read-only operation, but we have to expand environment
|
||||
// variables before we parse, which changes the underlying array (#4422)
|
||||
inputCopy := make([]byte, len(input))
|
||||
copy(inputCopy, input)
|
||||
|
||||
tokens, err := allTokens(filename, inputCopy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := parser{Dispenser: NewDispenser(tokens)}
|
||||
p := parser{
|
||||
Dispenser: NewDispenser(tokens),
|
||||
importGraph: importGraph{
|
||||
nodes: make(map[string]bool),
|
||||
edges: make(adjacency),
|
||||
},
|
||||
}
|
||||
return p.parseAll()
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order. It may mutate input as it expands env vars.
|
||||
func allTokens(filename string, input []byte) ([]Token, error) {
|
||||
inputCopy, err := replaceEnvVars(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens, err := Tokenize(inputCopy, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// replaceEnvVars replaces all occurrences of environment variables.
|
||||
// It mutates the underlying array and returns the updated slice.
|
||||
func replaceEnvVars(input []byte) ([]byte, error) {
|
||||
var offset int
|
||||
for {
|
||||
@@ -89,27 +118,13 @@ func replaceEnvVars(input []byte) ([]byte, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(filename string, input []byte) ([]Token, error) {
|
||||
input, err := replaceEnvVars(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens, err := Tokenize(input, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
*Dispenser
|
||||
block ServerBlock // current server block being parsed
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
definedSnippets map[string][]Token
|
||||
nesting int
|
||||
importGraph importGraph
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||
@@ -165,6 +180,15 @@ func (p *parser) begin() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Just as we need to track which file the token comes from, we need to
|
||||
// keep track of which snippets do the tokens come from. This is helpful
|
||||
// in tracking import cycles across files/snippets by namespacing them. Without
|
||||
// this we end up with false-positives in cycle-detection.
|
||||
for k, v := range tokens {
|
||||
v.inSnippet = true
|
||||
v.snippetName = name
|
||||
tokens[k] = v
|
||||
}
|
||||
p.definedSnippets[name] = tokens
|
||||
// empty block keys so we don't save this block as a real server.
|
||||
p.block.Keys = nil
|
||||
@@ -194,9 +218,20 @@ func (p *parser) addresses() error {
|
||||
if expectingAnother {
|
||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
||||
}
|
||||
// Mark this server block as being defined with braces.
|
||||
// This is used to provide a better error message when
|
||||
// the user may have tried to define two server blocks
|
||||
// without having used braces, which are required in
|
||||
// that case.
|
||||
p.block.HasBraces = true
|
||||
break
|
||||
}
|
||||
|
||||
// Users commonly forget to place a space between the address and the '{'
|
||||
if strings.HasSuffix(tkn, "{") {
|
||||
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn)
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed ""
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
@@ -207,6 +242,13 @@ func (p *parser) addresses() error {
|
||||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
// If there's a comma here, it's probably because they didn't use a space
|
||||
// between their two domains, e.g. "foo.com,bar.com", which would not be
|
||||
// parsed as two separate site addresses.
|
||||
if strings.Contains(tkn, ",") {
|
||||
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn)
|
||||
}
|
||||
|
||||
p.block.Keys = append(p.block.Keys, tkn)
|
||||
}
|
||||
|
||||
@@ -304,7 +346,7 @@ func (p *parser) doImport() error {
|
||||
args := p.RemainingArgs()
|
||||
|
||||
// add args to the replacer
|
||||
repl := caddy.NewReplacer()
|
||||
repl := caddy.NewEmptyReplacer()
|
||||
for index, arg := range args {
|
||||
repl.Set("args."+strconv.Itoa(index), arg)
|
||||
}
|
||||
@@ -314,10 +356,15 @@ func (p *parser) doImport() error {
|
||||
tokensBefore := p.tokens[:p.cursor-1-len(args)]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
var importedTokens []Token
|
||||
var nodes []string
|
||||
|
||||
// first check snippets. That is a simple, non-recursive replacement
|
||||
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
|
||||
importedTokens = p.definedSnippets[importPattern]
|
||||
if len(importedTokens) > 0 {
|
||||
// just grab the first one
|
||||
nodes = append(nodes, fmt.Sprintf("%s:%s", importedTokens[0].File, importedTokens[0].snippetName))
|
||||
}
|
||||
} else {
|
||||
// make path relative to the file of the _token_ being processed rather
|
||||
// than current working directory (issue #867) and then use glob to get
|
||||
@@ -346,14 +393,13 @@ func (p *parser) doImport() error {
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
if strings.ContainsAny(globPattern, "*?[]") {
|
||||
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
|
||||
caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern))
|
||||
} else {
|
||||
return p.Errf("File to import not found: %s", importPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// collect all the imported tokens
|
||||
|
||||
for _, importFile := range matches {
|
||||
newTokens, err := p.doSingleImport(importFile)
|
||||
if err != nil {
|
||||
@@ -361,6 +407,18 @@ func (p *parser) doImport() error {
|
||||
}
|
||||
importedTokens = append(importedTokens, newTokens...)
|
||||
}
|
||||
nodes = matches
|
||||
}
|
||||
|
||||
nodeName := p.File()
|
||||
if p.Token().inSnippet {
|
||||
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
||||
}
|
||||
p.importGraph.addNode(nodeName)
|
||||
p.importGraph.addNodes(nodes)
|
||||
if err := p.importGraph.addEdges(nodeName, nodes); err != nil {
|
||||
p.importGraph.removeNodes(nodes)
|
||||
return err
|
||||
}
|
||||
|
||||
// copy the tokens so we don't overwrite p.definedSnippets
|
||||
@@ -396,7 +454,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
return nil, p.Errf("Could not import %s: is a directory", importFile)
|
||||
}
|
||||
|
||||
input, err := ioutil.ReadAll(file)
|
||||
input, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
|
||||
}
|
||||
@@ -436,6 +494,13 @@ func (p *parser) directive() error {
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
p.nesting++
|
||||
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||
return p.Err("Unexpected next token after '{' on same line")
|
||||
}
|
||||
} else if p.Val() == "{}" {
|
||||
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||
return p.Err("Unexpected '{}' at end of line")
|
||||
}
|
||||
} else if p.isNewLine() && p.nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
@@ -526,8 +591,9 @@ func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// head of the server block with tokens, which are
|
||||
// grouped by segments.
|
||||
type ServerBlock struct {
|
||||
Keys []string
|
||||
Segments []Segment
|
||||
HasBraces bool
|
||||
Keys []string
|
||||
Segments []Segment
|
||||
}
|
||||
|
||||
// DispenseDirective returns a dispenser that contains
|
||||
|
||||
Executable → Regular
+46
-7
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 Light Code Labs, LLC
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,6 @@ package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -160,6 +159,10 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
"localhost",
|
||||
}, []int{}},
|
||||
|
||||
{`localhost{
|
||||
dir1
|
||||
}`, true, []string{}, []int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
@@ -188,6 +191,20 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
|
||||
{``, false, []string{}, []int{}},
|
||||
|
||||
// Unexpected next token after '{' on same line
|
||||
{`localhost
|
||||
dir1 { a b }`, true, []string{"localhost"}, []int{}},
|
||||
// Workaround with quotes
|
||||
{`localhost
|
||||
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
|
||||
|
||||
// Unexpected '{}' at end of line
|
||||
{`localhost
|
||||
dir1 {}`, true, []string{"localhost"}, []int{}},
|
||||
// Workaround with quotes
|
||||
{`localhost
|
||||
dir1 "{}"`, false, []string{"localhost"}, []int{2}},
|
||||
|
||||
// import with args
|
||||
{`import testdata/import_args0.txt a`, false, []string{"a"}, []int{}},
|
||||
{`import testdata/import_args1.txt a b`, false, []string{"a", "b"}, []int{}},
|
||||
@@ -276,7 +293,7 @@ func TestRecursiveImport(t *testing.T) {
|
||||
}
|
||||
|
||||
// test relative recursive import
|
||||
err = ioutil.WriteFile(recursiveFile1, []byte(
|
||||
err = os.WriteFile(recursiveFile1, []byte(
|
||||
`localhost
|
||||
dir1
|
||||
import recursive_import_test2`), 0644)
|
||||
@@ -285,7 +302,7 @@ func TestRecursiveImport(t *testing.T) {
|
||||
}
|
||||
defer os.Remove(recursiveFile1)
|
||||
|
||||
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
|
||||
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -310,7 +327,7 @@ func TestRecursiveImport(t *testing.T) {
|
||||
}
|
||||
|
||||
// test absolute recursive import
|
||||
err = ioutil.WriteFile(recursiveFile1, []byte(
|
||||
err = os.WriteFile(recursiveFile1, []byte(
|
||||
`localhost
|
||||
dir1
|
||||
import `+recursiveFile2), 0644)
|
||||
@@ -366,7 +383,7 @@ func TestDirectiveImport(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
|
||||
err = os.WriteFile(directiveFile, []byte(`prop1 1
|
||||
prop2 2`), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -444,6 +461,28 @@ func TestParseAll(t *testing.T) {
|
||||
|
||||
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
|
||||
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
|
||||
|
||||
// recursive self-import
|
||||
{`import testdata/import_recursive0.txt`, true, [][]string{}},
|
||||
{`import testdata/import_recursive3.txt
|
||||
import testdata/import_recursive1.txt`, true, [][]string{}},
|
||||
|
||||
// cyclic imports
|
||||
{`(A) {
|
||||
import A
|
||||
}
|
||||
:80
|
||||
import A
|
||||
`, true, [][]string{}},
|
||||
{`(A) {
|
||||
import B
|
||||
}
|
||||
(B) {
|
||||
import A
|
||||
}
|
||||
:80
|
||||
import A
|
||||
`, true, [][]string{}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
@@ -607,7 +646,7 @@ func TestSnippets(t *testing.T) {
|
||||
}
|
||||
|
||||
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
|
||||
file, err := ioutil.TempFile("", t.Name())
|
||||
file, err := os.CreateTemp("", t.Name())
|
||||
if err != nil {
|
||||
panic(err) // get a stack trace so we know where this was called from.
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import import_recursive0.txt
|
||||
@@ -0,0 +1 @@
|
||||
import import_recursive2.txt
|
||||
@@ -0,0 +1 @@
|
||||
import import_recursive3.txt
|
||||
@@ -0,0 +1 @@
|
||||
import import_recursive1.txt
|
||||
@@ -213,7 +213,11 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
||||
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
|
||||
}
|
||||
if len(lnHosts) == 0 {
|
||||
lnHosts = []string{""}
|
||||
if defaultBind, ok := options["default_bind"].(string); ok {
|
||||
lnHosts = []string{defaultBind}
|
||||
} else {
|
||||
lnHosts = []string{""}
|
||||
}
|
||||
}
|
||||
|
||||
// use a map to prevent duplication
|
||||
@@ -223,7 +227,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
||||
if err == nil && addr.IsUnixNetwork() {
|
||||
listeners[host] = struct{}{}
|
||||
} else {
|
||||
listeners[net.JoinHostPort(host, lnPort)] = struct{}{}
|
||||
listeners[host+":"+lnPort] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +341,9 @@ func (a Address) Normalize() Address {
|
||||
// ensure host is normalized if it's an IP address
|
||||
host := strings.TrimSpace(a.Host)
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
host = ip.String()
|
||||
if ipv6 := ip.To16(); ipv6 != nil && ipv6.DefaultMask() == nil {
|
||||
host = ipv6.String()
|
||||
}
|
||||
}
|
||||
|
||||
return Address{
|
||||
@@ -349,28 +355,6 @@ func (a Address) Normalize() Address {
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns a string form of a, much like String() does, but this
|
||||
// method doesn't add anything default that wasn't in the original.
|
||||
func (a Address) Key() string {
|
||||
res := ""
|
||||
if a.Scheme != "" {
|
||||
res += a.Scheme + "://"
|
||||
}
|
||||
if a.Host != "" {
|
||||
res += a.Host
|
||||
}
|
||||
// insert port only if the original has its own explicit port
|
||||
if a.Port != "" &&
|
||||
len(a.Original) >= len(res) &&
|
||||
strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
||||
res += ":" + a.Port
|
||||
}
|
||||
if a.Path != "" {
|
||||
res += a.Path
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// lowerExceptPlaceholders lowercases s except within
|
||||
// placeholders (substrings in non-escaped '{ }' spans).
|
||||
// See https://github.com/caddyserver/caddy/issues/3264
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package httpcaddyfile
|
||||
|
||||
@@ -106,67 +106,128 @@ func TestAddressString(t *testing.T) {
|
||||
func TestKeyNormalization(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expect string
|
||||
expect Address
|
||||
}{
|
||||
{
|
||||
input: "example.com",
|
||||
expect: "example.com",
|
||||
input: "example.com",
|
||||
expect: Address{
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "http://host:1234/path",
|
||||
expect: "http://host:1234/path",
|
||||
input: "http://host:1234/path",
|
||||
expect: Address{
|
||||
Scheme: "http",
|
||||
Host: "host",
|
||||
Port: "1234",
|
||||
Path: "/path",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "HTTP://A/ABCDEF",
|
||||
expect: "http://a/ABCDEF",
|
||||
input: "HTTP://A/ABCDEF",
|
||||
expect: Address{
|
||||
Scheme: "http",
|
||||
Host: "a",
|
||||
Path: "/ABCDEF",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "A/ABCDEF",
|
||||
expect: "a/ABCDEF",
|
||||
input: "A/ABCDEF",
|
||||
expect: Address{
|
||||
Host: "a",
|
||||
Path: "/ABCDEF",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "A:2015/Path",
|
||||
expect: "a:2015/Path",
|
||||
input: "A:2015/Path",
|
||||
expect: Address{
|
||||
Host: "a",
|
||||
Port: "2015",
|
||||
Path: "/Path",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "sub.{env.MY_DOMAIN}",
|
||||
expect: "sub.{env.MY_DOMAIN}",
|
||||
input: "sub.{env.MY_DOMAIN}",
|
||||
expect: Address{
|
||||
Host: "sub.{env.MY_DOMAIN}",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "sub.ExAmPle",
|
||||
expect: "sub.example",
|
||||
input: "sub.ExAmPle",
|
||||
expect: Address{
|
||||
Host: "sub.example",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "sub.\\{env.MY_DOMAIN\\}",
|
||||
expect: "sub.\\{env.my_domain\\}",
|
||||
input: "sub.\\{env.MY_DOMAIN\\}",
|
||||
expect: Address{
|
||||
Host: "sub.\\{env.my_domain\\}",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "sub.{env.MY_DOMAIN}.com",
|
||||
expect: "sub.{env.MY_DOMAIN}.com",
|
||||
input: "sub.{env.MY_DOMAIN}.com",
|
||||
expect: Address{
|
||||
Host: "sub.{env.MY_DOMAIN}.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: ":80",
|
||||
expect: ":80",
|
||||
input: ":80",
|
||||
expect: Address{
|
||||
Port: "80",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: ":443",
|
||||
expect: ":443",
|
||||
input: ":443",
|
||||
expect: Address{
|
||||
Port: "443",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: ":1234",
|
||||
expect: ":1234",
|
||||
input: ":1234",
|
||||
expect: Address{
|
||||
Port: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expect: "",
|
||||
expect: Address{},
|
||||
},
|
||||
{
|
||||
input: ":",
|
||||
expect: "",
|
||||
expect: Address{},
|
||||
},
|
||||
{
|
||||
input: "[::]",
|
||||
expect: "::",
|
||||
input: "[::]",
|
||||
expect: Address{
|
||||
Host: "::",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "127.0.0.1",
|
||||
expect: Address{
|
||||
Host: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:1234",
|
||||
expect: Address{
|
||||
Host: "2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
||||
Port: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
// IPv4 address in IPv6 form (#4381)
|
||||
input: "[::ffff:cff4:e77d]:1234",
|
||||
expect: Address{
|
||||
Host: "::ffff:cff4:e77d",
|
||||
Port: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "::ffff:cff4:e77d",
|
||||
expect: Address{
|
||||
Host: "::ffff:cff4:e77d",
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
@@ -175,9 +236,18 @@ func TestKeyNormalization(t *testing.T) {
|
||||
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
|
||||
continue
|
||||
}
|
||||
if actual := addr.Normalize().Key(); actual != tc.expect {
|
||||
t.Errorf("Test %d: Input '%s': Expected '%s' but got '%s'", i, tc.input, tc.expect, actual)
|
||||
actual := addr.Normalize()
|
||||
if actual.Scheme != tc.expect.Scheme {
|
||||
t.Errorf("Test %d: Input '%s': Expected Scheme='%s' but got Scheme='%s'", i, tc.input, tc.expect.Scheme, actual.Scheme)
|
||||
}
|
||||
if actual.Host != tc.expect.Host {
|
||||
t.Errorf("Test %d: Input '%s': Expected Host='%s' but got Host='%s'", i, tc.input, tc.expect.Host, actual.Host)
|
||||
}
|
||||
if actual.Port != tc.expect.Port {
|
||||
t.Errorf("Test %d: Input '%s': Expected Port='%s' but got Port='%s'", i, tc.input, tc.expect.Port, actual.Port)
|
||||
}
|
||||
if actual.Path != tc.expect.Path {
|
||||
t.Errorf("Test %d: Input '%s': Expected Path='%s' but got Path='%s'", i, tc.input, tc.expect.Path, actual.Path)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -39,6 +39,7 @@ func init() {
|
||||
RegisterDirective("bind", parseBind)
|
||||
RegisterDirective("tls", parseTLS)
|
||||
RegisterHandlerDirective("root", parseRoot)
|
||||
RegisterHandlerDirective("vars", parseVars)
|
||||
RegisterHandlerDirective("redir", parseRedir)
|
||||
RegisterHandlerDirective("respond", parseRespond)
|
||||
RegisterHandlerDirective("abort", parseAbort)
|
||||
@@ -82,6 +83,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
|
||||
// on_demand
|
||||
// eab <key_id> <mac_key>
|
||||
// issuer <module_name> [...]
|
||||
// get_certificate <module_name> [...]
|
||||
// }
|
||||
//
|
||||
func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
@@ -93,6 +95,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
var keyType string
|
||||
var internalIssuer *caddytls.InternalIssuer
|
||||
var issuers []certmagic.Issuer
|
||||
var certManagers []certmagic.Manager
|
||||
var onDemand bool
|
||||
|
||||
for h.Next() {
|
||||
@@ -126,10 +129,10 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
// must load each cert only once; otherwise, they each get a
|
||||
// different tag... since a cert loaded twice has the same
|
||||
// bytes, it will overwrite the first one in the cache, and
|
||||
// only the last cert (and its tag) will survive, so a any conn
|
||||
// policy that is looking for any tag but the last one to be
|
||||
// loaded won't find it, and TLS handshakes will fail (see end)
|
||||
// of issue #3004)
|
||||
// only the last cert (and its tag) will survive, so any conn
|
||||
// policy that is looking for any tag other than the last one
|
||||
// to be loaded won't find it, and TLS handshakes will fail
|
||||
// (see end of issue #3004)
|
||||
//
|
||||
// tlsCertTags maps certificate filenames to their tag.
|
||||
// This is used to remember which tag is used for each
|
||||
@@ -230,7 +233,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
filename := h.Val()
|
||||
certDataPEM, err := ioutil.ReadFile(filename)
|
||||
certDataPEM, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -307,6 +310,22 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
}
|
||||
issuers = append(issuers, issuer)
|
||||
|
||||
case "get_certificate":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
modName := h.Val()
|
||||
modID := "tls.get_certificate." + modName
|
||||
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certManager, ok := unm.(certmagic.Manager)
|
||||
if !ok {
|
||||
return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm)
|
||||
}
|
||||
certManagers = append(certManagers, certManager)
|
||||
|
||||
case "dns":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
@@ -344,6 +363,22 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
}
|
||||
acmeIssuer.Challenges.DNS.Resolvers = args
|
||||
|
||||
case "dns_challenge_override_domain":
|
||||
arg := h.RemainingArgs()
|
||||
if len(arg) != 1 {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if acmeIssuer == nil {
|
||||
acmeIssuer = new(caddytls.ACMEIssuer)
|
||||
}
|
||||
if acmeIssuer.Challenges == nil {
|
||||
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
||||
}
|
||||
if acmeIssuer.Challenges.DNS == nil {
|
||||
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
||||
}
|
||||
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
|
||||
|
||||
case "ca_root":
|
||||
arg := h.RemainingArgs()
|
||||
if len(arg) != 1 {
|
||||
@@ -453,6 +488,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
Value: true,
|
||||
})
|
||||
}
|
||||
for _, certManager := range certManagers {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_manager",
|
||||
Value: certManager,
|
||||
})
|
||||
}
|
||||
|
||||
// custom certificate selection
|
||||
if len(certSelector.AnyTag) > 0 {
|
||||
@@ -490,6 +531,13 @@ func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
return caddyhttp.VarsMiddleware{"root": root}, nil
|
||||
}
|
||||
|
||||
// parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax.
|
||||
func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
v := new(caddyhttp.VarsMiddleware)
|
||||
err := v.UnmarshalCaddyfile(h.Dispenser)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// parseRedir parses the redir directive. Syntax:
|
||||
//
|
||||
// redir [<matcher>] <to> [<code>]
|
||||
|
||||
@@ -37,8 +37,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
||||
format filter {
|
||||
wrap console
|
||||
fields {
|
||||
common_log delete
|
||||
request>remote_addr ip_mask {
|
||||
request>remote_ip ip_mask {
|
||||
ipv4 24
|
||||
ipv6 32
|
||||
}
|
||||
@@ -47,7 +46,7 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
||||
}
|
||||
}
|
||||
`,
|
||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"common_log":{"filter":"delete"},"request\u003eremote_addr":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"request\u003eremote_ip":{"filter":"ip_mask","ipv4_cidr":24,"ipv6_cidr":32}},"format":"filter","wrap":{"format":"console"}},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,16 +37,21 @@ import (
|
||||
// The header directive goes second so that headers
|
||||
// can be manipulated before doing redirects.
|
||||
var directiveOrder = []string{
|
||||
"tracing",
|
||||
|
||||
"map",
|
||||
"vars",
|
||||
"root",
|
||||
|
||||
"header",
|
||||
"copy_response_headers", // only in reverse_proxy's handle_response
|
||||
"request_body",
|
||||
|
||||
"redir",
|
||||
"rewrite",
|
||||
|
||||
// URI manipulation
|
||||
// incoming request manipulation
|
||||
"method",
|
||||
"rewrite",
|
||||
"uri",
|
||||
"try_files",
|
||||
|
||||
@@ -54,23 +59,24 @@ var directiveOrder = []string{
|
||||
"basicauth",
|
||||
"request_header",
|
||||
"encode",
|
||||
"push",
|
||||
"templates",
|
||||
|
||||
// special routing & dispatching directives
|
||||
"handle",
|
||||
"handle_path",
|
||||
"route",
|
||||
"push",
|
||||
|
||||
// handlers that typically respond to requests
|
||||
"abort",
|
||||
"error",
|
||||
"copy_response", // only in reverse_proxy's handle_response
|
||||
"respond",
|
||||
"metrics",
|
||||
"reverse_proxy",
|
||||
"php_fastcgi",
|
||||
"file_server",
|
||||
"acme_server",
|
||||
"abort",
|
||||
"error",
|
||||
}
|
||||
|
||||
// directiveIsOrdered returns true if dir is
|
||||
@@ -265,6 +271,13 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
|
||||
return []ConfigValue{{Class: "bind", Value: addrs}}
|
||||
}
|
||||
|
||||
// WithDispenser returns a new instance based on d. All others Helper
|
||||
// fields are copied, so typically maps are shared with this new instance.
|
||||
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
|
||||
h.Dispenser = d
|
||||
return h
|
||||
}
|
||||
|
||||
// ParseSegmentAsSubroute parses the segment such that its subdirectives
|
||||
// are themselves treated as directives, from which a subroute is built
|
||||
// and returned.
|
||||
@@ -322,7 +335,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
||||
dir := seg.Directive()
|
||||
dirFunc, ok := registeredDirectives[dir]
|
||||
if !ok {
|
||||
return nil, h.Errf("unrecognized directive: %s", dir)
|
||||
return nil, h.Errf("unrecognized directive: %s - are you sure your Caddyfile structure (nesting and braces) is correct?", dir)
|
||||
}
|
||||
|
||||
subHelper := h
|
||||
@@ -333,6 +346,9 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
||||
if err != nil {
|
||||
return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||
}
|
||||
|
||||
dir = normalizeDirectiveName(dir)
|
||||
|
||||
for _, result := range results {
|
||||
result.directive = dir
|
||||
allResults = append(allResults, result)
|
||||
@@ -408,14 +424,29 @@ func sortRoutes(routes []ConfigValue) {
|
||||
jPathLen = len(jPM[0])
|
||||
}
|
||||
|
||||
// if both directives have no path matcher, use whichever one
|
||||
// has any kind of matcher defined first.
|
||||
if iPathLen == 0 && jPathLen == 0 {
|
||||
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
||||
}
|
||||
// some directives involve setting values which can overwrite
|
||||
// eachother, so it makes most sense to reverse the order so
|
||||
// that the lease specific matcher is first; everything else
|
||||
// has most-specific matcher first
|
||||
if iDir == "vars" {
|
||||
// if both directives have no path matcher, use whichever one
|
||||
// has no matcher first.
|
||||
if iPathLen == 0 && jPathLen == 0 {
|
||||
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
|
||||
}
|
||||
|
||||
// sort with the most-specific (longest) path first
|
||||
return iPathLen > jPathLen
|
||||
// sort with the least-specific (shortest) path first
|
||||
return iPathLen < jPathLen
|
||||
} else {
|
||||
// if both directives have no path matcher, use whichever one
|
||||
// has any kind of matcher defined first.
|
||||
if iPathLen == 0 && jPathLen == 0 {
|
||||
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
|
||||
}
|
||||
|
||||
// sort with the most-specific (longest) path first
|
||||
return iPathLen > jPathLen
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -471,6 +502,27 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
|
||||
return sblockHosts
|
||||
}
|
||||
|
||||
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
|
||||
// ensure each entry in our list is unique
|
||||
hostMap := make(map[string]struct{})
|
||||
for _, addr := range sb.keys {
|
||||
if addr.Host == "" {
|
||||
continue
|
||||
}
|
||||
if addr.Scheme != "http" && addr.Port != httpPort {
|
||||
hostMap[addr.Host] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// convert map to slice
|
||||
sblockHosts := make([]string, 0, len(hostMap))
|
||||
for host := range hostMap {
|
||||
sblockHosts = append(sblockHosts, host)
|
||||
}
|
||||
|
||||
return sblockHosts
|
||||
}
|
||||
|
||||
// hasHostCatchAllKey returns true if sb has a key that
|
||||
// omits a host portion, i.e. it "catches all" hosts.
|
||||
func (sb serverBlock) hasHostCatchAllKey() bool {
|
||||
@@ -482,6 +534,17 @@ func (sb serverBlock) hasHostCatchAllKey() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isAllHTTP returns true if all sb keys explicitly specify
|
||||
// the http:// scheme
|
||||
func (sb serverBlock) isAllHTTP() bool {
|
||||
for _, addr := range sb.keys {
|
||||
if addr.Scheme != "http" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type (
|
||||
// UnmarshalFunc is a function which can unmarshal Caddyfile
|
||||
// tokens into zero or more config values using a Helper type.
|
||||
|
||||
@@ -17,7 +17,6 @@ package httpcaddyfile
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -30,6 +29,7 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -113,6 +113,8 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||
)
|
||||
|
||||
// these are placeholders that allow a user-defined final
|
||||
@@ -127,6 +129,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
||||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
||||
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||
}
|
||||
|
||||
for _, sb := range originalServerBlocks {
|
||||
@@ -169,7 +172,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
dirFunc, ok := registeredDirectives[dir]
|
||||
if !ok {
|
||||
tkn := segment[0]
|
||||
return nil, warnings, fmt.Errorf("%s:%d: unrecognized directive: %s", tkn.File, tkn.Line, dir)
|
||||
message := "%s:%d: unrecognized directive: %s"
|
||||
if !sb.block.HasBraces {
|
||||
message += "\nDid you mean to define a second site? If so, you must use curly braces around each site to separate their configurations."
|
||||
}
|
||||
return nil, warnings, fmt.Errorf(message, tkn.File, tkn.Line, dir)
|
||||
}
|
||||
|
||||
h := Helper{
|
||||
@@ -187,13 +194,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
return nil, warnings, fmt.Errorf("parsing caddyfile tokens for '%s': %v", dir, err)
|
||||
}
|
||||
|
||||
// As a special case, we want "handle_path" to be sorted
|
||||
// at the same level as "handle", so we force them to use
|
||||
// the same directive name after their parsing is complete.
|
||||
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
|
||||
if dir == "handle_path" {
|
||||
dir = "handle"
|
||||
}
|
||||
dir = normalizeDirectiveName(dir)
|
||||
|
||||
for _, result := range results {
|
||||
result.directive = dir
|
||||
@@ -220,9 +221,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
|
||||
// now that each server is configured, make the HTTP app
|
||||
httpApp := caddyhttp.App{
|
||||
HTTPPort: tryInt(options["http_port"], &warnings),
|
||||
HTTPSPort: tryInt(options["https_port"], &warnings),
|
||||
Servers: servers,
|
||||
HTTPPort: tryInt(options["http_port"], &warnings),
|
||||
HTTPSPort: tryInt(options["https_port"], &warnings),
|
||||
GracePeriod: tryDuration(options["grace_period"], &warnings),
|
||||
Servers: servers,
|
||||
}
|
||||
|
||||
// then make the TLS app
|
||||
@@ -445,11 +447,26 @@ func (st *ServerType) serversFromPairings(
|
||||
// handle the auto_https global option
|
||||
if autoHTTPS != "on" {
|
||||
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
|
||||
if autoHTTPS == "off" {
|
||||
switch autoHTTPS {
|
||||
case "off":
|
||||
srv.AutoHTTPS.Disabled = true
|
||||
}
|
||||
if autoHTTPS == "disable_redirects" {
|
||||
case "disable_redirects":
|
||||
srv.AutoHTTPS.DisableRedir = true
|
||||
case "disable_certs":
|
||||
srv.AutoHTTPS.DisableCerts = true
|
||||
case "ignore_loaded_certs":
|
||||
srv.AutoHTTPS.IgnoreLoadedCerts = true
|
||||
}
|
||||
}
|
||||
|
||||
// Using paths in site addresses is deprecated
|
||||
// See ParseAddress() where parsing should later reject paths
|
||||
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
|
||||
for _, sblock := range p.serverBlocks {
|
||||
for _, addr := range sblock.keys {
|
||||
if addr.Path != "" {
|
||||
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +535,16 @@ func (st *ServerType) serversFromPairings(
|
||||
}
|
||||
}
|
||||
|
||||
// if needed, the ServerLogConfig is initialized beforehand so
|
||||
// that all server blocks can populate it with data, even when not
|
||||
// coming with a log directive
|
||||
for _, sblock := range p.serverBlocks {
|
||||
if len(sblock.pile["custom_log"]) != 0 {
|
||||
srv.Logs = new(caddyhttp.ServerLogConfig)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// create a subroute for each site in the server block
|
||||
for _, sblock := range p.serverBlocks {
|
||||
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
|
||||
@@ -530,7 +557,7 @@ func (st *ServerType) serversFromPairings(
|
||||
// emit warnings if user put unspecified IP addresses; they probably want the bind directive
|
||||
for _, h := range hosts {
|
||||
if h == "0.0.0.0" || h == "::" {
|
||||
log.Printf("[WARNING] Site block has unspecified IP address %s which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", h)
|
||||
caddy.Log().Named("caddyfile").Warn("Site block has an unspecified IP address which only matches requests having that Host header; you probably want the 'bind' directive to configure the socket", zap.String("address", h))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,7 +593,7 @@ func (st *ServerType) serversFromPairings(
|
||||
}
|
||||
|
||||
for _, addr := range sblock.keys {
|
||||
// if server only uses HTTPS port, auto-HTTPS will not apply
|
||||
// if server only uses HTTP port, auto-HTTPS will not apply
|
||||
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
|
||||
// exclude any hosts that were defined explicitly with "http://"
|
||||
// in the key from automated cert management (issue #2998)
|
||||
@@ -632,9 +659,6 @@ func (st *ServerType) serversFromPairings(
|
||||
sblockLogHosts := sblock.hostsFromKeys(true)
|
||||
for _, cval := range sblock.pile["custom_log"] {
|
||||
ncl := cval.Value.(namedCustomLog)
|
||||
if srv.Logs == nil {
|
||||
srv.Logs = new(caddyhttp.ServerLogConfig)
|
||||
}
|
||||
if sblock.hasHostCatchAllKey() {
|
||||
// all requests for hosts not able to be listed should use
|
||||
// this log because it's a catch-all-hosts server block
|
||||
@@ -1044,6 +1068,19 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
|
||||
return subroute, nil
|
||||
}
|
||||
|
||||
// normalizeDirectiveName ensures directives that should be sorted
|
||||
// at the same level are named the same before sorting happens.
|
||||
func normalizeDirectiveName(directive string) string {
|
||||
// As a special case, we want "handle_path" to be sorted
|
||||
// at the same level as "handle", so we force them to use
|
||||
// the same directive name after their parsing is complete.
|
||||
// See https://github.com/caddyserver/caddy/issues/3675#issuecomment-678042377
|
||||
if directive == "handle_path" {
|
||||
directive = "handle"
|
||||
}
|
||||
return directive
|
||||
}
|
||||
|
||||
// consolidateRoutes combines routes with the same properties
|
||||
// (same matchers, same Terminal and Group settings) for a
|
||||
// cleaner overall output.
|
||||
@@ -1243,6 +1280,14 @@ func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
|
||||
return stringVal
|
||||
}
|
||||
|
||||
func tryDuration(val interface{}, warnings *[]caddyconfig.Warning) caddy.Duration {
|
||||
durationVal, ok := val.(caddy.Duration)
|
||||
if val != nil && !ok && warnings != nil {
|
||||
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a duration type"})
|
||||
}
|
||||
return durationVal
|
||||
}
|
||||
|
||||
// sliceContains returns true if needle is in haystack.
|
||||
func sliceContains(haystack []string, needle string) bool {
|
||||
for _, s := range haystack {
|
||||
|
||||
@@ -29,14 +29,19 @@ func init() {
|
||||
RegisterGlobalOption("debug", parseOptTrue)
|
||||
RegisterGlobalOption("http_port", parseOptHTTPPort)
|
||||
RegisterGlobalOption("https_port", parseOptHTTPSPort)
|
||||
RegisterGlobalOption("default_bind", parseOptSingleString)
|
||||
RegisterGlobalOption("grace_period", parseOptDuration)
|
||||
RegisterGlobalOption("default_sni", parseOptSingleString)
|
||||
RegisterGlobalOption("order", parseOptOrder)
|
||||
RegisterGlobalOption("storage", parseOptStorage)
|
||||
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
|
||||
RegisterGlobalOption("renew_interval", parseOptDuration)
|
||||
RegisterGlobalOption("acme_ca", parseOptSingleString)
|
||||
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
|
||||
RegisterGlobalOption("acme_dns", parseOptACMEDNS)
|
||||
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
|
||||
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
|
||||
RegisterGlobalOption("skip_install_trust", parseOptTrue)
|
||||
RegisterGlobalOption("email", parseOptSingleString)
|
||||
RegisterGlobalOption("admin", parseOptAdmin)
|
||||
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
|
||||
@@ -46,6 +51,7 @@ func init() {
|
||||
RegisterGlobalOption("servers", parseServerOptions)
|
||||
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
|
||||
RegisterGlobalOption("log", parseLogOptions)
|
||||
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
||||
}
|
||||
|
||||
func parseOptTrue(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { return true, nil }
|
||||
@@ -177,6 +183,20 @@ func parseOptStorage(d *caddyfile.Dispenser, _ interface{}) (interface{}, error)
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
func parseOptDuration(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||
if !d.Next() { // consume option name
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
if !d.Next() { // get duration value
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
dur, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caddy.Duration(dur), nil
|
||||
}
|
||||
|
||||
func parseOptACMEDNS(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||
if !d.Next() { // consume option name
|
||||
return nil, d.ArgErr()
|
||||
@@ -364,8 +384,8 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro
|
||||
if d.Next() {
|
||||
return "", d.ArgErr()
|
||||
}
|
||||
if val != "off" && val != "disable_redirects" {
|
||||
return "", d.Errf("auto_https must be either 'off' or 'disable_redirects'")
|
||||
if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
|
||||
return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@@ -435,3 +455,8 @@ func parseLogOptions(d *caddyfile.Dispenser, existingVal interface{}) (interface
|
||||
|
||||
return configValues, nil
|
||||
}
|
||||
|
||||
func parseOptPreferredChains(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||
d.Next()
|
||||
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
|
||||
}
|
||||
|
||||
@@ -16,26 +16,204 @@ package httpcaddyfile
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGlobalOption("pki", parsePKIApp)
|
||||
}
|
||||
|
||||
// parsePKIApp parses the global log option. Syntax:
|
||||
//
|
||||
// pki {
|
||||
// ca [<id>] {
|
||||
// name <name>
|
||||
// root_cn <name>
|
||||
// intermediate_cn <name>
|
||||
// root {
|
||||
// cert <path>
|
||||
// key <path>
|
||||
// format <format>
|
||||
// }
|
||||
// intermediate {
|
||||
// cert <path>
|
||||
// key <path>
|
||||
// format <format>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// When the CA ID is unspecified, 'local' is assumed.
|
||||
//
|
||||
func parsePKIApp(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {
|
||||
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||
|
||||
for d.Next() {
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "ca":
|
||||
pkiCa := new(caddypki.CA)
|
||||
if d.NextArg() {
|
||||
pkiCa.ID = d.Val()
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
}
|
||||
if pkiCa.ID == "" {
|
||||
pkiCa.ID = caddypki.DefaultCAID
|
||||
}
|
||||
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "name":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Name = d.Val()
|
||||
|
||||
case "root_cn":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.RootCommonName = d.Val()
|
||||
|
||||
case "intermediate_cn":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.IntermediateCommonName = d.Val()
|
||||
|
||||
case "root":
|
||||
if pkiCa.Root == nil {
|
||||
pkiCa.Root = new(caddypki.KeyPair)
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "cert":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Root.Certificate = d.Val()
|
||||
|
||||
case "key":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Root.PrivateKey = d.Val()
|
||||
|
||||
case "format":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Root.Format = d.Val()
|
||||
|
||||
default:
|
||||
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
case "intermediate":
|
||||
if pkiCa.Intermediate == nil {
|
||||
pkiCa.Intermediate = new(caddypki.KeyPair)
|
||||
}
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
case "cert":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Intermediate.Certificate = d.Val()
|
||||
|
||||
case "key":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Intermediate.PrivateKey = d.Val()
|
||||
|
||||
case "format":
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
pkiCa.Intermediate.Format = d.Val()
|
||||
|
||||
default:
|
||||
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
|
||||
pki.CAs[pkiCa.ID] = pkiCa
|
||||
|
||||
default:
|
||||
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pki, nil
|
||||
}
|
||||
|
||||
func (st ServerType) buildPKIApp(
|
||||
pairings []sbAddrAssociation,
|
||||
options map[string]interface{},
|
||||
warnings []caddyconfig.Warning,
|
||||
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||
|
||||
pkiApp := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||
skipInstallTrust := false
|
||||
if _, ok := options["skip_install_trust"]; ok {
|
||||
skipInstallTrust = true
|
||||
}
|
||||
falseBool := false
|
||||
|
||||
// Load the PKI app configured via global options
|
||||
var pkiApp *caddypki.PKI
|
||||
unwrappedPki, ok := options["pki"].(*caddypki.PKI)
|
||||
if ok {
|
||||
pkiApp = unwrappedPki
|
||||
} else {
|
||||
pkiApp = &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}
|
||||
}
|
||||
for _, ca := range pkiApp.CAs {
|
||||
if skipInstallTrust {
|
||||
ca.InstallTrust = &falseBool
|
||||
}
|
||||
pkiApp.CAs[ca.ID] = ca
|
||||
}
|
||||
|
||||
// Add in the CAs configured via directives
|
||||
for _, p := range pairings {
|
||||
for _, sblock := range p.serverBlocks {
|
||||
// find all the CAs that were defined and add them to the app config
|
||||
// i.e. from any "acme_server" directives
|
||||
for _, caCfgValue := range sblock.pile["pki.ca"] {
|
||||
ca := caCfgValue.Value.(*caddypki.CA)
|
||||
pkiApp.CAs[ca.ID] = ca
|
||||
if skipInstallTrust {
|
||||
ca.InstallTrust = &falseBool
|
||||
}
|
||||
|
||||
// the CA might already exist from global options, so
|
||||
// don't overwrite it in that case
|
||||
if _, ok := pkiApp.CAs[ca.ID]; !ok {
|
||||
pkiApp.CAs[ca.ID] = ca
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there was no CAs defined in any of the servers,
|
||||
// and we were requested to not install trust, then
|
||||
// add one for the default/local CA to do so
|
||||
if len(pkiApp.CAs) == 0 && skipInstallTrust {
|
||||
ca := new(caddypki.CA)
|
||||
ca.ID = caddypki.DefaultCAID
|
||||
ca.InstallTrust = &falseBool
|
||||
pkiApp.CAs[ca.ID] = ca
|
||||
}
|
||||
|
||||
return pkiApp, warnings, nil
|
||||
}
|
||||
|
||||
@@ -33,15 +33,16 @@ type serverOptions struct {
|
||||
ListenerAddress string
|
||||
|
||||
// These will all map 1:1 to the caddyhttp.Server struct
|
||||
ListenerWrappersRaw []json.RawMessage
|
||||
ReadTimeout caddy.Duration
|
||||
ReadHeaderTimeout caddy.Duration
|
||||
WriteTimeout caddy.Duration
|
||||
IdleTimeout caddy.Duration
|
||||
MaxHeaderBytes int
|
||||
AllowH2C bool
|
||||
ExperimentalHTTP3 bool
|
||||
StrictSNIHost *bool
|
||||
ListenerWrappersRaw []json.RawMessage
|
||||
ReadTimeout caddy.Duration
|
||||
ReadHeaderTimeout caddy.Duration
|
||||
WriteTimeout caddy.Duration
|
||||
IdleTimeout caddy.Duration
|
||||
MaxHeaderBytes int
|
||||
AllowH2C bool
|
||||
ExperimentalHTTP3 bool
|
||||
StrictSNIHost *bool
|
||||
ShouldLogCredentials bool
|
||||
}
|
||||
|
||||
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error) {
|
||||
@@ -134,6 +135,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
|
||||
}
|
||||
serverOpts.MaxHeaderBytes = int(size)
|
||||
|
||||
case "log_credentials":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
serverOpts.ShouldLogCredentials = true
|
||||
|
||||
case "protocol":
|
||||
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||
switch d.Val() {
|
||||
@@ -150,11 +157,14 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
|
||||
serverOpts.ExperimentalHTTP3 = true
|
||||
|
||||
case "strict_sni_host":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
|
||||
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
|
||||
}
|
||||
trueBool := true
|
||||
serverOpts.StrictSNIHost = &trueBool
|
||||
boolVal := true
|
||||
if d.Val() == "insecure_off" {
|
||||
boolVal = false
|
||||
}
|
||||
serverOpts.StrictSNIHost = &boolVal
|
||||
|
||||
default:
|
||||
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
|
||||
@@ -222,6 +232,12 @@ func applyServerOptions(
|
||||
server.AllowH2C = opts.AllowH2C
|
||||
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
|
||||
server.StrictSNIHost = opts.StrictSNIHost
|
||||
if opts.ShouldLogCredentials {
|
||||
if server.Logs == nil {
|
||||
server.Logs = &caddyhttp.ServerLogConfig{}
|
||||
}
|
||||
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -54,7 +54,7 @@ func (st ServerType) buildTLSApp(
|
||||
// a hostless key, so that they don't get forgotten/omitted
|
||||
// by auto-HTTPS (since they won't appear in route matchers)
|
||||
var serverBlocksWithTLSHostlessKey int
|
||||
hostsSharedWithHostlessKey := make(map[string]struct{})
|
||||
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
|
||||
for _, pair := range pairings {
|
||||
for _, sb := range pair.serverBlocks {
|
||||
for _, addr := range sb.keys {
|
||||
@@ -70,8 +70,8 @@ func (st ServerType) buildTLSApp(
|
||||
if otherAddr.Original == addr.Original {
|
||||
continue
|
||||
}
|
||||
if otherAddr.Host != "" {
|
||||
hostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
||||
if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
|
||||
httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -101,6 +101,12 @@ func (st ServerType) buildTLSApp(
|
||||
}
|
||||
|
||||
for _, sblock := range p.serverBlocks {
|
||||
// check the scheme of all the site addresses,
|
||||
// skip building AP if they all had http://
|
||||
if sblock.isAllHTTP() {
|
||||
continue
|
||||
}
|
||||
|
||||
// get values that populate an automation policy for this block
|
||||
ap, err := newBaseAutomationPolicy(options, warnings, true)
|
||||
if err != nil {
|
||||
@@ -133,6 +139,13 @@ func (st ServerType) buildTLSApp(
|
||||
ap.Issuers = issuers
|
||||
}
|
||||
|
||||
// certificate managers
|
||||
if certManagerVals, ok := sblock.pile["tls.cert_manager"]; ok {
|
||||
for _, certManager := range certManagerVals {
|
||||
certGetterName := certManager.Value.(caddy.Module).CaddyModule().ID.Name()
|
||||
ap.ManagersRaw = append(ap.ManagersRaw, caddyconfig.JSONModuleObject(certManager.Value, "via", certGetterName, &warnings))
|
||||
}
|
||||
}
|
||||
// custom bind host
|
||||
for _, cfgVal := range sblock.pile["bind"] {
|
||||
for _, iss := range ap.Issuers {
|
||||
@@ -189,7 +202,7 @@ func (st ServerType) buildTLSApp(
|
||||
}
|
||||
|
||||
// associate our new automation policy with this server block's hosts
|
||||
ap.Subjects = sblockHosts
|
||||
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort)
|
||||
sort.Strings(ap.Subjects) // solely for deterministic test results
|
||||
|
||||
// if a combination of public and internal names were given
|
||||
@@ -211,7 +224,7 @@ func (st ServerType) buildTLSApp(
|
||||
// it that we would need to check here) since the hostname is known at handshake;
|
||||
// and it is unexpected to switch to internal issuer when the user wants to get
|
||||
// regular certificates on-demand for a class of certs like *.*.tld.
|
||||
if !certmagic.SubjectIsIP(s) && !certmagic.SubjectIsInternal(s) && (strings.Count(s, "*.") < 2 || ap.OnDemand) {
|
||||
if subjectQualifiesForPublicCert(ap, s) {
|
||||
external = append(external, s)
|
||||
} else {
|
||||
internal = append(internal, s)
|
||||
@@ -278,6 +291,27 @@ func (st ServerType) buildTLSApp(
|
||||
tlsApp.Automation.OnDemand = onDemand
|
||||
}
|
||||
|
||||
// set the storage clean interval if configured
|
||||
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.StorageCleanInterval = storageCleanInterval
|
||||
}
|
||||
|
||||
// set the expired certificates renew interval if configured
|
||||
if renewCheckInterval, ok := options["renew_interval"].(caddy.Duration); ok {
|
||||
if tlsApp.Automation == nil {
|
||||
tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
tlsApp.Automation.RenewCheckInterval = renewCheckInterval
|
||||
}
|
||||
|
||||
// set whether OCSP stapling should be disabled for manually-managed certificates
|
||||
if ocspConfig, ok := options["ocsp_stapling"].(certmagic.OCSPConfig); ok {
|
||||
tlsApp.DisableOCSPStapling = ocspConfig.DisableStapling
|
||||
}
|
||||
|
||||
// if any hostnames appear on the same server block as a key with
|
||||
// no host, they will not be used with route matchers because the
|
||||
// hostless key matches all hosts, therefore, it wouldn't be
|
||||
@@ -289,7 +323,7 @@ func (st ServerType) buildTLSApp(
|
||||
internalAP := &caddytls.AutomationPolicy{
|
||||
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
|
||||
}
|
||||
for h := range hostsSharedWithHostlessKey {
|
||||
for h := range httpsHostsSharedWithHostlessKey {
|
||||
al = append(al, h)
|
||||
if !certmagic.SubjectQualifiesForPublicCert(h) {
|
||||
internalAP.Subjects = append(internalAP.Subjects, h)
|
||||
@@ -313,10 +347,15 @@ func (st ServerType) buildTLSApp(
|
||||
globalACMECARoot := options["acme_ca_root"]
|
||||
globalACMEDNS := options["acme_dns"]
|
||||
globalACMEEAB := options["acme_eab"]
|
||||
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
|
||||
if hasGlobalACMEDefaults {
|
||||
for _, ap := range tlsApp.Automation.Policies {
|
||||
if len(ap.Issuers) == 0 {
|
||||
// for _, ap := range tlsApp.Automation.Policies {
|
||||
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
|
||||
ap := tlsApp.Automation.Policies[i]
|
||||
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
|
||||
// for public names, create default issuers which will later be filled in with configured global defaults
|
||||
// (internal names will implicitly use the internal issuer at auto-https time)
|
||||
ap.Issuers = caddytls.DefaultIssuers()
|
||||
|
||||
// if a specific endpoint is configured, can't use multiple default issuers
|
||||
@@ -397,6 +436,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
||||
globalACMECARoot := options["acme_ca_root"]
|
||||
globalACMEDNS := options["acme_dns"]
|
||||
globalACMEEAB := options["acme_eab"]
|
||||
globalPreferredChains := options["preferred_chains"]
|
||||
|
||||
if globalEmail != nil && acmeIssuer.Email == "" {
|
||||
acmeIssuer.Email = globalEmail.(string)
|
||||
@@ -417,6 +457,9 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
|
||||
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
|
||||
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)
|
||||
}
|
||||
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
|
||||
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -480,25 +523,39 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
||||
return len(aps[i].Subjects) > len(aps[j].Subjects)
|
||||
})
|
||||
|
||||
// remove any empty policies (except subjects, of course)
|
||||
emptyAPCount := 0
|
||||
origLenAPs := len(aps)
|
||||
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||
emptyAP := new(caddytls.AutomationPolicy)
|
||||
for i := 0; i < len(aps); i++ {
|
||||
emptyAP.Subjects = aps[i].Subjects
|
||||
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||
aps = append(aps[:i], aps[i+1:]...)
|
||||
i--
|
||||
emptyAPCount++
|
||||
if !automationPolicyHasAllPublicNames(aps[i]) {
|
||||
// if this automation policy has internal names, we might as well remove it
|
||||
// so auto-https can implicitly use the internal issuer
|
||||
aps = append(aps[:i], aps[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
// If all policies are empty, we can return nil, as there is no need to set any policy
|
||||
if emptyAPCount == origLenAPs {
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove or combine duplicate policies
|
||||
outer:
|
||||
for i := 0; i < len(aps); i++ {
|
||||
// compare only with next policies; we sorted by specificity so we must not delete earlier policies
|
||||
for j := i + 1; j < len(aps); j++ {
|
||||
// if they're exactly equal in every way, just keep one of them
|
||||
if reflect.DeepEqual(aps[i], aps[j]) {
|
||||
aps = append(aps[:j], aps[j+1:]...)
|
||||
// must re-evaluate current i against next j; can't skip it!
|
||||
// even if i decrements to -1, will be incremented to 0 immediately
|
||||
i--
|
||||
break
|
||||
continue outer
|
||||
}
|
||||
|
||||
// if the policy is the same, we can keep just one, but we have
|
||||
@@ -524,6 +581,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
||||
if automationPolicyShadows(i, aps) >= j {
|
||||
aps = append(aps[:i], aps[i+1:]...)
|
||||
i--
|
||||
continue outer
|
||||
}
|
||||
} else {
|
||||
// avoid repeated subjects
|
||||
@@ -580,3 +638,21 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
|
||||
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
|
||||
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
|
||||
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
|
||||
return !certmagic.SubjectIsIP(subj) &&
|
||||
!certmagic.SubjectIsInternal(subj) &&
|
||||
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
|
||||
}
|
||||
|
||||
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
|
||||
for _, subj := range ap.Subjects {
|
||||
if !subjectQualifiesForPublicCert(ap, subj) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddyconfig
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -56,21 +71,28 @@ func (HTTPLoader) CaddyModule() caddy.ModuleInfo {
|
||||
|
||||
// LoadConfig loads a Caddy config.
|
||||
func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
||||
repl := caddy.NewReplacer()
|
||||
|
||||
client, err := hl.makeClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := hl.Method
|
||||
method := repl.ReplaceAll(hl.Method, "")
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, hl.URL, nil)
|
||||
url := repl.ReplaceAll(hl.URL, "")
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = hl.Headers
|
||||
for key, vals := range hl.Headers {
|
||||
for _, val := range vals {
|
||||
req.Header.Add(repl.ReplaceAll(key, ""), repl.ReplaceKnown(val, ""))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
@@ -81,7 +103,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
||||
return nil, fmt.Errorf("server responded with HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -130,7 +152,7 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
|
||||
if len(hl.TLS.RootCAPEMFiles) > 0 {
|
||||
rootPool := x509.NewCertPool()
|
||||
for _, pemFile := range hl.TLS.RootCAPEMFiles {
|
||||
pemData, err := ioutil.ReadFile(pemFile)
|
||||
pemData, err := os.ReadFile(pemFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed reading ca cert: %v", err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -129,7 +129,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
var out bytes.Buffer
|
||||
_ = json.Indent(&out, body, "", " ")
|
||||
@@ -162,7 +162,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
|
||||
timeElapsed(start, "caddytest: config load time")
|
||||
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
tc.t.Errorf("unable to read response. %s", err)
|
||||
return err
|
||||
@@ -202,7 +202,7 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
actualBytes, err := ioutil.ReadAll(resp.Body)
|
||||
actualBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -471,7 +471,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||
|
||||
defer resp.Body.Close()
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to read the response body %s", err)
|
||||
}
|
||||
|
||||
@@ -80,3 +80,46 @@ func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
||||
`, "json")
|
||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
||||
}
|
||||
|
||||
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAll(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
http://:9080 {
|
||||
respond "Foo"
|
||||
}
|
||||
http://baz.localhost:9080 {
|
||||
respond "Baz"
|
||||
}
|
||||
bar.localhost {
|
||||
respond "Bar"
|
||||
}
|
||||
`, "caddyfile")
|
||||
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
|
||||
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
||||
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Baz")
|
||||
}
|
||||
|
||||
func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSite(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
}
|
||||
http://:9080 {
|
||||
respond "Foo"
|
||||
}
|
||||
bar.localhost {
|
||||
respond "Bar"
|
||||
}
|
||||
`, "caddyfile")
|
||||
tester.AssertRedirect("http://bar.localhost:9080/", "https://bar.localhost/", http.StatusPermanentRedirect)
|
||||
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
||||
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
auto_https ignore_loaded_certs
|
||||
}
|
||||
|
||||
localhost
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"automatic_https": {
|
||||
"ignore_loaded_certificates": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
example.com {
|
||||
bind tcp6/[::]
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
"tcp6/[::]:443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
:80
|
||||
|
||||
# All the options
|
||||
encode gzip zstd {
|
||||
minimum_length 256
|
||||
prefer zstd gzip
|
||||
match {
|
||||
status 2xx 4xx 500
|
||||
header Content-Type text/*
|
||||
@@ -14,6 +14,12 @@ encode gzip zstd {
|
||||
header Content-Type image/svg+xml*
|
||||
}
|
||||
}
|
||||
|
||||
# Long way with a block for each encoding
|
||||
encode {
|
||||
zstd
|
||||
gzip 5
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
@@ -51,6 +57,19 @@ encode gzip zstd {
|
||||
]
|
||||
},
|
||||
"minimum_length": 256,
|
||||
"prefer": [
|
||||
"gzip",
|
||||
"zstd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"encodings": {
|
||||
"gzip": {
|
||||
"level": 5
|
||||
},
|
||||
"zstd": {}
|
||||
},
|
||||
"handler": "encode",
|
||||
"prefer": [
|
||||
"zstd",
|
||||
"gzip"
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
example.com {
|
||||
root * /srv
|
||||
|
||||
# Trigger errors for certain paths
|
||||
error /private* "Unauthorized" 403
|
||||
error /hidden* "Not found" 404
|
||||
|
||||
# Handle the error by serving an HTML page
|
||||
handle_errors {
|
||||
rewrite * /{http.error.status_code}.html
|
||||
file_server
|
||||
}
|
||||
|
||||
file_server
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "vars",
|
||||
"root": "/srv"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"error": "Unauthorized",
|
||||
"handler": "error",
|
||||
"status_code": 403
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/private*"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"error": "Not found",
|
||||
"handler": "error",
|
||||
"status_code": 404
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/hidden*"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"errors": {
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group0",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "rewrite",
|
||||
"uri": "/{http.error.status_code}.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
example.com
|
||||
|
||||
@a expression {http.error.status_code} == 400
|
||||
abort @a
|
||||
|
||||
@b expression {http.error.status_code} == "401"
|
||||
abort @b
|
||||
|
||||
@c expression {http.error.status_code} == `402`
|
||||
abort @c
|
||||
|
||||
@d expression "{http.error.status_code} == 403"
|
||||
abort @d
|
||||
|
||||
@e expression `{http.error.status_code} == 404`
|
||||
abort @e
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"abort": true,
|
||||
"handler": "static_response"
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"expression": "{http.error.status_code} == 400"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"abort": true,
|
||||
"handler": "static_response"
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"expression": "{http.error.status_code} == \"401\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"abort": true,
|
||||
"handler": "static_response"
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"expression": "{http.error.status_code} == `402`"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"abort": true,
|
||||
"handler": "static_response"
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"expression": "{http.error.status_code} == 403"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"abort": true,
|
||||
"handler": "static_response"
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"expression": "{http.error.status_code} == 404"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
:80
|
||||
|
||||
file_server {
|
||||
disable_canonical_uris
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"canonical_uris": false,
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
:80
|
||||
|
||||
file_server {
|
||||
pass_thru
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
],
|
||||
"pass_thru": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
localhost
|
||||
|
||||
root * /srv
|
||||
|
||||
handle /nope* {
|
||||
file_server {
|
||||
status 403
|
||||
}
|
||||
}
|
||||
|
||||
handle /custom-status* {
|
||||
file_server {
|
||||
status {env.CUSTOM_STATUS}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "vars",
|
||||
"root": "/srv"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
],
|
||||
"status_code": "{env.CUSTOM_STATUS}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/custom-status*"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
],
|
||||
"status_code": 403
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/nope*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
debug
|
||||
http_port 8080
|
||||
https_port 8443
|
||||
grace_period 5s
|
||||
default_sni localhost
|
||||
order root first
|
||||
storage file_system {
|
||||
@@ -9,6 +10,7 @@
|
||||
}
|
||||
acme_ca https://example.com
|
||||
acme_ca_root /path/to/ca.crt
|
||||
ocsp_stapling off
|
||||
|
||||
email test@example.com
|
||||
admin off
|
||||
@@ -42,6 +44,7 @@
|
||||
"http": {
|
||||
"http_port": 8080,
|
||||
"https_port": 8443,
|
||||
"grace_period": 5000000000,
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
@@ -59,7 +62,8 @@
|
||||
"module": "internal"
|
||||
}
|
||||
],
|
||||
"key_type": "ed25519"
|
||||
"key_type": "ed25519",
|
||||
"disable_ocsp_stapling": true
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
@@ -69,7 +73,8 @@
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable_ocsp_stapling": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
interval 30s
|
||||
burst 20
|
||||
}
|
||||
storage_clean_interval 7d
|
||||
renew_interval 1d
|
||||
|
||||
key_type ed25519
|
||||
}
|
||||
@@ -80,7 +82,9 @@
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
},
|
||||
"renew_interval": 86400000000000,
|
||||
"storage_clean_interval": 604800000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
default_bind tcp4/0.0.0.0
|
||||
}
|
||||
|
||||
example.com {
|
||||
}
|
||||
|
||||
example.org:12345 {
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
"tcp4/0.0.0.0:12345"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.org"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
"tcp4/0.0.0.0:443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
format filter {
|
||||
wrap console
|
||||
fields {
|
||||
common_log delete
|
||||
request>remote_addr ip_mask {
|
||||
request>remote_ip ip_mask {
|
||||
ipv4 24
|
||||
ipv6 32
|
||||
}
|
||||
@@ -19,10 +18,7 @@
|
||||
"custom-logger": {
|
||||
"encoder": {
|
||||
"fields": {
|
||||
"common_log": {
|
||||
"filter": "delete"
|
||||
},
|
||||
"request\u003eremote_addr": {
|
||||
"request\u003eremote_ip": {
|
||||
"filter": "ip_mask",
|
||||
"ipv4_cidr": 24,
|
||||
"ipv6_cidr": 32
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
preferred_chains smallest
|
||||
}
|
||||
|
||||
example.com
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "acme",
|
||||
"preferred_chains": {
|
||||
"smallest": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "zerossl",
|
||||
"preferred_chains": {
|
||||
"smallest": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
skip_install_trust
|
||||
pki {
|
||||
ca {
|
||||
name "Local"
|
||||
root_cn "Custom Local Root Name"
|
||||
intermediate_cn "Custom Local Intermediate Name"
|
||||
root {
|
||||
cert /path/to/cert.pem
|
||||
key /path/to/key.pem
|
||||
format pem_file
|
||||
}
|
||||
intermediate {
|
||||
cert /path/to/cert.pem
|
||||
key /path/to/key.pem
|
||||
format pem_file
|
||||
}
|
||||
}
|
||||
ca foo {
|
||||
name "Foo"
|
||||
root_cn "Custom Foo Root Name"
|
||||
intermediate_cn "Custom Foo Intermediate Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.example.com {
|
||||
tls internal
|
||||
}
|
||||
|
||||
acme.example.com {
|
||||
acme_server {
|
||||
ca foo
|
||||
}
|
||||
}
|
||||
|
||||
acme-bar.example.com {
|
||||
acme_server {
|
||||
ca bar
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"acme-bar.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"ca": "bar",
|
||||
"handler": "acme_server"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"acme.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"ca": "foo",
|
||||
"handler": "acme_server"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"a.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"pki": {
|
||||
"certificate_authorities": {
|
||||
"bar": {
|
||||
"install_trust": false
|
||||
},
|
||||
"foo": {
|
||||
"name": "Foo",
|
||||
"root_common_name": "Custom Foo Root Name",
|
||||
"intermediate_common_name": "Custom Foo Intermediate Name",
|
||||
"install_trust": false
|
||||
},
|
||||
"local": {
|
||||
"name": "Local",
|
||||
"root_common_name": "Custom Local Root Name",
|
||||
"intermediate_common_name": "Custom Local Intermediate Name",
|
||||
"install_trust": false,
|
||||
"root": {
|
||||
"certificate": "/path/to/cert.pem",
|
||||
"private_key": "/path/to/key.pem",
|
||||
"format": "pem_file"
|
||||
},
|
||||
"intermediate": {
|
||||
"certificate": "/path/to/cert.pem",
|
||||
"private_key": "/path/to/key.pem",
|
||||
"format": "pem_file"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"acme-bar.example.com",
|
||||
"acme.example.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"subjects": [
|
||||
"a.example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "internal"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
timeouts {
|
||||
idle 90s
|
||||
}
|
||||
protocol {
|
||||
strict_sni_host insecure_off
|
||||
}
|
||||
}
|
||||
servers :80 {
|
||||
timeouts {
|
||||
@@ -13,6 +16,9 @@
|
||||
timeouts {
|
||||
idle 30s
|
||||
}
|
||||
protocol {
|
||||
strict_sni_host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +52,8 @@ http://bar.com {
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
],
|
||||
"strict_sni_host": true
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
@@ -70,7 +77,8 @@ http://bar.com {
|
||||
"listen": [
|
||||
":8080"
|
||||
],
|
||||
"idle_timeout": 90000000000
|
||||
"idle_timeout": 90000000000,
|
||||
"strict_sni_host": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
servers {
|
||||
listener_wrappers {
|
||||
http_redirect
|
||||
tls
|
||||
}
|
||||
timeouts {
|
||||
@@ -10,6 +11,7 @@
|
||||
idle 30s
|
||||
}
|
||||
max_header_size 100MB
|
||||
log_credentials
|
||||
protocol {
|
||||
allow_h2c
|
||||
experimental_http3
|
||||
@@ -31,6 +33,9 @@ foo.com {
|
||||
":443"
|
||||
],
|
||||
"listener_wrappers": [
|
||||
{
|
||||
"wrapper": "http_redirect"
|
||||
},
|
||||
{
|
||||
"wrapper": "tls"
|
||||
}
|
||||
@@ -53,6 +58,9 @@ foo.com {
|
||||
}
|
||||
],
|
||||
"strict_sni_host": true,
|
||||
"logs": {
|
||||
"should_log_credentials": true
|
||||
},
|
||||
"experimental_http3": true,
|
||||
"allow_h2c": true
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
header @images {
|
||||
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
|
||||
}
|
||||
header {
|
||||
+Link "Foo"
|
||||
+Link "Bar"
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
@@ -121,6 +125,17 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "headers",
|
||||
"response": {
|
||||
"add": {
|
||||
"Link": [
|
||||
"Foo",
|
||||
"Bar"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Issue #4113
|
||||
:80, http://example.com {
|
||||
respond "foo"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "foo",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
(foo) {
|
||||
respond {env.FOO}
|
||||
}
|
||||
|
||||
:80 {
|
||||
import foo
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "{env.FOO}",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,24 @@ log {
|
||||
format filter {
|
||||
wrap console
|
||||
fields {
|
||||
uri query {
|
||||
replace foo REDACTED
|
||||
delete bar
|
||||
hash baz
|
||||
}
|
||||
request>headers>Authorization replace REDACTED
|
||||
request>headers>Server delete
|
||||
request>remote_addr ip_mask {
|
||||
request>headers>Cookie cookie {
|
||||
replace foo REDACTED
|
||||
delete bar
|
||||
hash baz
|
||||
}
|
||||
request>remote_ip ip_mask {
|
||||
ipv4 24
|
||||
ipv6 32
|
||||
}
|
||||
request>headers>Regexp regexp secret REDACTED
|
||||
request>headers>Hash hash
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,13 +45,57 @@ log {
|
||||
"filter": "replace",
|
||||
"value": "REDACTED"
|
||||
},
|
||||
"request\u003eheaders\u003eCookie": {
|
||||
"actions": [
|
||||
{
|
||||
"name": "foo",
|
||||
"type": "replace",
|
||||
"value": "REDACTED"
|
||||
},
|
||||
{
|
||||
"name": "bar",
|
||||
"type": "delete"
|
||||
},
|
||||
{
|
||||
"name": "baz",
|
||||
"type": "hash"
|
||||
}
|
||||
],
|
||||
"filter": "cookie"
|
||||
},
|
||||
"request\u003eheaders\u003eHash": {
|
||||
"filter": "hash"
|
||||
},
|
||||
"request\u003eheaders\u003eRegexp": {
|
||||
"filter": "regexp",
|
||||
"regexp": "secret",
|
||||
"value": "REDACTED"
|
||||
},
|
||||
"request\u003eheaders\u003eServer": {
|
||||
"filter": "delete"
|
||||
},
|
||||
"request\u003eremote_addr": {
|
||||
"request\u003eremote_ip": {
|
||||
"filter": "ip_mask",
|
||||
"ipv4_cidr": 24,
|
||||
"ipv6_cidr": 32
|
||||
},
|
||||
"uri": {
|
||||
"actions": [
|
||||
{
|
||||
"parameter": "foo",
|
||||
"type": "replace",
|
||||
"value": "REDACTED"
|
||||
},
|
||||
{
|
||||
"parameter": "bar",
|
||||
"type": "delete"
|
||||
},
|
||||
{
|
||||
"parameter": "baz",
|
||||
"type": "hash"
|
||||
}
|
||||
],
|
||||
"filter": "query"
|
||||
}
|
||||
},
|
||||
"format": "filter",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
log {
|
||||
output file /var/log/access.log {
|
||||
roll_size 1gb
|
||||
roll_uncompressed
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 90d
|
||||
}
|
||||
@@ -20,8 +22,10 @@ log {
|
||||
"writer": {
|
||||
"filename": "/var/log/access.log",
|
||||
"output": "file",
|
||||
"roll_gzip": false,
|
||||
"roll_keep": 5,
|
||||
"roll_keep_days": 90,
|
||||
"roll_local_time": true,
|
||||
"roll_size_mb": 954
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
one.example.com {
|
||||
log
|
||||
}
|
||||
|
||||
two.example.com {
|
||||
}
|
||||
|
||||
three.example.com {
|
||||
}
|
||||
|
||||
example.com {
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"three.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"one.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"two.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"logs": {
|
||||
"skip_hosts": [
|
||||
"three.example.com",
|
||||
"two.example.com",
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
example.com
|
||||
|
||||
map {host} {my_placeholder} {magic_number} {
|
||||
# Should output boolean "true" and an integer
|
||||
example.com true 3
|
||||
|
||||
# Should output a string and null
|
||||
foo.example.com "string value"
|
||||
|
||||
# Should output two strings (quoted int)
|
||||
(.*)\.example.com "${1} subdomain" "5"
|
||||
|
||||
# Should output null and a string (quoted int)
|
||||
~.*\.net$ - `7`
|
||||
|
||||
# Should output a float and the string "false"
|
||||
~.*\.xyz$ 123.456 "false"
|
||||
|
||||
# Should output two strings, second being escaped quote
|
||||
default "unknown domain" \"""
|
||||
}
|
||||
|
||||
vars foo bar
|
||||
vars {
|
||||
abc true
|
||||
def 1
|
||||
ghi 2.3
|
||||
jkl "mn op"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"defaults": [
|
||||
"unknown domain",
|
||||
"\""
|
||||
],
|
||||
"destinations": [
|
||||
"{my_placeholder}",
|
||||
"{magic_number}"
|
||||
],
|
||||
"handler": "map",
|
||||
"mappings": [
|
||||
{
|
||||
"input": "example.com",
|
||||
"outputs": [
|
||||
true,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"input": "foo.example.com",
|
||||
"outputs": [
|
||||
"string value",
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"input": "(.*)\\.example.com",
|
||||
"outputs": [
|
||||
"${1} subdomain",
|
||||
"5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"input_regexp": ".*\\.net$",
|
||||
"outputs": [
|
||||
null,
|
||||
"7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"input_regexp": ".*\\.xyz$",
|
||||
"outputs": [
|
||||
123.456,
|
||||
"false"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": "{http.request.host}"
|
||||
},
|
||||
{
|
||||
"foo": "bar",
|
||||
"handler": "vars"
|
||||
},
|
||||
{
|
||||
"abc": true,
|
||||
"def": 1,
|
||||
"ghi": 2.3,
|
||||
"handler": "vars",
|
||||
"jkl": "mn op"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,9 @@
|
||||
"match": [
|
||||
{
|
||||
"vars": {
|
||||
"{http.request.uri}": "/vars-matcher"
|
||||
"{http.request.uri}": [
|
||||
"/vars-matcher"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
:8080 {
|
||||
method FOO
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8080"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "rewrite",
|
||||
"method": "FOO"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
:8881 {
|
||||
php_fastcgi app:9000 {
|
||||
env FOO bar
|
||||
|
||||
@error status 4xx
|
||||
handle_response @error {
|
||||
root * /errors
|
||||
rewrite * /{http.reverse_proxy.status_code}.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8881"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"file": {
|
||||
"try_files": [
|
||||
"{http.request.uri.path}/index.php"
|
||||
]
|
||||
},
|
||||
"not": [
|
||||
{
|
||||
"path": [
|
||||
"*/"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.uri.path}/"
|
||||
]
|
||||
},
|
||||
"status_code": 308
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"file": {
|
||||
"try_files": [
|
||||
"{http.request.uri.path}",
|
||||
"{http.request.uri.path}/index.php",
|
||||
"index.php"
|
||||
],
|
||||
"split_path": [
|
||||
".php"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "rewrite",
|
||||
"uri": "{http.matchers.file.relative}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"*.php"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handle_response": [
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
4
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "vars",
|
||||
"root": "/errors"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group0",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "rewrite",
|
||||
"uri": "/{http.reverse_proxy.status_code}.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "file_server",
|
||||
"hide": [
|
||||
"./Caddyfile"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"env": {
|
||||
"FOO": "bar"
|
||||
},
|
||||
"protocol": "fastcgi",
|
||||
"split_path": [
|
||||
".php"
|
||||
]
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:9000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
:8884
|
||||
|
||||
php_fastcgi localhost:9000 {
|
||||
# some php_fastcgi-specific subdirectives
|
||||
split .php .php5
|
||||
env VAR1 value1
|
||||
env VAR2 value2
|
||||
root /var/www
|
||||
try_files {path} {path}/index.php =404
|
||||
dial_timeout 3s
|
||||
read_timeout 10s
|
||||
write_timeout 20s
|
||||
|
||||
# passed through to reverse_proxy (directive order doesn't matter!)
|
||||
lb_policy random
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8884"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"file": {
|
||||
"try_files": [
|
||||
"{http.request.uri.path}/index.php"
|
||||
]
|
||||
},
|
||||
"not": [
|
||||
{
|
||||
"path": [
|
||||
"*/"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.uri.path}/"
|
||||
]
|
||||
},
|
||||
"status_code": 308
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"file": {
|
||||
"try_files": [
|
||||
"{http.request.uri.path}",
|
||||
"{http.request.uri.path}/index.php",
|
||||
"=404"
|
||||
],
|
||||
"split_path": [
|
||||
".php",
|
||||
".php5"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "rewrite",
|
||||
"uri": "{http.matchers.file.relative}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"*.php",
|
||||
"*.php5"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"load_balancing": {
|
||||
"selection_policy": {
|
||||
"policy": "random"
|
||||
}
|
||||
},
|
||||
"transport": {
|
||||
"dial_timeout": 3000000000,
|
||||
"env": {
|
||||
"VAR1": "value1",
|
||||
"VAR2": "value2"
|
||||
},
|
||||
"protocol": "fastcgi",
|
||||
"read_timeout": 10000000000,
|
||||
"root": "/var/www",
|
||||
"split_path": [
|
||||
".php",
|
||||
".php5"
|
||||
],
|
||||
"write_timeout": 20000000000
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:9000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
:8884 {
|
||||
reverse_proxy {
|
||||
dynamic a foo 9000
|
||||
}
|
||||
|
||||
reverse_proxy {
|
||||
dynamic a {
|
||||
name foo
|
||||
port 9000
|
||||
refresh 5m
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
dial_timeout 2s
|
||||
dial_fallback_delay 300ms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:8885 {
|
||||
reverse_proxy {
|
||||
dynamic srv _api._tcp.example.com
|
||||
}
|
||||
|
||||
reverse_proxy {
|
||||
dynamic srv {
|
||||
service api
|
||||
proto tcp
|
||||
name example.com
|
||||
refresh 5m
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
dial_timeout 1s
|
||||
dial_fallback_delay -1s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8884"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"dynamic_upstreams": {
|
||||
"name": "foo",
|
||||
"port": "9000",
|
||||
"source": "a"
|
||||
},
|
||||
"handler": "reverse_proxy"
|
||||
},
|
||||
{
|
||||
"dynamic_upstreams": {
|
||||
"dial_fallback_delay": 300000000,
|
||||
"dial_timeout": 2000000000,
|
||||
"name": "foo",
|
||||
"port": "9000",
|
||||
"refresh": 300000000000,
|
||||
"resolver": {
|
||||
"addresses": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
},
|
||||
"source": "a"
|
||||
},
|
||||
"handler": "reverse_proxy"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":8885"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"dynamic_upstreams": {
|
||||
"name": "_api._tcp.example.com",
|
||||
"source": "srv"
|
||||
},
|
||||
"handler": "reverse_proxy"
|
||||
},
|
||||
{
|
||||
"dynamic_upstreams": {
|
||||
"dial_fallback_delay": -1000000000,
|
||||
"dial_timeout": 1000000000,
|
||||
"name": "example.com",
|
||||
"proto": "tcp",
|
||||
"refresh": 300000000000,
|
||||
"resolver": {
|
||||
"addresses": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
},
|
||||
"service": "api",
|
||||
"source": "srv"
|
||||
},
|
||||
"handler": "reverse_proxy"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
:8884
|
||||
|
||||
reverse_proxy 127.0.0.1:65535 {
|
||||
@changeStatus status 500
|
||||
replace_status @changeStatus 400
|
||||
|
||||
@accel header X-Accel-Redirect *
|
||||
handle_response @accel {
|
||||
respond "Header X-Accel-Redirect!"
|
||||
}
|
||||
|
||||
@another {
|
||||
header X-Another *
|
||||
}
|
||||
handle_response @another {
|
||||
respond "Header X-Another!"
|
||||
}
|
||||
|
||||
@401 status 401
|
||||
handle_response @401 {
|
||||
respond "Status 401!"
|
||||
}
|
||||
|
||||
handle_response {
|
||||
respond "Any! This should be last in the JSON!"
|
||||
}
|
||||
|
||||
@403 {
|
||||
status 403
|
||||
}
|
||||
handle_response @403 {
|
||||
respond "Status 403!"
|
||||
}
|
||||
|
||||
@multi {
|
||||
status 401 403
|
||||
status 404
|
||||
header Foo *
|
||||
header Bar *
|
||||
}
|
||||
handle_response @multi {
|
||||
respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
|
||||
}
|
||||
|
||||
@200 status 200
|
||||
handle_response @200 {
|
||||
copy_response_headers {
|
||||
include Foo Bar
|
||||
}
|
||||
respond "Copied headers from the response"
|
||||
}
|
||||
|
||||
@201 status 201
|
||||
handle_response @201 {
|
||||
header Foo "Copying the response"
|
||||
copy_response 404
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8884"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handle_response": [
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
500
|
||||
]
|
||||
},
|
||||
"status_code": 400
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"headers": {
|
||||
"X-Accel-Redirect": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Header X-Accel-Redirect!",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"headers": {
|
||||
"X-Another": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Header X-Another!",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
401
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Status 401!",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
403
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Status 403!",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"headers": {
|
||||
"Bar": [
|
||||
"*"
|
||||
],
|
||||
"Foo": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"status_code": [
|
||||
401,
|
||||
403,
|
||||
404
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
200
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "copy_response_headers",
|
||||
"include": [
|
||||
"Foo",
|
||||
"Bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"body": "Copied headers from the response",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
201
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "headers",
|
||||
"response": {
|
||||
"set": {
|
||||
"Foo": [
|
||||
"Copying the response"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "copy_response",
|
||||
"status_code": 404
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "Any! This should be last in the JSON!",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "127.0.0.1:65535"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
|
||||
X-Empty-Value
|
||||
}
|
||||
health_uri /health
|
||||
}
|
||||
----------
|
||||
{
|
||||
@@ -38,7 +39,8 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
"VbG4NZwWnipo",
|
||||
"335Q9/MhqcNU3s2TO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uri": "/health"
|
||||
}
|
||||
},
|
||||
"upstreams": [
|
||||
|
||||
@@ -17,11 +17,13 @@ https://example.com {
|
||||
dial_fallback_delay 5s
|
||||
response_header_timeout 8s
|
||||
expect_continue_timeout 9s
|
||||
resolvers 8.8.8.8 8.8.4.4
|
||||
|
||||
versions h2c 2
|
||||
compression off
|
||||
max_conns_per_host 5
|
||||
max_idle_conns_per_host 2
|
||||
keepalive_idle_conns_per_host 2
|
||||
keepalive_interval 30s
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,11 +81,20 @@ https://example.com {
|
||||
"dial_fallback_delay": 5000000000,
|
||||
"dial_timeout": 3000000000,
|
||||
"expect_continue_timeout": 9000000000,
|
||||
"keep_alive": {
|
||||
"max_idle_conns_per_host": 2,
|
||||
"probe_interval": 30000000000
|
||||
},
|
||||
"max_conns_per_host": 5,
|
||||
"max_idle_conns_per_host": 2,
|
||||
"max_response_header_size": 30000000,
|
||||
"protocol": "http",
|
||||
"read_buffer_size": 10000000,
|
||||
"resolver": {
|
||||
"addresses": [
|
||||
"8.8.8.8",
|
||||
"8.8.4.4"
|
||||
]
|
||||
},
|
||||
"response_header_timeout": 8000000000,
|
||||
"versions": [
|
||||
"h2c",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
:8884
|
||||
|
||||
reverse_proxy 127.0.0.1:65535 {
|
||||
trusted_proxies 127.0.0.1
|
||||
}
|
||||
|
||||
reverse_proxy 127.0.0.1:65535 {
|
||||
trusted_proxies private_ranges
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8884"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"trusted_proxies": [
|
||||
"127.0.0.1"
|
||||
],
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "127.0.0.1:65535"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"trusted_proxies": [
|
||||
"192.168.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"10.0.0.0/8",
|
||||
"127.0.0.1/8",
|
||||
"fd00::/8",
|
||||
"::1"
|
||||
],
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "127.0.0.1:65535"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
*.example.com {
|
||||
@foo host foo.example.com
|
||||
handle @foo {
|
||||
handle_path /strip* {
|
||||
respond "this should be first"
|
||||
}
|
||||
handle {
|
||||
respond "this should be second"
|
||||
}
|
||||
}
|
||||
handle {
|
||||
respond "this should be last"
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group5",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "rewrite",
|
||||
"strip_path_prefix": "/strip"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "this should be first",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/strip*"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "this should be second",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"foo.example.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group5",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "this should be last",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
:80
|
||||
|
||||
vars /foobar foo last
|
||||
vars /foo foo middle
|
||||
vars * foo first
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"foo": "first",
|
||||
"handler": "vars"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"foo": "middle",
|
||||
"handler": "vars"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/foobar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"foo": "last",
|
||||
"handler": "vars"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
localhost
|
||||
|
||||
tls {
|
||||
issuer acme {
|
||||
preferred_chains {
|
||||
any_common_name "Generic CA 1" "Generic CA 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "acme",
|
||||
"preferred_chains": {
|
||||
"any_common_name": [
|
||||
"Generic CA 1",
|
||||
"Generic CA 2"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
email my.email@example.com
|
||||
}
|
||||
|
||||
:82 {
|
||||
redir https://example.com{uri}
|
||||
}
|
||||
|
||||
:83 {
|
||||
redir https://example.com{uri}
|
||||
}
|
||||
|
||||
:84 {
|
||||
redir https://example.com{uri}
|
||||
}
|
||||
|
||||
abc.de {
|
||||
redir https://example.com{uri}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"abc.de"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"https://example.com{http.request.uri}"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":82"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"https://example.com{http.request.uri}"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv2": {
|
||||
"listen": [
|
||||
":83"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"https://example.com{http.request.uri}"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv3": {
|
||||
"listen": [
|
||||
":84"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"https://example.com{http.request.uri}"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"email": "my.email@example.com",
|
||||
"module": "acme"
|
||||
},
|
||||
{
|
||||
"email": "my.email@example.com",
|
||||
"module": "zerossl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
a.example.com {
|
||||
}
|
||||
|
||||
b.example.com {
|
||||
}
|
||||
|
||||
:443 {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"a.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"b.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"a.example.com",
|
||||
"b.example.com"
|
||||
]
|
||||
},
|
||||
{
|
||||
"on_demand": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# (this Caddyfile is contrived, but based on issue #4161)
|
||||
|
||||
example.com {
|
||||
tls {
|
||||
ca https://foobar
|
||||
}
|
||||
}
|
||||
|
||||
example.com:8443 {
|
||||
tls {
|
||||
ca https://foobar
|
||||
}
|
||||
}
|
||||
|
||||
example.com:8444 {
|
||||
tls {
|
||||
ca https://foobar
|
||||
}
|
||||
}
|
||||
|
||||
example.com:8445 {
|
||||
tls {
|
||||
ca https://foobar
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":8443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv2": {
|
||||
"listen": [
|
||||
":8444"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv3": {
|
||||
"listen": [
|
||||
":8445"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"ca": "https://foobar",
|
||||
"module": "acme"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# (this Caddyfile is contrived, but based on issues #4176 and #4198)
|
||||
|
||||
http://example.com {
|
||||
}
|
||||
|
||||
https://example.com {
|
||||
tls internal
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"module": "internal"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# (this Caddyfile is contrived, but based on issues #4176 and #4198)
|
||||
|
||||
http://example.com {
|
||||
}
|
||||
|
||||
https://example.com {
|
||||
tls abc@example.com
|
||||
}
|
||||
|
||||
http://localhost:8081 {
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"srv2": {
|
||||
"listen": [
|
||||
":8081"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"automatic_https": {
|
||||
"skip": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"email": "abc@example.com",
|
||||
"module": "acme"
|
||||
},
|
||||
{
|
||||
"email": "abc@example.com",
|
||||
"module": "zerossl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# example from issue #4640
|
||||
http://foo:8447, http://127.0.0.1:8447 {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8447"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"foo",
|
||||
"127.0.0.1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "127.0.0.1:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"automatic_https": {
|
||||
"skip": [
|
||||
"foo",
|
||||
"127.0.0.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
email foo@bar
|
||||
}
|
||||
|
||||
localhost {
|
||||
}
|
||||
|
||||
example.com {
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"email": "foo@bar",
|
||||
"module": "acme"
|
||||
},
|
||||
{
|
||||
"email": "foo@bar",
|
||||
"module": "zerossl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
a.example.com {
|
||||
tls {
|
||||
issuer internal {
|
||||
ca foo
|
||||
lifetime 24h
|
||||
sign_with_root
|
||||
}
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"a.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"a.example.com"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"ca": "foo",
|
||||
"lifetime": 86400000000000,
|
||||
"module": "internal",
|
||||
"sign_with_root": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
localhost
|
||||
|
||||
respond "hello from localhost"
|
||||
tls {
|
||||
issuer acme {
|
||||
propagation_delay 5m10s
|
||||
propagation_timeout 10m20s
|
||||
}
|
||||
issuer zerossl {
|
||||
propagation_delay 5m30s
|
||||
propagation_timeout -1
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "hello from localhost",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"localhost"
|
||||
],
|
||||
"issuers": [
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"propagation_delay": 310000000000,
|
||||
"propagation_timeout": 620000000000
|
||||
}
|
||||
},
|
||||
"module": "acme"
|
||||
},
|
||||
{
|
||||
"challenges": {
|
||||
"dns": {
|
||||
"propagation_delay": 330000000000,
|
||||
"propagation_timeout": -1
|
||||
}
|
||||
},
|
||||
"module": "zerossl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
:80 {
|
||||
tracing /myhandler {
|
||||
span my-span
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/myhandler"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "tracing",
|
||||
"span": "my-span"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package integration
|
||||
import (
|
||||
jsonMod "encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestCaddyfileAdaptToJSON(t *testing.T) {
|
||||
// load the list of test files from the dir
|
||||
files, err := ioutil.ReadDir("./caddyfile_adapt")
|
||||
files, err := os.ReadDir("./caddyfile_adapt")
|
||||
if err != nil {
|
||||
t.Errorf("failed to read caddyfile_adapt dir: %s", err)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func TestCaddyfileAdaptToJSON(t *testing.T) {
|
||||
|
||||
// read the test file
|
||||
filename := f.Name()
|
||||
data, err := ioutil.ReadFile("./caddyfile_adapt/" + filename)
|
||||
data, err := os.ReadFile("./caddyfile_adapt/" + filename)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read %s dir: %s", filename, err)
|
||||
}
|
||||
|
||||
@@ -101,3 +101,27 @@ func TestReadCookie(t *testing.T) {
|
||||
// act and assert
|
||||
tester.AssertGetResponse("http://localhost:9080/cookie.html", 200, "<h2>Cookie.ClientName caddytest</h2>")
|
||||
}
|
||||
|
||||
func TestReplIndex(t *testing.T) {
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
templates {
|
||||
root testdata
|
||||
}
|
||||
file_server {
|
||||
root testdata
|
||||
index "index.{host}.html"
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// act and assert
|
||||
tester.AssertGetResponse("http://localhost:9080/", 200, "")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -85,7 +84,7 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
f, err := ioutil.TempFile("", "*.sock")
|
||||
f, err := os.CreateTemp("", "*.sock")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create TempFile: %s", err)
|
||||
return
|
||||
@@ -371,7 +370,7 @@ func TestReverseProxyHealthCheck(t *testing.T) {
|
||||
reverse_proxy {
|
||||
to localhost:2020
|
||||
|
||||
health_path /health
|
||||
health_uri /health
|
||||
health_port 2021
|
||||
health_interval 2s
|
||||
health_timeout 5s
|
||||
@@ -387,7 +386,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
|
||||
t.SkipNow()
|
||||
}
|
||||
tester := caddytest.NewTester(t)
|
||||
f, err := ioutil.TempFile("", "*.sock")
|
||||
f, err := os.CreateTemp("", "*.sock")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create TempFile: %s", err)
|
||||
return
|
||||
@@ -426,7 +425,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
|
||||
reverse_proxy {
|
||||
to unix/%s
|
||||
|
||||
health_path /health
|
||||
health_uri /health
|
||||
health_port 2021
|
||||
health_interval 2s
|
||||
health_timeout 5s
|
||||
@@ -436,3 +435,57 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
|
||||
|
||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||
}
|
||||
|
||||
func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.SkipNow()
|
||||
}
|
||||
tester := caddytest.NewTester(t)
|
||||
f, err := os.CreateTemp("", "*.sock")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create TempFile: %s", err)
|
||||
return
|
||||
}
|
||||
// a hack to get a file name within a valid path to use as socket
|
||||
socketName := f.Name()
|
||||
os.Remove(f.Name())
|
||||
|
||||
server := http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if strings.HasPrefix(req.URL.Path, "/health") {
|
||||
w.Write([]byte("ok"))
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Hello, World!"))
|
||||
}),
|
||||
}
|
||||
|
||||
unixListener, err := net.Listen("unix", socketName)
|
||||
if err != nil {
|
||||
t.Errorf("failed to listen on the socket: %s", err)
|
||||
return
|
||||
}
|
||||
go server.Serve(unixListener)
|
||||
t.Cleanup(func() {
|
||||
server.Close()
|
||||
})
|
||||
runtime.Gosched() // Allow other goroutines to run
|
||||
|
||||
tester.InitServer(fmt.Sprintf(`
|
||||
{
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
}
|
||||
http://localhost:9080 {
|
||||
reverse_proxy {
|
||||
to unix/%s
|
||||
|
||||
health_uri /health
|
||||
health_interval 2s
|
||||
health_timeout 5s
|
||||
}
|
||||
}
|
||||
`, socketName), "caddyfile")
|
||||
|
||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -110,7 +109,7 @@ func TestH2ToH2CStream(t *testing.T) {
|
||||
r, w := io.Pipe()
|
||||
req := &http.Request{
|
||||
Method: "PUT",
|
||||
Body: ioutil.NopCloser(r),
|
||||
Body: io.NopCloser(r),
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "127.0.0.1:9443",
|
||||
@@ -134,7 +133,7 @@ func TestH2ToH2CStream(t *testing.T) {
|
||||
}()
|
||||
|
||||
defer resp.Body.Close()
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read the response body %s", err)
|
||||
}
|
||||
@@ -319,7 +318,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
|
||||
r, w := io.Pipe()
|
||||
req := &http.Request{
|
||||
Method: "PUT",
|
||||
Body: ioutil.NopCloser(r),
|
||||
Body: io.NopCloser(r),
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "127.0.0.1:9443",
|
||||
@@ -342,7 +341,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
bytes, err := ioutil.ReadAll(resp.Body)
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read the response body %s", err)
|
||||
}
|
||||
@@ -370,7 +369,7 @@ func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
bytes, err := ioutil.ReadAll(r.Body)
|
||||
bytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read the response body %s", err)
|
||||
}
|
||||
|
||||
+127
-266
@@ -19,21 +19,20 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@@ -45,6 +44,7 @@ func cmdStart(fl Flags) (int, error) {
|
||||
startCmdConfigAdapterFlag := fl.String("adapter")
|
||||
startCmdPidfileFlag := fl.String("pidfile")
|
||||
startCmdWatchFlag := fl.Bool("watch")
|
||||
startCmdEnvfileFlag := fl.String("envfile")
|
||||
|
||||
// open a listener to which the child process will connect when
|
||||
// it is ready to confirm that it has successfully started
|
||||
@@ -68,6 +68,9 @@ func cmdStart(fl Flags) (int, error) {
|
||||
if startCmdConfigFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
|
||||
}
|
||||
if startCmdEnvfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
|
||||
}
|
||||
if startCmdConfigAdapterFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
|
||||
}
|
||||
@@ -117,7 +120,7 @@ func cmdStart(fl Flags) (int, error) {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
if !errors.Is(err, net.ErrClosed) {
|
||||
log.Println(err)
|
||||
}
|
||||
break
|
||||
@@ -178,7 +181,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
var config []byte
|
||||
var err error
|
||||
if runCmdResumeFlag {
|
||||
config, err = ioutil.ReadFile(caddy.ConfigAutosavePath)
|
||||
config, err = os.ReadFile(caddy.ConfigAutosavePath)
|
||||
if os.IsNotExist(err) {
|
||||
// not a bad error; just can't resume if autosave file doesn't exist
|
||||
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
@@ -200,7 +203,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
if !runCmdResumeFlag {
|
||||
config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
||||
config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -216,7 +219,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
// if we are to report to another process the successful start
|
||||
// of the server, do so now by echoing back contents of stdin
|
||||
if runCmdPingbackFlag != "" {
|
||||
confirmationBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
confirmationBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
|
||||
@@ -269,42 +272,37 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := NotifyReadiness(); err != nil {
|
||||
caddy.Log().Error("unable to notify readiness to service manager", zap.Error(err))
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func cmdStop(fl Flags) (int, error) {
|
||||
stopCmdAddrFlag := fl.String("address")
|
||||
addrFlag := fl.String("address")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
|
||||
err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil, nil)
|
||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||
}
|
||||
|
||||
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
|
||||
if err != nil {
|
||||
caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdReload(fl Flags) (int, error) {
|
||||
reloadCmdConfigFlag := fl.String("config")
|
||||
reloadCmdConfigAdapterFlag := fl.String("adapter")
|
||||
reloadCmdAddrFlag := fl.String("address")
|
||||
reloadCmdForceFlag := fl.Bool("force")
|
||||
|
||||
if err := NotifyReloading(); err != nil {
|
||||
caddy.Log().Error("unable to notify reloading to service manager", zap.Error(err))
|
||||
}
|
||||
defer func() {
|
||||
if err := NotifyReadiness(); err != nil {
|
||||
caddy.Log().Error("unable to notify readiness to service manager", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
addrFlag := fl.String("address")
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
|
||||
config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -312,36 +310,28 @@ func cmdReload(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||
}
|
||||
|
||||
// get the address of the admin listener; use flag if specified
|
||||
adminAddr := reloadCmdAddrFlag
|
||||
if adminAddr == "" && len(config) > 0 {
|
||||
var tmpStruct struct {
|
||||
Admin caddy.AdminConfig `json:"admin"`
|
||||
}
|
||||
err = json.Unmarshal(config, &tmpStruct)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("unmarshaling admin listener address from config: %v", err)
|
||||
}
|
||||
adminAddr = tmpStruct.Admin.Listen
|
||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||
}
|
||||
|
||||
// optionally force a config reload
|
||||
headers := make(http.Header)
|
||||
if reloadCmdForceFlag {
|
||||
if forceFlag {
|
||||
headers.Set("Cache-Control", "must-revalidate")
|
||||
}
|
||||
|
||||
err = apiRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
|
||||
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdVersion(_ Flags) (int, error) {
|
||||
fmt.Println(caddyVersion())
|
||||
fmt.Println(CaddyVersion())
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
@@ -371,6 +361,7 @@ func cmdBuildInfo(fl Flags) (int, error) {
|
||||
func cmdListModules(fl Flags) (int, error) {
|
||||
packages := fl.Bool("packages")
|
||||
versions := fl.Bool("versions")
|
||||
skipStandard := fl.Bool("skip-standard")
|
||||
|
||||
printModuleInfo := func(mi moduleInfo) {
|
||||
fmt.Print(mi.caddyModuleID)
|
||||
@@ -399,14 +390,19 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
if len(standard) > 0 {
|
||||
for _, mod := range standard {
|
||||
printModuleInfo(mod)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n Standard modules: %d\n", len(standard))
|
||||
if len(nonstandard) > 0 {
|
||||
// Standard modules (always shipped with Caddy)
|
||||
if !skipStandard {
|
||||
if len(standard) > 0 {
|
||||
for _, mod := range standard {
|
||||
printModuleInfo(mod)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n Standard modules: %d\n", len(standard))
|
||||
}
|
||||
|
||||
// Non-standard modules (third party plugins)
|
||||
if len(nonstandard) > 0 {
|
||||
if len(standard) > 0 && !skipStandard {
|
||||
fmt.Println()
|
||||
}
|
||||
for _, mod := range nonstandard {
|
||||
@@ -414,8 +410,10 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
|
||||
|
||||
// Unknown modules (couldn't get Caddy module info)
|
||||
if len(unknown) > 0 {
|
||||
if len(standard) > 0 || len(nonstandard) > 0 {
|
||||
if (len(standard) > 0 && !skipStandard) || len(nonstandard) > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
for _, mod := range unknown {
|
||||
@@ -467,7 +465,7 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
|
||||
}
|
||||
|
||||
input, err := ioutil.ReadFile(adaptCmdInputFlag)
|
||||
input, err := os.ReadFile(adaptCmdInputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
@@ -498,7 +496,9 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
if warn.Directive != "" {
|
||||
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
|
||||
caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
|
||||
zap.String("file", warn.File),
|
||||
zap.Int("line", warn.Line))
|
||||
}
|
||||
|
||||
// validate output if requested
|
||||
@@ -521,7 +521,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
validateCmdConfigFlag := fl.String("config")
|
||||
validateCmdAdapterFlag := fl.String("adapter")
|
||||
|
||||
input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
|
||||
input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -551,7 +551,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
|
||||
// as a special case, read from stdin if the file name is "-"
|
||||
if formatCmdConfigFile == "-" {
|
||||
input, err := ioutil.ReadAll(os.Stdin)
|
||||
input, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading stdin: %v", err)
|
||||
@@ -560,7 +560,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
input, err := ioutil.ReadFile(formatCmdConfigFile)
|
||||
input, err := os.ReadFile(formatCmdConfigFile)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
@@ -569,8 +569,22 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
output := caddyfile.Format(input)
|
||||
|
||||
if fl.Bool("overwrite") {
|
||||
if err := ioutil.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, nil
|
||||
if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
||||
}
|
||||
} else if fl.Bool("diff") {
|
||||
diff := difflib.Diff(
|
||||
strings.Split(string(input), "\n"),
|
||||
strings.Split(string(output), "\n"))
|
||||
for _, d := range diff {
|
||||
switch d.Delta {
|
||||
case difflib.Common:
|
||||
fmt.Printf(" %s\n", d.Payload)
|
||||
case difflib.LeftOnly:
|
||||
fmt.Printf("- %s\n", d.Payload)
|
||||
case difflib.RightOnly:
|
||||
fmt.Printf("+ %s\n", d.Payload)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Print(string(output))
|
||||
@@ -579,147 +593,6 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdUpgrade(_ Flags) (int, error) {
|
||||
l := caddy.Log()
|
||||
|
||||
thisExecPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
|
||||
}
|
||||
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
|
||||
|
||||
// get the list of nonstandard plugins
|
||||
_, nonstandard, _, err := getModules()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
|
||||
}
|
||||
pluginPkgs := make(map[string]struct{})
|
||||
for _, mod := range nonstandard {
|
||||
if mod.goModule.Replace != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
|
||||
mod.goModule.Path, mod.goModule.Replace.Path)
|
||||
}
|
||||
l.Info("found non-standard module",
|
||||
zap.String("id", mod.caddyModuleID),
|
||||
zap.String("package", mod.goModule.Path))
|
||||
pluginPkgs[mod.goModule.Path] = struct{}{}
|
||||
}
|
||||
|
||||
// build the request URL to download this custom build
|
||||
qs := url.Values{
|
||||
"os": {runtime.GOOS},
|
||||
"arch": {runtime.GOARCH},
|
||||
}
|
||||
for pkg := range pluginPkgs {
|
||||
qs.Add("p", pkg)
|
||||
}
|
||||
urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
|
||||
|
||||
// initiate the build
|
||||
l.Info("requesting build",
|
||||
zap.String("os", qs.Get("os")),
|
||||
zap.String("arch", qs.Get("arch")),
|
||||
zap.Strings("packages", qs["p"]))
|
||||
resp, err := http.Get(urlStr)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
var details struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
} `json:"error"`
|
||||
}
|
||||
err2 := json.NewDecoder(resp.Body).Decode(&details)
|
||||
if err2 != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
|
||||
}
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
|
||||
}
|
||||
|
||||
// back up the current binary, in case something goes wrong we can replace it
|
||||
backupExecPath := thisExecPath + ".tmp"
|
||||
l.Info("build acquired; backing up current executable",
|
||||
zap.String("current_path", thisExecPath),
|
||||
zap.String("backup_path", backupExecPath))
|
||||
err = os.Rename(thisExecPath, backupExecPath)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err2 := os.Rename(backupExecPath, thisExecPath)
|
||||
if err2 != nil {
|
||||
l.Error("restoring original executable failed; will need to be restored manually",
|
||||
zap.String("backup_path", backupExecPath),
|
||||
zap.String("original_path", thisExecPath),
|
||||
zap.Error(err2))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// download the file; do this in a closure to close reliably before we execute it
|
||||
writeFile := func() error {
|
||||
destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0770)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open destination file: %v", err)
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
|
||||
|
||||
_, err = io.Copy(destFile, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to download file: %v", err)
|
||||
}
|
||||
|
||||
err = destFile.Sync()
|
||||
if err != nil {
|
||||
return fmt.Errorf("syncing downloaded file to device: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
err = writeFile()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
|
||||
|
||||
// use the new binary to print out version and module info
|
||||
fmt.Print("\nModule versions:\n\n")
|
||||
cmd := exec.Command(thisExecPath, "list-modules", "--versions")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
|
||||
}
|
||||
fmt.Println("\nVersion:")
|
||||
cmd = exec.Command(thisExecPath, "version")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// clean up the backup file
|
||||
err = os.Remove(backupExecPath)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
|
||||
}
|
||||
|
||||
l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
|
||||
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdHelp(fl Flags) (int, error) {
|
||||
const fullDocs = `Full documentation is available at:
|
||||
https://caddyserver.com/docs/command-line`
|
||||
@@ -784,77 +657,25 @@ commands:
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
err = fmt.Errorf("no build info")
|
||||
return
|
||||
}
|
||||
|
||||
for _, modID := range caddy.Modules() {
|
||||
modInfo, err := caddy.GetModule(modID)
|
||||
if err != nil {
|
||||
// that's weird, shouldn't happen
|
||||
unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
|
||||
continue
|
||||
}
|
||||
|
||||
// to get the Caddy plugin's version info, we need to know
|
||||
// the package that the Caddy module's value comes from; we
|
||||
// can use reflection but we need a non-pointer value (I'm
|
||||
// not sure why), and since New() should return a pointer
|
||||
// value, we need to dereference it first
|
||||
iface := interface{}(modInfo.New())
|
||||
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
|
||||
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
||||
}
|
||||
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
||||
|
||||
// now we find the Go module that the Caddy module's package
|
||||
// belongs to; we assume the Caddy module package path will
|
||||
// be prefixed by its Go module path, and we will choose the
|
||||
// longest matching prefix in case there are nested modules
|
||||
var matched *debug.Module
|
||||
for _, dep := range bi.Deps {
|
||||
if strings.HasPrefix(modPkgPath, dep.Path) {
|
||||
if matched == nil || len(dep.Path) > len(matched.Path) {
|
||||
matched = dep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
|
||||
|
||||
if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
|
||||
standard = append(standard, caddyModGoMod)
|
||||
} else {
|
||||
nonstandard = append(nonstandard, caddyModGoMod)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// apiRequest makes an API request to the endpoint adminAddr with the
|
||||
// given HTTP method and request URI. If body is non-nil, it will be
|
||||
// assumed to be Content-Type application/json.
|
||||
func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) error {
|
||||
// parse the admin address
|
||||
if adminAddr == "" {
|
||||
adminAddr = caddy.DefaultAdminListen
|
||||
}
|
||||
// AdminAPIRequest makes an API request according to the CLI flags given,
|
||||
// with the given HTTP method and request URI. If body is non-nil, it will
|
||||
// be assumed to be Content-Type application/json. The caller should close
|
||||
// the response body. Should only be used by Caddy CLI commands which
|
||||
// need to interact with a running instance of Caddy via the admin API.
|
||||
func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
|
||||
parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
|
||||
if err != nil || parsedAddr.PortRangeSize() > 1 {
|
||||
return fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
|
||||
return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
|
||||
}
|
||||
origin := parsedAddr.JoinHostPort(0)
|
||||
origin := "http://" + parsedAddr.JoinHostPort(0)
|
||||
if parsedAddr.IsUnixNetwork() {
|
||||
origin = "unixsocket" // hack so that http.NewRequest() is happy
|
||||
}
|
||||
|
||||
// form the request
|
||||
req, err := http.NewRequest(method, "http://"+origin+uri, body)
|
||||
req, err := http.NewRequest(method, origin+uri, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making request: %v", err)
|
||||
return nil, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
if parsedAddr.IsUnixNetwork() {
|
||||
// When listening on a unix socket, the admin endpoint doesn't
|
||||
@@ -894,20 +715,60 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("performing request: %v", err)
|
||||
return nil, fmt.Errorf("performing request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// if it didn't work, let the user know
|
||||
if resp.StatusCode >= 400 {
|
||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
|
||||
return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
|
||||
}
|
||||
return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
|
||||
return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
return nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DetermineAdminAPIAddress determines which admin API endpoint address should
|
||||
// be used based on the inputs. By priority: if `address` is specified, then
|
||||
// it is returned; if `configFile` (and `configAdapter`) are specified, then that
|
||||
// config will be loaded to find the admin address; otherwise, the default
|
||||
// admin listen address will be returned.
|
||||
func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) {
|
||||
// Prefer the address if specified and non-empty
|
||||
if address != "" {
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// Try to load the config from file if specified, with the given adapter name
|
||||
if configFile != "" {
|
||||
// get the config in caddy's native format
|
||||
config, loadedConfigFile, err := LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if loadedConfigFile == "" {
|
||||
return "", fmt.Errorf("no config file to load")
|
||||
}
|
||||
|
||||
// get the address of the admin listener if set
|
||||
if len(config) > 0 {
|
||||
var tmpStruct struct {
|
||||
Admin caddy.AdminConfig `json:"admin"`
|
||||
}
|
||||
err = json.Unmarshal(config, &tmpStruct)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
|
||||
}
|
||||
if tmpStruct.Admin.Listen != "" {
|
||||
return tmpStruct.Admin.Listen, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to the default listen address otherwise
|
||||
return caddy.DefaultAdminListen, nil
|
||||
}
|
||||
|
||||
type moduleInfo struct {
|
||||
|
||||
+64
-5
@@ -61,6 +61,12 @@ type Command struct {
|
||||
// any error that occurred.
|
||||
type CommandFunc func(Flags) (int, error)
|
||||
|
||||
// Commands returns a list of commands initialised by
|
||||
// RegisterCommand
|
||||
func Commands() map[string]Command {
|
||||
return commands
|
||||
}
|
||||
|
||||
var commands = make(map[string]Command)
|
||||
|
||||
func init() {
|
||||
@@ -74,18 +80,22 @@ func init() {
|
||||
RegisterCommand(Command{
|
||||
Name: "start",
|
||||
Func: cmdStart,
|
||||
Usage: "[--config <path> [--adapter <name>]] [--watch] [--pidfile <file>]",
|
||||
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]",
|
||||
Short: "Starts the Caddy process in the background and then returns",
|
||||
Long: `
|
||||
Starts the Caddy process, optionally bootstrapped with an initial config file.
|
||||
This command unblocks after the server starts running or fails to run.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
On Windows, the spawned child process will remain attached to the terminal, so
|
||||
closing the window will forcefully stop Caddy; to avoid forgetting this, try
|
||||
using 'caddy run' instead to keep it in the foreground.`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("start", flag.ExitOnError)
|
||||
fs.String("config", "", "Configuration file")
|
||||
fs.String("envfile", "", "Environment file to load")
|
||||
fs.String("adapter", "", "Name of config adapter to apply")
|
||||
fs.String("pidfile", "", "Path of file to which to write process ID")
|
||||
fs.Bool("watch", false, "Reload changed config file automatically")
|
||||
@@ -146,16 +156,19 @@ development environment.`,
|
||||
RegisterCommand(Command{
|
||||
Name: "stop",
|
||||
Func: cmdStop,
|
||||
Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
|
||||
Short: "Gracefully stops a started Caddy process",
|
||||
Long: `
|
||||
Stops the background Caddy process as gracefully as possible.
|
||||
|
||||
It requires that the admin API is enabled and accessible, since it will
|
||||
use the API's /stop endpoint. The address of this request can be
|
||||
customized using the --address flag if it is not the default.`,
|
||||
use the API's /stop endpoint. The address of this request can be customized
|
||||
using the --address flag, or from the given --config, if not the default.`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("stop", flag.ExitOnError)
|
||||
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
|
||||
fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used")
|
||||
fs.String("adapter", "", "Name of config adapter to apply (when --config is used)")
|
||||
return fs
|
||||
}(),
|
||||
})
|
||||
@@ -198,6 +211,7 @@ config file; otherwise the default is assumed.`,
|
||||
fs := flag.NewFlagSet("list-modules", flag.ExitOnError)
|
||||
fs.Bool("packages", false, "Print package paths")
|
||||
fs.Bool("versions", false, "Print version information")
|
||||
fs.Bool("skip-standard", false, "Skip printing standard modules")
|
||||
return fs
|
||||
}(),
|
||||
})
|
||||
@@ -249,7 +263,7 @@ Loads and provisions the provided config, but does not start running it.
|
||||
This reveals any errors with the configuration through the loading and
|
||||
provisioning stages.`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("load", flag.ExitOnError)
|
||||
fs := flag.NewFlagSet("validate", flag.ExitOnError)
|
||||
fs.String("config", "", "Input configuration file")
|
||||
fs.String("adapter", "", "Name of config adapter")
|
||||
return fs
|
||||
@@ -268,12 +282,18 @@ human readability. It prints the result to stdout.
|
||||
If --overwrite is specified, the output will be written to the config file
|
||||
directly instead of printing it.
|
||||
|
||||
If --diff is specified, the output will be compared against the input, and
|
||||
lines will be prefixed with '-' and '+' where they differ. Note that
|
||||
unchanged lines are prefixed with two spaces for alignment, and that this
|
||||
is not a valid patch format.
|
||||
|
||||
If you wish you use stdin instead of a regular file, use - as the path.
|
||||
When reading from stdin, the --overwrite flag has no effect: the result
|
||||
is always printed to stdout.`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("format", flag.ExitOnError)
|
||||
fs := flag.NewFlagSet("fmt", flag.ExitOnError)
|
||||
fs.Bool("overwrite", false, "Overwrite the input file with the results")
|
||||
fs.Bool("diff", false, "Print the differences between the input file and the formatted output")
|
||||
return fs
|
||||
}(),
|
||||
})
|
||||
@@ -285,6 +305,45 @@ is always printed to stdout.`,
|
||||
Long: `
|
||||
Downloads an updated Caddy binary with the same modules/plugins at the
|
||||
latest versions. EXPERIMENTAL: May be changed or removed.`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("upgrade", flag.ExitOnError)
|
||||
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
|
||||
return fs
|
||||
}(),
|
||||
})
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "add-package",
|
||||
Func: cmdAddPackage,
|
||||
Usage: "<packages...>",
|
||||
Short: "Adds Caddy packages (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Downloads an updated Caddy binary with the specified packages (module/plugin)
|
||||
added. Retains existing packages. Returns an error if the any of packages are
|
||||
already included. EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("add-package", flag.ExitOnError)
|
||||
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
|
||||
return fs
|
||||
}(),
|
||||
})
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "remove-package",
|
||||
Func: cmdRemovePackage,
|
||||
Usage: "<packages...>",
|
||||
Short: "Removes Caddy packages (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
||||
Returns an error if any of the packages are not included.
|
||||
EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
Flags: func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("remove-package", flag.ExitOnError)
|
||||
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
|
||||
return fs
|
||||
}(),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+58
-28
@@ -20,7 +20,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -94,7 +93,7 @@ func Main() {
|
||||
// the bytes in expect, or returns an error if it doesn't.
|
||||
func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
defer conn.Close()
|
||||
confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32))
|
||||
confirmationBytes, err := io.ReadAll(io.LimitReader(conn, 32))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,15 +103,15 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig loads the config from configFile and adapts it
|
||||
// LoadConfig loads the config from configFile and adapts it
|
||||
// using adapterName. If adapterName is specified, configFile
|
||||
// must be also. If no configFile is specified, it tries
|
||||
// loading a default config file. The lack of a config file is
|
||||
// not treated as an error, but false will be returned if
|
||||
// there is no config available. It prints any warnings to stderr,
|
||||
// and returns the resulting JSON config bytes along with
|
||||
// whether a config file was loaded or not.
|
||||
func loadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// the name of the loaded config file (if any).
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// specifying an adapter without a config file is ambiguous
|
||||
if adapterName != "" && configFile == "" {
|
||||
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
@@ -124,9 +123,9 @@ func loadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
var err error
|
||||
if configFile != "" {
|
||||
if configFile == "-" {
|
||||
config, err = ioutil.ReadAll(os.Stdin)
|
||||
config, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
config, err = ioutil.ReadFile(configFile)
|
||||
config, err = os.ReadFile(configFile)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config file: %v", err)
|
||||
@@ -140,7 +139,7 @@ func loadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// plugged in, and if so, try using a default Caddyfile
|
||||
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
|
||||
if cfgAdapter != nil {
|
||||
config, err = ioutil.ReadFile("Caddyfile")
|
||||
config, err = os.ReadFile("Caddyfile")
|
||||
if os.IsNotExist(err) {
|
||||
// okay, no default Caddyfile; pretend like this never happened
|
||||
cfgAdapter = nil
|
||||
@@ -263,7 +262,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
lastModified = info.ModTime()
|
||||
|
||||
// load the contents of the file
|
||||
config, _, err := loadConfig(filename, adapterName)
|
||||
config, _, err := LoadConfig(filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
continue
|
||||
@@ -361,45 +360,76 @@ func loadEnvFromFile(envFile string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the storage paths to ensure they have the proper
|
||||
// value after loading a specified env file.
|
||||
caddy.ConfigAutosavePath = filepath.Join(caddy.AppConfigDir(), "autosave.json")
|
||||
caddy.DefaultStorage = &certmagic.FileStorage{Path: caddy.AppDataDir()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseEnvFile parses an env file from KEY=VALUE format.
|
||||
// It's pretty naive. Limited value quotation is supported,
|
||||
// but variable and command expansions are not supported.
|
||||
func parseEnvFile(envInput io.Reader) (map[string]string, error) {
|
||||
envMap := make(map[string]string)
|
||||
|
||||
scanner := bufio.NewScanner(envInput)
|
||||
var line string
|
||||
lineNumber := 0
|
||||
var lineNumber int
|
||||
|
||||
for scanner.Scan() {
|
||||
line = strings.TrimSpace(scanner.Text())
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
lineNumber++
|
||||
|
||||
// skip lines starting with comment
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip empty line
|
||||
if len(line) == 0 {
|
||||
// skip empty lines and lines starting with comment
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// split line into key and value
|
||||
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)
|
||||
}
|
||||
key, val := fields[0], fields[1]
|
||||
|
||||
if strings.Contains(fields[0], " ") {
|
||||
return nil, fmt.Errorf("bad key on line %d: contains whitespace", lineNumber)
|
||||
}
|
||||
|
||||
key := fields[0]
|
||||
val := fields[1]
|
||||
// sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
|
||||
key = strings.TrimPrefix(key, "export ")
|
||||
|
||||
// validate key and value
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("missing or empty key on line %d", lineNumber)
|
||||
}
|
||||
if strings.Contains(key, " ") {
|
||||
return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key)
|
||||
}
|
||||
if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") {
|
||||
return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val)
|
||||
}
|
||||
|
||||
// remove any trailing comment after value
|
||||
if commentStart := strings.Index(val, "#"); commentStart > 0 {
|
||||
before := val[commentStart-1]
|
||||
if before == '\t' || before == ' ' {
|
||||
val = strings.TrimRight(val[:commentStart], " \t")
|
||||
}
|
||||
}
|
||||
|
||||
// quoted value: support newlines
|
||||
if strings.HasPrefix(val, `"`) {
|
||||
for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) {
|
||||
val = strings.ReplaceAll(val, `\"`, `"`)
|
||||
if !scanner.Scan() {
|
||||
break
|
||||
}
|
||||
lineNumber++
|
||||
line = strings.ReplaceAll(scanner.Text(), `\"`, `"`)
|
||||
val += "\n" + line
|
||||
}
|
||||
val = strings.TrimPrefix(val, `"`)
|
||||
val = strings.TrimSuffix(val, `"`)
|
||||
}
|
||||
|
||||
envMap[key] = val
|
||||
}
|
||||
|
||||
@@ -415,7 +445,7 @@ func printEnvironment() {
|
||||
fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
|
||||
fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
|
||||
fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
|
||||
fmt.Printf("caddy.Version=%s\n", caddyVersion())
|
||||
fmt.Printf("caddy.Version=%s\n", CaddyVersion())
|
||||
fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
|
||||
fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
|
||||
fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
|
||||
@@ -432,8 +462,8 @@ func printEnvironment() {
|
||||
}
|
||||
}
|
||||
|
||||
// caddyVersion returns a detailed version string, if available.
|
||||
func caddyVersion() string {
|
||||
// CaddyVersion returns a detailed version string, if available.
|
||||
func CaddyVersion() string {
|
||||
goModule := caddy.GoModule()
|
||||
ver := goModule.Version
|
||||
if goModule.Sum != "" {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user