// 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.Delete()
				dispenser.Delete()

			case "copy_headers":
				args := dispenser.RemainingArgs()
				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 {
					dispenser.Delete()
				}

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