Compare commits

...

35 Commits

Author SHA1 Message Date
Matthew Holt 1e6eed42bd Also reject null byte 2022-06-14 11:37:37 -06:00
Matthew Holt 98cd4333a1 caddyhttp: Introduce strict HTTP mode 2022-06-14 11:03:53 -06:00
Yaacov Akiba Slama aaf6794b31 reverseproxy: Add renegotiation param in TLS client (#4784)
* Add renegotiation option in reverseproxy tls client

* Update modules/caddyhttp/reverseproxy/httptransport.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-06-10 09:33:35 -06:00
Matthew Holt 1498132ea3 caddyhttp: Log error from CEL evaluation (fix #4832) 2022-06-08 16:42:24 -06:00
Francis Lavoie 7f9b1f43c9 reverseproxy: Correct the tls_server_name docs (#4827)
* reverseproxy: Correct the `tls_server_name` docs

* Update modules/caddyhttp/reverseproxy/httptransport.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2022-06-06 12:37:09 -06:00
Matt Holt 5e729c1e85 reverseproxy: HTTP 504 for upstream timeouts (#4824)
Closes #4823
2022-06-03 14:13:47 -06:00
Gr33nbl00d 0a14f97e49 caddytls: Make peer certificate verification pluggable (#4389)
* caddytls: Adding ClientCertValidator for custom client cert validations

* caddytls: Cleanups for ClientCertValidator changes

caddytls: Cleanups for ClientCertValidator changes

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* Update modules/caddytls/connpolicy.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* Unexported field Validators, corrected renaming of LeafVerificationValidator to LeafCertClientAuth

* admin: Write proper status on invalid requests (#4569) (fix #4561)

* Apply suggestions from code review

* Register module; fix compilation

* Add log for deprecation notice

Co-authored-by: Roettges Florian <roettges.florian@scheidt-bachmann.de>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Alok Naushad <alokme123@gmail.com>
2022-06-02 14:25:07 -06:00
Matthew Holt 9864b138fb reverseproxy: api: Remove misleading 'healthy' value
In v2.5.0, upstream health was fixed such that whether an upstream is
considered healthy or not is mostly up to each individual handler's
config. Since "healthy" is an opinion, it is not a global value.

I unintentionally left in the "healthy" field in the API endpoint for
checking upstreams, and it is now misleading (see #4792).

However, num_requests and fails remains, so health can be determined by
the API client, rather than having it be opaquely (and unhelpfully)
determined for the client.

If we do restore this value later on, it'd need to be replicated once
per reverse_proxy handler according to their individual configs.
2022-06-02 12:32:23 -06:00
Matthew Holt 3d18bc56b9 go.mod: Update go-yaml to v3 2022-06-01 15:15:20 -06:00
Matthew Holt 886ba84baa Fix #4822 and fix #4779
The fix for 4822 is the change at the top of the file, and
4779's fix is toward the bottom of the file.
2022-06-01 15:12:57 -06:00
Alexander M a9267791c4 reverseproxy: Add --internal-certs CLI flag #3589 (#4817)
added flag --internal-certs
when set, for non-local domains the internal CA will be used for cert generation
2022-05-29 14:33:01 -06:00
Francis Lavoie ef0aaca0d6 ci: Fix build caching on Windows (#4811)
* ci: Fix build caching on Windows

I was getting tired of Windows being slow as molasses in our CI jobs, so I went to look at our trusty source of github actions + golang information, and found a somewhat recent commit that actually fixed it. See https://github.com/mvdan/github-actions-golang/commit/4b754729baa709da219a5889c459010d4eda1888

I'll do a 2nd empty commit to re-trigger CI shortly to confirm that it actually fixes it.

* Retrigger CI
2022-05-25 11:56:39 -06:00
Aleks 6891f7f421 templates: Add humanize function (#4767)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2022-05-24 19:47:08 -04:00
Kévin Dunglas 499ad6d182 core: Micro-optim in run() (#4810) 2022-05-24 13:52:50 -06:00
Matthew Holt 8e6bc36084 go.mod: Upgrade some dependencies 2022-05-24 12:44:16 -06:00
Francis Lavoie 58970cae92 httpcaddyfile: Add {err.*} placeholder shortcut (#4798) 2022-05-24 10:06:46 -06:00
David Larlet 9e760e2e0c templates: Documentation consistency (#4796) 2022-05-17 18:56:40 -04:00
世界 4b4e99bdb2 chore: Bump quic-go to v0.27.0 (#4782) 2022-05-12 01:25:17 -04:00
Matt Holt 57d27c1b58 reverseproxy: Support http1.1>h2c (close #4777) (#4778) 2022-05-10 17:25:58 -04:00
Matthew Holt 693e9b5283 rewrite: Handle fragment before query (fix #4775) 2022-05-09 11:09:42 -06:00
Francis Lavoie b687d7b967 httpcaddyfile: Support multiple values for default_bind (#4774)
* httpcaddyfile: Support multiple values for `default_bind`

* Fix ordering of server blocks
2022-05-08 21:32:10 -04:00
Francis Lavoie f7be0ee101 map: Prevent output destinations overlap with Caddyfile shorthands (#4657) 2022-05-06 10:25:31 -06:00
Francis Lavoie f6900fcf53 reverseproxy: Support performing pre-check requests (#4739) 2022-05-06 10:50:26 -04:00
Francis Lavoie ec86a2f7a3 caddyfile: Shortcut for remote_ip for private IP ranges (#4753) 2022-05-04 12:42:37 -06:00
Francis Lavoie e7fbee8c82 reverseproxy: Permit resolver addresses to not specify a port (#4760)
Context: https://caddy.community/t/caddy-2-5-dynamic-upstreams-and-consul-srv-dns/15839

I realized it probably makes sense to allow `:53` to be omitted, since it's the default port for DNS.
2022-05-04 12:40:39 -06:00
Tyler Kropp e84e19a04e templates: Add custom template function registration (#4757)
* Add custom template function registration

* Rename TemplateFunctions to CustomFunctions

* Add documentation

* Document CustomFunctions interface

* Preallocate custom functions map list

* Fix interface name in error message
2022-05-02 14:55:34 -06:00
Francis Lavoie 4a223f5203 reverseproxy: Fix Caddyfile support for replace_status (#4754) 2022-05-02 11:44:28 -06:00
Francis Lavoie af7321511c httpcaddyfile: Fix duplicate access log when debug is on (#4746) 2022-04-28 12:16:25 -04:00
Francis Lavoie 0be3d99543 logging: Implement rename filter, changes field key names (#4745) 2022-04-28 11:38:44 -04:00
Francis Lavoie 3017b245c9 logging: Use RedirectStdLog to capture more stdlib logs (#4732)
* logging: Use `RedirectStdLog`

* .gitignore a file pattern that I'm constantly using for testing
2022-04-28 08:42:30 -06:00
Francis Lavoie 2e4c09155a cmd: Fix unix socket addresses for admin API requests (#4742)
Fixes a regression in c2327161f7
2022-04-28 08:31:59 -06:00
Francis Lavoie dcc98da4d2 caddyhttp: Improve listen addr error message for IPv6 (#4740) 2022-04-28 08:18:45 -06:00
Marco Kaufmann 3ab648382d templates: Add missing backticks in docs (#4737) 2022-04-27 11:41:37 -06:00
Matt Holt 40b193fb79 reverseproxy: Improve hashing LB policies with HRW (#4724)
* reverseproxy: Improve hashing LB policies with HRW

Previously, if a list of upstreams changed, hash-based LB policies
would be greatly affected because the hash relied on the position of
upstreams in the pool. Highest Random Weight or "rendezvous" hashing
is apparently robust to pool changes. It runs in O(n) instead of
O(log n), but n is very small usually.

* Fix bug and update tests
2022-04-27 10:39:22 -06:00
Francis Lavoie d543ad1ffd caddypki: Fix caddy trust command to use the correct API endpoint (#4730) 2022-04-25 22:00:39 -06:00
51 changed files with 1354 additions and 260 deletions
+10 -2
View File
@@ -78,12 +78,20 @@ jobs:
printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go }}-go-ci
+6 -2
View File
@@ -42,12 +42,16 @@ jobs:
go env
printf "\n\nSystem environment:\n\n"
env
echo "::set-output name=go_cache::$(go env GOCACHE)"
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
# In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
+6 -2
View File
@@ -56,7 +56,6 @@ jobs:
env
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
# Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH
@@ -91,7 +90,12 @@ jobs:
- name: Cache the build cache
uses: actions/cache@v2
with:
path: ${{ steps.vars.outputs.go_cache }}
# In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release
+1
View File
@@ -1,6 +1,7 @@
_gitignore/
*.log
Caddyfile
Caddyfile.*
!caddyfile/
# artifacts from pprof tooling
+1 -1
View File
@@ -442,7 +442,7 @@ func run(newCfg *Config, start bool) error {
// Start
err = func() error {
var started []string
started := make([]string, 0, len(newCfg.apps))
for name, a := range newCfg.apps {
err := a.Start()
if err != nil {
+14 -4
View File
@@ -102,12 +102,20 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
}
}
// make a slice of the map keys so we can iterate in sorted order
addrs := make([]string, 0, len(addrToKeys))
for k := range addrToKeys {
addrs = append(addrs, k)
}
sort.Strings(addrs)
// now that we know which addresses serve which keys of this
// server block, we iterate that mapping and create a list of
// new server blocks for each address where the keys of the
// server block are only the ones which use the address; but
// the contents (tokens) are of course the same
for addr, keys := range addrToKeys {
for _, addr := range addrs {
keys := addrToKeys[addr]
// parse keys so that we only have to do it once
parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys {
@@ -161,6 +169,7 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
delete(addrToServerBlocks, otherAddr)
}
}
sort.Strings(a.addresses)
sbaddrs = append(sbaddrs, a)
}
@@ -208,13 +217,13 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
}
// the bind directive specifies hosts, but is optional
lnHosts := make([]string, 0, len(sblock.pile))
lnHosts := make([]string, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] {
lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
}
if len(lnHosts) == 0 {
if defaultBind, ok := options["default_bind"].(string); ok {
lnHosts = []string{defaultBind}
if defaultBind, ok := options["default_bind"].([]string); ok {
lnHosts = defaultBind
} else {
lnHosts = []string{""}
}
@@ -236,6 +245,7 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
for lnStr := range listeners {
listenersList = append(listenersList, lnStr)
}
sort.Strings(listenersList)
return listenersList, nil
}
+14 -2
View File
@@ -580,12 +580,24 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
code = "302"
default:
// Allow placeholders for the code
if strings.HasPrefix(code, "{") {
break
}
// Try to validate as an integer otherwise
codeInt, err := strconv.Atoi(code)
if err != nil {
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
}
if codeInt < 300 || codeInt > 399 {
return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)
// Sometimes, a 401 with Location header is desirable because
// requests made with XHR will "eat" the 3xx redirect; so if
// the intent was to redirect to an auth page, a 3xx won't
// work. Responding with 401 allows JS code to read the
// Location header and do a window.location redirect manually.
// see https://stackoverflow.com/a/2573589/846934
// see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
}
}
+21 -6
View File
@@ -148,6 +148,27 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`,
expectError: false,
},
{
// this is now allowed so a Location header
// can be written and consumed by JS
// in the case of XHR requests
input: `:8080 {
redir * :8081 401
}`,
expectError: false,
},
{
input: `:8080 {
redir * :8081 402
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 {http.reverse_proxy.status_code}
}`,
expectError: false,
},
{
input: `:8080 {
redir /old.html /new.html htlm
@@ -160,12 +181,6 @@ func TestRedirDirectiveSyntax(t *testing.T) {
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 400
}`,
expectError: true,
},
{
input: `:8080 {
redir * :8081 temp
+1
View File
@@ -57,6 +57,7 @@ var directiveOrder = []string{
// middleware handlers; some wrap responses
"basicauth",
"forward_auth",
"request_header",
"encode",
"push",
+68 -36
View File
@@ -88,34 +88,10 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err
}
// replace shorthand placeholders (which are
// convenient when writing a Caddyfile) with
// their actual placeholder identifiers or
// variable names
replacer := strings.NewReplacer(
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
)
// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
@@ -130,6 +106,8 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
{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}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
}
for _, sb := range originalServerBlocks {
@@ -254,20 +232,13 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
}
customLogs = append(customLogs, ncl)
}
// Apply global log options, when set
if options["log"] != nil {
for _, logValue := range options["log"].([]ConfigValue) {
addCustomLog(logValue.Value.(namedCustomLog))
}
}
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
if !hasDefaultLog {
// if the default log was not customized, ensure we
@@ -280,6 +251,15 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
}
}
// Apply server-specific log options
for _, p := range pairings {
for _, sb := range p.serverBlocks {
for _, clVal := range sb.pile["custom_log"] {
addCustomLog(clVal.Value.(namedCustomLog))
}
}
}
// annnd the top-level config, then we're done!
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
@@ -1262,6 +1242,58 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
return msEncoded, nil
}
// placeholderShorthands returns a slice of old-new string pairs,
// where the left of the pair is a placeholder shorthand that may
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
}
}
// WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map
// output destinations from overlapping with one of the
// predefined shorthands.
func WasReplacedPlaceholderShorthand(token string) string {
prev := ""
for i, item := range placeholderShorthands() {
// only look at every 2nd item, which is the replacement
if i%2 == 0 {
prev = item
continue
}
if strings.Trim(token, "{}") == strings.Trim(item, "{}") {
// we return the original shorthand so it
// can be used for an error message
return prev
}
}
return ""
}
// tryInt tries to convert val to an integer. If it fails,
// it downgrades the error to a warning and returns 0.
func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
+10 -1
View File
@@ -29,7 +29,7 @@ func init() {
RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort)
RegisterGlobalOption("default_bind", parseOptSingleString)
RegisterGlobalOption("default_bind", parseOptStringList)
RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
@@ -279,6 +279,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ interface{}) (interface{}, e
return val, nil
}
func parseOptStringList(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
d.Next() // consume parameter name
val := d.RemainingArgs()
if len(val) == 0 {
return "", d.ArgErr()
}
return val, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
adminCfg := new(caddy.AdminConfig)
for d.Next() {
-1
View File
@@ -350,7 +350,6 @@ func (st ServerType) buildTLSApp(
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 {
for i := 0; i < len(tlsApp.Automation.Policies); i++ {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
@@ -0,0 +1,137 @@
app.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://authelia.example.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy backend:8080
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"app.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"Remote-Email": [
"{http.reverse_proxy.header.Remote-Email}"
],
"Remote-Groups": [
"{http.reverse_proxy.header.Remote-Groups}"
],
"Remote-Name": [
"{http.reverse_proxy.header.Remote-Name}"
],
"Remote-User": [
"{http.reverse_proxy.header.Remote-User}"
]
}
}
}
]
}
]
},
{
"routes": [
{
"handle": [
{
"exclude": [
"Connection",
"Keep-Alive",
"Te",
"Trailers",
"Transfer-Encoding",
"Upgrade"
],
"handler": "copy_response_headers"
}
]
},
{
"handle": [
{
"handler": "copy_response"
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/api/verify?rd=https://authelia.example.com"
},
"upstreams": [
{
"dial": "authelia:9091"
}
]
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "backend:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,45 @@
{
debug
}
:8881 {
log {
format console
}
}
----------
{
"logging": {
"logs": {
"default": {
"level": "DEBUG",
"exclude": [
"http.log.access.log0"
]
},
"log0": {
"encoder": {
"format": "console"
},
"level": "DEBUG",
"include": [
"http.log.access.log0"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"logs": {
"default_logger_name": "log0"
}
}
}
}
}
}
@@ -1,5 +1,5 @@
{
default_bind tcp4/0.0.0.0
default_bind tcp4/0.0.0.0 tcp6/[::]
}
example.com {
@@ -14,7 +14,8 @@ example.org:12345 {
"servers": {
"srv0": {
"listen": [
"tcp4/0.0.0.0:12345"
"tcp4/0.0.0.0:12345",
"tcp6/[::]:12345"
],
"routes": [
{
@@ -31,7 +32,8 @@ example.org:12345 {
},
"srv1": {
"listen": [
"tcp4/0.0.0.0:443"
"tcp4/0.0.0.0:443",
"tcp6/[::]:443"
],
"routes": [
{
@@ -37,6 +37,9 @@
header Bar foo
}
respond @matcher9 "header matcher with null field matcher"
@matcher10 remote_ip private_ranges
respond @matcher10 "remote_ip matcher with private ranges"
}
----------
{
@@ -209,6 +212,28 @@
"handler": "static_response"
}
]
},
{
"match": [
{
"remote_ip": {
"ranges": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
]
}
}
],
"handle": [
{
"body": "remote_ip matcher with private ranges",
"handler": "static_response"
}
]
}
]
}
@@ -1,8 +1,13 @@
:8884
reverse_proxy 127.0.0.1:65535 {
@changeStatus status 500
replace_status @changeStatus 400
@500 status 500
replace_status @500 400
@all status 2xx 3xx 4xx 5xx
replace_status @all {http.error.status_code}
replace_status {http.error.status_code}
@accel header X-Accel-Redirect *
handle_response @accel {
@@ -78,6 +83,17 @@ reverse_proxy 127.0.0.1:65535 {
},
"status_code": 400
},
{
"match": {
"status_code": [
2,
3,
4,
5
]
},
"status_code": "{http.error.status_code}"
},
{
"match": {
"headers": {
@@ -228,6 +244,9 @@ reverse_proxy 127.0.0.1:65535 {
}
]
},
{
"status_code": "{http.error.status_code}"
},
{
"routes": [
{
@@ -1,11 +1,11 @@
https://example.com {
reverse_proxy /path http://localhost:54321 {
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto "http"
reverse_proxy /path https://localhost:54321 {
header_up Host {upstream_hostport}
header_up Foo bar
method GET
rewrite /rewritten?uri={uri}
buffer_requests
@@ -24,6 +24,7 @@ https://example.com {
max_conns_per_host 5
keepalive_idle_conns_per_host 2
keepalive_interval 30s
renegotiation freely
}
}
}
@@ -58,24 +59,19 @@ https://example.com {
"headers": {
"request": {
"set": {
"Foo": [
"bar"
],
"Host": [
"{http.request.host}"
],
"X-Forwarded-For": [
"{http.request.remote}"
],
"X-Forwarded-Port": [
"{server_port}"
],
"X-Forwarded-Proto": [
"http"
],
"X-Real-Ip": [
"{http.request.remote}"
"{http.reverse_proxy.upstream.hostport}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/rewritten?uri={http.request.uri}"
},
"transport": {
"compression": false,
"dial_fallback_delay": 5000000000,
@@ -96,6 +92,9 @@ https://example.com {
]
},
"response_header_timeout": 8000000000,
"tls": {
"renegotiation": "freely"
},
"versions": [
"h2c",
"2"
+1 -1
View File
@@ -669,7 +669,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
}
origin := "http://" + parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() {
origin = "unixsocket" // hack so that http.NewRequest() is happy
origin = "http://unixsocket" // hack so that http.NewRequest() is happy
}
// form the request
+7 -7
View File
@@ -3,7 +3,7 @@ module github.com/caddyserver/caddy/v2
go 1.17
require (
github.com/BurntSushi/toml v1.0.0
github.com/BurntSushi/toml v1.1.0
github.com/Masterminds/sprig/v3 v3.2.2
github.com/alecthomas/chroma v0.10.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
@@ -12,17 +12,17 @@ require (
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.7.3
github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.15.0
github.com/klauspost/cpuid/v2 v2.0.11
github.com/lucas-clemente/quic-go v0.26.0
github.com/klauspost/compress v1.15.4
github.com/klauspost/cpuid/v2 v2.0.12
github.com/lucas-clemente/quic-go v0.27.1
github.com/mholt/acmez v1.0.2
github.com/prometheus/client_golang v1.12.1
github.com/smallstep/certificates v0.19.0
github.com/smallstep/cli v0.18.0
github.com/smallstep/nosql v0.4.0
github.com/smallstep/truststore v0.11.0
github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74
github.com/yuin/goldmark v1.4.8
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
github.com/yuin/goldmark v1.4.12
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0
go.opentelemetry.io/otel v1.4.0
@@ -35,7 +35,7 @@ require (
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)
require (
+14 -12
View File
@@ -104,8 +104,8 @@ github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYX
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo=
@@ -689,10 +689,11 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ=
github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -719,8 +720,8 @@ github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lucas-clemente/quic-go v0.26.0 h1:ALBQXr9UJ8A1LyzvceX4jd9QFsHvlI0RR6BkV16o00A=
github.com/lucas-clemente/quic-go v0.26.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
github.com/lucas-clemente/quic-go v0.27.1 h1:sOw+4kFSVrdWOYmUjufQ9GBVPqZ+tu+jMtXxXNmRJyk=
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
@@ -1059,8 +1060,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74 h1:uFx5aih29p2IaRUF0lJwtVViCXStlvnPPE3NEmM4Ivs=
github.com/tailscale/tscert v0.0.0-20220125204807-4509a5fbaf74/go.mod h1:hL4gB6APAasMR2NNi/JHzqKkxW3EPQlFgLEq9PMi2t0=
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 h1:xwMw7LFhV9dbvot9A7NLClP9udqbjrQlIwWMH8e7uiQ=
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2/go.mod h1:hL4gB6APAasMR2NNi/JHzqKkxW3EPQlFgLEq9PMi2t0=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
@@ -1101,8 +1102,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark v1.4.8 h1:zHPiabbIRssZOI0MAzJDHsyvG4MXCGqVaMOwR+HeoQQ=
github.com/yuin/goldmark v1.4.8/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg=
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@@ -1849,8 +1850,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+1 -1
View File
@@ -184,7 +184,7 @@ type fakeCloseQuicListener struct {
// server on which Accept would be called with non-empty contexts
// (mind that the default net listeners' Accept doesn't take a context argument)
// sounds way too rare for us to sacrifice efficiency here.
func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlySession, error) {
func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
conn, err := fcql.sharedQuicListener.Accept(fcql.context)
if err == nil {
return conn, nil
+7 -1
View File
@@ -661,9 +661,15 @@ func newDefaultProductionLog() (*defaultCustomLog, error) {
cl.buildCore()
logger := zap.New(cl.core)
// capture logs from other libraries which
// may not be using zap logging directly
_ = zap.RedirectStdLog(logger)
return &defaultCustomLog{
CustomLog: cl,
logger: zap.New(cl.core),
logger: logger,
}, nil
}
+10
View File
@@ -111,6 +111,8 @@ type App struct {
// be forcefully closed.
GracePeriod caddy.Duration `json:"grace_period,omitempty"`
Strict *StrictOptions `json:"strict,omitempty"`
// Servers is the list of servers, keyed by arbitrary names chosen
// at your discretion for your own convenience; the keys do not
// affect functionality.
@@ -127,6 +129,13 @@ type App struct {
allCertDomains []string
}
type StrictOptions struct {
Disabled bool `json:"disable,omitempty"`
LenientQueryStrings bool `json:"lenient_query_strings,omitempty"`
LenientPaths bool `json:"lenient_paths,omitempty"`
LenientHeaders bool `json:"lenient_headers,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
@@ -162,6 +171,7 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.tlsApp = app.tlsApp
srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error")
srv.strict = app.Strict
// only enable access logs if configured
if srv.Logs != nil {
+27 -11
View File
@@ -152,9 +152,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
}
// only include domain if it's not explicitly skipped and it's not a Tailscale domain
// (the implicit Tailscale manager module will get those certs at run-time)
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) && !isTailscaleDomain(d) {
if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
serverDomainSet[d] = struct{}{}
}
}
@@ -181,6 +179,11 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
} else {
for d := range serverDomainSet {
// the implicit Tailscale manager module will get its own certs at run-time
if isTailscaleDomain(d) {
continue
}
if certmagic.SubjectQualifiesForCert(d) &&
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
// if a certificate for this name is already loaded,
@@ -222,11 +225,15 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName))
// create HTTP->HTTPS redirects
for _, addr := range srv.Listen {
for _, listenAddr := range srv.Listen {
// figure out the address we will redirect to...
addr, err := caddy.ParseNetworkAddress(addr)
addr, err := caddy.ParseNetworkAddress(listenAddr)
if err != nil {
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
msg := "%s: invalid listener address: %v"
if strings.Count(listenAddr, ":") > 1 {
msg = msg + ", there are too many colons, so the port is ambiguous. Did you mean to wrap the IPv6 address with [] brackets?"
}
return fmt.Errorf(msg, srvName, listenAddr)
}
// this address might not have a hostname, i.e. might be a
@@ -432,7 +439,7 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
}
}
// createAutomationPolicy ensures that automated certificates for this
// createAutomationPolicies ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all
// automation policy exists, it will be shallow-copied and used as the
@@ -481,6 +488,12 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
return err
}
ap.Managers = []certmagic.Manager{ts}
// must reprovision the automation policy so that the underlying
// CertMagic config knows about the updated Managers
if err := ap.Provision(app.tlsApp); err != nil {
return fmt.Errorf("re-provisioning automation policy: %v", err)
}
}
// while we're here, is this the catch-all/base policy?
@@ -491,14 +504,17 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
}
if basePolicy == nil {
// no base policy found, we will make one! (with implicit Tailscale integration)
// no base policy found; we will make one
basePolicy = new(caddytls.AutomationPolicy)
}
if basePolicy.Managers == nil {
// add implicit Tailscale integration, for harmless convenience
ts, err := implicitTailscale(ctx)
if err != nil {
return err
}
basePolicy = &caddytls.AutomationPolicy{
Managers: []certmagic.Manager{ts},
}
basePolicy.Managers = []certmagic.Manager{ts}
}
// if the basePolicy has an existing ACMEIssuer (particularly to
+11 -2
View File
@@ -33,6 +33,7 @@ import (
"github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/ext"
"github.com/google/cel-go/interpreter/functions"
"go.uber.org/zap"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/proto"
)
@@ -62,6 +63,8 @@ type MatchExpression struct {
expandedExpr string
prg cel.Program
ta ref.TypeAdapter
log *zap.Logger
}
// CaddyModule returns the Caddy module information.
@@ -83,7 +86,9 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
}
// Provision sets ups m.
func (m *MatchExpression) Provision(_ caddy.Context) error {
func (m *MatchExpression) Provision(ctx caddy.Context) error {
m.log = ctx.Logger(m)
// replace placeholders with a function call - this is just some
// light (and possibly naïve) syntactic sugar
m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)
@@ -137,9 +142,13 @@ func (m *MatchExpression) Provision(_ caddy.Context) error {
// Match returns true if r matches m.
func (m MatchExpression) Match(r *http.Request) bool {
out, _, _ := m.prg.Eval(map[string]interface{}{
out, _, err := m.prg.Eval(map[string]interface{}{
"request": celHTTPRequest{r},
})
if err != nil {
m.log.Error("evaluating expression", zap.Error(err))
return false
}
if outBool, ok := out.Value().(bool); ok {
return outBool
}
+1 -1
View File
@@ -116,7 +116,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
}
if tt.expression.Match(req) != tt.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tt.wantResult, tt.expression)
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tt.wantResult, tt.expression.Expr)
}
})
+5
View File
@@ -56,6 +56,11 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if len(handler.Destinations) == 0 {
return nil, h.Err("missing destination argument(s)")
}
for _, dest := range handler.Destinations {
if shorthand := httpcaddyfile.WasReplacedPlaceholderShorthand(dest); shorthand != "" {
return nil, h.Errf("destination %s conflicts with a Caddyfile placeholder shorthand", shorthand)
}
}
// mappings
for h.NextBlock(0) {
+23 -7
View File
@@ -503,15 +503,20 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m. An empty m matches an empty query string.
func (m MatchQuery) Match(r *http.Request) bool {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
reqQuery, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return false
}
for param, vals := range m {
param = repl.ReplaceAll(param, "")
paramVal, found := r.URL.Query()[param]
if found {
for _, v := range vals {
v = repl.ReplaceAll(v, "")
if paramVal[0] == v || v == "*" {
return true
}
paramVal, found := reqQuery[param]
if !found {
continue
}
for _, v := range vals {
v = repl.ReplaceAll(v, "")
if paramVal[0] == v || v == "*" {
return true
}
}
}
@@ -867,6 +872,17 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
m.Forwarded = true
continue
}
if d.Val() == "private_ranges" {
m.Ranges = append(m.Ranges, []string{
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1",
}...)
continue
}
m.Ranges = append(m.Ranges, d.Val())
}
if d.NextBlock(0) {
+11 -2
View File
@@ -624,9 +624,18 @@ func TestQueryMatcher(t *testing.T) {
input: "/?somekey=1",
expect: true,
},
{
scenario: "invalid query string",
match: MatchQuery{"test": []string{"*"}},
input: "/?test=1;",
expect: false,
},
} {
u, _ := url.Parse(tc.input)
u, err := url.Parse(tc.input)
if err != nil {
t.Errorf("Test %d: Parsing URL: %v", i, err)
continue
}
req := &http.Request{URL: u}
repl := caddy.NewReplacer()
-1
View File
@@ -35,7 +35,6 @@ type adminUpstreams struct{}
// upstreamResults holds the status of a particular upstream
type upstreamStatus struct {
Address string `json:"address"`
Healthy bool `json:"healthy"`
NumRequests int `json:"num_requests"`
Fails int `json:"fails"`
}
+58 -18
View File
@@ -27,6 +27,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/dustin/go-humanize"
)
@@ -85,10 +86,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// buffer_responses
// max_buffer_size <size>
//
// # header manipulation
// # request manipulation
// trusted_proxies [private_ranges] <ranges...>
// header_up [+|-]<field> [<value|regexp> [<replacement>]]
// header_down [+|-]<field> [<value|regexp> [<replacement>]]
// method <method>
// rewrite <to>
//
// # round trip
// transport <name> {
@@ -600,6 +603,30 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Err(err.Error())
}
case "method":
if !d.NextArg() {
return d.ArgErr()
}
if h.Rewrite == nil {
h.Rewrite = &rewrite.Rewrite{}
}
h.Rewrite.Method = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "rewrite":
if !d.NextArg() {
return d.ArgErr()
}
if h.Rewrite == nil {
h.Rewrite = &rewrite.Rewrite{}
}
h.Rewrite.URI = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "transport":
if !d.NextArg() {
return d.ArgErr()
@@ -627,22 +654,24 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
case "replace_status":
args := d.RemainingArgs()
if len(args) != 2 {
return d.Errf("must have two arguments: a response matcher and a status code")
if len(args) != 1 && len(args) != 2 {
return d.Errf("must have one or two arguments: an optional response matcher, and a status code")
}
if !strings.HasPrefix(args[0], matcherPrefix) {
return d.Errf("must use a named response matcher, starting with '@'")
}
responseHandler := caddyhttp.ResponseHandler{}
foundMatcher, ok := h.responseMatchers[args[0]]
if !ok {
return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
}
_, err := strconv.Atoi(args[1])
if err != nil {
return d.Errf("bad integer value '%s': %v", args[1], err)
if len(args) == 2 {
if !strings.HasPrefix(args[0], matcherPrefix) {
return d.Errf("must use a named response matcher, starting with '@'")
}
foundMatcher, ok := h.responseMatchers[args[0]]
if !ok {
return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
}
responseHandler.Match = &foundMatcher
responseHandler.StatusCode = caddyhttp.WeakString(args[1])
} else if len(args) == 1 {
responseHandler.StatusCode = caddyhttp.WeakString(args[0])
}
// make sure there's no block, cause it doesn't make sense
@@ -652,10 +681,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.HandleResponse = append(
h.HandleResponse,
caddyhttp.ResponseHandler{
Match: &foundMatcher,
StatusCode: caddyhttp.WeakString(args[1]),
},
responseHandler,
)
default:
@@ -896,6 +922,20 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
case "renegotiation":
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
if !d.NextArg() {
return d.ArgErr()
}
switch renegotiation := d.Val(); renegotiation {
case "never", "once", "freely":
h.TLS.Renegotiation = renegotiation
default:
return d.ArgErr()
}
case "tls":
if h.TLS == nil {
h.TLS = new(TLSConfig)
+20 -4
View File
@@ -27,6 +27,7 @@ import (
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func init() {
@@ -59,6 +60,7 @@ default, all incoming headers are passed through unmodified.)
fs.String("to", "", "Upstream address to which traffic should be sent")
fs.Bool("change-host-header", false, "Set upstream Host header to address of upstream")
fs.Bool("insecure", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING SSL CERTIFICATES!)")
fs.Bool("internal-certs", false, "Use internal CA for issuing certs")
return fs
}(),
})
@@ -71,6 +73,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
to := fs.String("to")
changeHost := fs.Bool("change-host-header")
insecure := fs.Bool("insecure")
internalCerts := fs.Bool("internal-certs")
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
@@ -154,11 +157,24 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
Servers: map[string]*caddyhttp.Server{"proxy": server},
}
appsRaw := caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, nil),
}
if internalCerts && fromAddr.Host != "" {
tlsApp := caddytls.TLS{
Automation: &caddytls.AutomationConfig{
Policies: []*caddytls.AutomationPolicy{{
Subjects: []string{fromAddr.Host},
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}},
},
}
appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil)
}
cfg := &caddy.Config{
Admin: &caddy.AdminConfig{Disabled: true},
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, nil),
},
Admin: &caddy.AdminConfig{Disabled: true},
AppsRaw: appsRaw,
}
err = caddy.Run(cfg)
@@ -196,7 +196,15 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// NOTE: we delete the tokens as we go so that the reverse_proxy
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
for dispenser.NextBlock(0) && dispenser.Nesting() == 1 {
for dispenser.NextBlock(0) {
// ignore any sub-subdirectives that might
// have the same name somewhere within
// the reverse_proxy passthrough tokens
if dispenser.Nesting() != 1 {
continue
}
// parse the php_fastcgi subdirectives
switch dispenser.Val() {
case "root":
if !dispenser.NextArg() {
@@ -0,0 +1,269 @@
// 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 forwardauth
import (
"encoding/json"
"net/http"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
)
func init() {
httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile)
}
// parseCaddyfile parses the forward_auth directive, which has the same syntax
// as the reverse_proxy directive (in fact, the reverse_proxy's directive
// Unmarshaler is invoked by this function) but the resulting proxy is specially
// configured for most™️ auth gateways that support forward auth. The typical
// config which looks something like this:
//
// forward_auth auth-gateway:9091 {
// uri /authenticate?redirect=https://auth.example.com
// copy_headers Remote-User Remote-Email
// }
//
// is equivalent to a reverse_proxy directive like this:
//
// reverse_proxy auth-gateway:9091 {
// method GET
// rewrite /authenticate?redirect=https://auth.example.com
//
// header_up X-Forwarded-Method {method}
// header_up X-Forwarded-Uri {uri}
//
// @good status 2xx
// handle_response @good {
// request_header {
// Remote-User {http.reverse_proxy.header.Remote-User}
// Remote-Email {http.reverse_proxy.header.Remote-Email}
// }
// }
//
// handle_response {
// copy_response_headers {
// exclude Connection Keep-Alive Te Trailers Transfer-Encoding Upgrade
// }
// copy_response
// }
// }
//
func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
if !h.Next() {
return nil, h.ArgErr()
}
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
// either way, strip the matcher token and pass
// the remaining tokens to the unmarshaler so that
// we can gain the rest of the reverse_proxy syntax
userMatcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
// make a new dispenser from the remaining tokens so that we
// can reset the dispenser back to this point for the
// reverse_proxy unmarshaler to read from it as well
dispenser := h.NewFromNextSegment()
// create the reverse proxy handler
rpHandler := &reverseproxy.Handler{
// set up defaults for header_up; reverse_proxy already deals with
// adding the other three X-Forwarded-* headers, but for this flow,
// we want to also send along the incoming method and URI since this
// request will have a rewritten URI and method.
Headers: &headers.Handler{
Request: &headers.HeaderOps{
Set: http.Header{
"X-Forwarded-Method": []string{"{http.request.method}"},
"X-Forwarded-Uri": []string{"{http.request.uri}"},
},
},
},
// we always rewrite the method to GET, which implicitly
// turns off sending the incoming request's body, which
// allows later middleware handlers to consume it
Rewrite: &rewrite.Rewrite{
Method: "GET",
},
HandleResponse: []caddyhttp.ResponseHandler{},
}
// collect the headers to copy from the auth response
// onto the original request, so they can get passed
// through to a backend app
headersToCopy := []string{}
// read the subdirectives for configuring the forward_auth shortcut
// NOTE: we delete the tokens as we go so that the reverse_proxy
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
for dispenser.NextBlock(0) {
// ignore any sub-subdirectives that might
// have the same name somewhere within
// the reverse_proxy passthrough tokens
if dispenser.Nesting() != 1 {
continue
}
// parse the forward_auth subdirectives
switch dispenser.Val() {
case "uri":
if !dispenser.NextArg() {
return nil, dispenser.ArgErr()
}
rpHandler.Rewrite.URI = dispenser.Val()
dispenser.Delete()
dispenser.Delete()
case "copy_headers":
args := dispenser.RemainingArgs()
dispenser.Delete()
for _, headerField := range args {
dispenser.Delete()
headersToCopy = append(headersToCopy, headerField)
}
if len(headersToCopy) == 0 {
return nil, dispenser.ArgErr()
}
}
}
}
// reset the dispenser after we're done so that the reverse_proxy
// unmarshaler can read it from the start
dispenser.Reset()
// the auth target URI must not be empty
if rpHandler.Rewrite.URI == "" {
return nil, dispenser.Errf("the 'uri' subdirective is required")
}
// set up handler for good responses; when a response
// has 2xx status, then we will copy some headers from
// the response onto the original request, and allow
// handling to continue down the middleware chain,
// by _not_ executing a terminal handler.
goodResponseHandler := caddyhttp.ResponseHandler{
Match: &caddyhttp.ResponseMatcher{
StatusCode: []int{2},
},
Routes: []caddyhttp.Route{},
}
if len(headersToCopy) > 0 {
handler := &headers.Handler{
Request: &headers.HeaderOps{
Set: http.Header{},
},
}
for _, headerField := range headersToCopy {
handler.Request.Set[headerField] = []string{
"{http.reverse_proxy.header." + headerField + "}",
}
}
goodResponseHandler.Routes = append(
goodResponseHandler.Routes,
caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
handler,
"handler",
"headers",
nil,
)},
},
)
}
rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
// set up handler for denial responses; when a response
// has any other status than 2xx, then we copy the response
// back to the client, and terminate handling.
denialResponseHandler := caddyhttp.ResponseHandler{
Routes: []caddyhttp.Route{
{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
&reverseproxy.CopyResponseHeadersHandler{
Exclude: []string{
"Connection",
"Keep-Alive",
"Te",
"Trailers",
"Transfer-Encoding",
"Upgrade",
},
},
"handler",
"copy_response_headers",
nil,
)},
},
{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
&reverseproxy.CopyResponseHandler{},
"handler",
"copy_response",
nil,
)},
},
},
}
rpHandler.HandleResponse = append(rpHandler.HandleResponse, denialResponseHandler)
// the rest of the config is specified by the user
// using the reverse_proxy directive syntax
err = rpHandler.UnmarshalCaddyfile(dispenser)
if err != nil {
return nil, err
}
err = rpHandler.FinalizeUnmarshalCaddyfile(h)
if err != nil {
return nil, err
}
// create the final reverse proxy route
rpRoute := caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
rpHandler,
"handler",
"reverse_proxy",
nil,
)},
}
// apply the user's matcher if any
if userMatcherSet != nil {
rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet}
}
return []httpcaddyfile.ConfigValue{
{
Class: "route",
Value: rpRoute,
},
}, nil
}
@@ -247,8 +247,8 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
h.SetScheme(req)
// if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is
// HTTP/2 without TLS, use the alternate H2C-capable transport instead
if req.ProtoMajor == 2 && req.URL.Scheme == "http" && h.h2cTransport != nil {
// HTTP without TLS, use the alternate H2C-capable transport instead
if req.URL.Scheme == "http" && h.h2cTransport != nil {
return h.h2cTransport.RoundTrip(req)
}
@@ -315,8 +315,23 @@ type TLSConfig struct {
// The duration to allow a TLS handshake to a server. Default: No timeout.
HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"`
// The server name (SNI) to use in TLS handshakes.
// The server name used when verifying the certificate received in the TLS
// handshake. By default, this will use the upstream address' host part.
// You only need to override this if your upstream address does not match the
// certificate the upstream is likely to use. For example if the upstream
// address is an IP address, then you would need to configure this to the
// hostname being served by the upstream server. Currently, this does not
// support placeholders because the TLS config is not provisioned on each
// connection, so a static value must be used.
ServerName string `json:"server_name,omitempty"`
// TLS renegotiation level. TLS renegotiation is the act of performing
// subsequent handshakes on a connection after the first.
// The level can be:
// - "never": (the default) disables renegotiation.
// - "once": allows a remote server to request renegotiation once per connection.
// - "freely": allows a remote server to repeatedly request renegotiation.
Renegotiation string `json:"renegotiation,omitempty"`
}
// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
@@ -386,7 +401,19 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
cfg.RootCAs = rootPool
}
// custom SNI
// Renegotiation
switch t.Renegotiation {
case "never":
cfg.Renegotiation = tls.RenegotiateNever
case "once":
cfg.Renegotiation = tls.RenegotiateOnceAsClient
case "freely":
cfg.Renegotiation = tls.RenegotiateFreelyAsClient
default:
return nil, fmt.Errorf("invalid TLS renegotiation level: %v", t.Renegotiation)
}
// override for the server name used verify the TLS handshake
cfg.ServerName = t.ServerName
// throw all security out the window
@@ -402,16 +429,16 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
// KeepAlive holds configuration pertaining to HTTP Keep-Alive.
type KeepAlive struct {
// Whether HTTP Keep-Alive is enabled. Default: true
// Whether HTTP Keep-Alive is enabled. Default: `true`
Enabled *bool `json:"enabled,omitempty"`
// How often to probe for liveness. Default: `30s`.
ProbeInterval caddy.Duration `json:"probe_interval,omitempty"`
// Maximum number of idle connections. Default: 0, which means no limit.
// Maximum number of idle connections. Default: `0`, which means no limit.
MaxIdleConns int `json:"max_idle_conns,omitempty"`
// Maximum number of idle connections per host. Default: 32.
// Maximum number of idle connections per host. Default: `32`.
MaxIdleConnsPerHost int `json:"max_idle_conns_per_host,omitempty"`
// How long connections should be kept alive when idle. Default: `2m`.
+49 -14
View File
@@ -35,6 +35,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"go.uber.org/zap"
"golang.org/x/net/http/httpguts"
)
@@ -136,6 +137,18 @@ type Handler struct {
// used for the requests and responses (in bytes).
MaxBufferSize int64 `json:"max_buffer_size,omitempty"`
// If configured, rewrites the copy of the upstream request.
// Allows changing the request method and URI (path and query).
// Since the rewrite is applied to the copy, it does not persist
// past the reverse proxy handler.
// If the method is changed to `GET` or `HEAD`, the request body
// will not be copied to the backend. This allows a later request
// handler -- either in a `handle_response` route, or after -- to
// read the body.
// By default, no rewrite is performed, and the method and URI
// from the incoming request is used as-is for proxying.
Rewrite *rewrite.Rewrite `json:"rewrite,omitempty"`
// List of handlers and their associated matchers to evaluate
// after successful roundtrips. The first handler that matches
// the response from a backend will be invoked. The response
@@ -258,6 +271,13 @@ func (h *Handler) Provision(ctx caddy.Context) error {
}
}
if h.Rewrite != nil {
err := h.Rewrite.Provision(ctx)
if err != nil {
return fmt.Errorf("provisioning rewrite: %v", err)
}
}
// set up transport
if h.Transport == nil {
t := &HTTPTransport{}
@@ -385,7 +405,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// prepare the request for proxying; this is needed only once
clonedReq, err := h.prepareRequest(r)
clonedReq, err := h.prepareRequest(r, repl)
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("preparing request for upstream round-trip: %v", err))
@@ -412,7 +432,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
var proxyErr error
for {
var done bool
done, proxyErr = h.proxyLoopIteration(clonedReq, w, proxyErr, start, repl, reqHeader, reqHost, next)
done, proxyErr = h.proxyLoopIteration(clonedReq, r, w, proxyErr, start, repl, reqHeader, reqHost, next)
if done {
break
}
@@ -429,7 +449,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
// that has to be passed in, we brought this into its own method so that we could run defer more easily.
// It returns true when the loop is done and should break; false otherwise. The error value returned should
// be assigned to the proxyErr value for the next iteration of the loop (or the error handled after break).
func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
repl *caddy.Replacer, reqHeader http.Header, reqHost string, next caddyhttp.Handler) (bool, error) {
// get the updated list of upstreams
upstreams := h.Upstreams
@@ -503,8 +523,8 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
}
// proxy the request to that upstream
proxyErr = h.reverseProxy(w, r, repl, dialInfo, next)
if proxyErr == nil || proxyErr == context.Canceled {
proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
if proxyErr == nil || errors.Is(proxyErr, context.Canceled) {
// context.Canceled happens when the downstream client
// cancels the request, which is not our failure
return true, nil
@@ -536,9 +556,20 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
// properties of the cloned request and should be done just once (before
// proxying) regardless of proxy retries. This assumes that no mutations
// of the cloned request are performed by h during or after proxying.
func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.Request, error) {
req = cloneRequest(req)
// if enabled, perform rewrites on the cloned request; if
// the method is GET or HEAD, prevent the request body
// from being copied to the upstream
if h.Rewrite != nil {
changed := h.Rewrite.Rewrite(req, repl)
if changed && (h.Rewrite.Method == "GET" || h.Rewrite.Method == "HEAD") {
req.ContentLength = 0
req.Body = nil
}
}
// if enabled, buffer client request; this should only be
// enabled if the upstream requires it and does not work
// with "slow clients" (gunicorn, etc.) - this obviously
@@ -547,7 +578,7 @@ func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
// attacks, so it is strongly recommended to only use this
// feature if absolutely required, if read timeouts are
// set, and if body size is limited
if h.BufferRequests {
if h.BufferRequests && req.Body != nil {
req.Body = h.bufferedBody(req.Body)
}
@@ -673,10 +704,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
// we pass through the request Host as-is, but in situations
// where we proxy over HTTPS, the user may need to override
// Host themselves, so it's helpful to send the original too.
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
host = req.Host // OK; there probably was no port
}
host := req.Host
prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Host")
if trusted && ok && prior != "" {
host = prior
@@ -691,7 +719,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
// Go standard library which was used as the foundation.)
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
_ = di.Upstream.Host.countRequest(1)
//nolint:errcheck
defer di.Upstream.Host.countRequest(-1)
@@ -798,17 +826,19 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
// we make some data available via request context to child routes
// so that they may inherit some options and functions from the
// handler, and be able to copy the response.
// we use the original request here, so that any routes from 'next'
// see the original request rather than the proxy cloned request.
hrc := &handleResponseContext{
handler: h,
response: res,
start: start,
logger: logger,
}
ctx := req.Context()
ctx := origReq.Context()
ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
// pass the request through the response handler routes
routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req.WithContext(ctx))
routeErr := rh.Routes.Compile(next).ServeHTTP(rw, origReq.WithContext(ctx))
// if the response handler routes already finalized the response,
// we can return early. It should be finalized if the routes executed
@@ -1150,6 +1180,11 @@ func statusError(err error) error {
// errors proxying usually mean there is a problem with the upstream(s)
statusCode := http.StatusBadGateway
// timeout errors have a standard status code (see issue #4823)
if err, ok := err.(net.Error); ok && err.Timeout() {
statusCode = http.StatusGatewayTimeout
}
// if the client canceled the request (usually this means they closed
// the connection, so they won't see any response), we can report it
// as a client error (4xx) and not a server error (5xx); unfortunately
@@ -514,21 +514,26 @@ func leastRequests(upstreams []*Upstream) *Upstream {
return best[weakrand.Intn(len(best))]
}
// hostByHashing returns an available host
// from pool based on a hashable string s.
// hostByHashing returns an available host from pool based on a hashable string s.
func hostByHashing(pool []*Upstream, s string) *Upstream {
poolLen := uint32(len(pool))
if poolLen == 0 {
return nil
}
index := hash(s) % poolLen
for i := uint32(0); i < poolLen; i++ {
upstream := pool[(index+i)%poolLen]
if upstream.Available() {
return upstream
// Highest Random Weight (HRW, or "Rendezvous") hashing,
// guarantees stability when the list of upstreams changes;
// see https://medium.com/i0exception/rendezvous-hashing-8c00e2fb58b0,
// https://randorithms.com/2020/12/26/rendezvous-hashing.html,
// and https://en.wikipedia.org/wiki/Rendezvous_hashing.
var highestHash uint32
var upstream *Upstream
for _, up := range pool {
if !up.Available() {
continue
}
h := hash(s + up.String()) // important to hash key and server together
if h > highestHash {
highestHash = h
upstream = up
}
}
return nil
return upstream
}
// hash calculates a fast hash based on s.
@@ -22,9 +22,9 @@ import (
func testPool() UpstreamPool {
return UpstreamPool{
{Host: new(Host)},
{Host: new(Host)},
{Host: new(Host)},
{Host: new(Host), Dial: "0.0.0.1"},
{Host: new(Host), Dial: "0.0.0.2"},
{Host: new(Host), Dial: "0.0.0.3"},
}
}
@@ -95,13 +95,13 @@ func TestIPHashPolicy(t *testing.T) {
// We should be able to predict where every request is routed.
req.RemoteAddr = "172.0.0.1:80"
h := ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.2:80"
h = ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.3:80"
h = ipHash.Select(pool, req, nil)
@@ -117,13 +117,13 @@ func TestIPHashPolicy(t *testing.T) {
// we should get the same results without a port
req.RemoteAddr = "172.0.0.1"
h = ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.2"
h = ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.3"
h = ipHash.Select(pool, req, nil)
@@ -138,7 +138,7 @@ func TestIPHashPolicy(t *testing.T) {
// we should get a healthy host if the original host is unhealthy and a
// healthy host is available
req.RemoteAddr = "172.0.0.1"
req.RemoteAddr = "172.0.0.4"
pool[1].setHealthy(false)
h = ipHash.Select(pool, req, nil)
if h != pool[2] {
@@ -147,16 +147,16 @@ func TestIPHashPolicy(t *testing.T) {
req.RemoteAddr = "172.0.0.2"
h = ipHash.Select(pool, req, nil)
if h != pool[2] {
t.Error("Expected ip hash policy host to be the third host.")
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
pool[1].setHealthy(true)
req.RemoteAddr = "172.0.0.3"
pool[2].setHealthy(false)
h = ipHash.Select(pool, req, nil)
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
}
req.RemoteAddr = "172.0.0.4"
h = ipHash.Select(pool, req, nil)
@@ -167,29 +167,29 @@ func TestIPHashPolicy(t *testing.T) {
// We should be able to resize the host pool and still be able to predict
// where a req will be routed with the same IP's used above
pool = UpstreamPool{
{Host: new(Host)},
{Host: new(Host)},
{Host: new(Host), Dial: "0.0.0.2"},
{Host: new(Host), Dial: "0.0.0.3"},
}
req.RemoteAddr = "172.0.0.1:80"
h = ipHash.Select(pool, req, nil)
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
}
req.RemoteAddr = "172.0.0.2:80"
h = ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
}
req.RemoteAddr = "172.0.0.3:80"
h = ipHash.Select(pool, req, nil)
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
req.RemoteAddr = "172.0.0.4:80"
req.RemoteAddr = "172.0.0.3:80"
h = ipHash.Select(pool, req, nil)
if h != pool[1] {
t.Error("Expected ip hash policy host to be the second host.")
}
req.RemoteAddr = "172.0.0.4:80"
h = ipHash.Select(pool, req, nil)
if h != pool[0] {
t.Error("Expected ip hash policy host to be the first host.")
}
// We should get nil when there are no healthy hosts
pool[0].setHealthy(false)
@@ -252,14 +252,14 @@ func TestURIHashPolicy(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/test", nil)
h := uriPolicy.Select(pool, request, nil)
if h != pool[0] {
t.Error("Expected uri policy host to be the first host.")
if h != pool[2] {
t.Error("Expected uri policy host to be the third host.")
}
pool[0].setHealthy(false)
pool[2].setHealthy(false)
h = uriPolicy.Select(pool, request, nil)
if h != pool[1] {
t.Error("Expected uri policy host to be the first host.")
t.Error("Expected uri policy host to be the second host.")
}
request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
+9 -1
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -372,7 +373,14 @@ func (u *UpstreamResolver) ParseAddresses() error {
for _, v := range u.Addresses {
addr, err := caddy.ParseNetworkAddress(v)
if err != nil {
return err
// If a port wasn't specified for the resolver,
// try defaulting to 53 and parse again
if strings.Contains(err.Error(), "missing port in address") {
addr, err = caddy.ParseNetworkAddress(v + ":53")
}
if err != nil {
return err
}
}
if addr.PortRangeSize() != 1 {
return fmt.Errorf("resolver address must have exactly one address; cannot call %v", addr)
+12 -5
View File
@@ -106,7 +106,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
)
changed := rewr.rewrite(r, repl, logger)
changed := rewr.Rewrite(r, repl)
if changed {
logger.Debug("rewrote request",
@@ -121,7 +121,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
// rewrite performs the rewrites on r using repl, which should
// have been obtained from r, but is passed in for efficiency.
// It returns true if any changes were made to r.
func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
oldMethod := r.Method
oldURI := r.RequestURI
@@ -135,13 +135,20 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
// find the bounds of each part of the URI that exist
pathStart, qsStart, fragStart := -1, -1, -1
pathEnd, qsEnd := -1, -1
loop:
for i, ch := range uri {
switch {
case ch == '?' && qsStart < 0:
pathEnd, qsStart = i, i+1
case ch == '#' && fragStart < 0:
qsEnd, fragStart = i, i+1
case pathStart < 0 && qsStart < 0 && fragStart < 0:
case ch == '#' && fragStart < 0: // everything after fragment is fragment (very clear in RFC 3986 section 4.2)
if qsStart < 0 {
pathEnd = i
} else {
qsEnd = i
}
fragStart = i + 1
break loop
case pathStart < 0 && qsStart < 0:
pathStart = i
}
}
+13 -2
View File
@@ -204,6 +204,16 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/%C2%B7%E2%88%B5.png?a=b"),
expect: newRequest(t, "GET", "/i/%C2%B7%E2%88%B5.png?a=b"),
},
{
rule: Rewrite{URI: "/bar#?"},
input: newRequest(t, "GET", "/foo#fragFirst?c=d"), // not a valid query string (is part of fragment)
expect: newRequest(t, "GET", "/bar#?"), // I think this is right? but who knows; std lib drops fragment when parsing
},
{
rule: Rewrite{URI: "/bar"},
input: newRequest(t, "GET", "/foo#fragFirst?c=d"),
expect: newRequest(t, "GET", "/bar#fragFirst?c=d"),
},
{
rule: Rewrite{StripPathPrefix: "/prefix"},
@@ -271,10 +281,11 @@ func TestRewrite(t *testing.T) {
} {
// copy the original input just enough so that we can
// compare it after the rewrite to see if it changed
urlCopy := *tc.input.URL
originalInput := &http.Request{
Method: tc.input.Method,
RequestURI: tc.input.RequestURI,
URL: &*tc.input.URL,
URL: &urlCopy,
}
// populate the replacer just enough for our tests
@@ -292,7 +303,7 @@ func TestRewrite(t *testing.T) {
rep.re = re
}
changed := tc.rule.rewrite(tc.input, repl, nil)
changed := tc.rule.Rewrite(tc.input, repl)
if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
+38
View File
@@ -132,6 +132,7 @@ type Server struct {
primaryHandlerChain Handler
errorHandlerChain Handler
listenerWrappers []caddy.ListenerWrapper
strict *StrictOptions
tlsApp *caddytls.TLS
logger *zap.Logger
@@ -315,9 +316,46 @@ func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next
return Error(http.StatusMisdirectedRequest, err)
}
}
if err := s.strict.enforce(r); err != nil {
return err
}
return next.ServeHTTP(w, r)
}
func (strict *StrictOptions) enforce(r *http.Request) error {
if strict != nil && strict.Disabled {
return nil
}
// Reject query strings with unencoded ;
if strict == nil || !strict.LenientQueryStrings {
_, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return Error(http.StatusBadRequest, fmt.Errorf("invalid query string: %w", err))
}
}
// Reject header fields with _ in name (#4830)
if strict == nil || !strict.LenientHeaders {
for field := range r.Header {
if strings.Contains(field, "_") {
return Error(http.StatusBadRequest, fmt.Errorf("invalid header field name: %s", field))
}
}
}
// Reject paths with // or ..
if strict == nil || !strict.LenientPaths {
if strings.Contains(r.URL.Path, "//") || strings.Contains(r.URL.Path, "..") || strings.Contains(r.URL.Path, "\x00") {
return Error(http.StatusBadRequest, fmt.Errorf("invalid request path: %s", r.URL.RawPath))
}
}
return nil
}
// listenersUseAnyPortOtherThan returns true if there are any
// listeners in s that use a port which is not otherPort.
func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
+1
View File
@@ -15,6 +15,7 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/forwardauth"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing"
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"unicode"
"github.com/BurntSushi/toml"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func extractFrontMatter(input string) (map[string]interface{}, string, error) {
+53 -5
View File
@@ -21,6 +21,7 @@ import (
"net/http"
"strconv"
"strings"
"text/template"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -36,6 +37,8 @@ func init() {
//
// ⚠️ Template functions/actions are still experimental, so they are subject to change.
//
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface.
//
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
//
// In addition to the standard functions and the Sprig library, Caddy adds
@@ -100,10 +103,11 @@ func init() {
// {{ define "main" }}
// content
// {{ end }}
// ```
//
// **index.html**
// ```
// {{ import "/path/to/file.html" }}
// {{ import "/path/to/filename.html" }}
// {{ template "main" }}
// ```
//
@@ -234,6 +238,29 @@ func init() {
// {{stripHTML "Shows <b>only</b> text content"}}
// ```
//
// ##### `humanize`
//
// Transforms size and time inputs to a human readable format.
// This uses the [go-humanize](https://github.com/dustin/go-humanize) library.
//
// The first argument must be a format type, and the last argument
// is the input, or the input can be piped in. The supported format
// types are:
// - **size** which turns an integer amount of bytes into a string like `2.3 MB`
// - **time** which turns a time string into a relative time string like `2 weeks ago`
//
// For the `time` format, the layout for parsing the input can be configured
// by appending a colon `:` followed by the desired time layout. You can
// find the documentation on time layouts [in Go's docs](https://pkg.go.dev/time#pkg-constants).
// The default time layout is `RFC1123Z`, i.e. `Mon, 02 Jan 2006 15:04:05 -0700`.
//
// ```
// {{humanize "size" "2048000"}}
// {{placeholder "http.response.header.Content-Length" | humanize "size"}}
// {{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}}
// {{humanize "time:2006-Jan-02" "2022-May-05"}}
// ```
type Templates struct {
// The root path from which to load files. Required if template functions
// accessing the file system are used (such as include). Default is
@@ -248,6 +275,14 @@ type Templates struct {
// The template action delimiters. If set, must be precisely two elements:
// the opening and closing delimiters. Default: `["{{", "}}"]`
Delimiters []string `json:"delimiters,omitempty"`
customFuncs []template.FuncMap
}
// Customfunctions is the interface for registering custom template functions.
type CustomFunctions interface {
// CustomTemplateFunctions should return the mapping from custom function names to implementations.
CustomTemplateFunctions() template.FuncMap
}
// CaddyModule returns the Caddy module information.
@@ -260,6 +295,18 @@ func (Templates) CaddyModule() caddy.ModuleInfo {
// Provision provisions t.
func (t *Templates) Provision(ctx caddy.Context) error {
fnModInfos := caddy.GetModules("http.handlers.templates.functions")
customFuncs := make([]template.FuncMap, len(fnModInfos), 0)
for _, modInfo := range fnModInfos {
mod := modInfo.New()
fnMod, ok := mod.(CustomFunctions)
if !ok {
return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID)
}
customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions())
}
t.customFuncs = customFuncs
if t.MIMETypes == nil {
t.MIMETypes = defaultMIMETypes
}
@@ -330,10 +377,11 @@ func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Reque
}
ctx := &TemplateContext{
Root: fs,
Req: r,
RespHeader: WrappedHeader{rr.Header()},
config: t,
Root: fs,
Req: r,
RespHeader: WrappedHeader{rr.Header()},
config: t,
CustomFuncs: t.customFuncs,
}
err := ctx.executeTemplateInBuffer(r.URL.Path, rr.Buffer())
+50 -4
View File
@@ -26,11 +26,13 @@ import (
"strings"
"sync"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/alecthomas/chroma/formatters/html"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting"
"github.com/yuin/goldmark/extension"
@@ -40,10 +42,11 @@ import (
// TemplateContext is the TemplateContext with which HTTP templates are executed.
type TemplateContext struct {
Root http.FileSystem
Req *http.Request
Args []interface{} // defined by arguments to funcInclude
RespHeader WrappedHeader
Root http.FileSystem
Req *http.Request
Args []interface{} // defined by arguments to funcInclude
RespHeader WrappedHeader
CustomFuncs []template.FuncMap // functions added by plugins
config *Templates
tpl *template.Template
@@ -62,6 +65,11 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
// add sprig library
c.tpl.Funcs(sprigFuncMap)
// add all custom functions
for _, funcMap := range c.CustomFuncs {
c.tpl.Funcs(funcMap)
}
// add our own library
c.tpl.Funcs(template.FuncMap{
"include": c.funcInclude,
@@ -75,6 +83,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
"placeholder": c.funcPlaceholder,
"fileExists": c.funcFileExists,
"httpError": c.funcHTTPError,
"humanize": c.funcHumanize,
})
return c.tpl
}
@@ -392,6 +401,43 @@ func (c TemplateContext) funcHTTPError(statusCode int) (bool, error) {
return false, caddyhttp.Error(statusCode, nil)
}
// funcHumanize transforms size and time inputs to a human readable format.
//
// Size inputs are expected to be integers, and are formatted as a
// byte size, such as "83 MB".
//
// Time inputs are parsed using the given layout (default layout is RFC1123Z)
// and are formatted as a relative time, such as "2 weeks ago".
// See https://pkg.go.dev/time#pkg-constants for time layout docs.
func (c TemplateContext) funcHumanize(formatType, data string) (string, error) {
// The format type can optionally be followed
// by a colon to provide arguments for the format
parts := strings.Split(formatType, ":")
switch parts[0] {
case "size":
dataint, dataerr := strconv.ParseUint(data, 10, 64)
if dataerr != nil {
return "", fmt.Errorf("humanize: size cannot be parsed: %s", dataerr.Error())
}
return humanize.Bytes(dataint), nil
case "time":
timelayout := time.RFC1123Z
if len(parts) > 1 {
timelayout = parts[1]
}
dataint, dataerr := time.Parse(timelayout, data)
if dataerr != nil {
return "", fmt.Errorf("humanize: time cannot be parsed: %s", dataerr.Error())
}
return humanize.Time(dataint), nil
}
return "", fmt.Errorf("no know function was given")
}
// WrappedHeader wraps niladic functions so that they
// can be used in templates. (Template functions must
// return a value.)
@@ -606,6 +606,55 @@ title = "Welcome"
}
func TestHumanize(t *testing.T) {
tplContext := getContextOrFail(t)
for i, test := range []struct {
format string
inputData string
expect string
errorCase bool
verifyErr func(actual_string, substring string) bool
}{
{
format: "size",
inputData: "2048000",
expect: "2.0 MB",
errorCase: false,
verifyErr: strings.Contains,
},
{
format: "time",
inputData: "Fri, 05 May 2022 15:04:05 +0200",
expect: "ago",
errorCase: false,
verifyErr: strings.HasSuffix,
},
{
format: "time:2006-Jan-02",
inputData: "2022-May-05",
expect: "ago",
errorCase: false,
verifyErr: strings.HasSuffix,
},
{
format: "time",
inputData: "Fri, 05 May 2022 15:04:05 GMT+0200",
expect: "error:",
errorCase: true,
verifyErr: strings.HasPrefix,
},
} {
if actual, err := tplContext.funcHumanize(test.format, test.inputData); !test.verifyErr(actual, test.expect) {
if !test.errorCase {
t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, actual)
if err != nil {
t.Errorf("Test %d: error: %s", i, err.Error())
}
}
}
}
}
func getContextOrFail(t *testing.T) TemplateContext {
tplContext, err := initTestContext()
t.Cleanup(func() {
+2 -2
View File
@@ -133,7 +133,7 @@ func cmdTrust(fl caddycmd.Flags) (int, error) {
ca := CA{
log: caddy.Log(),
root: rootCert,
rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, caID, "certificates"),
rootCertPath: adminAddr + path.Join(adminPKIEndpointBase, "ca", caID),
}
// Install the cert!
@@ -207,7 +207,7 @@ func cmdUntrust(fl caddycmd.Flags) (int, error) {
// rootCertFromAdmin makes the API request to fetch the root certificate for the named CA via admin API.
func rootCertFromAdmin(adminAddr string, caID string) (*x509.Certificate, error) {
uri := path.Join(adminPKIEndpointBase, caID, "certificates")
uri := path.Join(adminPKIEndpointBase, "ca", caID)
// Make the request to fetch the CA info
resp, err := caddycmd.AdminAPIRequest(adminAddr, http.MethodGet, uri, make(http.Header), nil)
+82 -26
View File
@@ -18,6 +18,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
@@ -26,6 +27,10 @@ import (
"github.com/mholt/acmez"
)
func init() {
caddy.RegisterModule(LeafCertClientAuth{})
}
// ConnectionPolicies govern the establishment of TLS connections. It is
// an ordered group of connection policies; the first matching policy will
// be used to configure TLS connections at handshake-time.
@@ -55,6 +60,16 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
if err != nil {
return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err)
}
if pol.ClientAuthentication != nil && len(pol.ClientAuthentication.VerifiersRaw) > 0 {
clientCertValidations, err := ctx.LoadModule(pol.ClientAuthentication, "VerifiersRaw")
if err != nil {
return fmt.Errorf("loading client cert verifiers: %v", err)
}
for _, validator := range clientCertValidations.([]interface{}) {
cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier))
}
}
}
return nil
@@ -62,7 +77,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
// TLSConfig returns a standard-lib-compatible TLS configuration which
// selects the first matching policy based on the ClientHello.
func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config {
// using ServerName to match policies is extremely common, especially in configs
// with lots and lots of different policies; we can fast-track those by indexing
// them by SNI, so we don't have to iterate potentially thousands of policies
@@ -293,11 +308,22 @@ type ClientAuthentication struct {
// these CA certificates will be rejected.
TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"`
// DEPRECATED: This field is deprecated and will be removed in
// a future version. Please use the `validators` field instead
// with the tls.client_auth.leaf module instead.
//
// A list of base64 DER-encoded client leaf certs
// to accept. If this list is not empty, client certs
// which are not in this list will be rejected.
TrustedLeafCerts []string `json:"trusted_leaf_certs,omitempty"`
// Client certificate verification modules. These can perform
// custom client authentication checks, such as ensuring the
// certificate is not revoked.
VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth inline_key=verifier"`
verifiers []ClientCertificateVerifier
// The mode for authenticating the client. Allowed values are:
//
// Mode | Description
@@ -312,8 +338,6 @@ type ClientAuthentication struct {
// are provided; otherwise, the default mode is `require`.
Mode string `json:"mode,omitempty"`
// state established with the last call to ConfigureTLSConfig
trustedLeafCerts []*x509.Certificate
existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error
}
@@ -321,7 +345,8 @@ type ClientAuthentication struct {
func (clientauth ClientAuthentication) Active() bool {
return len(clientauth.TrustedCACerts) > 0 ||
len(clientauth.TrustedCACertPEMFiles) > 0 ||
len(clientauth.TrustedLeafCerts) > 0 ||
len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED
len(clientauth.VerifiersRaw) > 0 ||
len(clientauth.Mode) > 0
}
@@ -378,28 +403,31 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
cfg.ClientCAs = caPool
}
// enforce leaf verification by writing our own verify function
// TODO: DEPRECATED: Only here for backwards compatibility.
// If leaf cert is specified, enforce by adding a client auth module
if len(clientauth.TrustedLeafCerts) > 0 {
clientauth.trustedLeafCerts = []*x509.Certificate{}
caddy.Log().Named("tls.connection_policy").Warn("trusted_leaf_certs is deprecated; use leaf verifier module instead")
var trustedLeafCerts []*x509.Certificate
for _, clientCertString := range clientauth.TrustedLeafCerts {
clientCert, err := decodeBase64DERCert(clientCertString)
if err != nil {
return fmt.Errorf("parsing certificate: %v", err)
}
clientauth.trustedLeafCerts = append(clientauth.trustedLeafCerts, clientCert)
trustedLeafCerts = append(trustedLeafCerts, clientCert)
}
// if a custom verification function already exists, wrap it
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts})
}
// if a custom verification function already exists, wrap it
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
return nil
}
// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate
// callback to do custom client certificate verification. It is intended
// for installation only by clientauth.ConfigureTLSConfig().
func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
func (clientauth *ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// first use any pre-existing custom verification function
if clientauth.existingVerifyPeerCert != nil {
err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains)
@@ -407,23 +435,13 @@ func (clientauth ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte,
return err
}
}
if len(rawCerts) == 0 {
return fmt.Errorf("no client certificate provided")
}
remoteLeafCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("can't parse the given certificate: %s", err.Error())
}
for _, trustedLeafCert := range clientauth.trustedLeafCerts {
if remoteLeafCert.Equal(trustedLeafCert) {
return nil
for _, verifier := range clientauth.verifiers {
err := verifier.VerifyClientCertificate(rawCerts, verifiedChains)
if err != nil {
return err
}
}
return fmt.Errorf("client leaf certificate failed validation")
return nil
}
// decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
@@ -461,6 +479,38 @@ func setDefaultTLSParams(cfg *tls.Config) {
cfg.PreferServerCipherSuites = true
}
// LeafCertClientAuth verifies the client's leaf certificate.
type LeafCertClientAuth struct {
TrustedLeafCerts []*x509.Certificate
}
// CaddyModule returns the Caddy module information.
func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.client_auth.leaf",
New: func() caddy.Module { return new(LeafCertClientAuth) },
}
}
func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no client certificate provided")
}
remoteLeafCert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("can't parse the given certificate: %s", err.Error())
}
for _, trustedLeafCert := range l.TrustedLeafCerts {
if remoteLeafCert.Equal(trustedLeafCert) {
return nil
}
}
return fmt.Errorf("client leaf certificate failed validation")
}
// PublicKeyAlgorithm is a JSON-unmarshalable wrapper type.
type PublicKeyAlgorithm x509.PublicKeyAlgorithm
@@ -481,4 +531,10 @@ type ConnectionMatcher interface {
Match(*tls.ClientHelloInfo) bool
}
// ClientCertificateVerifier is a type which verifies client certificates.
// It is called during verifyPeerCertificate in the TLS handshake.
type ClientCertificateVerifier interface {
VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
}
var defaultALPN = []string{"h2", "http/1.1"}
+37 -1
View File
@@ -31,12 +31,13 @@ import (
func init() {
caddy.RegisterModule(DeleteFilter{})
caddy.RegisterModule(HashFilter{})
caddy.RegisterModule(ReplaceFilter{})
caddy.RegisterModule(IPMaskFilter{})
caddy.RegisterModule(QueryFilter{})
caddy.RegisterModule(CookieFilter{})
caddy.RegisterModule(RegexpFilter{})
caddy.RegisterModule(HashFilter{})
caddy.RegisterModule(RenameFilter{})
}
// LogFieldFilter can filter (or manipulate)
@@ -542,21 +543,56 @@ func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
return in
}
// RenameFilter is a Caddy log field filter that
// renames the field's key with the indicated name.
type RenameFilter struct {
Name string `json:"name,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (RenameFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.encoders.filter.rename",
New: func() caddy.Module { return new(RenameFilter) },
}
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
if d.NextArg() {
f.Name = d.Val()
}
}
return nil
}
// Filter renames the input field with the replacement name.
func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field {
in.Type = zapcore.StringType
in.Key = f.Name
return in
}
// Interface guards
var (
_ LogFieldFilter = (*DeleteFilter)(nil)
_ LogFieldFilter = (*HashFilter)(nil)
_ LogFieldFilter = (*ReplaceFilter)(nil)
_ LogFieldFilter = (*IPMaskFilter)(nil)
_ LogFieldFilter = (*QueryFilter)(nil)
_ LogFieldFilter = (*CookieFilter)(nil)
_ LogFieldFilter = (*RegexpFilter)(nil)
_ LogFieldFilter = (*RenameFilter)(nil)
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
_ caddyfile.Unmarshaler = (*HashFilter)(nil)
_ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
_ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
_ caddyfile.Unmarshaler = (*QueryFilter)(nil)
_ caddyfile.Unmarshaler = (*CookieFilter)(nil)
_ caddyfile.Unmarshaler = (*RegexpFilter)(nil)
_ caddyfile.Unmarshaler = (*RenameFilter)(nil)
_ caddy.Provisioner = (*IPMaskFilter)(nil)
_ caddy.Provisioner = (*RegexpFilter)(nil)