// 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"
	"sort"
	"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()
				}
				uri := dispenser.Val()
				if rpHandler.Rewrite.URI != "" {
					return nil, dispenser.Errf("cannot re-declare uri: %s", uri)
				}
				rpHandler.Rewrite.URI = uri
				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. We must have at least one
	// route in the response handler, even if it's no-op, so that the
	// response handling logic in reverse_proxy doesn't skip this entry.
	goodResponseHandler := caddyhttp.ResponseHandler{
		Match: &caddyhttp.ResponseMatcher{
			StatusCode: []int{2},
		},
		Routes: []caddyhttp.Route{
			{
				HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
					&caddyhttp.VarsMiddleware{},
					"handler",
					"vars",
					nil,
				)},
			},
		},
	}

	// Sort the headers so that the order in the JSON output is deterministic.
	sortedHeadersToCopy := make([]string, 0, len(headersToCopy))
	for k := range headersToCopy {
		sortedHeadersToCopy = append(sortedHeadersToCopy, k)
	}
	sort.Strings(sortedHeadersToCopy)

	// Set up handlers to copy headers from the auth response onto the
	// original request. We use vars matchers to test that the placeholder
	// values aren't empty, because the header handler would not replace
	// placeholders which have no value.
	copyHeaderRoutes := []caddyhttp.Route{}
	for _, from := range sortedHeadersToCopy {
		to := http.CanonicalHeaderKey(headersToCopy[from])
		placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)

		// Always delete the client-supplied header before conditionally setting
		// it from the auth response. Without this, a client that pre-supplies a
		// header listed in copy_headers can inject arbitrary values when the auth
		// service does not return that header: the MatchNot guard below would
		// skip the Set entirely, leaving the original client-controlled value
		// intact and forwarding it to the backend.
		copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
			HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
				&headers.Handler{
					Request: &headers.HeaderOps{
						Delete: []string{to},
					},
				},
				"handler", "headers", nil,
			)},
		})

		handler := &headers.Handler{
			Request: &headers.HeaderOps{
				Set: http.Header{
					to: []string{"{" + placeholderName + "}"},
				},
			},
		}
		copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
			MatcherSetsRaw: []caddy.ModuleMap{{
				"not": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{
					"vars": h.JSON(caddyhttp.VarsMatcher{"{" + placeholderName + "}": []string{""}}),
				}}}),
			}},
			HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
				handler,
				"handler",
				"headers",
				nil,
			)},
		})
	}

	goodResponseHandler.Routes = append(goodResponseHandler.Routes, copyHeaderRoutes...)

	// 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
}
