From 484cee1ac12ee590128d85da22b6b9df2efe02d4 Mon Sep 17 00:00:00 2001
From: Matt Holt <mholt@users.noreply.github.com>
Date: Tue, 17 Sep 2019 15:16:17 -0600
Subject: [PATCH] fastcgi: Implement / redirect for index.php with php_fastcgi
 directive (#2754)

* fastcgi: Implement / redirect for index.php with php_fastcgi directive

See #2752 and https://caddy.community/t/v2-redirect-path-to-path-index-php-with-assets/6196?u=matt

* caddyhttp: MatchNegate implements json.Marshaler

* fastcgi: Add /index.php element to try_files matcher

* fastcgi: Make /index.php redirect permanent
---
 modules/caddyhttp/matchers.go                 | 22 +++++++++-----
 .../reverseproxy/fastcgi/caddyfile.go         | 29 +++++++++++++++++--
 2 files changed, 42 insertions(+), 9 deletions(-)

diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index 94d051ecc..9cf52dcc3 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -66,9 +66,9 @@ type (
 
 	// MatchNegate matches requests by negating its matchers' results.
 	MatchNegate struct {
-		matchersRaw map[string]json.RawMessage
+		MatchersRaw map[string]json.RawMessage `json:"-"`
 
-		matchers MatcherSet
+		Matchers MatcherSet `json:"-"`
 	}
 
 	// MatchStarlarkExpr matches requests by evaluating a Starlark expression.
@@ -400,7 +400,12 @@ func (MatchNegate) CaddyModule() caddy.ModuleInfo {
 // the struct, but we need a struct because we need another
 // field just for the provisioned modules.
 func (m *MatchNegate) UnmarshalJSON(data []byte) error {
-	return json.Unmarshal(data, &m.matchersRaw)
+	return json.Unmarshal(data, &m.MatchersRaw)
+}
+
+// MarshalJSON marshals m's matchers.
+func (m MatchNegate) MarshalJSON() ([]byte, error) {
+	return json.Marshal(m.MatchersRaw)
 }
 
 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -411,21 +416,21 @@ func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 
 // Provision loads the matcher modules to be negated.
 func (m *MatchNegate) Provision(ctx caddy.Context) error {
-	for modName, rawMsg := range m.matchersRaw {
+	for modName, rawMsg := range m.MatchersRaw {
 		val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
 		if err != nil {
 			return fmt.Errorf("loading matcher module '%s': %v", modName, err)
 		}
-		m.matchers = append(m.matchers, val.(RequestMatcher))
+		m.Matchers = append(m.Matchers, val.(RequestMatcher))
 	}
-	m.matchersRaw = nil // allow GC to deallocate
+	m.MatchersRaw = nil // allow GC to deallocate
 	return nil
 }
 
 // Match returns true if r matches m. Since this matcher negates the
 // embedded matchers, false is returned if any of its matchers match.
 func (m MatchNegate) Match(r *http.Request) bool {
-	return !m.matchers.Match(r)
+	return !m.Matchers.Match(r)
 }
 
 // CaddyModule returns the Caddy module information.
@@ -686,4 +691,7 @@ var (
 	_ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil)
 	_ caddyfile.Unmarshaler = (*MatchProtocol)(nil)
 	_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
+
+	_ json.Marshaler   = (*MatchNegate)(nil)
+	_ json.Unmarshaler = (*MatchNegate)(nil)
 )
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
index fd82c5a86..b7326afa4 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -16,6 +16,7 @@ package fastcgi
 
 import (
 	"encoding/json"
+	"net/http"
 
 	"github.com/caddyserver/caddy/v2/caddyconfig"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -114,10 +115,30 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
 		return nil, h.ArgErr()
 	}
 
+	// route to redirect to canonical path if index PHP file
+	redirMatcherSet := map[string]json.RawMessage{
+		"file": h.JSON(fileserver.MatchFile{
+			TryFiles: []string{"{http.request.uri.path}/index.php"},
+		}, nil),
+		"not": h.JSON(caddyhttp.MatchNegate{
+			MatchersRaw: map[string]json.RawMessage{
+				"path": h.JSON(caddyhttp.MatchPath{"*/"}, nil),
+			},
+		}, nil),
+	}
+	redirHandler := caddyhttp.StaticResponse{
+		StatusCode: caddyhttp.WeakString("308"),
+		Headers:    http.Header{"Location": []string{"{http.request.uri.path}/"}},
+	}
+	redirRoute := caddyhttp.Route{
+		MatcherSetsRaw: []map[string]json.RawMessage{redirMatcherSet},
+		HandlersRaw:    []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
+	}
+
 	// route to rewrite to PHP index file
 	rewriteMatcherSet := map[string]json.RawMessage{
 		"file": h.JSON(fileserver.MatchFile{
-			TryFiles: []string{"{http.request.uri.path}", "index.php"},
+			TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php", "index.php"},
 		}, nil),
 	}
 	rewriteHandler := rewrite.Rewrite{
@@ -175,7 +196,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
 	// wrap ours in a subroute and return that
 	if hasUserMatcher {
 		subroute := caddyhttp.Subroute{
-			Routes: caddyhttp.RouteList{rewriteRoute, rpRoute},
+			Routes: caddyhttp.RouteList{redirRoute, rewriteRoute, rpRoute},
 		}
 		return []httpcaddyfile.ConfigValue{
 			{
@@ -191,6 +212,10 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
 	// if the user did not specify a matcher, then
 	// we can just use our own matchers
 	return []httpcaddyfile.ConfigValue{
+		{
+			Class: "route",
+			Value: redirRoute,
+		},
 		{
 			Class: "route",
 			Value: rewriteRoute,