mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 14:13:48 +03:00
* 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>
This commit is contained in:
parent
e6f6d3a476
commit
e4a22de9d1
10 changed files with 635 additions and 253 deletions
|
@ -265,6 +265,13 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
|
||||||
return []ConfigValue{{Class: "bind", Value: addrs}}
|
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
|
// ParseSegmentAsSubroute parses the segment such that its subdirectives
|
||||||
// are themselves treated as directives, from which a subroute is built
|
// are themselves treated as directives, from which a subroute is built
|
||||||
// and returned.
|
// and returned.
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,9 +15,7 @@
|
||||||
package encode
|
package encode
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
@ -95,7 +93,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
enc.Prefer = encs
|
enc.Prefer = encs
|
||||||
case "match":
|
case "match":
|
||||||
err := enc.parseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers)
|
err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -123,70 +121,5 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return nil
|
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
|
// Interface guard
|
||||||
var _ caddyfile.Unmarshaler = (*Encode)(nil)
|
var _ caddyfile.Unmarshaler = (*Encode)(nil)
|
||||||
|
|
|
@ -971,40 +971,6 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return nil
|
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+`)
|
var wordRE = regexp.MustCompile(`\w+`)
|
||||||
|
|
||||||
const regexpPlaceholderPrefix = "http.regexp"
|
const regexpPlaceholderPrefix = "http.regexp"
|
||||||
|
|
|
@ -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) {
|
func TestNotMatcher(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
host, path string
|
host, path string
|
||||||
|
|
122
modules/caddyhttp/responsematchers.go
Normal file
122
modules/caddyhttp/responsematchers.go
Normal file
|
@ -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
|
||||||
|
}
|
169
modules/caddyhttp/responsematchers_test.go
Normal file
169
modules/caddyhttp/responsematchers_test.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,6 +42,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = rp.FinalizeUnmarshalCaddyfile(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return rp, nil
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +90,24 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||||
// transport <name> {
|
// 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
|
// Proxy upstream addresses should be network dial addresses such
|
||||||
// as `host:port`, or a URL such as `scheme://host:port`. Scheme
|
// 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
|
// and port may be inferred from other parts of the address/URL; if
|
||||||
// either are missing, defaults to HTTP.
|
// 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 {
|
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
// currently, all backends must use the same scheme/protocol (the
|
// currently, all backends must use the same scheme/protocol (the
|
||||||
// underlying JSON does not yet support per-backend transports)
|
// 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 transport http.RoundTripper
|
||||||
var transportModuleName string
|
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
|
// TODO: the logic in this function is kind of sensitive, we need
|
||||||
// to write tests before making any more changes to it
|
// to write tests before making any more changes to it
|
||||||
upstreamDialAddress := func(upstreamAddr string) (string, error) {
|
upstreamDialAddress := func(upstreamAddr string) (string, error) {
|
||||||
|
@ -227,6 +247,16 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for d.NextBlock(0) {
|
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() {
|
switch d.Val() {
|
||||||
case "to":
|
case "to":
|
||||||
args := d.RemainingArgs()
|
args := d.RemainingArgs()
|
||||||
|
@ -617,6 +647,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
transport = rt
|
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:
|
default:
|
||||||
return d.Errf("unrecognized subdirective %s", d.Val())
|
return d.Errf("unrecognized subdirective %s", d.Val())
|
||||||
}
|
}
|
||||||
|
@ -659,6 +695,100 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return nil
|
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.
|
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
|
||||||
//
|
//
|
||||||
// transport http {
|
// transport http {
|
||||||
|
@ -892,6 +1022,8 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matcherPrefix = "@"
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddyfile.Unmarshaler = (*Handler)(nil)
|
_ caddyfile.Unmarshaler = (*Handler)(nil)
|
||||||
|
|
|
@ -353,12 +353,14 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
|
||||||
|
|
||||||
// the rest of the config is specified by the user
|
// the rest of the config is specified by the user
|
||||||
// using the reverse_proxy directive syntax
|
// 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)
|
err = rpHandler.UnmarshalCaddyfile(dispenser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = rpHandler.FinalizeUnmarshalCaddyfile(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// create the final reverse proxy route which is
|
// create the final reverse proxy route which is
|
||||||
// conditional on matching PHP files
|
// conditional on matching PHP files
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"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"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
@ -127,6 +128,12 @@ type Handler struct {
|
||||||
Transport http.RoundTripper `json:"-"`
|
Transport http.RoundTripper `json:"-"`
|
||||||
CB CircuitBreaker `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
|
ctx caddy.Context
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue