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