From 47717fee88cd207d885dc43629cd8fd1fca4111a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 5 May 2015 15:50:42 -0600 Subject: [PATCH] Expanded index file support to other middlewares (fixes #27) --- config/setup/fastcgi.go | 14 ++++--- config/setup/markdown.go | 8 +++- config/setup/templates.go | 13 +++++-- middleware/fastcgi/fastcgi.go | 64 ++++++++++++++++--------------- middleware/markdown/markdown.go | 22 +++++++++-- middleware/middleware.go | 26 ++++++++++++- middleware/templates/templates.go | 22 ++++++++--- 7 files changed, 117 insertions(+), 52 deletions(-) diff --git a/config/setup/fastcgi.go b/config/setup/fastcgi.go index fe5e734a..a2a7e879 100644 --- a/config/setup/fastcgi.go +++ b/config/setup/fastcgi.go @@ -2,6 +2,7 @@ package setup import ( "errors" + "net/http" "path/filepath" "github.com/mholt/caddy/middleware" @@ -10,7 +11,7 @@ import ( // FastCGI configures a new FastCGI middleware instance. func FastCGI(c *Controller) (middleware.Middleware, error) { - root, err := filepath.Abs(c.Root) + absRoot, err := filepath.Abs(c.Root) if err != nil { return nil, err } @@ -24,7 +25,9 @@ func FastCGI(c *Controller) (middleware.Middleware, error) { return fastcgi.Handler{ Next: next, Rules: rules, - Root: root, + Root: c.Root, + AbsRoot: absRoot, + FileSys: http.Dir(c.Root), SoftwareName: c.AppName, SoftwareVersion: c.AppVersion, ServerName: c.Host, @@ -72,10 +75,11 @@ func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) { } rule.SplitPath = c.Val() case "index": - if !c.NextArg() { + args := c.RemainingArgs() + if len(args) == 0 { return rules, c.ArgErr() } - rule.IndexFile = c.Val() + rule.IndexFiles = args case "env": envArgs := c.RemainingArgs() if len(envArgs) < 2 { @@ -98,7 +102,7 @@ func fastcgiPreset(name string, rule *fastcgi.Rule) error { case "php": rule.Ext = ".php" rule.SplitPath = ".php" - rule.IndexFile = "index.php" + rule.IndexFiles = []string{"index.php"} default: return errors.New(name + " is not a valid preset name") } diff --git a/config/setup/markdown.go b/config/setup/markdown.go index 525458ac..3f9b967a 100644 --- a/config/setup/markdown.go +++ b/config/setup/markdown.go @@ -1,6 +1,8 @@ package setup import ( + "net/http" + "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/markdown" "github.com/russross/blackfriday" @@ -14,8 +16,10 @@ func Markdown(c *Controller) (middleware.Middleware, error) { } md := markdown.Markdown{ - Root: c.Root, - Configs: mdconfigs, + Root: c.Root, + FileSys: http.Dir(c.Root), + Configs: mdconfigs, + IndexFiles: []string{"index.md"}, } return func(next middleware.Handler) middleware.Handler { diff --git a/config/setup/templates.go b/config/setup/templates.go index d3af7ca5..8d04d40b 100644 --- a/config/setup/templates.go +++ b/config/setup/templates.go @@ -1,6 +1,8 @@ package setup import ( + "net/http" + "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/templates" ) @@ -13,8 +15,9 @@ func Templates(c *Controller) (middleware.Middleware, error) { } tmpls := templates.Templates{ - Root: c.Root, - Rules: rules, + Rules: rules, + Root: c.Root, + FileSys: http.Dir(c.Root), } return func(next middleware.Handler) middleware.Handler { @@ -43,6 +46,10 @@ func templatesParse(c *Controller) ([]templates.Rule, error) { rule.Extensions = defaultExtensions } + for _, ext := range rule.Extensions { + rule.IndexFiles = append(rule.IndexFiles, "index"+ext) + } + rules = append(rules, rule) } @@ -51,4 +58,4 @@ func templatesParse(c *Controller) ([]templates.Rule, error) { const defaultPath = "/" -var defaultExtensions = []string{".html", ".htm", ".txt"} +var defaultExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"} diff --git a/middleware/fastcgi/fastcgi.go b/middleware/fastcgi/fastcgi.go index cc811a06..e797f43d 100644 --- a/middleware/fastcgi/fastcgi.go +++ b/middleware/fastcgi/fastcgi.go @@ -16,9 +16,11 @@ import ( // Handler is a middleware type that can handle requests as a FastCGI client. type Handler struct { - Next middleware.Handler - Root string // must be absolute path to site root - Rules []Rule + Next middleware.Handler + Rules []Rule + Root string + AbsRoot string // same as root, but absolute path + FileSys http.FileSystem // These are sent to CGI scripts in env variables SoftwareName string @@ -30,26 +32,27 @@ type Handler struct { // ServeHTTP satisfies the middleware.Handler interface. func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range h.Rules { + + // First requirement: Base path must match + if !middleware.Path(r.URL.Path).Matches(rule.Path) { + continue + } + // In addition to matching the path, a request must meet some // other criteria before being proxied as FastCGI. For example, // we probably want to exclude static assets (CSS, JS, images...) // but we also want to be flexible for the script we proxy to. - path := r.URL.Path + fpath := r.URL.Path + if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok { + fpath = idx + } // These criteria work well in this order for PHP sites - if middleware.Path(path).Matches(rule.Path) && - (path[len(path)-1] == '/' || - strings.HasSuffix(path, rule.Ext) || - !h.exists(path)) { - - if path[len(path)-1] == '/' && h.exists(path+rule.IndexFile) { - // If index file in specified folder exists, send request to it - path += rule.IndexFile - } + if fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) || !h.exists(fpath) { // Create environment for CGI script - env, err := h.buildEnv(r, rule, path) + env, err := h.buildEnv(r, rule, fpath) if err != nil { return http.StatusInternalServerError, err } @@ -115,14 +118,12 @@ func (h Handler) exists(path string) bool { return false } -func (h Handler) buildEnv(r *http.Request, rule Rule, path string) (map[string]string, error) { +// buildEnv returns a set of CGI environment variables for the request. +func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) { var env map[string]string // Get absolute path of requested resource - absPath, err := filepath.Abs(h.Root + path) - if err != nil { - return env, err - } + absPath := filepath.Join(h.AbsRoot, fpath) // Separate remote IP and port; more lenient than net.SplitHostPort var ip, port string @@ -134,19 +135,19 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, path string) (map[string]s } // Split path in preparation for env variables - splitPos := strings.Index(path, rule.SplitPath) + splitPos := strings.Index(fpath, rule.SplitPath) var docURI, scriptName, scriptFilename, pathInfo string if splitPos == -1 { // Request doesn't have the extension, so assume index file in root - docURI = "/" + rule.IndexFile - scriptName = "/" + rule.IndexFile - scriptFilename = h.Root + "/" + rule.IndexFile - pathInfo = path + docURI = "/" + rule.IndexFiles[0] + scriptName = "/" + rule.IndexFiles[0] + scriptFilename = filepath.Join(h.AbsRoot, rule.IndexFiles[0]) + pathInfo = fpath } else { // Request has the extension; path was split successfully - docURI = path[:splitPos+len(rule.SplitPath)] - pathInfo = path[splitPos+len(rule.SplitPath):] - scriptName = path + docURI = fpath[:splitPos+len(rule.SplitPath)] + pathInfo = fpath[splitPos+len(rule.SplitPath):] + scriptName = fpath scriptFilename = absPath } @@ -160,7 +161,7 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, path string) (map[string]s "CONTENT_TYPE": r.Header.Get("Content-Type"), "GATEWAY_INTERFACE": "CGI/1.1", "PATH_INFO": pathInfo, - "PATH_TRANSLATED": h.Root + "/" + pathInfo, // Source for path_translated: http://www.oreilly.com/openbook/cgi/ch02_04.html + "PATH_TRANSLATED": filepath.Join(h.AbsRoot, pathInfo), // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html "QUERY_STRING": r.URL.RawQuery, "REMOTE_ADDR": ip, "REMOTE_HOST": ip, // For speed, remote host lookups disabled @@ -174,7 +175,7 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, path string) (map[string]s "SERVER_SOFTWARE": h.SoftwareName + "/" + h.SoftwareVersion, // Other variables - "DOCUMENT_ROOT": h.Root, + "DOCUMENT_ROOT": h.AbsRoot, "DOCUMENT_URI": docURI, "HTTP_HOST": r.Host, // added here, since not always part of headers "REQUEST_URI": r.URL.RequestURI(), @@ -214,8 +215,9 @@ type Rule struct { // PATH_INFO for the CGI script to use. SplitPath string - // If the URL does not indicate a file, an index file with this name will be assumed. - IndexFile string + // If the URL ends with '/' (which indicates a directory), these index + // files will be tried instead. + IndexFiles []string // Environment Variables EnvVars [][2]string diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index db2ff3b6..70c3f825 100644 --- a/middleware/markdown/markdown.go +++ b/middleware/markdown/markdown.go @@ -20,11 +20,17 @@ type Markdown struct { // Server root Root string + // Jail the requests to site root with a mock file system + FileSys http.FileSystem + // Next HTTP handler in the chain Next middleware.Handler // The list of markdown configurations Configs []Config + + // The list of index files to try + IndexFiles []string } // Config stores markdown middleware configurations. @@ -52,11 +58,14 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error continue } - for _, ext := range m.Extensions { - if strings.HasSuffix(r.URL.Path, ext) { - fpath := md.Root + r.URL.Path + fpath := r.URL.Path + if idx, ok := middleware.IndexFile(md.FileSys, fpath, md.IndexFiles); ok { + fpath = idx + } - body, err := ioutil.ReadFile(fpath) + for _, ext := range m.Extensions { + if strings.HasSuffix(fpath, ext) { + f, err := md.FileSys.Open(fpath) if err != nil { if os.IsPermission(err) { return http.StatusForbidden, err @@ -64,6 +73,11 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error return http.StatusNotFound, nil } + body, err := ioutil.ReadAll(f) + if err != nil { + return http.StatusInternalServerError, err + } + content := blackfriday.Markdown(body, m.Renderer, 0) var scripts, styles string diff --git a/middleware/middleware.go b/middleware/middleware.go index 6f00c032..0310ba2f 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -1,7 +1,10 @@ // Package middleware provides some types and functions common among middleware. package middleware -import "net/http" +import ( + "net/http" + "path/filepath" +) type ( // Middleware is the middle layer which represents the traditional @@ -47,3 +50,24 @@ type ( func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { return f(w, r) } + +// IndexFile looks for a file in /root/fpath/indexFile for each string +// in indexFiles. If an index file is found, it returns the root-relative +// path to the file and true. If no index file is found, empty string +// and false is returned. fpath must end in a forward slash '/' +// otherwise no index files will be tried (directory paths must end +// in a forward slash according to HTTP). +func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) { + if fpath[len(fpath)-1] != '/' || root == nil { + return "", false + } + for _, indexFile := range indexFiles { + fp := filepath.Join(fpath, indexFile) + f, err := root.Open(fp) + if err == nil { + f.Close() + return fp, true + } + } + return "", false +} diff --git a/middleware/templates/templates.go b/middleware/templates/templates.go index 7c2ca34e..81dcdb3a 100644 --- a/middleware/templates/templates.go +++ b/middleware/templates/templates.go @@ -5,6 +5,7 @@ import ( "bytes" "net/http" "path" + "path/filepath" "text/template" "github.com/mholt/caddy/middleware" @@ -17,15 +18,22 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error continue } - reqExt := path.Ext(r.URL.Path) + // Check for index files + fpath := r.URL.Path + if idx, ok := middleware.IndexFile(t.FileSys, fpath, rule.IndexFiles); ok { + fpath = idx + } + + // Check the extension + reqExt := path.Ext(fpath) for _, ext := range rule.Extensions { if reqExt == ext { // Create execution context - ctx := context{root: http.Dir(t.Root), req: r, URL: r.URL} + ctx := context{root: t.FileSys, req: r, URL: r.URL} // Build the template - tpl, err := template.ParseFiles(t.Root + r.URL.Path) + tpl, err := template.ParseFiles(filepath.Join(t.Root, fpath)) if err != nil { return http.StatusInternalServerError, err } @@ -48,9 +56,10 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // Templates is middleware to render templated files as the HTTP response. type Templates struct { - Next middleware.Handler - Root string - Rules []Rule + Next middleware.Handler + Rules []Rule + Root string + FileSys http.FileSystem } // Rule represents a template rule. A template will only execute @@ -59,4 +68,5 @@ type Templates struct { type Rule struct { Path string Extensions []string + IndexFiles []string }