@marcel91200 / caddy Mirror · read-only / commits / e4a22de9
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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>

Francis Lavoie committed May 2, 2021 at 14:39 UTC e4a22de9d1c4d7aa83126ee13e40b61e7b0e9df0
10 files changed +635 -253
caddyconfig/httpcaddyfile/directives.go
+7
@@ -265,6 +265,13 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
265 return []ConfigValue{{Class: "bind", Value: addrs}}
266 }
267
268 +// WithDispenser returns a new instance based on d. All others Helper
269 +// fields are copied, so typically maps are shared with this new instance.
270 +func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
271 + h.Dispenser = d
272 + return h
273 +}
274 +
275 // ParseSegmentAsSubroute parses the segment such that its subdirectives
276 // are themselves treated as directives, from which a subroute is built
277 // and returned.
caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt new
+193
@@ -0,0 +1,193 @@
1 +:8884
2 +
3 +reverse_proxy 127.0.0.1:65535 {
4 + @accel header X-Accel-Redirect *
5 + handle_response @accel {
6 + respond "Header X-Accel-Redirect!"
7 + }
8 +
9 + @another {
10 + header X-Another *
11 + }
12 + handle_response @another {
13 + respond "Header X-Another!"
14 + }
15 +
16 + @401 status 401
17 + handle_response @401 {
18 + respond "Status 401!"
19 + }
20 +
21 + handle_response {
22 + respond "Any! This should be last in the JSON!"
23 + }
24 +
25 + @403 {
26 + status 403
27 + }
28 + handle_response @403 {
29 + respond "Status 403!"
30 + }
31 +
32 + @multi {
33 + status 401 403
34 + status 404
35 + header Foo *
36 + header Bar *
37 + }
38 + handle_response @multi {
39 + respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
40 + }
41 +
42 + @changeStatus status 500
43 + handle_response @changeStatus 400
44 +}
45 +----------
46 +{
47 + "apps": {
48 + "http": {
49 + "servers": {
50 + "srv0": {
51 + "listen": [
52 + ":8884"
53 + ],
54 + "routes": [
55 + {
56 + "handle": [
57 + {
58 + "handle_response": [
59 + {
60 + "match": {
61 + "headers": {
62 + "X-Accel-Redirect": [
63 + "*"
64 + ]
65 + }
66 + },
67 + "routes": [
68 + {
69 + "handle": [
70 + {
71 + "body": "Header X-Accel-Redirect!",
72 + "handler": "static_response"
73 + }
74 + ]
75 + }
76 + ]
77 + },
78 + {
79 + "match": {
80 + "headers": {
81 + "X-Another": [
82 + "*"
83 + ]
84 + }
85 + },
86 + "routes": [
87 + {
88 + "handle": [
89 + {
90 + "body": "Header X-Another!",
91 + "handler": "static_response"
92 + }
93 + ]
94 + }
95 + ]
96 + },
97 + {
98 + "match": {
99 + "status_code": [
100 + 401
101 + ]
102 + },
103 + "routes": [
104 + {
105 + "handle": [
106 + {
107 + "body": "Status 401!",
108 + "handler": "static_response"
109 + }
110 + ]
111 + }
112 + ]
113 + },
114 + {
115 + "match": {
116 + "status_code": [
117 + 403
118 + ]
119 + },
120 + "routes": [
121 + {
122 + "handle": [
123 + {
124 + "body": "Status 403!",
125 + "handler": "static_response"
126 + }
127 + ]
128 + }
129 + ]
130 + },
131 + {
132 + "match": {
133 + "headers": {
134 + "Bar": [
135 + "*"
136 + ],
137 + "Foo": [
138 + "*"
139 + ]
140 + },
141 + "status_code": [
142 + 401,
143 + 403,
144 + 404
145 + ]
146 + },
147 + "routes": [
148 + {
149 + "handle": [
150 + {
151 + "body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
152 + "handler": "static_response"
153 + }
154 + ]
155 + }
156 + ]
157 + },
158 + {
159 + "match": {
160 + "status_code": [
161 + 500
162 + ]
163 + },
164 + "status_code": 400
165 + },
166 + {
167 + "routes": [
168 + {
169 + "handle": [
170 + {
171 + "body": "Any! This should be last in the JSON!",
172 + "handler": "static_response"
173 + }
174 + ]
175 + }
176 + ]
177 + }
178 + ],
179 + "handler": "reverse_proxy",
180 + "upstreams": [
181 + {
182 + "dial": "127.0.0.1:65535"
183 + }
184 + ]
185 + }
186 + ]
187 + }
188 + ]
189 + }
190 + }
191 + }
192 + }
193 +}
modules/caddyhttp/encode/caddyfile.go
+1 -68
@@ -15,9 +15,7 @@
15 package encode
16
17 import (
18 - "net/http"
18 "strconv"
20 - "strings"
19
20 "github.com/caddyserver/caddy/v2"
21 "github.com/caddyserver/caddy/v2/caddyconfig"
@@ -95,7 +93,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
93 }
94 enc.Prefer = encs
95 case "match":
98 - err := enc.parseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers)
96 + err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers)
97 if err != nil {
98 return err
99 }
@@ -123,70 +121,5 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
121 return nil
122 }
123
126 -// Parse the tokens of a named response matcher.
127 -//
128 -// match {
129 -// header <field> [<value>]
130 -// status <code...>
131 -// }
132 -//
133 -// Or, single line syntax:
134 -//
135 -// match [header <field> [<value>]] | [status <code...>]
136 -//
137 -func (enc *Encode) parseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]caddyhttp.ResponseMatcher) error {
138 - for d.Next() {
139 - definitionName := d.Val()
140 -
141 - if _, ok := matchers[definitionName]; ok {
142 - return d.Errf("matcher is defined more than once: %s", definitionName)
143 - }
144 -
145 - matcher := caddyhttp.ResponseMatcher{}
146 - for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
147 - switch d.Val() {
148 - case "header":
149 - if matcher.Headers == nil {
150 - matcher.Headers = http.Header{}
151 - }
152 -
153 - // reuse the header request matcher's unmarshaler
154 - headerMatcher := caddyhttp.MatchHeader(matcher.Headers)
155 - err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment())
156 - if err != nil {
157 - return err
158 - }
159 -
160 - matcher.Headers = http.Header(headerMatcher)
161 - case "status":
162 - if matcher.StatusCode == nil {
163 - matcher.StatusCode = []int{}
164 - }
165 -
166 - args := d.RemainingArgs()
167 - if len(args) == 0 {
168 - return d.ArgErr()
169 - }
170 -
171 - for _, arg := range args {
172 - if len(arg) == 3 && strings.HasSuffix(arg, "xx") {
173 - arg = arg[:1]
174 - }
175 - statusNum, err := strconv.Atoi(arg)
176 - if err != nil {
177 - return d.Errf("bad status value '%s': %v", arg, err)
178 - }
179 - matcher.StatusCode = append(matcher.StatusCode, statusNum)
180 - }
181 - default:
182 - return d.Errf("unrecognized response matcher %s", d.Val())
183 - }
184 - }
185 -
186 - matchers[definitionName] = matcher
187 - }
188 - return nil
189 -}
190 -
124 // Interface guard
125 var _ caddyfile.Unmarshaler = (*Encode)(nil)
modules/caddyhttp/matchers.go
-34
@@ -971,40 +971,6 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
971 return nil
972 }
973
974 -// ResponseMatcher is a type which can determine if an
975 -// HTTP response matches some criteria.
976 -type ResponseMatcher struct {
977 - // If set, one of these status codes would be required.
978 - // A one-digit status can be used to represent all codes
979 - // in that class (e.g. 3 for all 3xx codes).
980 - StatusCode []int `json:"status_code,omitempty"`
981 -
982 - // If set, each header specified must be one of the
983 - // specified values, with the same logic used by the
984 - // request header matcher.
985 - Headers http.Header `json:"headers,omitempty"`
986 -}
987 -
988 -// Match returns true if the given statusCode and hdr match rm.
989 -func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
990 - if !rm.matchStatusCode(statusCode) {
991 - return false
992 - }
993 - return matchHeaders(hdr, rm.Headers, "", nil)
994 -}
995 -
996 -func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
997 - if rm.StatusCode == nil {
998 - return true
999 - }
1000 - for _, code := range rm.StatusCode {
1001 - if StatusCodeMatches(statusCode, code) {
1002 - return true
1003 - }
1004 - }
1005 - return false
1006 -}
1007 -
974 var wordRE = regexp.MustCompile(`\w+`)
975
976 const regexpPlaceholderPrefix = "http.regexp"
modules/caddyhttp/matchers_test.go
-149
@@ -804,155 +804,6 @@ func TestVarREMatcher(t *testing.T) {
804 }
805 }
806
807 -func TestResponseMatcher(t *testing.T) {
808 - for i, tc := range []struct {
809 - require ResponseMatcher
810 - status int
811 - hdr http.Header // make sure these are canonical cased (std lib will do that in a real request)
812 - expect bool
813 - }{
814 - {
815 - require: ResponseMatcher{},
816 - status: 200,
817 - expect: true,
818 - },
819 - {
820 - require: ResponseMatcher{
821 - StatusCode: []int{200},
822 - },
823 - status: 200,
824 - expect: true,
825 - },
826 - {
827 - require: ResponseMatcher{
828 - StatusCode: []int{2},
829 - },
830 - status: 200,
831 - expect: true,
832 - },
833 - {
834 - require: ResponseMatcher{
835 - StatusCode: []int{201},
836 - },
837 - status: 200,
838 - expect: false,
839 - },
840 - {
841 - require: ResponseMatcher{
842 - StatusCode: []int{2},
843 - },
844 - status: 301,
845 - expect: false,
846 - },
847 - {
848 - require: ResponseMatcher{
849 - StatusCode: []int{3},
850 - },
851 - status: 301,
852 - expect: true,
853 - },
854 - {
855 - require: ResponseMatcher{
856 - StatusCode: []int{3},
857 - },
858 - status: 399,
859 - expect: true,
860 - },
861 - {
862 - require: ResponseMatcher{
863 - StatusCode: []int{3},
864 - },
865 - status: 400,
866 - expect: false,
867 - },
868 - {
869 - require: ResponseMatcher{
870 - StatusCode: []int{3, 4},
871 - },
872 - status: 400,
873 - expect: true,
874 - },
875 - {
876 - require: ResponseMatcher{
877 - StatusCode: []int{3, 401},
878 - },
879 - status: 401,
880 - expect: true,
881 - },
882 - {
883 - require: ResponseMatcher{
884 - Headers: http.Header{
885 - "Foo": []string{"bar"},
886 - },
887 - },
888 - hdr: http.Header{"Foo": []string{"bar"}},
889 - expect: true,
890 - },
891 - {
892 - require: ResponseMatcher{
893 - Headers: http.Header{
894 - "Foo2": []string{"bar"},
895 - },
896 - },
897 - hdr: http.Header{"Foo": []string{"bar"}},
898 - expect: false,
899 - },
900 - {
901 - require: ResponseMatcher{
902 - Headers: http.Header{
903 - "Foo": []string{"bar", "baz"},
904 - },
905 - },
906 - hdr: http.Header{"Foo": []string{"baz"}},
907 - expect: true,
908 - },
909 - {
910 - require: ResponseMatcher{
911 - Headers: http.Header{
912 - "Foo": []string{"bar"},
913 - "Foo2": []string{"baz"},
914 - },
915 - },
916 - hdr: http.Header{"Foo": []string{"baz"}},
917 - expect: false,
918 - },
919 - {
920 - require: ResponseMatcher{
921 - Headers: http.Header{
922 - "Foo": []string{"bar"},
923 - "Foo2": []string{"baz"},
924 - },
925 - },
926 - hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}},
927 - expect: true,
928 - },
929 - {
930 - require: ResponseMatcher{
931 - Headers: http.Header{
932 - "Foo": []string{"foo*"},
933 - },
934 - },
935 - hdr: http.Header{"Foo": []string{"foobar"}},
936 - expect: true,
937 - },
938 - {
939 - require: ResponseMatcher{
940 - Headers: http.Header{
941 - "Foo": []string{"foo*"},
942 - },
943 - },
944 - hdr: http.Header{"Foo": []string{"foobar"}},
945 - expect: true,
946 - },
947 - } {
948 - actual := tc.require.Match(tc.status, tc.hdr)
949 - if actual != tc.expect {
950 - t.Errorf("Test %d %v: Expected %t, got %t for HTTP %d %v", i, tc.require, tc.expect, actual, tc.status, tc.hdr)
951 - continue
952 - }
953 - }
954 -}
955 -
807 func TestNotMatcher(t *testing.T) {
808 for i, tc := range []struct {
809 host, path string
modules/caddyhttp/responsematchers.go new
+122
@@ -0,0 +1,122 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyhttp
16 +
17 +import (
18 + "net/http"
19 + "strconv"
20 + "strings"
21 +
22 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
23 +)
24 +
25 +// ResponseMatcher is a type which can determine if an
26 +// HTTP response matches some criteria.
27 +type ResponseMatcher struct {
28 + // If set, one of these status codes would be required.
29 + // A one-digit status can be used to represent all codes
30 + // in that class (e.g. 3 for all 3xx codes).
31 + StatusCode []int `json:"status_code,omitempty"`
32 +
33 + // If set, each header specified must be one of the
34 + // specified values, with the same logic used by the
35 + // request header matcher.
36 + Headers http.Header `json:"headers,omitempty"`
37 +}
38 +
39 +// Match returns true if the given statusCode and hdr match rm.
40 +func (rm ResponseMatcher) Match(statusCode int, hdr http.Header) bool {
41 + if !rm.matchStatusCode(statusCode) {
42 + return false
43 + }
44 + return matchHeaders(hdr, rm.Headers, "", nil)
45 +}
46 +
47 +func (rm ResponseMatcher) matchStatusCode(statusCode int) bool {
48 + if rm.StatusCode == nil {
49 + return true
50 + }
51 + for _, code := range rm.StatusCode {
52 + if StatusCodeMatches(statusCode, code) {
53 + return true
54 + }
55 + }
56 + return false
57 +}
58 +
59 +// ParseNamedResponseMatcher parses the tokens of a named response matcher.
60 +//
61 +// @name {
62 +// header <field> [<value>]
63 +// status <code...>
64 +// }
65 +//
66 +// Or, single line syntax:
67 +//
68 +// @name [header <field> [<value>]] | [status <code...>]
69 +//
70 +func ParseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]ResponseMatcher) error {
71 + for d.Next() {
72 + definitionName := d.Val()
73 +
74 + if _, ok := matchers[definitionName]; ok {
75 + return d.Errf("matcher is defined more than once: %s", definitionName)
76 + }
77 +
78 + matcher := ResponseMatcher{}
79 + for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
80 + switch d.Val() {
81 + case "header":
82 + if matcher.Headers == nil {
83 + matcher.Headers = http.Header{}
84 + }
85 +
86 + // reuse the header request matcher's unmarshaler
87 + headerMatcher := MatchHeader(matcher.Headers)
88 + err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment())
89 + if err != nil {
90 + return err
91 + }
92 +
93 + matcher.Headers = http.Header(headerMatcher)
94 + case "status":
95 + if matcher.StatusCode == nil {
96 + matcher.StatusCode = []int{}
97 + }
98 +
99 + args := d.RemainingArgs()
100 + if len(args) == 0 {
101 + return d.ArgErr()
102 + }
103 +
104 + for _, arg := range args {
105 + if len(arg) == 3 && strings.HasSuffix(arg, "xx") {
106 + arg = arg[:1]
107 + }
108 + statusNum, err := strconv.Atoi(arg)
109 + if err != nil {
110 + return d.Errf("bad status value '%s': %v", arg, err)
111 + }
112 + matcher.StatusCode = append(matcher.StatusCode, statusNum)
113 + }
114 + default:
115 + return d.Errf("unrecognized response matcher %s", d.Val())
116 + }
117 + }
118 +
119 + matchers[definitionName] = matcher
120 + }
121 + return nil
122 +}
modules/caddyhttp/responsematchers_test.go new
+169
@@ -0,0 +1,169 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyhttp
16 +
17 +import (
18 + "net/http"
19 + "testing"
20 +)
21 +
22 +func TestResponseMatcher(t *testing.T) {
23 + for i, tc := range []struct {
24 + require ResponseMatcher
25 + status int
26 + hdr http.Header // make sure these are canonical cased (std lib will do that in a real request)
27 + expect bool
28 + }{
29 + {
30 + require: ResponseMatcher{},
31 + status: 200,
32 + expect: true,
33 + },
34 + {
35 + require: ResponseMatcher{
36 + StatusCode: []int{200},
37 + },
38 + status: 200,
39 + expect: true,
40 + },
41 + {
42 + require: ResponseMatcher{
43 + StatusCode: []int{2},
44 + },
45 + status: 200,
46 + expect: true,
47 + },
48 + {
49 + require: ResponseMatcher{
50 + StatusCode: []int{201},
51 + },
52 + status: 200,
53 + expect: false,
54 + },
55 + {
56 + require: ResponseMatcher{
57 + StatusCode: []int{2},
58 + },
59 + status: 301,
60 + expect: false,
61 + },
62 + {
63 + require: ResponseMatcher{
64 + StatusCode: []int{3},
65 + },
66 + status: 301,
67 + expect: true,
68 + },
69 + {
70 + require: ResponseMatcher{
71 + StatusCode: []int{3},
72 + },
73 + status: 399,
74 + expect: true,
75 + },
76 + {
77 + require: ResponseMatcher{
78 + StatusCode: []int{3},
79 + },
80 + status: 400,
81 + expect: false,
82 + },
83 + {
84 + require: ResponseMatcher{
85 + StatusCode: []int{3, 4},
86 + },
87 + status: 400,
88 + expect: true,
89 + },
90 + {
91 + require: ResponseMatcher{
92 + StatusCode: []int{3, 401},
93 + },
94 + status: 401,
95 + expect: true,
96 + },
97 + {
98 + require: ResponseMatcher{
99 + Headers: http.Header{
100 + "Foo": []string{"bar"},
101 + },
102 + },
103 + hdr: http.Header{"Foo": []string{"bar"}},
104 + expect: true,
105 + },
106 + {
107 + require: ResponseMatcher{
108 + Headers: http.Header{
109 + "Foo2": []string{"bar"},
110 + },
111 + },
112 + hdr: http.Header{"Foo": []string{"bar"}},
113 + expect: false,
114 + },
115 + {
116 + require: ResponseMatcher{
117 + Headers: http.Header{
118 + "Foo": []string{"bar", "baz"},
119 + },
120 + },
121 + hdr: http.Header{"Foo": []string{"baz"}},
122 + expect: true,
123 + },
124 + {
125 + require: ResponseMatcher{
126 + Headers: http.Header{
127 + "Foo": []string{"bar"},
128 + "Foo2": []string{"baz"},
129 + },
130 + },
131 + hdr: http.Header{"Foo": []string{"baz"}},
132 + expect: false,
133 + },
134 + {
135 + require: ResponseMatcher{
136 + Headers: http.Header{
137 + "Foo": []string{"bar"},
138 + "Foo2": []string{"baz"},
139 + },
140 + },
141 + hdr: http.Header{"Foo": []string{"bar"}, "Foo2": []string{"baz"}},
142 + expect: true,
143 + },
144 + {
145 + require: ResponseMatcher{
146 + Headers: http.Header{
147 + "Foo": []string{"foo*"},
148 + },
149 + },
150 + hdr: http.Header{"Foo": []string{"foobar"}},
151 + expect: true,
152 + },
153 + {
154 + require: ResponseMatcher{
155 + Headers: http.Header{
156 + "Foo": []string{"foo*"},
157 + },
158 + },
159 + hdr: http.Header{"Foo": []string{"foobar"}},
160 + expect: true,
161 + },
162 + } {
163 + actual := tc.require.Match(tc.status, tc.hdr)
164 + if actual != tc.expect {
165 + t.Errorf("Test %d %v: Expected %t, got %t for HTTP %d %v", i, tc.require, tc.expect, actual, tc.status, tc.hdr)
166 + continue
167 + }
168 + }
169 +}
modules/caddyhttp/reverseproxy/caddyfile.go
+132
@@ -42,6 +42,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
42 if err != nil {
43 return nil, err
44 }
45 + err = rp.FinalizeUnmarshalCaddyfile(h)
46 + if err != nil {
47 + return nil, err
48 + }
49 return rp, nil
50 }
51
@@ -86,12 +90,24 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
90 // transport <name> {
91 // ...
92 // }
93 +//
94 +// # handle responses
95 +// @name {
96 +// status <code...>
97 +// header <field> [<value>]
98 +// }
99 +// handle_response [<matcher>] [status_code] {
100 +// <directives...>
101 +// }
102 // }
103 //
104 // Proxy upstream addresses should be network dial addresses such
105 // as `host:port`, or a URL such as `scheme://host:port`. Scheme
106 // and port may be inferred from other parts of the address/URL; if
107 // either are missing, defaults to HTTP.
108 +//
109 +// The FinalizeUnmarshalCaddyfile method should be called after this
110 +// to finalize parsing of "handle_response" blocks, if possible.
111 func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
112 // currently, all backends must use the same scheme/protocol (the
113 // underlying JSON does not yet support per-backend transports)
@@ -102,6 +118,10 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
118 var transport http.RoundTripper
119 var transportModuleName string
120
121 + // collect the response matchers defined as subdirectives
122 + // prefixed with "@" for use with "handle_response" blocks
123 + h.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)
124 +
125 // TODO: the logic in this function is kind of sensitive, we need
126 // to write tests before making any more changes to it
127 upstreamDialAddress := func(upstreamAddr string) (string, error) {
@@ -227,6 +247,16 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
247 }
248
249 for d.NextBlock(0) {
250 + // if the subdirective has an "@" prefix then we
251 + // parse it as a response matcher for use with "handle_response"
252 + if strings.HasPrefix(d.Val(), matcherPrefix) {
253 + err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), h.responseMatchers)
254 + if err != nil {
255 + return err
256 + }
257 + continue
258 + }
259 +
260 switch d.Val() {
261 case "to":
262 args := d.RemainingArgs()
@@ -617,6 +647,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
647 }
648 transport = rt
649
650 + case "handle_response":
651 + // delegate the parsing of handle_response to the caller,
652 + // since we need the httpcaddyfile.Helper to parse subroutes.
653 + // See h.FinalizeUnmarshalCaddyfile
654 + h.handleResponseSegments = append(h.handleResponseSegments, d.NewFromNextSegment())
655 +
656 default:
657 return d.Errf("unrecognized subdirective %s", d.Val())
658 }
@@ -659,6 +695,100 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
695 return nil
696 }
697
698 +// FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which
699 +// requires having an httpcaddyfile.Helper to function, to parse subroutes.
700 +func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error {
701 + for _, d := range h.handleResponseSegments {
702 + // consume the "handle_response" token
703 + d.Next()
704 +
705 + var matcher *caddyhttp.ResponseMatcher
706 + args := d.RemainingArgs()
707 +
708 + // the first arg should be a matcher (optional)
709 + // the second arg should be a status code (optional)
710 + // any more than that isn't currently supported
711 + if len(args) > 2 {
712 + return d.Errf("too many arguments for 'handle_response': %s", args)
713 + }
714 +
715 + // the first arg should always be a matcher.
716 + // it doesn't really make sense to support status code without a matcher.
717 + if len(args) > 0 {
718 + if !strings.HasPrefix(args[0], matcherPrefix) {
719 + return d.Errf("must use a named response matcher, starting with '@'")
720 + }
721 +
722 + foundMatcher, ok := h.responseMatchers[args[0]]
723 + if !ok {
724 + return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
725 + }
726 + matcher = &foundMatcher
727 + }
728 +
729 + // a second arg should be a status code, in which case
730 + // we skip parsing the block for routes
731 + if len(args) == 2 {
732 + _, err := strconv.Atoi(args[1])
733 + if err != nil {
734 + return d.Errf("bad integer value '%s': %v", args[1], err)
735 + }
736 +
737 + // make sure there's no block, cause it doesn't make sense
738 + if d.NextBlock(1) {
739 + return d.Errf("cannot define routes for 'handle_response' when changing the status code")
740 + }
741 +
742 + h.HandleResponse = append(
743 + h.HandleResponse,
744 + caddyhttp.ResponseHandler{
745 + Match: matcher,
746 + StatusCode: caddyhttp.WeakString(args[1]),
747 + },
748 + )
749 + continue
750 + }
751 +
752 + // parse the block as routes
753 + handler, err := httpcaddyfile.ParseSegmentAsSubroute(helper.WithDispenser(d.NewFromNextSegment()))
754 + if err != nil {
755 + return err
756 + }
757 + subroute, ok := handler.(*caddyhttp.Subroute)
758 + if !ok {
759 + return helper.Errf("segment was not parsed as a subroute")
760 + }
761 + h.HandleResponse = append(
762 + h.HandleResponse,
763 + caddyhttp.ResponseHandler{
764 + Match: matcher,
765 + Routes: subroute.Routes,
766 + },
767 + )
768 + }
769 +
770 + // move the handle_response entries without a matcher to the end.
771 + // we can't use sort.SliceStable because it will reorder the rest of the
772 + // entries which may be undesirable because we don't have a good
773 + // heuristic to use for sorting.
774 + withoutMatchers := []caddyhttp.ResponseHandler{}
775 + withMatchers := []caddyhttp.ResponseHandler{}
776 + for _, hr := range h.HandleResponse {
777 + if hr.Match == nil {
778 + withoutMatchers = append(withoutMatchers, hr)
779 + } else {
780 + withMatchers = append(withMatchers, hr)
781 + }
782 + }
783 + h.HandleResponse = append(withMatchers, withoutMatchers...)
784 +
785 + // clean up the bits we only needed for adapting
786 + h.handleResponseSegments = nil
787 + h.responseMatchers = nil
788 +
789 + return nil
790 +}
791 +
792 // UnmarshalCaddyfile deserializes Caddyfile tokens into h.
793 //
794 // transport http {
@@ -892,6 +1022,8 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1022 return nil
1023 }
1024
1025 +const matcherPrefix = "@"
1026 +
1027 // Interface guards
1028 var (
1029 _ caddyfile.Unmarshaler = (*Handler)(nil)
modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+4 -2
@@ -353,12 +353,14 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
353
354 // the rest of the config is specified by the user
355 // using the reverse_proxy directive syntax
356 - // TODO: this can overwrite our fcgiTransport that we encoded and
357 - // set on the rpHandler... even with a non-fastcgi transport!
356 err = rpHandler.UnmarshalCaddyfile(dispenser)
357 if err != nil {
358 return nil, err
359 }
360 + err = rpHandler.FinalizeUnmarshalCaddyfile(h)
361 + if err != nil {
362 + return nil, err
363 + }
364
365 // create the final reverse proxy route which is
366 // conditional on matching PHP files
modules/caddyhttp/reverseproxy/reverseproxy.go
+7
@@ -31,6 +31,7 @@ import (
31 "time"
32
33 "github.com/caddyserver/caddy/v2"
34 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
35 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
36 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
37 "go.uber.org/zap"
@@ -127,6 +128,12 @@ type Handler struct {
128 Transport http.RoundTripper `json:"-"`
129 CB CircuitBreaker `json:"-"`
130
131 + // Holds the named response matchers from the Caddyfile while adapting
132 + responseMatchers map[string]caddyhttp.ResponseMatcher
133 +
134 + // Holds the handle_response Caddyfile tokens while adapting
135 + handleResponseSegments []*caddyfile.Dispenser
136 +
137 ctx caddy.Context
138 logger *zap.Logger
139 }