2022-05-06 09:50:26 -05:00
|
|
|
|
// 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"
|
2022-06-16 15:28:11 -05:00
|
|
|
|
"strings"
|
2022-05-06 09:50:26 -05:00
|
|
|
|
|
|
|
|
|
"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}
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
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
|
2022-06-16 15:28:11 -05:00
|
|
|
|
headersToCopy := make(map[string]string)
|
2022-05-06 09:50:26 -05:00
|
|
|
|
|
|
|
|
|
// 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()
|
2022-06-16 15:28:11 -05:00
|
|
|
|
hadBlock := false
|
|
|
|
|
for nesting := dispenser.Nesting(); dispenser.NextBlock(nesting); {
|
|
|
|
|
hadBlock = true
|
|
|
|
|
args = append(args, dispenser.Val())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispenser.Delete() // directive name
|
|
|
|
|
if hadBlock {
|
|
|
|
|
dispenser.Delete() // opening brace
|
|
|
|
|
dispenser.Delete() // closing brace
|
|
|
|
|
}
|
|
|
|
|
for range args {
|
2022-05-06 09:50:26 -05:00
|
|
|
|
dispenser.Delete()
|
2022-06-16 15:28:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, headerField := range args {
|
|
|
|
|
if strings.Contains(headerField, ">") {
|
|
|
|
|
parts := strings.Split(headerField, ">")
|
|
|
|
|
headersToCopy[parts[0]] = parts[1]
|
|
|
|
|
} else {
|
|
|
|
|
headersToCopy[headerField] = headerField
|
|
|
|
|
}
|
2022-05-06 09:50:26 -05:00
|
|
|
|
}
|
|
|
|
|
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{},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-16 15:28:11 -05:00
|
|
|
|
for from, to := range headersToCopy {
|
|
|
|
|
handler.Request.Set[to] = []string{
|
|
|
|
|
"{http.reverse_proxy.header." + from + "}",
|
2022-05-06 09:50:26 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
goodResponseHandler.Routes = append(
|
|
|
|
|
goodResponseHandler.Routes,
|
|
|
|
|
caddyhttp.Route{
|
|
|
|
|
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
|
|
|
|
handler,
|
|
|
|
|
"handler",
|
|
|
|
|
"headers",
|
|
|
|
|
nil,
|
|
|
|
|
)},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-22 14:10:14 -05:00
|
|
|
|
// note that when a response has any other status than 2xx, then we
|
|
|
|
|
// use the reverse proxy's default behaviour of copying the response
|
|
|
|
|
// back to the client, so we don't need to explicitly add a response
|
|
|
|
|
// handler specifically for that behaviour; we do need the 2xx handler
|
|
|
|
|
// though, to make handling fall through to handlers deeper in the chain.
|
|
|
|
|
rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
|
2022-05-06 09:50:26 -05:00
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|