diff --git a/config/directives.go b/config/directives.go index 6a9124d64..c7d0d06a2 100644 --- a/config/directives.go +++ b/config/directives.go @@ -57,6 +57,7 @@ var directiveOrder = []directive{ {"rewrite", setup.Rewrite}, {"redir", setup.Redir}, {"ext", setup.Ext}, + {"mime", setup.Mime}, {"basicauth", setup.BasicAuth}, {"internal", setup.Internal}, {"proxy", setup.Proxy}, diff --git a/config/setup/mime.go b/config/setup/mime.go new file mode 100644 index 000000000..760056eba --- /dev/null +++ b/config/setup/mime.go @@ -0,0 +1,62 @@ +package setup + +import ( + "fmt" + "strings" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/mime" +) + +// Mime configures a new mime middleware instance. +func Mime(c *Controller) (middleware.Middleware, error) { + configs, err := mimeParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return mime.Mime{Next: next, Configs: configs} + }, nil +} + +func mimeParse(c *Controller) ([]mime.Config, error) { + var configs []mime.Config + + for c.Next() { + // At least one extension is required + + args := c.RemainingArgs() + switch len(args) { + case 2: + if err := validateExt(args[0]); err != nil { + return configs, err + } + configs = append(configs, mime.Config{Ext: args[0], ContentType: args[1]}) + case 1: + return configs, c.ArgErr() + case 0: + for c.NextBlock() { + ext := c.Val() + if err := validateExt(ext); err != nil { + return configs, err + } + if !c.NextArg() { + return configs, c.ArgErr() + } + configs = append(configs, mime.Config{Ext: ext, ContentType: c.Val()}) + } + } + + } + + return configs, nil +} + +// validateExt checks for valid file name extension. +func validateExt(ext string) error { + if !strings.HasPrefix(ext, ".") { + return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext) + } + return nil +} diff --git a/config/setup/mime_test.go b/config/setup/mime_test.go new file mode 100644 index 000000000..7f8d8de68 --- /dev/null +++ b/config/setup/mime_test.go @@ -0,0 +1,59 @@ +package setup + +import ( + "testing" + + "github.com/mholt/caddy/middleware/mime" +) + +func TestMime(t *testing.T) { + + c := NewTestController(`mime .txt text/plain`) + + mid, err := Mime(c) + if err != nil { + t.Errorf("Expected no errors, but got: %v", err) + } + if mid == nil { + t.Fatal("Expected middleware, was nil instead") + } + + handler := mid(EmptyNext) + myHandler, ok := handler.(mime.Mime) + if !ok { + t.Fatalf("Expected handler to be type Mime, got: %#v", handler) + } + + if !SameNext(myHandler.Next, EmptyNext) { + t.Error("'Next' field of handler was not set properly") + } + + tests := []struct { + input string + shouldErr bool + }{ + {`mime {`, true}, + {`mime {}`, true}, + {`mime a b`, true}, + {`mime a {`, true}, + {`mime { txt f } `, true}, + {`mime { html } `, true}, + {`mime { + .html text/html + .txt text/plain + } `, false}, + {`mime { .html text/html } `, false}, + {`mime { .html + } `, true}, + {`mime .txt text/plain`, false}, + } + for i, test := range tests { + c := NewTestController(test.input) + m, err := mimeParse(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil %v", i, m) + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + } + } +} diff --git a/middleware/mime/mime.go b/middleware/mime/mime.go new file mode 100644 index 000000000..cde699ff3 --- /dev/null +++ b/middleware/mime/mime.go @@ -0,0 +1,41 @@ +package mime + +import ( + "net/http" + "path/filepath" + + "github.com/mholt/caddy/middleware" +) + +// Config represent a mime config. +type Config struct { + Ext string + ContentType string +} + +// SetContent sets the Content-Type header of the request if the request path +// is supported. +func (c Config) SetContent(w http.ResponseWriter, r *http.Request) bool { + ext := filepath.Ext(r.URL.Path) + if ext != c.Ext { + return false + } + w.Header().Set("Content-Type", c.ContentType) + return true +} + +// Mime sets Content-Type header of requests based on configurations. +type Mime struct { + Next middleware.Handler + Configs []Config +} + +// ServeHTTP implements the middleware.Handler interface. +func (e Mime) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for _, c := range e.Configs { + if ok := c.SetContent(w, r); ok { + break + } + } + return e.Next.ServeHTTP(w, r) +} diff --git a/middleware/mime/mime_test.go b/middleware/mime/mime_test.go new file mode 100644 index 000000000..e70233032 --- /dev/null +++ b/middleware/mime/mime_test.go @@ -0,0 +1,75 @@ +package mime + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mholt/caddy/middleware" +) + +func TestMimeHandler(t *testing.T) { + + mimes := map[string]string{ + ".html": "text/html", + ".txt": "text/plain", + ".swf": "application/x-shockwave-flash", + } + + var configs []Config + for ext, contentType := range mimes { + configs = append(configs, Config{Ext: ext, ContentType: contentType}) + } + + m := Mime{Configs: configs} + + w := httptest.NewRecorder() + exts := []string{ + ".html", ".txt", ".swf", + } + for _, e := range exts { + url := "/file" + e + r, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Error(err) + } + m.Next = nextFunc(true, mimes[e]) + _, err = m.ServeHTTP(w, r) + if err != nil { + t.Error(err) + } + } + + w = httptest.NewRecorder() + exts = []string{ + ".htm1", ".abc", ".mdx", + } + for _, e := range exts { + url := "/file" + e + r, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Error(err) + } + m.Next = nextFunc(false, "") + _, err = m.ServeHTTP(w, r) + if err != nil { + t.Error(err) + } + } +} + +func nextFunc(shouldMime bool, contentType string) middleware.Handler { + return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + if shouldMime { + if w.Header().Get("Content-Type") != contentType { + return 0, fmt.Errorf("expected Content-Type: %v, found %v", contentType, r.Header.Get("Content-Type")) + } + return 0, nil + } + if w.Header().Get("Content-Type") != "" { + return 0, fmt.Errorf("Content-Type header not expected") + } + return 0, nil + }) +}