summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrancis Lavoie <lavofr@gmail.com>2021-05-02 14:39:06 -0400
committerGitHub <noreply@github.com>2021-05-02 12:39:06 -0600
commite4a22de9d1c4d7aa83126ee13e40b61e7b0e9df0 (patch)
tree82e3134b3c83d258fb08299e917cfa9b9d980ff7
parente6f6d3a4765565b09f95a29a2e75be34e1d70359 (diff)
reverseproxy: Add `handle_response` blocks to `reverse_proxy` (#3710) (#4021)
* reverseproxy: Add `handle_response` blocks to `reverse_proxy` (#3710) * reverseproxy: complete handle_response test * reverseproxy: Change handle_response matchers to use named matchers reverseproxy: Add support for changing status code * fastcgi: Remove obsolete TODO We already have d.Err("transport already specified") in the reverse_proxy parsing code which covers this case * reverseproxy: Fix support for "4xx" type status codes * Apply suggestions from code review Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * caddyhttp: Reorganize response matchers * reverseproxy: Reintroduce caddyfile.Unmarshaler * reverseproxy: Add comment mentioning Finalize should be called Co-authored-by: Maxime Soulé <btik-git@scoubidou.com> Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
-rw-r--r--caddyconfig/httpcaddyfile/directives.go7
-rw-r--r--caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt193
-rw-r--r--modules/caddyhttp/encode/caddyfile.go69
-rw-r--r--modules/caddyhttp/matchers.go34
-rw-r--r--modules/caddyhttp/matchers_test.go149
-rw-r--r--modules/caddyhttp/responsematchers.go122
-rw-r--r--modules/caddyhttp/responsematchers_test.go169
-rw-r--r--modules/caddyhttp/reverseproxy/caddyfile.go132
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go6
-rw-r--r--modules/caddyhttp/reverseproxy/reverseproxy.go7
10 files changed, 635 insertions, 253 deletions
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index b4a8407..5e19474 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -265,6 +265,13 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
+// WithDispenser returns a new instance based on d. All others Helper
+// fields are copied, so typically maps are shared with this new instance.
+func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
+ h.Dispenser = d
+ return h
+}
+
// ParseSegmentAsSubroute parses the segment such that its subdirectives
// are themselves treated as directives, from which a subroute is built
// and returned.
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt
new file mode 100644
index 0000000..35d9631
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt
@@ -0,0 +1,193 @@
+:8884
+
+reverse_proxy 127.0.0.1:65535 {
+ @accel header X-Accel-Redirect *
+ handle_response @accel {
+ respond "Header X-Accel-Redirect!"
+ }
+
+ @another {
+ header X-Another *
+ }
+ handle_response @another {
+ respond "Header X-Another!"
+ }
+
+ @401 status 401
+ handle_response @401 {
+ respond "Status 401!"
+ }
+
+ handle_response {
+ respond "Any! This should be last in the JSON!"
+ }
+
+ @403 {
+ status 403
+ }
+ handle_response @403 {
+ respond "Status 403!"
+ }
+
+ @multi {
+ status 401 403
+ status 404
+ header Foo *
+ header Bar *
+ }
+ handle_response @multi {
+ respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
+ }
+
+ @changeStatus status 500
+ handle_response @changeStatus 400
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handle_response": [
+ {
+ "match": {
+ "headers": {
+ "X-Accel-Redirect": [
+ "*"
+ ]
+ }
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Header X-Accel-Redirect!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "headers": {
+ "X-Another": [
+ "*"
+ ]
+ }
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Header X-Another!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "status_code": [
+ 401
+ ]
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Status 401!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "status_code": [
+ 403
+ ]
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Status 403!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "headers": {
+ "Bar": [
+ "*"
+ ],
+ "Foo": [
+ "*"
+ ]
+ },
+ "status_code": [
+ 401,
+ 403,
+ 404
+ ]
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "status_code": [
+ 500
+ ]
+ },
+ "status_code": 400
+ },
+ {
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Any! This should be last in the JSON!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go
index c45f159..b236520 100644
--- a/modules/caddyhttp/encode/caddyfile.go
+++ b/modules/caddyhttp/encode/caddyfile.go
@@ -15,9 +15,7 @@
package encode
import (
- "net/http"
"strconv"
- "strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -95,7 +93,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
enc.Prefer = encs
case "match":
- err := enc.parseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers)
+ err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers)
if err != nil {
return err
}
@@ -123,70 +121,5 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
-// Parse the tokens of a named response matcher.
-//
-// match {
-// header <field> [<value>]
-// status <code...>
-// }
-//
-// Or, single line syntax:
-//
-// match [header <field> [<value>]] | [status <code...>]
-//
-func (enc *Encode) parseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]caddyhttp.ResponseMatcher) error {
- for d.Next() {
- definitionName := d.Val()
-
- if _, ok := matchers[definitionName]; ok {
- return d.Errf("matcher is defined more than once: %s", definitionName)
- }
-
- matcher := caddyhttp.ResponseMatcher{}
- for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
- switch d.Val() {
- case "header":
- if matcher.Headers == nil {
- matcher.Headers = http.Header{}
- }
-
- // reuse the header request matcher's unmarshaler
- headerMatcher := caddyhttp.MatchHeader(matcher.Headers)
- err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment())
- if err != nil {
- return err
- }
-
- matcher.Headers = http.Header(headerMatcher)
- case "status":
- if matcher.StatusCode == nil {
- matcher.StatusCode = []int{}
- }
-
- args := d.RemainingArgs()
- if len(args) == 0 {
- return d.ArgErr()
- }
-
- for _, arg := range args {
- if len(arg) == 3 && strings.HasSuffix(arg, "xx") {
- arg = arg[:1]
- }
- statusNum, err := strconv.Atoi(arg)
- if err != nil {
- return d.Errf("bad status value '%s': %v", arg, err)
- }
- matcher.StatusCode = append(matcher.StatusCode, statusNum)
- }
- default:
- return d.Errf("unrecognized response matcher %s", d.Val())
- }
- }
-
- matchers[definitionName] = matcher
- }
- return nil
-}
-
// Interface guard
var _ caddyfile.Unmarshaler = (*Encode)(nil)
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index eaf43e9..9b127db 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -971,40 +971,6 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
-// ResponseMatcher is a type which can determine if an
-// HTTP response matches some criteria.
-type ResponseMatcher struct {
- // If set, one of these status codes would be required.
- // A one-digit status can be used to represent all codes
- // in that class (e.g. 3 for all 3xx codes).
- StatusCode []int `json:"status_code,omitempty"`
-
- // If set, each header specified must be one of the
- // specified values, with the same logic used by the
- // request header matcher.
- Headers http.Header `json:"headers,omitempty"`
-}
-
-// Match returns true if the given statusCode and hdr match rm.
-func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
- if !rm.matchStatusCode(statusCode) {
- return false
- }
- return matchHeaders(hdr, rm.Headers, "", nil)
-}
-
-func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
- if rm.StatusCode == nil {
- return true
- }
- for _, code := range rm.StatusCode {
- if StatusCodeMatches(statusCode, code) {
- return true
- }
- }
- return false
-}
-
var wordRE = regexp.MustCompile(`\w+`)
const regexpPlaceholderPrefix = "http.regexp"
diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go
index 950020f..2ec7039 100644
--- a/modules/caddyhttp/matchers_test.go
+++ b/modules/caddyhttp/matchers_test.go
@@ -804,155 +804,6 @@ func TestVarREMatcher(t *testing.T) {
}
}
-func TestResponseMatcher(t *testing.T) {
- for i, tc := range []struct {
- require ResponseMatcher
- status int
- hdr http.Header // make sure these are canonical cased (std lib will do that in a real request)
- expect bool
- }{
- {
- require: ResponseMatcher{},
- status: 200,
- expect: true,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{200},
- },
- status: 200,
- expect: true,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{2},
- },
- status: 200,
- expect: true,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{201},
- },
- status: 200,
- expect: false,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{2},
- },
- status: 301,
- expect: false,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{3},
- },
- status: 301,
- expect: true,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{3},
- },
- status: 399,
- expect: true,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{3},
- },
- status: 400,
- expect: false,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{3, 4},
- },
- status: 400,
- expect: true,
- },
- {
- require: ResponseMatcher{
- StatusCode: []int{3, 401},
- },
- status: 401,
- expect: true,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo": []string{"bar"},
- },
- },
- hdr: http.Header{"Foo": []string{"bar"}},
- expect: true,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo2": []string{"bar"},
- },
- },
- hdr: http.Header{"Foo": []string{"bar"}},
- expect: false,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo": []string{"bar", "baz"},
- },
- },
- hdr: http.Header{"Foo": []string{"baz"}},
- expect: true,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo": []string{"bar"},
- "Foo2": []string{"baz"},
- },
- },
- hdr: http.Header{"Foo": []string{"baz"}},
- expect: false,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo": []string{"bar"},
- "Foo2": []string{"baz"},
- },
- },
- hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}},
- expect: true,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo": []string{"foo*"},
- },
- },
- hdr: http.Header{"Foo": []string{"foobar"}},
- expect: true,
- },
- {
- require: ResponseMatcher{
- Headers: http.Header{
- "Foo": []string{"foo*"},
- },
- },
- hdr: http.Header{"Foo": []string{"foobar"}},
- expect: true,
- },
- } {
- actual := tc.require.Match(tc.status, tc.hdr)
- if actual != tc.expect {
- t.Errorf("Test %d %v: Expected %t, got %t for HTTP %d %v", i, tc.require, tc.expect, actual, tc.status, tc.hdr)
- continue
- }
- }
-}
-
func TestNotMatcher(t *testing.T) {
for i, tc := range []struct {
host, path string
diff --git a/modules/caddyhttp/responsematchers.go b/modules/caddyhttp/responsematchers.go
new file mode 100644
index 0000000..d9ad848
--- /dev/null
+++ b/modules/caddyhttp/responsematchers.go
@@ -0,0 +1,122 @@
+// 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 caddyhttp
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+)
+
+// ResponseMatcher is a type which can determine if an
+// HTTP response matches some criteria.
+type ResponseMatcher struct {
+ // If set, one of these status codes would be required.
+ // A one-digit status can be used to represent all codes
+ // in that class (e.g. 3 for all 3xx codes).
+ StatusCode []int `json:"status_code,omitempty"`
+
+ // If set, each header specified must be one of the
+ // specified values, with the same logic used by the
+ // request header matcher.
+ Headers http.Header `json:"headers,omitempty"`
+}
+
+// Match returns true if the given statusCode and hdr match rm.
+func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
+ if !rm.matchStatusCode(statusCode) {
+ return false
+ }
+ return matchHeaders(hdr, rm.Headers, "", nil)
+}
+
+func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
+ if rm.StatusCode == nil {
+ return true
+ }
+ for _, code := range rm.StatusCode {
+ if StatusCodeMatches(statusCode, code) {
+ return true
+ }
+ }
+ return false
+}
+
+// ParseNamedResponseMatcher parses the tokens of a named response matcher.
+//
+// @name {
+// header <field> [<value>]
+// status <code...>
+// }
+//
+// Or, single line syntax:
+//
+// @name [header <field> [<value>]] | [status <code...>]
+//
+func ParseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]ResponseMatcher) error {
+ for d.Next() {
+ definitionName := d.Val()
+
+ if _, ok := matchers[definitionName]; ok {
+ return d.Errf("matcher is defined more than once: %s", definitionName)
+ }
+
+ matcher := ResponseMatcher{}
+ for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
+ switch d.Val() {
+ case "header":
+ if matcher.Headers == nil {
+ matcher.Headers = http.Header{}
+ }
+
+ // reuse the header request matcher's unmarshaler
+ headerMatcher := MatchHeader(matcher.Headers)
+ err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment())
+ if err != nil {
+ return err
+ }
+
+ matcher.Headers = http.Header(headerMatcher)
+ case "status":
+ if matcher.StatusCode == nil {
+ matcher.StatusCode = []int{}
+ }
+
+ args := d.RemainingArgs()
+ if len(args) == 0 {
+ return d.ArgErr()
+ }
+
+ for _, arg := range args {
+ if len(arg) == 3 && strings.HasSuffix(arg, "xx") {
+ arg = arg[:1]
+ }
+ statusNum, err := strconv.Atoi(arg)
+ if err != nil {
+ return d.Errf("bad status value '%s': %v", arg, err)
+ }
+ matcher.StatusCode = append(matcher.StatusCode, statusNum)
+ }
+ default:
+ return d.Errf("unrecognized response matcher %s", d.Val())
+ }
+ }
+
+ matchers[definitionName] = matcher
+ }
+ return nil
+}
diff --git a/modules/caddyhttp/responsematchers_test.go b/modules/caddyhttp/responsematchers_test.go
new file mode 100644
index 0000000..f5bb6f1
--- /dev/null
+++ b/modules/caddyhttp/responsematchers_test.go
@@ -0,0 +1,169 @@
+// 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 caddyhttp
+
+import (
+ "net/http"
+ "testing"
+)
+
+func TestResponseMatcher(t *testing.T) {
+ for i, tc := range []struct {
+ require ResponseMatcher
+ status int
+ hdr http.Header // make sure these are canonical cased (std lib will do that in a real request)
+ expect bool
+ }{
+ {
+ require: ResponseMatcher{},
+ status: 200,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{200},
+ },
+ status: 200,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{2},
+ },
+ status: 200,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{201},
+ },
+ status: 200,
+ expect: false,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{2},
+ },
+ status: 301,
+ expect: false,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{3},
+ },
+ status: 301,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{3},
+ },
+ status: 399,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{3},
+ },
+ status: 400,
+ expect: false,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{3, 4},
+ },
+ status: 400,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ StatusCode: []int{3, 401},
+ },
+ status: 401,
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo": []string{"bar"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"bar"}},
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo2": []string{"bar"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"bar"}},
+ expect: false,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo": []string{"bar", "baz"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"baz"}},
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo": []string{"bar"},
+ "Foo2": []string{"baz"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"baz"}},
+ expect: false,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo": []string{"bar"},
+ "Foo2": []string{"baz"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}},
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo": []string{"foo*"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"foobar"}},
+ expect: true,
+ },
+ {
+ require: ResponseMatcher{
+ Headers: http.Header{
+ "Foo": []string{"foo*"},
+ },
+ },
+ hdr: http.Header{"Foo": []string{"foobar"}},
+ expect: true,
+ },
+ } {
+ actual := tc.require.Match(tc.status, tc.hdr)
+ if actual != tc.expect {
+ t.Errorf("Test %d %v: Expected %t, got %t for HTTP %d %v", i, tc.require, tc.expect, actual, tc.status, tc.hdr)
+ continue
+ }
+ }
+}
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index dbadef6..61eac7e 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -42,6 +42,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if err != nil {
return nil, err
}
+ err = rp.FinalizeUnmarshalCaddyfile(h)
+ if err != nil {
+ return nil, err
+ }
return rp, nil
}
@@ -86,12 +90,24 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// transport <name> {
// ...
// }
+//
+// # handle responses
+// @name {
+// status <code...>
+// header <field> [<value>]
+// }
+// handle_response [<matcher>] [status_code] {
+// <directives...>
+// }
// }
//
// Proxy upstream addresses should be network dial addresses such
// as `host:port`, or a URL such as `scheme://host:port`. Scheme
// and port may be inferred from other parts of the address/URL; if
// either are missing, defaults to HTTP.
+//
+// The FinalizeUnmarshalCaddyfile method should be called after this
+// to finalize parsing of "handle_response" blocks, if possible.
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// currently, all backends must use the same scheme/protocol (the
// underlying JSON does not yet support per-backend transports)
@@ -102,6 +118,10 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
var transport http.RoundTripper
var transportModuleName string
+ // collect the response matchers defined as subdirectives
+ // prefixed with "@" for use with "handle_response" blocks
+ h.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)
+
// TODO: the logic in this function is kind of sensitive, we need
// to write tests before making any more changes to it
upstreamDialAddress := func(upstreamAddr string) (string, error) {
@@ -227,6 +247,16 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
for d.NextBlock(0) {
+ // if the subdirective has an "@" prefix then we
+ // parse it as a response matcher for use with "handle_response"
+ if strings.HasPrefix(d.Val(), matcherPrefix) {
+ err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), h.responseMatchers)
+ if err != nil {
+ return err
+ }
+ continue
+ }
+
switch d.Val() {
case "to":
args := d.RemainingArgs()
@@ -617,6 +647,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
transport = rt
+ case "handle_response":
+ // delegate the parsing of handle_response to the caller,
+ // since we need the httpcaddyfile.Helper to parse subroutes.
+ // See h.FinalizeUnmarshalCaddyfile
+ h.handleResponseSegments = append(h.handleResponseSegments, d.NewFromNextSegment())
+
default:
return d.Errf("unrecognized subdirective %s", d.Val())
}
@@ -659,6 +695,100 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
+// FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which
+// requires having an httpcaddyfile.Helper to function, to parse subroutes.
+func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error {
+ for _, d := range h.handleResponseSegments {
+ // consume the "handle_response" token
+ d.Next()
+
+ var matcher *caddyhttp.ResponseMatcher
+ args := d.RemainingArgs()
+
+ // the first arg should be a matcher (optional)
+ // the second arg should be a status code (optional)
+ // any more than that isn't currently supported
+ if len(args) > 2 {
+ return d.Errf("too many arguments for 'handle_response': %s", args)
+ }
+
+ // the first arg should always be a matcher.
+ // it doesn't really make sense to support status code without a matcher.
+ if len(args) > 0 {
+ 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:])
+ }
+ matcher = &foundMatcher
+ }
+
+ // a second arg should be a status code, in which case
+ // we skip parsing the block for routes
+ if len(args) == 2 {
+ _, err := strconv.Atoi(args[1])
+ if err != nil {
+ return d.Errf("bad integer value '%s': %v", args[1], err)
+ }
+
+ // make sure there's no block, cause it doesn't make sense
+ if d.NextBlock(1) {
+ return d.Errf("cannot define routes for 'handle_response' when changing the status code")
+ }
+
+ h.HandleResponse = append(
+ h.HandleResponse,
+ caddyhttp.ResponseHandler{
+ Match: matcher,
+ StatusCode: caddyhttp.WeakString(args[1]),
+ },
+ )
+ continue
+ }
+
+ // parse the block as routes
+ handler, err := httpcaddyfile.ParseSegmentAsSubroute(helper.WithDispenser(d.NewFromNextSegment()))
+ if err != nil {
+ return err
+ }
+ subroute, ok := handler.(*caddyhttp.Subroute)
+ if !ok {
+ return helper.Errf("segment was not parsed as a subroute")
+ }
+ h.HandleResponse = append(
+ h.HandleResponse,
+ caddyhttp.ResponseHandler{
+ Match: matcher,
+ Routes: subroute.Routes,
+ },
+ )
+ }
+
+ // move the handle_response entries without a matcher to the end.
+ // we can't use sort.SliceStable because it will reorder the rest of the
+ // entries which may be undesirable because we don't have a good
+ // heuristic to use for sorting.
+ withoutMatchers := []caddyhttp.ResponseHandler{}
+ withMatchers := []caddyhttp.ResponseHandler{}
+ for _, hr := range h.HandleResponse {
+ if hr.Match == nil {
+ withoutMatchers = append(withoutMatchers, hr)
+ } else {
+ withMatchers = append(withMatchers, hr)
+ }
+ }
+ h.HandleResponse = append(withMatchers, withoutMatchers...)
+
+ // clean up the bits we only needed for adapting
+ h.handleResponseSegments = nil
+ h.responseMatchers = nil
+
+ return nil
+}
+
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
//
// transport http {
@@ -892,6 +1022,8 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
+const matcherPrefix = "@"
+
// Interface guards
var (
_ caddyfile.Unmarshaler = (*Handler)(nil)
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
index 4d0b23b..0ccd9fe 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -353,12 +353,14 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// the rest of the config is specified by the user
// using the reverse_proxy directive syntax
- // TODO: this can overwrite our fcgiTransport that we encoded and
- // set on the rpHandler... even with a non-fastcgi transport!
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 which is
// conditional on matching PHP files
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index b552d96..b6c24f3 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -31,6 +31,7 @@ import (
"time"
"github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"go.uber.org/zap"
@@ -127,6 +128,12 @@ type Handler struct {
Transport http.RoundTripper `json:"-"`
CB CircuitBreaker `json:"-"`
+ // Holds the named response matchers from the Caddyfile while adapting
+ responseMatchers map[string]caddyhttp.ResponseMatcher
+
+ // Holds the handle_response Caddyfile tokens while adapting
+ handleResponseSegments []*caddyfile.Dispenser
+
ctx caddy.Context
logger *zap.Logger
}