mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			251 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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"
 | 
						||
	"strings"
 | 
						||
 | 
						||
	"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
 | 
						||
	headersToCopy := make(map[string]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.DeleteN(2)
 | 
						||
 | 
						||
			case "copy_headers":
 | 
						||
				args := dispenser.RemainingArgs()
 | 
						||
				hadBlock := false
 | 
						||
				for nesting := dispenser.Nesting(); dispenser.NextBlock(nesting); {
 | 
						||
					hadBlock = true
 | 
						||
					args = append(args, dispenser.Val())
 | 
						||
				}
 | 
						||
 | 
						||
				// directive name + args
 | 
						||
				dispenser.DeleteN(len(args) + 1)
 | 
						||
				if hadBlock {
 | 
						||
					// opening & closing brace
 | 
						||
					dispenser.DeleteN(2)
 | 
						||
				}
 | 
						||
 | 
						||
				for _, headerField := range args {
 | 
						||
					if strings.Contains(headerField, ">") {
 | 
						||
						parts := strings.Split(headerField, ">")
 | 
						||
						headersToCopy[parts[0]] = parts[1]
 | 
						||
					} else {
 | 
						||
						headersToCopy[headerField] = 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{},
 | 
						||
	}
 | 
						||
 | 
						||
	handler := &headers.Handler{
 | 
						||
		Request: &headers.HeaderOps{
 | 
						||
			Set: http.Header{},
 | 
						||
		},
 | 
						||
	}
 | 
						||
 | 
						||
	// the list of headers to copy may be empty, but that's okay; we
 | 
						||
	// need at least one handler in the routes for the response handling
 | 
						||
	// logic in reverse_proxy to not skip this entry as empty.
 | 
						||
	for from, to := range headersToCopy {
 | 
						||
		handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}")
 | 
						||
	}
 | 
						||
 | 
						||
	goodResponseHandler.Routes = append(
 | 
						||
		goodResponseHandler.Routes,
 | 
						||
		caddyhttp.Route{
 | 
						||
			HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
 | 
						||
				handler,
 | 
						||
				"handler",
 | 
						||
				"headers",
 | 
						||
				nil,
 | 
						||
			)},
 | 
						||
		},
 | 
						||
	)
 | 
						||
 | 
						||
	// 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)
 | 
						||
 | 
						||
	// the rest of the config is specified by the user
 | 
						||
	// using the reverse_proxy directive syntax
 | 
						||
	dispenser.Next() // consume the directive name
 | 
						||
	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
 | 
						||
}
 |