diff --git a/caddyhttp/browse/browse.go b/caddyhttp/browse/browse.go
new file mode 100644
index 000000000..4e804f05e
--- /dev/null
+++ b/caddyhttp/browse/browse.go
@@ -0,0 +1,432 @@
+// Package browse provides middleware for listing files in a directory
+// when directory path is requested instead of a specific file.
+package browse
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "sort"
+ "strconv"
+ "strings"
+ "text/template"
+ "time"
+
+ "github.com/dustin/go-humanize"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+ "github.com/mholt/caddy/caddyhttp/staticfiles"
+)
+
+// Browse is an http.Handler that can show a file listing when
+// directories in the given paths are specified.
+type Browse struct {
+ Next httpserver.Handler
+ Configs []Config
+ IgnoreIndexes bool
+}
+
+// Config is a configuration for browsing in a particular path.
+type Config struct {
+ PathScope string
+ Root http.FileSystem
+ Variables interface{}
+ Template *template.Template
+}
+
+// A Listing is the context used to fill out a template.
+type Listing struct {
+ // The name of the directory (the last element of the path)
+ Name string
+
+ // The full path of the request
+ Path string
+
+ // Whether the parent directory is browsable
+ CanGoUp bool
+
+ // The items (files and folders) in the path
+ Items []FileInfo
+
+ // The number of directories in the listing
+ NumDirs int
+
+ // The number of files (items that aren't directories) in the listing
+ NumFiles int
+
+ // Which sorting order is used
+ Sort string
+
+ // And which order
+ Order string
+
+ // If ≠0 then Items have been limited to that many elements
+ ItemsLimitedTo int
+
+ // Optional custom variables for use in browse templates
+ User interface{}
+
+ httpserver.Context
+}
+
+// BreadcrumbMap returns l.Path where every element is a map
+// of URLs and path segment names.
+func (l Listing) BreadcrumbMap() map[string]string {
+ result := map[string]string{}
+
+ if len(l.Path) == 0 {
+ return result
+ }
+
+ // skip trailing slash
+ lpath := l.Path
+ if lpath[len(lpath)-1] == '/' {
+ lpath = lpath[:len(lpath)-1]
+ }
+
+ parts := strings.Split(lpath, "/")
+ for i, part := range parts {
+ if i == 0 && part == "" {
+ // Leading slash (root)
+ result["/"] = "/"
+ continue
+ }
+ result[strings.Join(parts[:i+1], "/")] = part
+ }
+
+ return result
+}
+
+// FileInfo is the info about a particular file or directory
+type FileInfo struct {
+ IsDir bool
+ Name string
+ Size int64
+ URL string
+ ModTime time.Time
+ Mode os.FileMode
+}
+
+// HumanSize returns the size of the file as a human-readable string
+// in IEC format (i.e. power of 2 or base 1024).
+func (fi FileInfo) HumanSize() string {
+ return humanize.IBytes(uint64(fi.Size))
+}
+
+// HumanModTime returns the modified time of the file as a human-readable string.
+func (fi FileInfo) HumanModTime(format string) string {
+ return fi.ModTime.Format(format)
+}
+
+// Implement sorting for Listing
+type byName Listing
+type bySize Listing
+type byTime Listing
+
+// By Name
+func (l byName) Len() int { return len(l.Items) }
+func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
+
+// Treat upper and lower case equally
+func (l byName) Less(i, j int) bool {
+ return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
+}
+
+// By Size
+func (l bySize) Len() int { return len(l.Items) }
+func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
+
+const directoryOffset = -1 << 31 // = math.MinInt32
+func (l bySize) Less(i, j int) bool {
+ iSize, jSize := l.Items[i].Size, l.Items[j].Size
+ if l.Items[i].IsDir {
+ iSize = directoryOffset + iSize
+ }
+ if l.Items[j].IsDir {
+ jSize = directoryOffset + jSize
+ }
+ return iSize < jSize
+}
+
+// By Time
+func (l byTime) Len() int { return len(l.Items) }
+func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
+func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
+
+// Add sorting method to "Listing"
+// it will apply what's in ".Sort" and ".Order"
+func (l Listing) applySort() {
+ // Check '.Order' to know how to sort
+ if l.Order == "desc" {
+ switch l.Sort {
+ case "name":
+ sort.Sort(sort.Reverse(byName(l)))
+ case "size":
+ sort.Sort(sort.Reverse(bySize(l)))
+ case "time":
+ sort.Sort(sort.Reverse(byTime(l)))
+ default:
+ // If not one of the above, do nothing
+ return
+ }
+ } else { // If we had more Orderings we could add them here
+ switch l.Sort {
+ case "name":
+ sort.Sort(byName(l))
+ case "size":
+ sort.Sort(bySize(l))
+ case "time":
+ sort.Sort(byTime(l))
+ default:
+ // If not one of the above, do nothing
+ return
+ }
+ }
+}
+
+func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string) (Listing, bool) {
+ var (
+ fileinfos []FileInfo
+ dirCount, fileCount int
+ hasIndexFile bool
+ )
+
+ for _, f := range files {
+ name := f.Name()
+
+ for _, indexName := range staticfiles.IndexPages {
+ if name == indexName {
+ hasIndexFile = true
+ break
+ }
+ }
+
+ if f.IsDir() {
+ name += "/"
+ dirCount++
+ } else {
+ fileCount++
+ }
+
+ url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
+
+ fileinfos = append(fileinfos, FileInfo{
+ IsDir: f.IsDir(),
+ Name: f.Name(),
+ Size: f.Size(),
+ URL: url.String(),
+ ModTime: f.ModTime().UTC(),
+ Mode: f.Mode(),
+ })
+ }
+
+ return Listing{
+ Name: path.Base(urlPath),
+ Path: urlPath,
+ CanGoUp: canGoUp,
+ Items: fileinfos,
+ NumDirs: dirCount,
+ NumFiles: fileCount,
+ }, hasIndexFile
+}
+
+// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
+// If so, control is handed over to ServeListing.
+func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+ var bc *Config
+ // See if there's a browse configuration to match the path
+ for i := range b.Configs {
+ if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
+ bc = &b.Configs[i]
+ goto inScope
+ }
+ }
+ return b.Next.ServeHTTP(w, r)
+inScope:
+
+ // Browse works on existing directories; delegate everything else
+ requestedFilepath, err := bc.Root.Open(r.URL.Path)
+ if err != nil {
+ switch {
+ case os.IsPermission(err):
+ return http.StatusForbidden, err
+ case os.IsExist(err):
+ return http.StatusNotFound, err
+ default:
+ return b.Next.ServeHTTP(w, r)
+ }
+ }
+ defer requestedFilepath.Close()
+
+ info, err := requestedFilepath.Stat()
+ if err != nil {
+ switch {
+ case os.IsPermission(err):
+ return http.StatusForbidden, err
+ case os.IsExist(err):
+ return http.StatusGone, err
+ default:
+ return b.Next.ServeHTTP(w, r)
+ }
+ }
+ if !info.IsDir() {
+ return b.Next.ServeHTTP(w, r)
+ }
+
+ // Do not reply to anything else because it might be nonsensical
+ switch r.Method {
+ case http.MethodGet, http.MethodHead:
+ // proceed, noop
+ case "PROPFIND", http.MethodOptions:
+ return http.StatusNotImplemented, nil
+ default:
+ return b.Next.ServeHTTP(w, r)
+ }
+
+ // Browsing navigation gets messed up if browsing a directory
+ // that doesn't end in "/" (which it should, anyway)
+ if !strings.HasSuffix(r.URL.Path, "/") {
+ http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
+ return 0, nil
+ }
+
+ return b.ServeListing(w, r, requestedFilepath, bc)
+}
+
+func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string) (*Listing, bool, error) {
+ files, err := requestedFilepath.Readdir(-1)
+ if err != nil {
+ return nil, false, err
+ }
+
+ // Determine if user can browse up another folder
+ var canGoUp bool
+ curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
+ for _, other := range b.Configs {
+ if strings.HasPrefix(curPathDir, other.PathScope) {
+ canGoUp = true
+ break
+ }
+ }
+
+ // Assemble listing of directory contents
+ listing, hasIndex := directoryListing(files, canGoUp, urlPath)
+
+ return &listing, hasIndex, nil
+}
+
+// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
+// and reads 'limit' if given. The latter is 0 if not given.
+//
+// This sets Cookies.
+func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
+ sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
+
+ // If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
+ switch sort {
+ case "":
+ sort = "name"
+ if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
+ sort = sortCookie.Value
+ }
+ case "name", "size", "type":
+ http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
+ }
+
+ switch order {
+ case "":
+ order = "asc"
+ if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
+ order = orderCookie.Value
+ }
+ case "asc", "desc":
+ http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
+ }
+
+ if limitQuery != "" {
+ limit, err = strconv.Atoi(limitQuery)
+ if err != nil { // if the 'limit' query can't be interpreted as a number, return err
+ return
+ }
+ }
+
+ return
+}
+
+// ServeListing returns a formatted view of 'requestedFilepath' contents'.
+func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
+ listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path)
+ if err != nil {
+ switch {
+ case os.IsPermission(err):
+ return http.StatusForbidden, err
+ case os.IsExist(err):
+ return http.StatusGone, err
+ default:
+ return http.StatusInternalServerError, err
+ }
+ }
+ if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
+ return b.Next.ServeHTTP(w, r)
+ }
+ listing.Context = httpserver.Context{
+ Root: bc.Root,
+ Req: r,
+ URL: r.URL,
+ }
+ listing.User = bc.Variables
+
+ // Copy the query values into the Listing struct
+ var limit int
+ listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+
+ listing.applySort()
+
+ if limit > 0 && limit <= len(listing.Items) {
+ listing.Items = listing.Items[:limit]
+ listing.ItemsLimitedTo = limit
+ }
+
+ var buf *bytes.Buffer
+ acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
+ switch {
+ case strings.Contains(acceptHeader, "application/json"):
+ if buf, err = b.formatAsJSON(listing, bc); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+ default: // There's no 'application/json' in the 'Accept' header; browse normally
+ if buf, err = b.formatAsHTML(listing, bc); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ }
+
+ buf.WriteTo(w)
+
+ return http.StatusOK, nil
+}
+
+func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
+ marsh, err := json.Marshal(listing.Items)
+ if err != nil {
+ return nil, err
+ }
+
+ buf := new(bytes.Buffer)
+ _, err = buf.Write(marsh)
+ return buf, err
+}
+
+func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
+ buf := new(bytes.Buffer)
+ err := bc.Template.Execute(buf, listing)
+ return buf, err
+}
diff --git a/caddyhttp/browse/browse_test.go b/caddyhttp/browse/browse_test.go
new file mode 100644
index 000000000..eb200d3f6
--- /dev/null
+++ b/caddyhttp/browse/browse_test.go
@@ -0,0 +1,355 @@
+package browse
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sort"
+ "testing"
+ "text/template"
+ "time"
+
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func TestSort(t *testing.T) {
+ // making up []fileInfo with bogus values;
+ // to be used to make up our "listing"
+ fileInfos := []FileInfo{
+ {
+ Name: "fizz",
+ Size: 4,
+ ModTime: time.Now().AddDate(-1, 1, 0),
+ },
+ {
+ Name: "buzz",
+ Size: 2,
+ ModTime: time.Now().AddDate(0, -3, 3),
+ },
+ {
+ Name: "bazz",
+ Size: 1,
+ ModTime: time.Now().AddDate(0, -2, -23),
+ },
+ {
+ Name: "jazz",
+ Size: 3,
+ ModTime: time.Now(),
+ },
+ }
+ listing := Listing{
+ Name: "foobar",
+ Path: "/fizz/buzz",
+ CanGoUp: false,
+ Items: fileInfos,
+ }
+
+ // sort by name
+ listing.Sort = "name"
+ listing.applySort()
+ if !sort.IsSorted(byName(listing)) {
+ t.Errorf("The listing isn't name sorted: %v", listing.Items)
+ }
+
+ // sort by size
+ listing.Sort = "size"
+ listing.applySort()
+ if !sort.IsSorted(bySize(listing)) {
+ t.Errorf("The listing isn't size sorted: %v", listing.Items)
+ }
+
+ // sort by Time
+ listing.Sort = "time"
+ listing.applySort()
+ if !sort.IsSorted(byTime(listing)) {
+ t.Errorf("The listing isn't time sorted: %v", listing.Items)
+ }
+
+ // reverse by name
+ listing.Sort = "name"
+ listing.Order = "desc"
+ listing.applySort()
+ if !isReversed(byName(listing)) {
+ t.Errorf("The listing isn't reversed by name: %v", listing.Items)
+ }
+
+ // reverse by size
+ listing.Sort = "size"
+ listing.Order = "desc"
+ listing.applySort()
+ if !isReversed(bySize(listing)) {
+ t.Errorf("The listing isn't reversed by size: %v", listing.Items)
+ }
+
+ // reverse by time
+ listing.Sort = "time"
+ listing.Order = "desc"
+ listing.applySort()
+ if !isReversed(byTime(listing)) {
+ t.Errorf("The listing isn't reversed by time: %v", listing.Items)
+ }
+}
+
+func TestBrowseHTTPMethods(t *testing.T) {
+ tmpl, err := template.ParseFiles("testdata/photos.tpl")
+ if err != nil {
+ t.Fatalf("An error occured while parsing the template: %v", err)
+ }
+
+ b := Browse{
+ Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+ return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
+ }),
+ Configs: []Config{
+ {
+ PathScope: "/photos",
+ Root: http.Dir("./testdata"),
+ Template: tmpl,
+ },
+ },
+ }
+
+ rec := httptest.NewRecorder()
+ for method, expected := range map[string]int{
+ http.MethodGet: http.StatusOK,
+ http.MethodHead: http.StatusOK,
+ http.MethodOptions: http.StatusNotImplemented,
+ "PROPFIND": http.StatusNotImplemented,
+ } {
+ req, err := http.NewRequest(method, "/photos/", nil)
+ if err != nil {
+ t.Fatalf("Test: Could not create HTTP request: %v", err)
+ }
+
+ code, _ := b.ServeHTTP(rec, req)
+ if code != expected {
+ t.Errorf("Wrong status with HTTP Method %s: expected %d, got %d", method, expected, code)
+ }
+ }
+}
+
+func TestBrowseTemplate(t *testing.T) {
+ tmpl, err := template.ParseFiles("testdata/photos.tpl")
+ if err != nil {
+ t.Fatalf("An error occured while parsing the template: %v", err)
+ }
+
+ b := Browse{
+ Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+ t.Fatalf("Next shouldn't be called")
+ return 0, nil
+ }),
+ Configs: []Config{
+ {
+ PathScope: "/photos",
+ Root: http.Dir("./testdata"),
+ Template: tmpl,
+ },
+ },
+ }
+
+ req, err := http.NewRequest("GET", "/photos/", nil)
+ if err != nil {
+ t.Fatalf("Test: Could not create HTTP request: %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+
+ code, _ := b.ServeHTTP(rec, req)
+ if code != http.StatusOK {
+ t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
+ }
+
+ respBody := rec.Body.String()
+ expectedBody := `
+
+
+Template
+
+
+
Header
+
+
/photos/
+
+test.html
+
+test2.html
+
+test3.html
+
+
+
+`
+
+ if respBody != expectedBody {
+ t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
+ }
+
+}
+
+func TestBrowseJson(t *testing.T) {
+ b := Browse{
+ Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+ t.Fatalf("Next shouldn't be called")
+ return 0, nil
+ }),
+ Configs: []Config{
+ {
+ PathScope: "/photos/",
+ Root: http.Dir("./testdata"),
+ },
+ },
+ }
+
+ //Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
+ testDataPath := filepath.Join("./testdata", "photos")
+ file, err := os.Open(testDataPath)
+ if err != nil {
+ if os.IsPermission(err) {
+ t.Fatalf("Os Permission Error")
+ }
+ }
+ defer file.Close()
+
+ files, err := file.Readdir(-1)
+ if err != nil {
+ t.Fatalf("Unable to Read Contents of the directory")
+ }
+ var fileinfos []FileInfo
+
+ for i, f := range files {
+ name := f.Name()
+
+ // Tests fail in CI environment because all file mod times are the same for
+ // some reason, making the sorting unpredictable. To hack around this,
+ // we ensure here that each file has a different mod time.
+ chTime := f.ModTime().UTC().Add(-(time.Duration(i) * time.Second))
+ if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
+ t.Fatal(err)
+ }
+
+ if f.IsDir() {
+ name += "/"
+ }
+
+ url := url.URL{Path: "./" + name}
+
+ fileinfos = append(fileinfos, FileInfo{
+ IsDir: f.IsDir(),
+ Name: f.Name(),
+ Size: f.Size(),
+ URL: url.String(),
+ ModTime: chTime,
+ Mode: f.Mode(),
+ })
+ }
+ listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
+
+ tests := []struct {
+ QueryURL string
+ SortBy string
+ OrderBy string
+ Limit int
+ shouldErr bool
+ expectedResult []FileInfo
+ }{
+ //test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
+ //without the limit query entire listing will be produced
+ {"/", "", "", -1, false, listing.Items},
+ //test case 2: limit is set to 1, orderBy and sortBy is default
+ {"/?limit=1", "", "", 1, false, listing.Items[:1]},
+ //test case 3 : if the listing request is bigger than total size of listing then it should return everything
+ {"/?limit=100000000", "", "", 100000000, false, listing.Items},
+ //test case 4 : testing for negative limit
+ {"/?limit=-1", "", "", -1, false, listing.Items},
+ //test case 5 : testing with limit set to -1 and order set to descending
+ {"/?limit=-1&order=desc", "", "desc", -1, false, listing.Items},
+ //test case 6 : testing with limit set to 2 and order set to descending
+ {"/?limit=2&order=desc", "", "desc", 2, false, listing.Items},
+ //test case 7 : testing with limit set to 3 and order set to descending
+ {"/?limit=3&order=desc", "", "desc", 3, false, listing.Items},
+ //test case 8 : testing with limit set to 3 and order set to ascending
+ {"/?limit=3&order=asc", "", "asc", 3, false, listing.Items},
+ //test case 9 : testing with limit set to 1111111 and order set to ascending
+ {"/?limit=1111111&order=asc", "", "asc", 1111111, false, listing.Items},
+ //test case 10 : testing with limit set to default and order set to ascending and sorting by size
+ {"/?order=asc&sort=size", "size", "asc", -1, false, listing.Items},
+ //test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
+ {"/?order=asc&sort=time", "time", "asc", -1, false, listing.Items},
+ //test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
+ {"/?order=asc&sort=time&limit=1", "time", "asc", 1, false, listing.Items},
+ //test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
+ {"/?order=asc&sort=time&limit=-100", "time", "asc", -100, false, listing.Items},
+ //test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
+ {"/?order=asc&sort=size&limit=-100", "size", "asc", -100, false, listing.Items},
+ }
+
+ for i, test := range tests {
+ var marsh []byte
+ req, err := http.NewRequest("GET", "/photos"+test.QueryURL, nil)
+
+ if err == nil && test.shouldErr {
+ t.Errorf("Test %d didn't error, but it should have", i)
+ } else if err != nil && !test.shouldErr {
+ t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
+ }
+
+ req.Header.Set("Accept", "application/json")
+ rec := httptest.NewRecorder()
+
+ code, err := b.ServeHTTP(rec, req)
+
+ if code != http.StatusOK {
+ t.Fatalf("In test %d: Wrong status, expected %d, got %d", i, http.StatusOK, code)
+ }
+ if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
+ t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type"))
+ }
+
+ actualJSONResponse := rec.Body.String()
+ copyOflisting := listing
+ if test.SortBy == "" {
+ copyOflisting.Sort = "name"
+ } else {
+ copyOflisting.Sort = test.SortBy
+ }
+ if test.OrderBy == "" {
+ copyOflisting.Order = "asc"
+ } else {
+ copyOflisting.Order = test.OrderBy
+ }
+
+ copyOflisting.applySort()
+
+ limit := test.Limit
+ if limit <= len(copyOflisting.Items) && limit > 0 {
+ marsh, err = json.Marshal(copyOflisting.Items[:limit])
+ } else { // if the 'limit' query is empty, or has the wrong value, list everything
+ marsh, err = json.Marshal(copyOflisting.Items)
+ }
+
+ if err != nil {
+ t.Fatalf("Unable to Marshal the listing ")
+ }
+ expectedJSON := string(marsh)
+
+ if actualJSONResponse != expectedJSON {
+ t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n",
+ i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse)
+ }
+ }
+}
+
+// "sort" package has "IsSorted" function, but no "IsReversed";
+func isReversed(data sort.Interface) bool {
+ n := data.Len()
+ for i := n - 1; i > 0; i-- {
+ if !data.Less(i, i-1) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/caddyhttp/browse/setup.go b/caddyhttp/browse/setup.go
new file mode 100644
index 000000000..88f7d44d4
--- /dev/null
+++ b/caddyhttp/browse/setup.go
@@ -0,0 +1,428 @@
+package browse
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "text/template"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func init() {
+ caddy.RegisterPlugin(caddy.Plugin{
+ Name: "browse",
+ ServerType: "http",
+ Action: setup,
+ })
+}
+
+// setup configures a new Browse middleware instance.
+func setup(c *caddy.Controller) error {
+ configs, err := browseParse(c)
+ if err != nil {
+ return err
+ }
+
+ b := Browse{
+ Configs: configs,
+ IgnoreIndexes: false,
+ }
+
+ httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
+ b.Next = next
+ return b
+ })
+
+ return nil
+}
+
+func browseParse(c *caddy.Controller) ([]Config, error) {
+ var configs []Config
+
+ cfg := httpserver.GetConfig(c.Key)
+
+ appendCfg := func(bc Config) error {
+ for _, c := range configs {
+ if c.PathScope == bc.PathScope {
+ return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
+ }
+ }
+ configs = append(configs, bc)
+ return nil
+ }
+
+ for c.Next() {
+ var bc Config
+
+ // First argument is directory to allow browsing; default is site root
+ if c.NextArg() {
+ bc.PathScope = c.Val()
+ } else {
+ bc.PathScope = "/"
+ }
+ bc.Root = http.Dir(cfg.Root)
+ theRoot, err := bc.Root.Open("/") // catch a missing path early
+ if err != nil {
+ return configs, err
+ }
+ defer theRoot.Close()
+ _, err = theRoot.Readdir(-1)
+ if err != nil {
+ return configs, err
+ }
+
+ // Second argument would be the template file to use
+ var tplText string
+ if c.NextArg() {
+ tplBytes, err := ioutil.ReadFile(c.Val())
+ if err != nil {
+ return configs, err
+ }
+ tplText = string(tplBytes)
+ } else {
+ tplText = defaultTemplate
+ }
+
+ // Build the template
+ tpl, err := template.New("listing").Parse(tplText)
+ if err != nil {
+ return configs, err
+ }
+ bc.Template = tpl
+
+ // Save configuration
+ err = appendCfg(bc)
+ if err != nil {
+ return configs, err
+ }
+ }
+
+ return configs, nil
+}
+
+// The default template to use when serving up directory listings
+const defaultTemplate = `
+
+
+ {{.Name}}
+
+
+
+
+
+
+
+
+ {{range $url, $name := .BreadcrumbMap}}{{$name}}{{if ne $url "/"}}/{{end}}{{end}}
+
+
+
+
+
+ {{.NumDirs}} director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}
+ {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}}
+ {{- if ne 0 .ItemsLimitedTo}}
+ (of which only {{.ItemsLimitedTo}} are displayed)
+ {{- end}}
+
+
+
+
+
+
+
+ {{- if and (eq .Sort "name") (ne .Order "desc")}}
+ Name
+ {{- else if and (eq .Sort "name") (ne .Order "asc")}}
+ Name
+ {{- else}}
+ Name
+ {{- end}}
+
+
+ {{- if and (eq .Sort "size") (ne .Order "desc")}}
+ Size
+ {{- else if and (eq .Sort "size") (ne .Order "asc")}}
+ Size
+ {{- else}}
+ Size
+ {{- end}}
+
+
+ {{- if and (eq .Sort "time") (ne .Order "desc")}}
+ Modified
+ {{- else if and (eq .Sort "time") (ne .Order "asc")}}
+ Modified
+ {{- else}}
+ Modified
+ {{- end}}
+
+
+
+`
+
+ if !equalStrings(respBody, expectedBody) {
+ t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
+ }
+}
+
+func equalStrings(s1, s2 string) bool {
+ s1 = strings.TrimSpace(s1)
+ s2 = strings.TrimSpace(s2)
+ in := bufio.NewScanner(strings.NewReader(s1))
+ for in.Scan() {
+ txt := strings.TrimSpace(in.Text())
+ if !strings.HasPrefix(strings.TrimSpace(s2), txt) {
+ return false
+ }
+ s2 = strings.Replace(s2, txt, "", 1)
+ }
+ return true
+}
+
+func setDefaultTemplate(filename string) *template.Template {
+ buf, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil
+ }
+
+ return template.Must(GetDefaultTemplate().Parse(string(buf)))
+}
diff --git a/caddyhttp/markdown/metadata/metadata.go b/caddyhttp/markdown/metadata/metadata.go
new file mode 100644
index 000000000..ade7fcc9d
--- /dev/null
+++ b/caddyhttp/markdown/metadata/metadata.go
@@ -0,0 +1,158 @@
+package metadata
+
+import (
+ "bufio"
+ "bytes"
+ "time"
+)
+
+var (
+ // Date format YYYY-MM-DD HH:MM:SS or YYYY-MM-DD
+ timeLayout = []string{
+ `2006-01-02 15:04:05-0700`,
+ `2006-01-02 15:04:05`,
+ `2006-01-02`,
+ }
+)
+
+// Metadata stores a page's metadata
+type Metadata struct {
+ // Page title
+ Title string
+
+ // Page template
+ Template string
+
+ // Publish date
+ Date time.Time
+
+ // Variables to be used with Template
+ Variables map[string]string
+
+ // Flags to be used with Template
+ Flags map[string]bool
+}
+
+// NewMetadata() returns a new Metadata struct, loaded with the given map
+func NewMetadata(parsedMap map[string]interface{}) Metadata {
+ md := Metadata{
+ Variables: make(map[string]string),
+ Flags: make(map[string]bool),
+ }
+ md.load(parsedMap)
+
+ return md
+}
+
+// load loads parsed values in parsedMap into Metadata
+func (m *Metadata) load(parsedMap map[string]interface{}) {
+
+ // Pull top level things out
+ if title, ok := parsedMap["title"]; ok {
+ m.Title, _ = title.(string)
+ }
+ if template, ok := parsedMap["template"]; ok {
+ m.Template, _ = template.(string)
+ }
+ if date, ok := parsedMap["date"].(string); ok {
+ for _, layout := range timeLayout {
+ if t, err := time.Parse(layout, date); err == nil {
+ m.Date = t
+ break
+ }
+ }
+ }
+
+ // Store everything as a flag or variable
+ for key, val := range parsedMap {
+ switch v := val.(type) {
+ case bool:
+ m.Flags[key] = v
+ case string:
+ m.Variables[key] = v
+ }
+ }
+}
+
+// MetadataParser is a an interface that must be satisfied by each parser
+type MetadataParser interface {
+ // Initialize a parser
+ Init(b *bytes.Buffer) bool
+
+ // Type of metadata
+ Type() string
+
+ // Parsed metadata.
+ Metadata() Metadata
+
+ // Raw markdown.
+ Markdown() []byte
+}
+
+// GetParser returns a parser for the given data
+func GetParser(buf []byte) MetadataParser {
+ for _, p := range parsers() {
+ b := bytes.NewBuffer(buf)
+ if p.Init(b) {
+ return p
+ }
+ }
+
+ return nil
+}
+
+// parsers returns all available parsers
+func parsers() []MetadataParser {
+ return []MetadataParser{
+ &TOMLMetadataParser{},
+ &YAMLMetadataParser{},
+ &JSONMetadataParser{},
+
+ // This one must be last
+ &NoneMetadataParser{},
+ }
+}
+
+// Split out prefixed/suffixed metadata with given delimiter
+func splitBuffer(b *bytes.Buffer, delim string) (*bytes.Buffer, *bytes.Buffer) {
+ scanner := bufio.NewScanner(b)
+
+ // Read and check first line
+ if !scanner.Scan() {
+ return nil, nil
+ }
+ if string(bytes.TrimSpace(scanner.Bytes())) != delim {
+ return nil, nil
+ }
+
+ // Accumulate metadata, until delimiter
+ meta := bytes.NewBuffer(nil)
+ for scanner.Scan() {
+ if string(bytes.TrimSpace(scanner.Bytes())) == delim {
+ break
+ }
+ if _, err := meta.Write(scanner.Bytes()); err != nil {
+ return nil, nil
+ }
+ if _, err := meta.WriteRune('\n'); err != nil {
+ return nil, nil
+ }
+ }
+ // Make sure we saw closing delimiter
+ if string(bytes.TrimSpace(scanner.Bytes())) != delim {
+ return nil, nil
+ }
+
+ // The rest is markdown
+ markdown := new(bytes.Buffer)
+ for scanner.Scan() {
+ if _, err := markdown.Write(scanner.Bytes()); err != nil {
+ return nil, nil
+ }
+ if _, err := markdown.WriteRune('\n'); err != nil {
+ return nil, nil
+ }
+ }
+
+ return meta, markdown
+}
diff --git a/caddyhttp/markdown/metadata/metadata_json.go b/caddyhttp/markdown/metadata/metadata_json.go
new file mode 100644
index 000000000..d3b9991ff
--- /dev/null
+++ b/caddyhttp/markdown/metadata/metadata_json.go
@@ -0,0 +1,53 @@
+package metadata
+
+import (
+ "bytes"
+ "encoding/json"
+)
+
+// JSONMetadataParser is the MetadataParser for JSON
+type JSONMetadataParser struct {
+ metadata Metadata
+ markdown *bytes.Buffer
+}
+
+func (j *JSONMetadataParser) Type() string {
+ return "JSON"
+}
+
+// Parse metadata/markdown file
+func (j *JSONMetadataParser) Init(b *bytes.Buffer) bool {
+ m := make(map[string]interface{})
+
+ err := json.Unmarshal(b.Bytes(), &m)
+ if err != nil {
+ var offset int
+
+ if jerr, ok := err.(*json.SyntaxError); !ok {
+ return false
+ } else {
+ offset = int(jerr.Offset)
+ }
+
+ m = make(map[string]interface{})
+ err = json.Unmarshal(b.Next(offset-1), &m)
+ if err != nil {
+ return false
+ }
+ }
+
+ j.metadata = NewMetadata(m)
+ j.markdown = bytes.NewBuffer(b.Bytes())
+
+ return true
+}
+
+// Metadata returns parsed metadata. It should be called
+// only after a call to Parse returns without error.
+func (j *JSONMetadataParser) Metadata() Metadata {
+ return j.metadata
+}
+
+func (j *JSONMetadataParser) Markdown() []byte {
+ return j.markdown.Bytes()
+}
diff --git a/caddyhttp/markdown/metadata/metadata_none.go b/caddyhttp/markdown/metadata/metadata_none.go
new file mode 100644
index 000000000..ed034f2fa
--- /dev/null
+++ b/caddyhttp/markdown/metadata/metadata_none.go
@@ -0,0 +1,39 @@
+package metadata
+
+import (
+ "bytes"
+)
+
+// TOMLMetadataParser is the MetadataParser for TOML
+type NoneMetadataParser struct {
+ metadata Metadata
+ markdown *bytes.Buffer
+}
+
+func (n *NoneMetadataParser) Type() string {
+ return "None"
+}
+
+// Parse metadata/markdown file
+func (n *NoneMetadataParser) Init(b *bytes.Buffer) bool {
+ m := make(map[string]interface{})
+ n.metadata = NewMetadata(m)
+ n.markdown = bytes.NewBuffer(b.Bytes())
+
+ return true
+}
+
+// Parse the metadata
+func (n *NoneMetadataParser) Parse(b []byte) ([]byte, error) {
+ return nil, nil
+}
+
+// Metadata returns parsed metadata. It should be called
+// only after a call to Parse returns without error.
+func (n *NoneMetadataParser) Metadata() Metadata {
+ return n.metadata
+}
+
+func (n *NoneMetadataParser) Markdown() []byte {
+ return n.markdown.Bytes()
+}
diff --git a/caddyhttp/markdown/metadata/metadata_test.go b/caddyhttp/markdown/metadata/metadata_test.go
new file mode 100644
index 000000000..0c155d37c
--- /dev/null
+++ b/caddyhttp/markdown/metadata/metadata_test.go
@@ -0,0 +1,261 @@
+package metadata
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func check(t *testing.T, err error) {
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+var TOML = [5]string{`
+title = "A title"
+template = "default"
+name = "value"
+positive = true
+negative = false
+`,
+ `+++
+title = "A title"
+template = "default"
+name = "value"
+positive = true
+negative = false
++++
+Page content
+ `,
+ `+++
+title = "A title"
+template = "default"
+name = "value"
+positive = true
+negative = false
+ `,
+ `title = "A title" template = "default" [variables] name = "value"`,
+ `+++
+title = "A title"
+template = "default"
+name = "value"
+positive = true
+negative = false
++++
+`,
+}
+
+var YAML = [5]string{`
+title : A title
+template : default
+name : value
+positive : true
+negative : false
+`,
+ `---
+title : A title
+template : default
+name : value
+positive : true
+negative : false
+---
+ Page content
+ `,
+ `---
+title : A title
+template : default
+name : value
+ `,
+ `title : A title template : default variables : name : value : positive : true : negative : false`,
+ `---
+title : A title
+template : default
+name : value
+positive : true
+negative : false
+---
+`,
+}
+
+var JSON = [5]string{`
+ "title" : "A title",
+ "template" : "default",
+ "name" : "value",
+ "positive" : true,
+ "negative" : false
+`,
+ `{
+ "title" : "A title",
+ "template" : "default",
+ "name" : "value",
+ "positive" : true,
+ "negative" : false
+}
+Page content
+ `,
+ `
+{
+ "title" : "A title",
+ "template" : "default",
+ "name" : "value",
+ "positive" : true,
+ "negative" : false
+ `,
+ `
+{
+ "title" :: "A title",
+ "template" : "default",
+ "name" : "value",
+ "positive" : true,
+ "negative" : false
+}
+ `,
+ `{
+ "title" : "A title",
+ "template" : "default",
+ "name" : "value",
+ "positive" : true,
+ "negative" : false
+}
+`,
+}
+
+func TestParsers(t *testing.T) {
+ expected := Metadata{
+ Title: "A title",
+ Template: "default",
+ Variables: map[string]string{
+ "name": "value",
+ "title": "A title",
+ "template": "default",
+ },
+ Flags: map[string]bool{
+ "positive": true,
+ "negative": false,
+ },
+ }
+ compare := func(m Metadata) bool {
+ if m.Title != expected.Title {
+ return false
+ }
+ if m.Template != expected.Template {
+ return false
+ }
+ for k, v := range m.Variables {
+ if v != expected.Variables[k] {
+ return false
+ }
+ }
+ for k, v := range m.Flags {
+ if v != expected.Flags[k] {
+ return false
+ }
+ }
+ varLenOK := len(m.Variables) == len(expected.Variables)
+ flagLenOK := len(m.Flags) == len(expected.Flags)
+ return varLenOK && flagLenOK
+ }
+
+ data := []struct {
+ parser MetadataParser
+ testData [5]string
+ name string
+ }{
+ {&JSONMetadataParser{}, JSON, "JSON"},
+ {&YAMLMetadataParser{}, YAML, "YAML"},
+ {&TOMLMetadataParser{}, TOML, "TOML"},
+ }
+
+ for _, v := range data {
+ // metadata without identifiers
+ if v.parser.Init(bytes.NewBufferString(v.testData[0])) {
+ t.Fatalf("Expected error for invalid metadata for %v", v.name)
+ }
+
+ // metadata with identifiers
+ if !v.parser.Init(bytes.NewBufferString(v.testData[1])) {
+ t.Fatalf("Metadata failed to initialize, type %v", v.parser.Type())
+ }
+ md := v.parser.Markdown()
+ if !compare(v.parser.Metadata()) {
+ t.Fatalf("Expected %v, found %v for %v", expected, v.parser.Metadata(), v.name)
+ }
+ if "Page content" != strings.TrimSpace(string(md)) {
+ t.Fatalf("Expected %v, found %v for %v", "Page content", string(md), v.name)
+ }
+ // Check that we find the correct metadata parser type
+ if p := GetParser([]byte(v.testData[1])); p.Type() != v.name {
+ t.Fatalf("Wrong parser found, expected %v, found %v", v.name, p.Type())
+ }
+
+ // metadata without closing identifier
+ if v.parser.Init(bytes.NewBufferString(v.testData[2])) {
+ t.Fatalf("Expected error for missing closing identifier for %v parser", v.name)
+ }
+
+ // invalid metadata
+ if v.parser.Init(bytes.NewBufferString(v.testData[3])) {
+ t.Fatalf("Expected error for invalid metadata for %v", v.name)
+ }
+
+ // front matter but no body
+ if !v.parser.Init(bytes.NewBufferString(v.testData[4])) {
+ t.Fatalf("Unexpected error for valid metadata but no body for %v", v.name)
+ }
+ }
+}
+
+func TestLargeBody(t *testing.T) {
+
+ var JSON = `{
+"template": "chapter"
+}
+
+Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
+
+ `
+ var TOML = `+++
+template = "chapter"
++++
+
+Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
+
+ `
+ var YAML = `---
+template : chapter
+---
+
+Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
+
+ `
+ var NONE = `
+
+Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
+
+ `
+ var expectedBody = `Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
+`
+
+ data := []struct {
+ pType string
+ testData string
+ }{
+ {"JSON", JSON},
+ {"TOML", TOML},
+ {"YAML", YAML},
+ {"None", NONE},
+ }
+ for _, v := range data {
+ p := GetParser([]byte(v.testData))
+ if v.pType != p.Type() {
+ t.Fatalf("Wrong parser type, expected %v, got %v", v.pType, p.Type())
+ }
+ md := p.Markdown()
+ if strings.TrimSpace(string(md)) != strings.TrimSpace(expectedBody) {
+ t.Log("Provided:", v.testData)
+ t.Log("Returned:", p.Markdown())
+ t.Fatalf("Error, mismatched body in expected type %v, matched type %v", v.pType, p.Type())
+ }
+ }
+}
diff --git a/caddyhttp/markdown/metadata/metadata_toml.go b/caddyhttp/markdown/metadata/metadata_toml.go
new file mode 100644
index 000000000..75c2067f0
--- /dev/null
+++ b/caddyhttp/markdown/metadata/metadata_toml.go
@@ -0,0 +1,44 @@
+package metadata
+
+import (
+ "bytes"
+
+ "github.com/BurntSushi/toml"
+)
+
+// TOMLMetadataParser is the MetadataParser for TOML
+type TOMLMetadataParser struct {
+ metadata Metadata
+ markdown *bytes.Buffer
+}
+
+func (t *TOMLMetadataParser) Type() string {
+ return "TOML"
+}
+
+// Parse metadata/markdown file
+func (t *TOMLMetadataParser) Init(b *bytes.Buffer) bool {
+ meta, data := splitBuffer(b, "+++")
+ if meta == nil || data == nil {
+ return false
+ }
+ t.markdown = data
+
+ m := make(map[string]interface{})
+ if err := toml.Unmarshal(meta.Bytes(), &m); err != nil {
+ return false
+ }
+ t.metadata = NewMetadata(m)
+
+ return true
+}
+
+// Metadata returns parsed metadata. It should be called
+// only after a call to Parse returns without error.
+func (t *TOMLMetadataParser) Metadata() Metadata {
+ return t.metadata
+}
+
+func (t *TOMLMetadataParser) Markdown() []byte {
+ return t.markdown.Bytes()
+}
diff --git a/caddyhttp/markdown/metadata/metadata_yaml.go b/caddyhttp/markdown/metadata/metadata_yaml.go
new file mode 100644
index 000000000..f7ef5bb4f
--- /dev/null
+++ b/caddyhttp/markdown/metadata/metadata_yaml.go
@@ -0,0 +1,43 @@
+package metadata
+
+import (
+ "bytes"
+
+ "gopkg.in/yaml.v2"
+)
+
+// YAMLMetadataParser is the MetadataParser for YAML
+type YAMLMetadataParser struct {
+ metadata Metadata
+ markdown *bytes.Buffer
+}
+
+func (y *YAMLMetadataParser) Type() string {
+ return "YAML"
+}
+
+func (y *YAMLMetadataParser) Init(b *bytes.Buffer) bool {
+ meta, data := splitBuffer(b, "---")
+ if meta == nil || data == nil {
+ return false
+ }
+ y.markdown = data
+
+ m := make(map[string]interface{})
+ if err := yaml.Unmarshal(meta.Bytes(), &m); err != nil {
+ return false
+ }
+ y.metadata = NewMetadata(m)
+
+ return true
+}
+
+// Metadata returns parsed metadata. It should be called
+// only after a call to Parse returns without error.
+func (y *YAMLMetadataParser) Metadata() Metadata {
+ return y.metadata
+}
+
+func (y *YAMLMetadataParser) Markdown() []byte {
+ return y.markdown.Bytes()
+}
diff --git a/caddyhttp/markdown/process.go b/caddyhttp/markdown/process.go
new file mode 100644
index 000000000..32c887c72
--- /dev/null
+++ b/caddyhttp/markdown/process.go
@@ -0,0 +1,74 @@
+package markdown
+
+import (
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+ "github.com/mholt/caddy/caddyhttp/markdown/metadata"
+ "github.com/mholt/caddy/caddyhttp/markdown/summary"
+ "github.com/russross/blackfriday"
+)
+
+type FileInfo struct {
+ os.FileInfo
+ ctx httpserver.Context
+}
+
+func (f FileInfo) Summarize(wordcount int) (string, error) {
+ fp, err := f.ctx.Root.Open(f.Name())
+ if err != nil {
+ return "", err
+ }
+ defer fp.Close()
+
+ buf, err := ioutil.ReadAll(fp)
+ if err != nil {
+ return "", err
+ }
+
+ return string(summary.Markdown(buf, wordcount)), nil
+}
+
+// Markdown processes the contents of a page in b. It parses the metadata
+// (if any) and uses the template (if found).
+func (c *Config) Markdown(title string, r io.Reader, dirents []os.FileInfo, ctx httpserver.Context) ([]byte, error) {
+ body, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ parser := metadata.GetParser(body)
+ markdown := parser.Markdown()
+ mdata := parser.Metadata()
+
+ // process markdown
+ extns := 0
+ extns |= blackfriday.EXTENSION_TABLES
+ extns |= blackfriday.EXTENSION_FENCED_CODE
+ extns |= blackfriday.EXTENSION_STRIKETHROUGH
+ extns |= blackfriday.EXTENSION_DEFINITION_LISTS
+ html := blackfriday.Markdown(markdown, c.Renderer, extns)
+
+ // set it as body for template
+ mdata.Variables["body"] = string(html)
+
+ // fixup title
+ mdata.Variables["title"] = mdata.Title
+ if mdata.Variables["title"] == "" {
+ mdata.Variables["title"] = title
+ }
+
+ // massage possible files
+ files := []FileInfo{}
+ for _, ent := range dirents {
+ file := FileInfo{
+ FileInfo: ent,
+ ctx: ctx,
+ }
+ files = append(files, file)
+ }
+
+ return execTemplate(c, mdata, files, ctx)
+}
diff --git a/caddyhttp/markdown/setup.go b/caddyhttp/markdown/setup.go
new file mode 100644
index 000000000..4bf9426aa
--- /dev/null
+++ b/caddyhttp/markdown/setup.go
@@ -0,0 +1,141 @@
+package markdown
+
+import (
+ "net/http"
+ "path/filepath"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+ "github.com/russross/blackfriday"
+)
+
+func init() {
+ caddy.RegisterPlugin(caddy.Plugin{
+ Name: "markdown",
+ ServerType: "http",
+ Action: setup,
+ })
+}
+
+// setup configures a new Markdown middleware instance.
+func setup(c *caddy.Controller) error {
+ mdconfigs, err := markdownParse(c)
+ if err != nil {
+ return err
+ }
+
+ cfg := httpserver.GetConfig(c.Key)
+
+ md := Markdown{
+ Root: cfg.Root,
+ FileSys: http.Dir(cfg.Root),
+ Configs: mdconfigs,
+ IndexFiles: []string{"index.md"},
+ }
+
+ cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
+ md.Next = next
+ return md
+ })
+
+ return nil
+}
+
+func markdownParse(c *caddy.Controller) ([]*Config, error) {
+ var mdconfigs []*Config
+
+ for c.Next() {
+ md := &Config{
+ Renderer: blackfriday.HtmlRenderer(0, "", ""),
+ Extensions: make(map[string]struct{}),
+ Template: GetDefaultTemplate(),
+ }
+
+ // Get the path scope
+ args := c.RemainingArgs()
+ switch len(args) {
+ case 0:
+ md.PathScope = "/"
+ case 1:
+ md.PathScope = args[0]
+ default:
+ return mdconfigs, c.ArgErr()
+ }
+
+ // Load any other configuration parameters
+ for c.NextBlock() {
+ if err := loadParams(c, md); err != nil {
+ return mdconfigs, err
+ }
+ }
+
+ // If no extensions were specified, assume some defaults
+ if len(md.Extensions) == 0 {
+ md.Extensions[".md"] = struct{}{}
+ md.Extensions[".markdown"] = struct{}{}
+ md.Extensions[".mdown"] = struct{}{}
+ }
+
+ mdconfigs = append(mdconfigs, md)
+ }
+
+ return mdconfigs, nil
+}
+
+func loadParams(c *caddy.Controller, mdc *Config) error {
+ cfg := httpserver.GetConfig(c.Key)
+
+ switch c.Val() {
+ case "ext":
+ for _, ext := range c.RemainingArgs() {
+ mdc.Extensions[ext] = struct{}{}
+ }
+ return nil
+ case "css":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ mdc.Styles = append(mdc.Styles, c.Val())
+ return nil
+ case "js":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ mdc.Scripts = append(mdc.Scripts, c.Val())
+ return nil
+ case "template":
+ tArgs := c.RemainingArgs()
+ switch len(tArgs) {
+ default:
+ return c.ArgErr()
+ case 1:
+ fpath := filepath.ToSlash(filepath.Clean(cfg.Root + string(filepath.Separator) + tArgs[0]))
+
+ if err := SetTemplate(mdc.Template, "", fpath); err != nil {
+ c.Errf("default template parse error: %v", err)
+ }
+ return nil
+ case 2:
+ fpath := filepath.ToSlash(filepath.Clean(cfg.Root + string(filepath.Separator) + tArgs[1]))
+
+ if err := SetTemplate(mdc.Template, tArgs[0], fpath); err != nil {
+ c.Errf("template parse error: %v", err)
+ }
+ return nil
+ }
+ case "templatedir":
+ if !c.NextArg() {
+ return c.ArgErr()
+ }
+ _, err := mdc.Template.ParseGlob(c.Val())
+ if err != nil {
+ c.Errf("template load error: %v", err)
+ }
+ if c.NextArg() {
+ return c.ArgErr()
+ }
+ return nil
+ default:
+ return c.Err("Expected valid markdown configuration property")
+ }
+}
diff --git a/caddyhttp/markdown/setup_test.go b/caddyhttp/markdown/setup_test.go
new file mode 100644
index 000000000..2880a230f
--- /dev/null
+++ b/caddyhttp/markdown/setup_test.go
@@ -0,0 +1,152 @@
+package markdown
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+ "text/template"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func TestSetup(t *testing.T) {
+ err := setup(caddy.NewTestController(`markdown /blog`))
+ if err != nil {
+ t.Errorf("Expected no errors, got: %v", err)
+ }
+ mids := httpserver.GetConfig("").Middleware()
+ if len(mids) == 0 {
+ t.Fatal("Expected middleware, got 0 instead")
+ }
+
+ handler := mids[0](httpserver.EmptyNext)
+ myHandler, ok := handler.(Markdown)
+
+ if !ok {
+ t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
+ }
+
+ if myHandler.Configs[0].PathScope != "/blog" {
+ t.Errorf("Expected /blog as the Path Scope")
+ }
+ if len(myHandler.Configs[0].Extensions) != 3 {
+ t.Error("Expected 3 markdown extensions")
+ }
+ for _, key := range []string{".md", ".markdown", ".mdown"} {
+ if ext, ok := myHandler.Configs[0].Extensions[key]; !ok {
+ t.Errorf("Expected extensions to contain %v", ext)
+ }
+ }
+}
+
+func TestMarkdownParse(t *testing.T) {
+ tests := []struct {
+ inputMarkdownConfig string
+ shouldErr bool
+ expectedMarkdownConfig []Config
+ }{
+
+ {`markdown /blog {
+ ext .md .txt
+ css /resources/css/blog.css
+ js /resources/js/blog.js
+}`, false, []Config{{
+ PathScope: "/blog",
+ Extensions: map[string]struct{}{
+ ".md": {},
+ ".txt": {},
+ },
+ Styles: []string{"/resources/css/blog.css"},
+ Scripts: []string{"/resources/js/blog.js"},
+ Template: GetDefaultTemplate(),
+ }}},
+ {`markdown /blog {
+ ext .md
+ template tpl_with_include.html
+}`, false, []Config{{
+ PathScope: "/blog",
+ Extensions: map[string]struct{}{
+ ".md": {},
+ },
+ Template: GetDefaultTemplate(),
+ }}},
+ }
+ // Setup the extra template
+ tmpl := tests[1].expectedMarkdownConfig[0].Template
+ SetTemplate(tmpl, "", "./testdata/tpl_with_include.html")
+
+ for i, test := range tests {
+ c := caddy.NewTestController(test.inputMarkdownConfig)
+ httpserver.GetConfig("").Root = "./testdata"
+ actualMarkdownConfigs, err := markdownParse(c)
+
+ if err == nil && test.shouldErr {
+ t.Errorf("Test %d didn't error, but it should have", i)
+ } else if err != nil && !test.shouldErr {
+ t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
+ }
+ if len(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
+ t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
+ i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
+ }
+ for j, actualMarkdownConfig := range actualMarkdownConfigs {
+
+ if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
+ t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
+ i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
+ }
+
+ if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
+ t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
+ i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
+ }
+ if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
+ t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
+ i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
+ }
+ if ok, tx, ty := equalTemplates(actualMarkdownConfig.Template, test.expectedMarkdownConfig[j].Template); !ok {
+ t.Errorf("Test %d the %dth Markdown Config Templates did not match, expected %s to be %s", i, j, tx, ty)
+ }
+ }
+ }
+}
+
+func equalTemplates(i, j *template.Template) (bool, string, string) {
+ // Just in case :)
+ if i == j {
+ return true, "", ""
+ }
+
+ // We can't do much here, templates can't really be compared. However,
+ // we can execute the templates and compare their outputs to be reasonably
+ // sure that they're the same.
+
+ // This is exceedingly ugly.
+ ctx := httpserver.Context{
+ Root: http.Dir("./testdata"),
+ }
+
+ md := Data{
+ Context: ctx,
+ Doc: make(map[string]string),
+ DocFlags: make(map[string]bool),
+ Styles: []string{"style1"},
+ Scripts: []string{"js1"},
+ }
+ md.Doc["title"] = "some title"
+ md.Doc["body"] = "some body"
+
+ bufi := new(bytes.Buffer)
+ bufj := new(bytes.Buffer)
+
+ if err := i.Execute(bufi, md); err != nil {
+ return false, fmt.Sprintf("%v", err), ""
+ }
+ if err := j.Execute(bufj, md); err != nil {
+ return false, "", fmt.Sprintf("%v", err)
+ }
+
+ return bytes.Equal(bufi.Bytes(), bufj.Bytes()), string(bufi.Bytes()), string(bufj.Bytes())
+}
diff --git a/caddyhttp/markdown/summary/render.go b/caddyhttp/markdown/summary/render.go
new file mode 100644
index 000000000..b23affbd1
--- /dev/null
+++ b/caddyhttp/markdown/summary/render.go
@@ -0,0 +1,153 @@
+package summary
+
+import (
+ "bytes"
+
+ "github.com/russross/blackfriday"
+)
+
+// Ensure we implement the Blackfriday Markdown Renderer interface
+var _ blackfriday.Renderer = (*renderer)(nil)
+
+// renderer renders Markdown to plain-text meant for listings and excerpts,
+// and implements the blackfriday.Renderer interface.
+//
+// Many of the methods are stubs with no output to prevent output of HTML markup.
+type renderer struct{}
+
+// Blocklevel callbacks
+
+// BlockCode is the code tag callback.
+func (r renderer) BlockCode(out *bytes.Buffer, text []byte, land string) {}
+
+// BlockQuote is the quote tag callback.
+func (r renderer) BlockQuote(out *bytes.Buffer, text []byte) {}
+
+// BlockHtml is the HTML tag callback.
+func (r renderer) BlockHtml(out *bytes.Buffer, text []byte) {}
+
+// Header is the header tag callback.
+func (r renderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {}
+
+// HRule is the horizontal rule tag callback.
+func (r renderer) HRule(out *bytes.Buffer) {}
+
+// List is the list tag callback.
+func (r renderer) List(out *bytes.Buffer, text func() bool, flags int) {
+ // TODO: This is not desired (we'd rather not write lists as part of summary),
+ // but see this issue: https://github.com/russross/blackfriday/issues/189
+ marker := out.Len()
+ if !text() {
+ out.Truncate(marker)
+ }
+ out.Write([]byte{' '})
+}
+
+// ListItem is the list item tag callback.
+func (r renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {}
+
+// Paragraph is the paragraph tag callback. This renders simple paragraph text
+// into plain text, such that summaries can be easily generated.
+func (r renderer) Paragraph(out *bytes.Buffer, text func() bool) {
+ marker := out.Len()
+ if !text() {
+ out.Truncate(marker)
+ }
+ out.Write([]byte{' '})
+}
+
+// Table is the table tag callback.
+func (r renderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {}
+
+// TableRow is the table row tag callback.
+func (r renderer) TableRow(out *bytes.Buffer, text []byte) {}
+
+// TableHeaderCell is the table header cell tag callback.
+func (r renderer) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {}
+
+// TableCell is the table cell tag callback.
+func (r renderer) TableCell(out *bytes.Buffer, text []byte, flags int) {}
+
+// Footnotes is the foot notes tag callback.
+func (r renderer) Footnotes(out *bytes.Buffer, text func() bool) {}
+
+// FootnoteItem is the footnote item tag callback.
+func (r renderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {}
+
+// TitleBlock is the title tag callback.
+func (r renderer) TitleBlock(out *bytes.Buffer, text []byte) {}
+
+// Spanlevel callbacks
+
+// AutoLink is the autolink tag callback.
+func (r renderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {}
+
+// CodeSpan is the code span tag callback. Outputs a simple Markdown version
+// of the code span.
+func (r renderer) CodeSpan(out *bytes.Buffer, text []byte) {
+ out.Write([]byte("`"))
+ out.Write(text)
+ out.Write([]byte("`"))
+}
+
+// DoubleEmphasis is the double emphasis tag callback. Outputs a simple
+// plain-text version of the input.
+func (r renderer) DoubleEmphasis(out *bytes.Buffer, text []byte) {
+ out.Write(text)
+}
+
+// Emphasis is the emphasis tag callback. Outputs a simple plain-text
+// version of the input.
+func (r renderer) Emphasis(out *bytes.Buffer, text []byte) {
+ out.Write(text)
+}
+
+// Image is the image tag callback.
+func (r renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {}
+
+// LineBreak is the line break tag callback.
+func (r renderer) LineBreak(out *bytes.Buffer) {}
+
+// Link is the link tag callback. Outputs a sipmle plain-text version
+// of the input.
+func (r renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
+ out.Write(content)
+}
+
+// RawHtmlTag is the raw HTML tag callback.
+func (r renderer) RawHtmlTag(out *bytes.Buffer, tag []byte) {}
+
+// TripleEmphasis is the triple emphasis tag callback. Outputs a simple plain-text
+// version of the input.
+func (r renderer) TripleEmphasis(out *bytes.Buffer, text []byte) {
+ out.Write(text)
+}
+
+// StrikeThrough is the strikethrough tag callback.
+func (r renderer) StrikeThrough(out *bytes.Buffer, text []byte) {}
+
+// FootnoteRef is the footnote ref tag callback.
+func (r renderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {}
+
+// Lowlevel callbacks
+
+// Entity callback. Outputs a simple plain-text version of the input.
+func (r renderer) Entity(out *bytes.Buffer, entity []byte) {
+ out.Write(entity)
+}
+
+// NormalText callback. Outputs a simple plain-text version of the input.
+func (r renderer) NormalText(out *bytes.Buffer, text []byte) {
+ out.Write(text)
+}
+
+// Header and footer
+
+// DocumentHeader callback.
+func (r renderer) DocumentHeader(out *bytes.Buffer) {}
+
+// DocumentFooter callback.
+func (r renderer) DocumentFooter(out *bytes.Buffer) {}
+
+// GetFlags returns zero.
+func (r renderer) GetFlags() int { return 0 }
diff --git a/caddyhttp/markdown/summary/summary.go b/caddyhttp/markdown/summary/summary.go
new file mode 100644
index 000000000..e55bba2c9
--- /dev/null
+++ b/caddyhttp/markdown/summary/summary.go
@@ -0,0 +1,17 @@
+package summary
+
+import (
+ "bytes"
+
+ "github.com/russross/blackfriday"
+)
+
+// Markdown formats input using a plain-text renderer, and
+// then returns up to the first `wordcount` words as a summary.
+func Markdown(input []byte, wordcount int) []byte {
+ words := bytes.Fields(blackfriday.Markdown(input, renderer{}, 0))
+ if wordcount > len(words) {
+ wordcount = len(words)
+ }
+ return bytes.Join(words[0:wordcount], []byte{' '})
+}
diff --git a/caddyhttp/markdown/template.go b/caddyhttp/markdown/template.go
new file mode 100644
index 000000000..fcd8f31a1
--- /dev/null
+++ b/caddyhttp/markdown/template.go
@@ -0,0 +1,88 @@
+package markdown
+
+import (
+ "bytes"
+ "io/ioutil"
+ "text/template"
+
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+ "github.com/mholt/caddy/caddyhttp/markdown/metadata"
+)
+
+// Data represents a markdown document.
+type Data struct {
+ httpserver.Context
+ Doc map[string]string
+ DocFlags map[string]bool
+ Styles []string
+ Scripts []string
+ Files []FileInfo
+}
+
+// Include "overrides" the embedded httpserver.Context's Include()
+// method so that included files have access to d's fields.
+// Note: using {{template 'template-name' .}} instead might be better.
+func (d Data) Include(filename string) (string, error) {
+ return httpserver.ContextInclude(filename, d, d.Root)
+}
+
+// execTemplate executes a template given a requestPath, template, and metadata
+func execTemplate(c *Config, mdata metadata.Metadata, files []FileInfo, ctx httpserver.Context) ([]byte, error) {
+ mdData := Data{
+ Context: ctx,
+ Doc: mdata.Variables,
+ DocFlags: mdata.Flags,
+ Styles: c.Styles,
+ Scripts: c.Scripts,
+ Files: files,
+ }
+
+ b := new(bytes.Buffer)
+ if err := c.Template.ExecuteTemplate(b, mdata.Template, mdData); err != nil {
+ return nil, err
+ }
+
+ return b.Bytes(), nil
+}
+
+func SetTemplate(t *template.Template, name, filename string) error {
+
+ // Read template
+ buf, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+
+ // Update if exists
+ if tt := t.Lookup(name); tt != nil {
+ _, err = tt.Parse(string(buf))
+ return err
+ }
+
+ // Allocate new name if not
+ _, err = t.New(name).Parse(string(buf))
+ return err
+}
+
+func GetDefaultTemplate() *template.Template {
+ return template.Must(template.New("").Parse(defaultTemplate))
+}
+
+const (
+ defaultTemplate = `
+
+
+ {{.Doc.title}}
+
+ {{- range .Styles}}
+
+ {{- end}}
+ {{- range .Scripts}}
+
+ {{- end}}
+
+
+ {{.Doc.body}}
+
+`
+)
diff --git a/caddyhttp/templates/setup.go b/caddyhttp/templates/setup.go
new file mode 100644
index 000000000..e291b532d
--- /dev/null
+++ b/caddyhttp/templates/setup.go
@@ -0,0 +1,102 @@
+package templates
+
+import (
+ "net/http"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func init() {
+ caddy.RegisterPlugin(caddy.Plugin{
+ Name: "templates",
+ ServerType: "http",
+ Action: setup,
+ })
+}
+
+// setup configures a new Templates middleware instance.
+func setup(c *caddy.Controller) error {
+ rules, err := templatesParse(c)
+ if err != nil {
+ return err
+ }
+
+ cfg := httpserver.GetConfig(c.Key)
+
+ tmpls := Templates{
+ Rules: rules,
+ Root: cfg.Root,
+ FileSys: http.Dir(cfg.Root),
+ }
+
+ cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
+ tmpls.Next = next
+ return tmpls
+ })
+
+ return nil
+}
+
+func templatesParse(c *caddy.Controller) ([]Rule, error) {
+ var rules []Rule
+
+ for c.Next() {
+ var rule Rule
+
+ rule.Path = defaultTemplatePath
+ rule.Extensions = defaultTemplateExtensions
+
+ args := c.RemainingArgs()
+
+ switch len(args) {
+ case 0:
+ // Optional block
+ for c.NextBlock() {
+ switch c.Val() {
+ case "path":
+ args := c.RemainingArgs()
+ if len(args) != 1 {
+ return nil, c.ArgErr()
+ }
+ rule.Path = args[0]
+
+ case "ext":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return nil, c.ArgErr()
+ }
+ rule.Extensions = args
+
+ case "between":
+ args := c.RemainingArgs()
+ if len(args) != 2 {
+ return nil, c.ArgErr()
+ }
+ rule.Delims[0] = args[0]
+ rule.Delims[1] = args[1]
+ }
+ }
+ default:
+ // First argument would be the path
+ rule.Path = args[0]
+
+ // Any remaining arguments are extensions
+ rule.Extensions = args[1:]
+ if len(rule.Extensions) == 0 {
+ rule.Extensions = defaultTemplateExtensions
+ }
+ }
+
+ for _, ext := range rule.Extensions {
+ rule.IndexFiles = append(rule.IndexFiles, "index"+ext)
+ }
+
+ rules = append(rules, rule)
+ }
+ return rules, nil
+}
+
+const defaultTemplatePath = "/"
+
+var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"}
diff --git a/caddyhttp/templates/setup_test.go b/caddyhttp/templates/setup_test.go
new file mode 100644
index 000000000..2427afd45
--- /dev/null
+++ b/caddyhttp/templates/setup_test.go
@@ -0,0 +1,107 @@
+package templates
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func TestSetup(t *testing.T) {
+ err := setup(caddy.NewTestController(`templates`))
+ if err != nil {
+ t.Errorf("Expected no errors, got: %v", err)
+ }
+ mids := httpserver.GetConfig("").Middleware()
+ if len(mids) == 0 {
+ t.Fatal("Expected middleware, got 0 instead")
+ }
+
+ handler := mids[0](httpserver.EmptyNext)
+ myHandler, ok := handler.(Templates)
+
+ if !ok {
+ t.Fatalf("Expected handler to be type Templates, got: %#v", handler)
+ }
+
+ if myHandler.Rules[0].Path != defaultTemplatePath {
+ t.Errorf("Expected / as the default Path")
+ }
+ if fmt.Sprint(myHandler.Rules[0].Extensions) != fmt.Sprint(defaultTemplateExtensions) {
+ t.Errorf("Expected %v to be the Default Extensions", defaultTemplateExtensions)
+ }
+ var indexFiles []string
+ for _, extension := range defaultTemplateExtensions {
+ indexFiles = append(indexFiles, "index"+extension)
+ }
+ if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
+ t.Errorf("Expected %v to be the Default Index files", indexFiles)
+ }
+ if myHandler.Rules[0].Delims != [2]string{} {
+ t.Errorf("Expected %v to be the Default Delims", [2]string{})
+ }
+}
+
+func TestTemplatesParse(t *testing.T) {
+ tests := []struct {
+ inputTemplateConfig string
+ shouldErr bool
+ expectedTemplateConfig []Rule
+ }{
+ {`templates /api1`, false, []Rule{{
+ Path: "/api1",
+ Extensions: defaultTemplateExtensions,
+ Delims: [2]string{},
+ }}},
+ {`templates /api2 .txt .htm`, false, []Rule{{
+ Path: "/api2",
+ Extensions: []string{".txt", ".htm"},
+ Delims: [2]string{},
+ }}},
+
+ {`templates /api3 .htm .html
+ templates /api4 .txt .tpl `, false, []Rule{{
+ Path: "/api3",
+ Extensions: []string{".htm", ".html"},
+ Delims: [2]string{},
+ }, {
+ Path: "/api4",
+ Extensions: []string{".txt", ".tpl"},
+ Delims: [2]string{},
+ }}},
+ {`templates {
+ path /api5
+ ext .html
+ between {% %}
+ }`, false, []Rule{{
+ Path: "/api5",
+ Extensions: []string{".html"},
+ Delims: [2]string{"{%", "%}"},
+ }}},
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController(test.inputTemplateConfig)
+ actualTemplateConfigs, err := templatesParse(c)
+
+ if err == nil && test.shouldErr {
+ t.Errorf("Test %d didn't error, but it should have", i)
+ } else if err != nil && !test.shouldErr {
+ t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
+ }
+ if len(actualTemplateConfigs) != len(test.expectedTemplateConfig) {
+ t.Fatalf("Test %d expected %d no of Template configs, but got %d ",
+ i, len(test.expectedTemplateConfig), len(actualTemplateConfigs))
+ }
+ for j, actualTemplateConfig := range actualTemplateConfigs {
+ if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path {
+ t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s",
+ i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path)
+ }
+ if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) {
+ t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions)
+ }
+ }
+ }
+
+}
diff --git a/caddyhttp/templates/templates.go b/caddyhttp/templates/templates.go
new file mode 100644
index 000000000..91491f115
--- /dev/null
+++ b/caddyhttp/templates/templates.go
@@ -0,0 +1,96 @@
+// Package templates implements template execution for files to be
+// dynamically rendered for the client.
+package templates
+
+import (
+ "bytes"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "text/template"
+
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+// ServeHTTP implements the httpserver.Handler interface.
+func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+ for _, rule := range t.Rules {
+ if !httpserver.Path(r.URL.Path).Matches(rule.Path) {
+ continue
+ }
+
+ // Check for index files
+ fpath := r.URL.Path
+ if idx, ok := httpserver.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 := httpserver.Context{Root: t.FileSys, Req: r, URL: r.URL}
+
+ // New template
+ templateName := filepath.Base(fpath)
+ tpl := template.New(templateName)
+
+ // Set delims
+ if rule.Delims != [2]string{} {
+ tpl.Delims(rule.Delims[0], rule.Delims[1])
+ }
+
+ // Build the template
+ templatePath := filepath.Join(t.Root, fpath)
+ tpl, err := tpl.ParseFiles(templatePath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return http.StatusNotFound, nil
+ } else if os.IsPermission(err) {
+ return http.StatusForbidden, nil
+ }
+ return http.StatusInternalServerError, err
+ }
+
+ // Execute it
+ var buf bytes.Buffer
+ err = tpl.Execute(&buf, ctx)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+
+ templateInfo, err := os.Stat(templatePath)
+ if err == nil {
+ // add the Last-Modified header if we were able to read the stamp
+ httpserver.SetLastModifiedHeader(w, templateInfo.ModTime())
+ }
+ buf.WriteTo(w)
+
+ return http.StatusOK, nil
+ }
+ }
+ }
+
+ return t.Next.ServeHTTP(w, r)
+}
+
+// Templates is middleware to render templated files as the HTTP response.
+type Templates struct {
+ Next httpserver.Handler
+ Rules []Rule
+ Root string
+ FileSys http.FileSystem
+}
+
+// Rule represents a template rule. A template will only execute
+// with this rule if the request path matches the Path specified
+// and requests a resource with one of the extensions specified.
+type Rule struct {
+ Path string
+ Extensions []string
+ IndexFiles []string
+ Delims [2]string
+}
diff --git a/caddyhttp/templates/templates_test.go b/caddyhttp/templates/templates_test.go
new file mode 100644
index 000000000..841cf2027
--- /dev/null
+++ b/caddyhttp/templates/templates_test.go
@@ -0,0 +1,138 @@
+package templates
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func TestTemplates(t *testing.T) {
+ tmpl := Templates{
+ Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+ return 0, nil
+ }),
+ Rules: []Rule{
+ {
+ Extensions: []string{".html"},
+ IndexFiles: []string{"index.html"},
+ Path: "/photos",
+ },
+ {
+ Extensions: []string{".html", ".htm"},
+ IndexFiles: []string{"index.html", "index.htm"},
+ Path: "/images",
+ Delims: [2]string{"{%", "%}"},
+ },
+ },
+ Root: "./testdata",
+ FileSys: http.Dir("./testdata"),
+ }
+
+ tmplroot := Templates{
+ Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+ return 0, nil
+ }),
+ Rules: []Rule{
+ {
+ Extensions: []string{".html"},
+ IndexFiles: []string{"index.html"},
+ Path: "/",
+ },
+ },
+ Root: "./testdata",
+ FileSys: http.Dir("./testdata"),
+ }
+
+ // Test tmpl on /photos/test.html
+ req, err := http.NewRequest("GET", "/photos/test.html", nil)
+ if err != nil {
+ t.Fatalf("Test: Could not create HTTP request: %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+
+ tmpl.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
+ }
+
+ respBody := rec.Body.String()
+ expectedBody := `test page
Header title
+
+`
+
+ if respBody != expectedBody {
+ t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
+ }
+
+ // Test tmpl on /images/img.htm
+ req, err = http.NewRequest("GET", "/images/img.htm", nil)
+ if err != nil {
+ t.Fatalf("Could not create HTTP request: %v", err)
+ }
+
+ rec = httptest.NewRecorder()
+
+ tmpl.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
+ }
+
+ respBody = rec.Body.String()
+ expectedBody = `img
Header title
+
+`
+
+ if respBody != expectedBody {
+ t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
+ }
+
+ // Test tmpl on /images/img2.htm
+ req, err = http.NewRequest("GET", "/images/img2.htm", nil)
+ if err != nil {
+ t.Fatalf("Could not create HTTP request: %v", err)
+ }
+
+ rec = httptest.NewRecorder()
+
+ tmpl.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
+ }
+
+ respBody = rec.Body.String()
+ expectedBody = `img{{.Include "header.html"}}
+`
+
+ if respBody != expectedBody {
+ t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
+ }
+
+ // Test tmplroot on /root.html
+ req, err = http.NewRequest("GET", "/root.html", nil)
+ if err != nil {
+ t.Fatalf("Could not create HTTP request: %v", err)
+ }
+
+ rec = httptest.NewRecorder()
+
+ tmplroot.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
+ }
+
+ respBody = rec.Body.String()
+ expectedBody = `root
Header title
+
+`
+
+ if respBody != expectedBody {
+ t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
+ }
+}
diff --git a/caddyhttp/templates/testdata/header.html b/caddyhttp/templates/testdata/header.html
new file mode 100644
index 000000000..9c96e0e37
--- /dev/null
+++ b/caddyhttp/templates/testdata/header.html
@@ -0,0 +1 @@
+
Header title
diff --git a/caddyhttp/templates/testdata/images/header.html b/caddyhttp/templates/testdata/images/header.html
new file mode 100644
index 000000000..9c96e0e37
--- /dev/null
+++ b/caddyhttp/templates/testdata/images/header.html
@@ -0,0 +1 @@
+
Header title
diff --git a/caddyhttp/templates/testdata/images/img.htm b/caddyhttp/templates/testdata/images/img.htm
new file mode 100644
index 000000000..c90602044
--- /dev/null
+++ b/caddyhttp/templates/testdata/images/img.htm
@@ -0,0 +1 @@
+img{%.Include "header.html"%}
diff --git a/caddyhttp/templates/testdata/images/img2.htm b/caddyhttp/templates/testdata/images/img2.htm
new file mode 100644
index 000000000..865a73809
--- /dev/null
+++ b/caddyhttp/templates/testdata/images/img2.htm
@@ -0,0 +1 @@
+img{{.Include "header.html"}}
diff --git a/caddyhttp/templates/testdata/photos/test.html b/caddyhttp/templates/testdata/photos/test.html
new file mode 100644
index 000000000..e2e95e133
--- /dev/null
+++ b/caddyhttp/templates/testdata/photos/test.html
@@ -0,0 +1 @@
+test page{{.Include "../header.html"}}
diff --git a/caddyhttp/templates/testdata/root.html b/caddyhttp/templates/testdata/root.html
new file mode 100644
index 000000000..e1720e726
--- /dev/null
+++ b/caddyhttp/templates/testdata/root.html
@@ -0,0 +1 @@
+root{{.Include "header.html"}}
diff --git a/caddyhttp/websocket/setup.go b/caddyhttp/websocket/setup.go
new file mode 100644
index 000000000..fe930de1e
--- /dev/null
+++ b/caddyhttp/websocket/setup.go
@@ -0,0 +1,97 @@
+package websocket
+
+import (
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func init() {
+ caddy.RegisterPlugin(caddy.Plugin{
+ Name: "websocket",
+ ServerType: "http",
+ Action: setup,
+ })
+}
+
+// setup configures a new WebSocket middleware instance.
+func setup(c *caddy.Controller) error {
+ websocks, err := webSocketParse(c)
+ if err != nil {
+ return err
+ }
+
+ GatewayInterface = caddy.AppName + "-CGI/1.1"
+ ServerSoftware = caddy.AppName + "/" + caddy.AppVersion
+
+ httpserver.GetConfig(c.Key).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
+ return WebSocket{Next: next, Sockets: websocks}
+ })
+
+ return nil
+}
+
+func webSocketParse(c *caddy.Controller) ([]Config, error) {
+ var websocks []Config
+ var respawn bool
+
+ optionalBlock := func() (hadBlock bool, err error) {
+ for c.NextBlock() {
+ hadBlock = true
+ if c.Val() == "respawn" {
+ respawn = true
+ } else {
+ return true, c.Err("Expected websocket configuration parameter in block")
+ }
+ }
+ return
+ }
+
+ for c.Next() {
+ var val, path, command string
+
+ // Path or command; not sure which yet
+ if !c.NextArg() {
+ return nil, c.ArgErr()
+ }
+ val = c.Val()
+
+ // Extra configuration may be in a block
+ hadBlock, err := optionalBlock()
+ if err != nil {
+ return nil, err
+ }
+
+ if !hadBlock {
+ // The next argument on this line will be the command or an open curly brace
+ if c.NextArg() {
+ path = val
+ command = c.Val()
+ } else {
+ path = "/"
+ command = val
+ }
+
+ // Okay, check again for optional block
+ _, err = optionalBlock()
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Split command into the actual command and its arguments
+ cmd, args, err := caddy.SplitCommandAndArgs(command)
+ if err != nil {
+ return nil, err
+ }
+
+ websocks = append(websocks, Config{
+ Path: path,
+ Command: cmd,
+ Arguments: args,
+ Respawn: respawn, // TODO: This isn't used currently
+ })
+ }
+
+ return websocks, nil
+
+}
diff --git a/caddyhttp/websocket/setup_test.go b/caddyhttp/websocket/setup_test.go
new file mode 100644
index 000000000..f8b283304
--- /dev/null
+++ b/caddyhttp/websocket/setup_test.go
@@ -0,0 +1,102 @@
+package websocket
+
+import (
+ "testing"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+func TestWebSocket(t *testing.T) {
+ err := setup(caddy.NewTestController(`websocket cat`))
+ if err != nil {
+ t.Errorf("Expected no errors, got: %v", err)
+ }
+ mids := httpserver.GetConfig("").Middleware()
+ if len(mids) == 0 {
+ t.Fatal("Expected middleware, got 0 instead")
+ }
+
+ handler := mids[0](httpserver.EmptyNext)
+ myHandler, ok := handler.(WebSocket)
+
+ if !ok {
+ t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
+ }
+
+ if myHandler.Sockets[0].Path != "/" {
+ t.Errorf("Expected / as the default Path")
+ }
+ if myHandler.Sockets[0].Command != "cat" {
+ t.Errorf("Expected %s as the command", "cat")
+ }
+
+}
+func TestWebSocketParse(t *testing.T) {
+ tests := []struct {
+ inputWebSocketConfig string
+ shouldErr bool
+ expectedWebSocketConfig []Config
+ }{
+ {`websocket /api1 cat`, false, []Config{{
+ Path: "/api1",
+ Command: "cat",
+ }}},
+
+ {`websocket /api3 cat
+ websocket /api4 cat `, false, []Config{{
+ Path: "/api3",
+ Command: "cat",
+ }, {
+ Path: "/api4",
+ Command: "cat",
+ }}},
+
+ {`websocket /api5 "cmd arg1 arg2 arg3"`, false, []Config{{
+ Path: "/api5",
+ Command: "cmd",
+ Arguments: []string{"arg1", "arg2", "arg3"},
+ }}},
+
+ // accept respawn
+ {`websocket /api6 cat {
+ respawn
+ }`, false, []Config{{
+ Path: "/api6",
+ Command: "cat",
+ }}},
+
+ // invalid configuration
+ {`websocket /api7 cat {
+ invalid
+ }`, true, []Config{}},
+ }
+ for i, test := range tests {
+ c := caddy.NewTestController(test.inputWebSocketConfig)
+ actualWebSocketConfigs, err := webSocketParse(c)
+
+ if err == nil && test.shouldErr {
+ t.Errorf("Test %d didn't error, but it should have", i)
+ } else if err != nil && !test.shouldErr {
+ t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
+ }
+ if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) {
+ t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
+ i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs))
+ }
+ for j, actualWebSocketConfig := range actualWebSocketConfigs {
+
+ if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path {
+ t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s",
+ i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path)
+ }
+
+ if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command {
+ t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s",
+ i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command)
+ }
+
+ }
+ }
+
+}
diff --git a/caddyhttp/websocket/websocket.go b/caddyhttp/websocket/websocket.go
new file mode 100644
index 000000000..ca135f33f
--- /dev/null
+++ b/caddyhttp/websocket/websocket.go
@@ -0,0 +1,259 @@
+// Package websocket implements a WebSocket server by executing
+// a command and piping its input and output through the WebSocket
+// connection.
+package websocket
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/mholt/caddy/caddyhttp/httpserver"
+)
+
+const (
+ // Time allowed to write a message to the peer.
+ writeWait = 10 * time.Second
+
+ // Time allowed to read the next pong message from the peer.
+ pongWait = 60 * time.Second
+
+ // Send pings to peer with this period. Must be less than pongWait.
+ pingPeriod = (pongWait * 9) / 10
+
+ // Maximum message size allowed from peer.
+ maxMessageSize = 1024 * 1024 * 10 // 10 MB default.
+)
+
+var (
+ // GatewayInterface is the dialect of CGI being used by the server
+ // to communicate with the script. See CGI spec, 4.1.4
+ GatewayInterface string
+
+ // ServerSoftware is the name and version of the information server
+ // software making the CGI request. See CGI spec, 4.1.17
+ ServerSoftware string
+)
+
+type (
+ // WebSocket is a type that holds configuration for the
+ // websocket middleware generally, like a list of all the
+ // websocket endpoints.
+ WebSocket struct {
+ // Next is the next HTTP handler in the chain for when the path doesn't match
+ Next httpserver.Handler
+
+ // Sockets holds all the web socket endpoint configurations
+ Sockets []Config
+ }
+
+ // Config holds the configuration for a single websocket
+ // endpoint which may serve multiple websocket connections.
+ Config struct {
+ Path string
+ Command string
+ Arguments []string
+ Respawn bool // TODO: Not used, but parser supports it until we decide on it
+ }
+)
+
+// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
+func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+ for _, sockconfig := range ws.Sockets {
+ if httpserver.Path(r.URL.Path).Matches(sockconfig.Path) {
+ return serveWS(w, r, &sockconfig)
+ }
+ }
+
+ // Didn't match a websocket path, so pass-thru
+ return ws.Next.ServeHTTP(w, r)
+}
+
+// serveWS is used for setting and upgrading the HTTP connection to a websocket connection.
+// It also spawns the child process that is associated with matched HTTP path/url.
+func serveWS(w http.ResponseWriter, r *http.Request, config *Config) (int, error) {
+ upgrader := websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool { return true },
+ }
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return http.StatusBadRequest, err
+ }
+ defer conn.Close()
+
+ cmd := exec.Command(config.Command, config.Arguments...)
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return http.StatusBadGateway, err
+ }
+ defer stdout.Close()
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return http.StatusBadGateway, err
+ }
+ defer stdin.Close()
+
+ metavars, err := buildEnv(cmd.Path, r)
+ if err != nil {
+ return http.StatusBadGateway, err
+ }
+
+ cmd.Env = metavars
+
+ if err := cmd.Start(); err != nil {
+ return http.StatusBadGateway, err
+ }
+
+ done := make(chan struct{})
+ go pumpStdout(conn, stdout, done)
+ pumpStdin(conn, stdin)
+
+ stdin.Close() // close stdin to end the process
+
+ if err := cmd.Process.Signal(os.Interrupt); err != nil { // signal an interrupt to kill the process
+ return http.StatusInternalServerError, err
+ }
+
+ select {
+ case <-done:
+ case <-time.After(time.Second):
+ // terminate with extreme prejudice.
+ if err := cmd.Process.Signal(os.Kill); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ <-done
+ }
+
+ // not sure what we want to do here.
+ // status for an "exited" process is greater
+ // than 0, but isn't really an error per se.
+ // just going to ignore it for now.
+ cmd.Wait()
+
+ return 0, nil
+}
+
+// buildEnv creates the meta-variables for the child process according
+// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1
+// cmdPath should be the path of the command being run.
+// The returned string slice can be set to the command's Env property.
+func buildEnv(cmdPath string, r *http.Request) (metavars []string, err error) {
+ if !strings.Contains(r.RemoteAddr, ":") {
+ r.RemoteAddr += ":"
+ }
+ remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return
+ }
+
+ if !strings.Contains(r.Host, ":") {
+ r.Host += ":"
+ }
+ serverHost, serverPort, err := net.SplitHostPort(r.Host)
+ if err != nil {
+ return
+ }
+
+ metavars = []string{
+ `AUTH_TYPE=`, // Not used
+ `CONTENT_LENGTH=`, // Not used
+ `CONTENT_TYPE=`, // Not used
+ `GATEWAY_INTERFACE=` + GatewayInterface,
+ `PATH_INFO=`, // TODO
+ `PATH_TRANSLATED=`, // TODO
+ `QUERY_STRING=` + r.URL.RawQuery,
+ `REMOTE_ADDR=` + remoteHost,
+ `REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them
+ `REMOTE_IDENT=`, // Not used
+ `REMOTE_PORT=` + remotePort,
+ `REMOTE_USER=`, // Not used,
+ `REQUEST_METHOD=` + r.Method,
+ `REQUEST_URI=` + r.RequestURI,
+ `SCRIPT_NAME=` + cmdPath, // path of the program being executed
+ `SERVER_NAME=` + serverHost,
+ `SERVER_PORT=` + serverPort,
+ `SERVER_PROTOCOL=` + r.Proto,
+ `SERVER_SOFTWARE=` + ServerSoftware,
+ }
+
+ // Add each HTTP header to the environment as well
+ for header, values := range r.Header {
+ value := strings.Join(values, ", ")
+ header = strings.ToUpper(header)
+ header = strings.Replace(header, "-", "_", -1)
+ value = strings.Replace(value, "\n", " ", -1)
+ metavars = append(metavars, "HTTP_"+header+"="+value)
+ }
+
+ return
+}
+
+// pumpStdin handles reading data from the websocket connection and writing
+// it to stdin of the process.
+func pumpStdin(conn *websocket.Conn, stdin io.WriteCloser) {
+ // Setup our connection's websocket ping/pong handlers from our const values.
+ defer conn.Close()
+ conn.SetReadLimit(maxMessageSize)
+ conn.SetReadDeadline(time.Now().Add(pongWait))
+ conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
+ for {
+ _, message, err := conn.ReadMessage()
+ if err != nil {
+ break
+ }
+ message = append(message, '\n')
+ if _, err := stdin.Write(message); err != nil {
+ break
+ }
+ }
+}
+
+// pumpStdout handles reading data from stdout of the process and writing
+// it to websocket connection.
+func pumpStdout(conn *websocket.Conn, stdout io.Reader, done chan struct{}) {
+ go pinger(conn, done)
+ defer func() {
+ conn.Close()
+ close(done) // make sure to close the pinger when we are done.
+ }()
+
+ s := bufio.NewScanner(stdout)
+ for s.Scan() {
+ conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := conn.WriteMessage(websocket.TextMessage, bytes.TrimSpace(s.Bytes())); err != nil {
+ break
+ }
+ }
+ if s.Err() != nil {
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, s.Err().Error()), time.Time{})
+ }
+}
+
+// pinger simulates the websocket to keep it alive with ping messages.
+func pinger(conn *websocket.Conn, done chan struct{}) {
+ ticker := time.NewTicker(pingPeriod)
+ defer ticker.Stop()
+
+ for { // blocking loop with select to wait for stimulation.
+ select {
+ case <-ticker.C:
+ if err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
+ conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, err.Error()), time.Time{})
+ return
+ }
+ case <-done:
+ return // clean up this routine.
+ }
+ }
+}
diff --git a/caddyhttp/websocket/websocket_test.go b/caddyhttp/websocket/websocket_test.go
new file mode 100644
index 000000000..61d7d382b
--- /dev/null
+++ b/caddyhttp/websocket/websocket_test.go
@@ -0,0 +1,22 @@
+package websocket
+
+import (
+ "net/http"
+ "testing"
+)
+
+func TestBuildEnv(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://localhost", nil)
+ if err != nil {
+ t.Fatal("Error setting up request:", err)
+ }
+ req.RemoteAddr = "localhost:50302"
+
+ env, err := buildEnv("/bin/command", req)
+ if err != nil {
+ t.Fatal("Didn't expect an error:", err)
+ }
+ if len(env) == 0 {
+ t.Fatalf("Expected non-empty environment; got %#v", env)
+ }
+}