Browse Source
* 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>master
committed by
GitHub
10 changed files with 635 additions and 253 deletions
@ -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" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
} |
@ -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 |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue