mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
reverseproxy: Support performing pre-check requests (#4739)
This commit is contained in:
parent
ec86a2f7a3
commit
f6900fcf53
13 changed files with 542 additions and 45 deletions
|
@ -580,12 +580,24 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
|
||||
code = "302"
|
||||
default:
|
||||
// Allow placeholders for the code
|
||||
if strings.HasPrefix(code, "{") {
|
||||
break
|
||||
}
|
||||
// Try to validate as an integer otherwise
|
||||
codeInt, err := strconv.Atoi(code)
|
||||
if err != nil {
|
||||
return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
|
||||
}
|
||||
if codeInt < 300 || codeInt > 399 {
|
||||
return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)
|
||||
// Sometimes, a 401 with Location header is desirable because
|
||||
// requests made with XHR will "eat" the 3xx redirect; so if
|
||||
// the intent was to redirect to an auth page, a 3xx won't
|
||||
// work. Responding with 401 allows JS code to read the
|
||||
// Location header and do a window.location redirect manually.
|
||||
// see https://stackoverflow.com/a/2573589/846934
|
||||
// see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
|
||||
if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
|
||||
return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -148,6 +148,27 @@ func TestRedirDirectiveSyntax(t *testing.T) {
|
|||
}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
// this is now allowed so a Location header
|
||||
// can be written and consumed by JS
|
||||
// in the case of XHR requests
|
||||
input: `:8080 {
|
||||
redir * :8081 401
|
||||
}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
redir * :8081 402
|
||||
}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
redir * :8081 {http.reverse_proxy.status_code}
|
||||
}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
redir /old.html /new.html htlm
|
||||
|
@ -160,12 +181,6 @@ func TestRedirDirectiveSyntax(t *testing.T) {
|
|||
}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
redir * :8081 400
|
||||
}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
redir * :8081 temp
|
||||
|
|
|
@ -57,6 +57,7 @@ var directiveOrder = []string{
|
|||
|
||||
// middleware handlers; some wrap responses
|
||||
"basicauth",
|
||||
"forward_auth",
|
||||
"request_header",
|
||||
"encode",
|
||||
"push",
|
||||
|
|
|
@ -130,6 +130,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
|||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
||||
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
||||
}
|
||||
|
||||
for _, sb := range originalServerBlocks {
|
||||
|
|
137
caddytest/integration/caddyfile_adapt/forward_auth_authelia.txt
Normal file
137
caddytest/integration/caddyfile_adapt/forward_auth_authelia.txt
Normal file
|
@ -0,0 +1,137 @@
|
|||
app.example.com {
|
||||
forward_auth authelia:9091 {
|
||||
uri /api/verify?rd=https://authelia.example.com
|
||||
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
|
||||
}
|
||||
|
||||
reverse_proxy backend:8080
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"app.example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handle_response": [
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
2
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "headers",
|
||||
"request": {
|
||||
"set": {
|
||||
"Remote-Email": [
|
||||
"{http.reverse_proxy.header.Remote-Email}"
|
||||
],
|
||||
"Remote-Groups": [
|
||||
"{http.reverse_proxy.header.Remote-Groups}"
|
||||
],
|
||||
"Remote-Name": [
|
||||
"{http.reverse_proxy.header.Remote-Name}"
|
||||
],
|
||||
"Remote-User": [
|
||||
"{http.reverse_proxy.header.Remote-User}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"exclude": [
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Te",
|
||||
"Trailers",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade"
|
||||
],
|
||||
"handler": "copy_response_headers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "copy_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handler": "reverse_proxy",
|
||||
"headers": {
|
||||
"request": {
|
||||
"set": {
|
||||
"X-Forwarded-Method": [
|
||||
"{http.request.method}"
|
||||
],
|
||||
"X-Forwarded-Uri": [
|
||||
"{http.request.uri}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rewrite": {
|
||||
"method": "GET",
|
||||
"uri": "/api/verify?rd=https://authelia.example.com"
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "authelia:9091"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "backend:8080"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
|
||||
https://example.com {
|
||||
reverse_proxy /path http://localhost:54321 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {remote}
|
||||
header_up X-Forwarded-Port {server_port}
|
||||
header_up X-Forwarded-Proto "http"
|
||||
reverse_proxy /path https://localhost:54321 {
|
||||
header_up Host {upstream_hostport}
|
||||
header_up Foo bar
|
||||
|
||||
method GET
|
||||
rewrite /rewritten?uri={uri}
|
||||
|
||||
buffer_requests
|
||||
|
||||
|
@ -58,24 +58,19 @@ https://example.com {
|
|||
"headers": {
|
||||
"request": {
|
||||
"set": {
|
||||
"Foo": [
|
||||
"bar"
|
||||
],
|
||||
"Host": [
|
||||
"{http.request.host}"
|
||||
],
|
||||
"X-Forwarded-For": [
|
||||
"{http.request.remote}"
|
||||
],
|
||||
"X-Forwarded-Port": [
|
||||
"{server_port}"
|
||||
],
|
||||
"X-Forwarded-Proto": [
|
||||
"http"
|
||||
],
|
||||
"X-Real-Ip": [
|
||||
"{http.request.remote}"
|
||||
"{http.reverse_proxy.upstream.hostport}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rewrite": {
|
||||
"method": "GET",
|
||||
"uri": "/rewritten?uri={http.request.uri}"
|
||||
},
|
||||
"transport": {
|
||||
"compression": false,
|
||||
"dial_fallback_delay": 5000000000,
|
||||
|
@ -96,6 +91,7 @@ https://example.com {
|
|||
]
|
||||
},
|
||||
"response_header_timeout": 8000000000,
|
||||
"tls": {},
|
||||
"versions": [
|
||||
"h2c",
|
||||
"2"
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
|
@ -85,10 +86,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||
// buffer_responses
|
||||
// max_buffer_size <size>
|
||||
//
|
||||
// # header manipulation
|
||||
// # request manipulation
|
||||
// trusted_proxies [private_ranges] <ranges...>
|
||||
// header_up [+|-]<field> [<value|regexp> [<replacement>]]
|
||||
// header_down [+|-]<field> [<value|regexp> [<replacement>]]
|
||||
// method <method>
|
||||
// rewrite <to>
|
||||
//
|
||||
// # round trip
|
||||
// transport <name> {
|
||||
|
@ -600,6 +603,30 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||
return d.Err(err.Error())
|
||||
}
|
||||
|
||||
case "method":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if h.Rewrite == nil {
|
||||
h.Rewrite = &rewrite.Rewrite{}
|
||||
}
|
||||
h.Rewrite.Method = d.Val()
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "rewrite":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if h.Rewrite == nil {
|
||||
h.Rewrite = &rewrite.Rewrite{}
|
||||
}
|
||||
h.Rewrite.URI = d.Val()
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "transport":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
|
|
|
@ -196,7 +196,15 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
|
|||
// NOTE: we delete the tokens as we go so that the reverse_proxy
|
||||
// unmarshal doesn't see these subdirectives which it cannot handle
|
||||
for dispenser.Next() {
|
||||
for dispenser.NextBlock(0) && dispenser.Nesting() == 1 {
|
||||
for dispenser.NextBlock(0) {
|
||||
// ignore any sub-subdirectives that might
|
||||
// have the same name somewhere within
|
||||
// the reverse_proxy passthrough tokens
|
||||
if dispenser.Nesting() != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the php_fastcgi subdirectives
|
||||
switch dispenser.Val() {
|
||||
case "root":
|
||||
if !dispenser.NextArg() {
|
||||
|
|
269
modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
Normal file
269
modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
// 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 forwardauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile)
|
||||
}
|
||||
|
||||
// parseCaddyfile parses the forward_auth directive, which has the same syntax
|
||||
// as the reverse_proxy directive (in fact, the reverse_proxy's directive
|
||||
// Unmarshaler is invoked by this function) but the resulting proxy is specially
|
||||
// configured for most™️ auth gateways that support forward auth. The typical
|
||||
// config which looks something like this:
|
||||
//
|
||||
// forward_auth auth-gateway:9091 {
|
||||
// uri /authenticate?redirect=https://auth.example.com
|
||||
// copy_headers Remote-User Remote-Email
|
||||
// }
|
||||
//
|
||||
// is equivalent to a reverse_proxy directive like this:
|
||||
//
|
||||
// reverse_proxy auth-gateway:9091 {
|
||||
// method GET
|
||||
// rewrite /authenticate?redirect=https://auth.example.com
|
||||
//
|
||||
// header_up X-Forwarded-Method {method}
|
||||
// header_up X-Forwarded-Uri {uri}
|
||||
//
|
||||
// @good status 2xx
|
||||
// handle_response @good {
|
||||
// request_header {
|
||||
// Remote-User {http.reverse_proxy.header.Remote-User}
|
||||
// Remote-Email {http.reverse_proxy.header.Remote-Email}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// handle_response {
|
||||
// copy_response_headers {
|
||||
// exclude Connection Keep-Alive Te Trailers Transfer-Encoding Upgrade
|
||||
// }
|
||||
// copy_response
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
if !h.Next() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
// if the user specified a matcher token, use that
|
||||
// matcher in a route that wraps both of our routes;
|
||||
// either way, strip the matcher token and pass
|
||||
// the remaining tokens to the unmarshaler so that
|
||||
// we can gain the rest of the reverse_proxy syntax
|
||||
userMatcherSet, err := h.ExtractMatcherSet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make a new dispenser from the remaining tokens so that we
|
||||
// can reset the dispenser back to this point for the
|
||||
// reverse_proxy unmarshaler to read from it as well
|
||||
dispenser := h.NewFromNextSegment()
|
||||
|
||||
// create the reverse proxy handler
|
||||
rpHandler := &reverseproxy.Handler{
|
||||
// set up defaults for header_up; reverse_proxy already deals with
|
||||
// adding the other three X-Forwarded-* headers, but for this flow,
|
||||
// we want to also send along the incoming method and URI since this
|
||||
// request will have a rewritten URI and method.
|
||||
Headers: &headers.Handler{
|
||||
Request: &headers.HeaderOps{
|
||||
Set: http.Header{
|
||||
"X-Forwarded-Method": []string{"{http.request.method}"},
|
||||
"X-Forwarded-Uri": []string{"{http.request.uri}"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// we always rewrite the method to GET, which implicitly
|
||||
// turns off sending the incoming request's body, which
|
||||
// allows later middleware handlers to consume it
|
||||
Rewrite: &rewrite.Rewrite{
|
||||
Method: "GET",
|
||||
},
|
||||
|
||||
HandleResponse: []caddyhttp.ResponseHandler{},
|
||||
}
|
||||
|
||||
// collect the headers to copy from the auth response
|
||||
// onto the original request, so they can get passed
|
||||
// through to a backend app
|
||||
headersToCopy := []string{}
|
||||
|
||||
// read the subdirectives for configuring the forward_auth shortcut
|
||||
// NOTE: we delete the tokens as we go so that the reverse_proxy
|
||||
// unmarshal doesn't see these subdirectives which it cannot handle
|
||||
for dispenser.Next() {
|
||||
for dispenser.NextBlock(0) {
|
||||
// ignore any sub-subdirectives that might
|
||||
// have the same name somewhere within
|
||||
// the reverse_proxy passthrough tokens
|
||||
if dispenser.Nesting() != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// parse the forward_auth subdirectives
|
||||
switch dispenser.Val() {
|
||||
case "uri":
|
||||
if !dispenser.NextArg() {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
rpHandler.Rewrite.URI = dispenser.Val()
|
||||
dispenser.Delete()
|
||||
dispenser.Delete()
|
||||
|
||||
case "copy_headers":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.Delete()
|
||||
for _, headerField := range args {
|
||||
dispenser.Delete()
|
||||
headersToCopy = append(headersToCopy, headerField)
|
||||
}
|
||||
if len(headersToCopy) == 0 {
|
||||
return nil, dispenser.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the dispenser after we're done so that the reverse_proxy
|
||||
// unmarshaler can read it from the start
|
||||
dispenser.Reset()
|
||||
|
||||
// the auth target URI must not be empty
|
||||
if rpHandler.Rewrite.URI == "" {
|
||||
return nil, dispenser.Errf("the 'uri' subdirective is required")
|
||||
}
|
||||
|
||||
// set up handler for good responses; when a response
|
||||
// has 2xx status, then we will copy some headers from
|
||||
// the response onto the original request, and allow
|
||||
// handling to continue down the middleware chain,
|
||||
// by _not_ executing a terminal handler.
|
||||
goodResponseHandler := caddyhttp.ResponseHandler{
|
||||
Match: &caddyhttp.ResponseMatcher{
|
||||
StatusCode: []int{2},
|
||||
},
|
||||
Routes: []caddyhttp.Route{},
|
||||
}
|
||||
if len(headersToCopy) > 0 {
|
||||
handler := &headers.Handler{
|
||||
Request: &headers.HeaderOps{
|
||||
Set: http.Header{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, headerField := range headersToCopy {
|
||||
handler.Request.Set[headerField] = []string{
|
||||
"{http.reverse_proxy.header." + headerField + "}",
|
||||
}
|
||||
}
|
||||
|
||||
goodResponseHandler.Routes = append(
|
||||
goodResponseHandler.Routes,
|
||||
caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
handler,
|
||||
"handler",
|
||||
"headers",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
)
|
||||
}
|
||||
rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
|
||||
|
||||
// set up handler for denial responses; when a response
|
||||
// has any other status than 2xx, then we copy the response
|
||||
// back to the client, and terminate handling.
|
||||
denialResponseHandler := caddyhttp.ResponseHandler{
|
||||
Routes: []caddyhttp.Route{
|
||||
{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
&reverseproxy.CopyResponseHeadersHandler{
|
||||
Exclude: []string{
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Te",
|
||||
"Trailers",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
},
|
||||
},
|
||||
"handler",
|
||||
"copy_response_headers",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
&reverseproxy.CopyResponseHandler{},
|
||||
"handler",
|
||||
"copy_response",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
},
|
||||
}
|
||||
rpHandler.HandleResponse = append(rpHandler.HandleResponse, denialResponseHandler)
|
||||
|
||||
// the rest of the config is specified by the user
|
||||
// using the reverse_proxy directive syntax
|
||||
err = rpHandler.UnmarshalCaddyfile(dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rpHandler.FinalizeUnmarshalCaddyfile(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the final reverse proxy route
|
||||
rpRoute := caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
rpHandler,
|
||||
"handler",
|
||||
"reverse_proxy",
|
||||
nil,
|
||||
)},
|
||||
}
|
||||
|
||||
// apply the user's matcher if any
|
||||
if userMatcherSet != nil {
|
||||
rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet}
|
||||
}
|
||||
|
||||
return []httpcaddyfile.ConfigValue{
|
||||
{
|
||||
Class: "route",
|
||||
Value: rpRoute,
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -35,6 +35,7 @@ import (
|
|||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
@ -136,6 +137,18 @@ type Handler struct {
|
|||
// used for the requests and responses (in bytes).
|
||||
MaxBufferSize int64 `json:"max_buffer_size,omitempty"`
|
||||
|
||||
// If configured, rewrites the copy of the upstream request.
|
||||
// Allows changing the request method and URI (path and query).
|
||||
// Since the rewrite is applied to the copy, it does not persist
|
||||
// past the reverse proxy handler.
|
||||
// If the method is changed to `GET` or `HEAD`, the request body
|
||||
// will not be copied to the backend. This allows a later request
|
||||
// handler -- either in a `handle_response` route, or after -- to
|
||||
// read the body.
|
||||
// By default, no rewrite is performed, and the method and URI
|
||||
// from the incoming request is used as-is for proxying.
|
||||
Rewrite *rewrite.Rewrite `json:"rewrite,omitempty"`
|
||||
|
||||
// List of handlers and their associated matchers to evaluate
|
||||
// after successful roundtrips. The first handler that matches
|
||||
// the response from a backend will be invoked. The response
|
||||
|
@ -258,6 +271,13 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
if h.Rewrite != nil {
|
||||
err := h.Rewrite.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("provisioning rewrite: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// set up transport
|
||||
if h.Transport == nil {
|
||||
t := &HTTPTransport{}
|
||||
|
@ -385,7 +405,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
// prepare the request for proxying; this is needed only once
|
||||
clonedReq, err := h.prepareRequest(r)
|
||||
clonedReq, err := h.prepareRequest(r, repl)
|
||||
if err != nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
fmt.Errorf("preparing request for upstream round-trip: %v", err))
|
||||
|
@ -412,7 +432,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||
var proxyErr error
|
||||
for {
|
||||
var done bool
|
||||
done, proxyErr = h.proxyLoopIteration(clonedReq, w, proxyErr, start, repl, reqHeader, reqHost, next)
|
||||
done, proxyErr = h.proxyLoopIteration(clonedReq, r, w, proxyErr, start, repl, reqHeader, reqHost, next)
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
@ -429,7 +449,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||
// that has to be passed in, we brought this into its own method so that we could run defer more easily.
|
||||
// It returns true when the loop is done and should break; false otherwise. The error value returned should
|
||||
// be assigned to the proxyErr value for the next iteration of the loop (or the error handled after break).
|
||||
func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
|
||||
func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
|
||||
repl *caddy.Replacer, reqHeader http.Header, reqHost string, next caddyhttp.Handler) (bool, error) {
|
||||
// get the updated list of upstreams
|
||||
upstreams := h.Upstreams
|
||||
|
@ -503,7 +523,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
|
|||
}
|
||||
|
||||
// proxy the request to that upstream
|
||||
proxyErr = h.reverseProxy(w, r, repl, dialInfo, next)
|
||||
proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
|
||||
if proxyErr == nil || proxyErr == context.Canceled {
|
||||
// context.Canceled happens when the downstream client
|
||||
// cancels the request, which is not our failure
|
||||
|
@ -536,9 +556,20 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
|
|||
// properties of the cloned request and should be done just once (before
|
||||
// proxying) regardless of proxy retries. This assumes that no mutations
|
||||
// of the cloned request are performed by h during or after proxying.
|
||||
func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
|
||||
func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.Request, error) {
|
||||
req = cloneRequest(req)
|
||||
|
||||
// if enabled, perform rewrites on the cloned request; if
|
||||
// the method is GET or HEAD, prevent the request body
|
||||
// from being copied to the upstream
|
||||
if h.Rewrite != nil {
|
||||
changed := h.Rewrite.Rewrite(req, repl)
|
||||
if changed && (h.Rewrite.Method == "GET" || h.Rewrite.Method == "HEAD") {
|
||||
req.ContentLength = 0
|
||||
req.Body = nil
|
||||
}
|
||||
}
|
||||
|
||||
// if enabled, buffer client request; this should only be
|
||||
// enabled if the upstream requires it and does not work
|
||||
// with "slow clients" (gunicorn, etc.) - this obviously
|
||||
|
@ -547,7 +578,7 @@ func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
|
|||
// attacks, so it is strongly recommended to only use this
|
||||
// feature if absolutely required, if read timeouts are
|
||||
// set, and if body size is limited
|
||||
if h.BufferRequests {
|
||||
if h.BufferRequests && req.Body != nil {
|
||||
req.Body = h.bufferedBody(req.Body)
|
||||
}
|
||||
|
||||
|
@ -673,10 +704,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
|
|||
// we pass through the request Host as-is, but in situations
|
||||
// where we proxy over HTTPS, the user may need to override
|
||||
// Host themselves, so it's helpful to send the original too.
|
||||
host, _, err := net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
host = req.Host // OK; there probably was no port
|
||||
}
|
||||
host := req.Host
|
||||
prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Host")
|
||||
if trusted && ok && prior != "" {
|
||||
host = prior
|
||||
|
@ -691,7 +719,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
|
|||
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
|
||||
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
|
||||
// Go standard library which was used as the foundation.)
|
||||
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
|
||||
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
|
||||
_ = di.Upstream.Host.countRequest(1)
|
||||
//nolint:errcheck
|
||||
defer di.Upstream.Host.countRequest(-1)
|
||||
|
@ -798,17 +826,19 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
|
|||
// we make some data available via request context to child routes
|
||||
// so that they may inherit some options and functions from the
|
||||
// handler, and be able to copy the response.
|
||||
// we use the original request here, so that any routes from 'next'
|
||||
// see the original request rather than the proxy cloned request.
|
||||
hrc := &handleResponseContext{
|
||||
handler: h,
|
||||
response: res,
|
||||
start: start,
|
||||
logger: logger,
|
||||
}
|
||||
ctx := req.Context()
|
||||
ctx := origReq.Context()
|
||||
ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
|
||||
|
||||
// pass the request through the response handler routes
|
||||
routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req.WithContext(ctx))
|
||||
routeErr := rh.Routes.Compile(next).ServeHTTP(rw, origReq.WithContext(ctx))
|
||||
|
||||
// if the response handler routes already finalized the response,
|
||||
// we can return early. It should be finalized if the routes executed
|
||||
|
|
|
@ -106,7 +106,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
|||
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
|
||||
)
|
||||
|
||||
changed := rewr.rewrite(r, repl, logger)
|
||||
changed := rewr.Rewrite(r, repl)
|
||||
|
||||
if changed {
|
||||
logger.Debug("rewrote request",
|
||||
|
@ -121,7 +121,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
|||
// rewrite performs the rewrites on r using repl, which should
|
||||
// have been obtained from r, but is passed in for efficiency.
|
||||
// It returns true if any changes were made to r.
|
||||
func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
|
||||
func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
||||
oldMethod := r.Method
|
||||
oldURI := r.RequestURI
|
||||
|
||||
|
|
|
@ -292,7 +292,7 @@ func TestRewrite(t *testing.T) {
|
|||
rep.re = re
|
||||
}
|
||||
|
||||
changed := tc.rule.rewrite(tc.input, repl, nil)
|
||||
changed := tc.rule.Rewrite(tc.input, repl)
|
||||
|
||||
if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
|
||||
t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/forwardauth"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing"
|
||||
|
|
Loading…
Reference in a new issue