Skip to content

Commit

Permalink
reverseproxy: Add handle_response blocks to reverse_proxy (#3710) (
Browse files Browse the repository at this point in the history
…#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 <[email protected]>

* caddyhttp: Reorganize response matchers

* reverseproxy: Reintroduce caddyfile.Unmarshaler

* reverseproxy: Add comment mentioning Finalize should be called

Co-authored-by: Maxime Soulé <[email protected]>
Co-authored-by: Matt Holt <[email protected]>
  • Loading branch information
3 people authored May 2, 2021
1 parent e6f6d3a commit e4a22de
Show file tree
Hide file tree
Showing 10 changed files with 635 additions and 253 deletions.
7 changes: 7 additions & 0 deletions caddyconfig/httpcaddyfile/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
]
}
}
}
}
}
69 changes: 1 addition & 68 deletions modules/caddyhttp/encode/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
package encode

import (
"net/http"
"strconv"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
34 changes: 0 additions & 34 deletions modules/caddyhttp/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit e4a22de

Please sign in to comment.