diff --git a/config/directives.go b/config/directives.go index fe9a9f92..9db5df44 100644 --- a/config/directives.go +++ b/config/directives.go @@ -59,6 +59,7 @@ var directiveOrder = []directive{ {"redir", setup.Redir}, {"ext", setup.Ext}, {"basicauth", setup.BasicAuth}, + {"internal", setup.Internal}, {"proxy", setup.Proxy}, {"fastcgi", setup.FastCGI}, {"websocket", setup.WebSocket}, diff --git a/config/parse/dispenser.go b/config/parse/dispenser.go index 13789f12..cf902a17 100644 --- a/config/parse/dispenser.go +++ b/config/parse/dispenser.go @@ -109,6 +109,10 @@ func (d *Dispenser) NextBlock() bool { return false } d.Next() + if d.Val() == "}" { + // Open and then closed right away + return false + } d.nesting++ return true } diff --git a/config/parse/dispenser_test.go b/config/parse/dispenser_test.go index bfcb30a3..20a7ddca 100644 --- a/config/parse/dispenser_test.go +++ b/config/parse/dispenser_test.go @@ -149,9 +149,8 @@ func TestDispenser_NextBlock(t *testing.T) { assertNextBlock(true, 3, 1) assertNextBlock(true, 4, 1) assertNextBlock(false, 5, 0) - d.Next() // foobar2 - assertNextBlock(true, 8, 1) - assertNextBlock(false, 8, 0) + d.Next() // foobar2 + assertNextBlock(false, 8, 0) // empty block is as if it didn't exist } func TestDispenser_Args(t *testing.T) { diff --git a/config/setup/basicauth_test.go b/config/setup/basicauth_test.go new file mode 100644 index 00000000..0f9d044e --- /dev/null +++ b/config/setup/basicauth_test.go @@ -0,0 +1,100 @@ +package setup + +import ( + "fmt" + "testing" + + "github.com/mholt/caddy/middleware/basicauth" +) + +func TestBasicAuth(t *testing.T) { + c := newTestController(`basicauth user pwd`) + + mid, err := BasicAuth(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.(basicauth.BasicAuth) + if !ok { + t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler) + } + + if !sameNext(myHandler.Next, emptyNext) { + t.Error("'Next' field of handler was not set properly") + } +} + +func TestBasicAuthParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expected []basicauth.Rule + }{ + {`basicauth user pwd`, false, []basicauth.Rule{ + {Username: "user", Password: "pwd"}, + }}, + {`basicauth user pwd { + }`, false, []basicauth.Rule{ + {Username: "user", Password: "pwd"}, + }}, + {`basicauth user pwd { + /resource1 + /resource2 + }`, false, []basicauth.Rule{ + {Username: "user", Password: "pwd", Resources: []string{"/resource1", "/resource2"}}, + }}, + {`basicauth /resource user pwd`, false, []basicauth.Rule{ + {Username: "user", Password: "pwd", Resources: []string{"/resource"}}, + }}, + {`basicauth /res1 user1 pwd1 + basicauth /res2 user2 pwd2`, false, []basicauth.Rule{ + {Username: "user1", Password: "pwd1", Resources: []string{"/res1"}}, + {Username: "user2", Password: "pwd2", Resources: []string{"/res2"}}, + }}, + {`basicauth user`, true, []basicauth.Rule{}}, + {`basicauth`, true, []basicauth.Rule{}}, + {`basicauth /resource user pwd asdf`, true, []basicauth.Rule{}}, + } + + for i, test := range tests { + c := newTestController(test.input) + actual, err := basicAuthParse(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(actual) != len(test.expected) { + t.Fatalf("Test %d expected %d rules, but got %d", + i, len(test.expected), len(actual)) + } + + for j, expectedRule := range test.expected { + actualRule := actual[j] + + if actualRule.Username != expectedRule.Username { + t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'", + i, j, expectedRule.Username, actualRule.Username) + } + + if actualRule.Password != expectedRule.Password { + t.Errorf("Test %d, rule %d: Expected password '%s', got '%s'", + i, j, expectedRule.Password, actualRule.Password) + } + + expectedRes := fmt.Sprintf("%v", expectedRule.Resources) + actualRes := fmt.Sprintf("%v", actualRule.Resources) + if actualRes != expectedRes { + t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s", + i, j, expectedRes, actualRes) + } + } + } +} diff --git a/config/setup/controller_test.go b/config/setup/controller_test.go index 7d0b0ec5..225d031e 100644 --- a/config/setup/controller_test.go +++ b/config/setup/controller_test.go @@ -1,9 +1,12 @@ package setup import ( + "fmt" + "net/http" "strings" "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/server" ) @@ -15,3 +18,15 @@ func newTestController(input string) *Controller { Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), } } + +// emptyNext is a no-op function that can be passed into +// middleware.Middleware functions so that the assignment +// to the Next field of the Handler can be tested. +var emptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + return 0, nil +}) + +// sameNext does a pointer comparison between next1 and next2. +func sameNext(next1, next2 middleware.Handler) bool { + return fmt.Sprintf("%p", next1) == fmt.Sprintf("%p", next2) +} diff --git a/config/setup/gzip_test.go b/config/setup/gzip_test.go new file mode 100644 index 00000000..d9978184 --- /dev/null +++ b/config/setup/gzip_test.go @@ -0,0 +1,29 @@ +package setup + +import ( + "testing" + + "github.com/mholt/caddy/middleware/gzip" +) + +func TestGzip(t *testing.T) { + c := newTestController(`gzip`) + + mid, err := Gzip(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.(gzip.Gzip) + if !ok { + t.Fatalf("Expected handler to be type Gzip, got: %#v", handler) + } + + if !sameNext(myHandler.Next, emptyNext) { + t.Error("'Next' field of handler was not set properly") + } +} diff --git a/config/setup/internal.go b/config/setup/internal.go new file mode 100644 index 00000000..21ab71b6 --- /dev/null +++ b/config/setup/internal.go @@ -0,0 +1,31 @@ +package setup + +import ( + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/internal" +) + +// Internal configures a new Internal middleware instance. +func Internal(c *Controller) (middleware.Middleware, error) { + paths, err := internalParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return internal.Internal{Next: next, Paths: paths} + }, nil +} + +func internalParse(c *Controller) ([]string, error) { + var paths []string + + for c.Next() { + if !c.NextArg() { + return paths, c.ArgErr() + } + paths = append(paths, c.Val()) + } + + return paths, nil +} diff --git a/config/setup/rewrite_test.go b/config/setup/rewrite_test.go new file mode 100644 index 00000000..17a0e97b --- /dev/null +++ b/config/setup/rewrite_test.go @@ -0,0 +1,85 @@ +package setup + +import ( + "testing" + + "github.com/mholt/caddy/middleware/rewrite" +) + +func TestRewrite(t *testing.T) { + c := newTestController(`rewrite /from /to`) + + mid, err := Rewrite(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.(rewrite.Rewrite) + if !ok { + t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler) + } + + if !sameNext(myHandler.Next, emptyNext) { + t.Error("'Next' field of handler was not set properly") + } + + if len(myHandler.Rules) != 1 { + t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules)) + } +} + +func TestRewriteParse(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expected []rewrite.Rule + }{ + {`rewrite /from /to`, false, []rewrite.Rule{ + {From: "/from", To: "/to"}, + }}, + {`rewrite /from /to + rewrite a b`, false, []rewrite.Rule{ + {From: "/from", To: "/to"}, + {From: "a", To: "b"}, + }}, + {`rewrite a`, true, []rewrite.Rule{}}, + {`rewrite`, true, []rewrite.Rule{}}, + {`rewrite a b c`, true, []rewrite.Rule{ + {From: "a", To: "b"}, + }}, + } + + for i, test := range tests { + c := newTestController(test.input) + actual, err := rewriteParse(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(actual) != len(test.expected) { + t.Fatalf("Test %d expected %d rules, but got %d", + i, len(test.expected), len(actual)) + } + + for j, expectedRule := range test.expected { + actualRule := actual[j] + + if actualRule.From != expectedRule.From { + t.Errorf("Test %d, rule %d: Expected From=%s, got %s", + i, j, expectedRule.From, actualRule.From) + } + + if actualRule.To != expectedRule.To { + t.Errorf("Test %d, rule %d: Expected To=%s, got %s", + i, j, expectedRule.To, actualRule.To) + } + } + } +} diff --git a/main.go b/main.go index f9dbb489..82509461 100644 --- a/main.go +++ b/main.go @@ -20,10 +20,11 @@ import ( ) var ( - conf string - http2 bool // TODO: temporary flag until http2 is standard - quiet bool - cpu string + conf string + http2 bool // TODO: temporary flag until http2 is standard + quiet bool + cpu string + version bool ) func init() { @@ -34,6 +35,7 @@ func init() { flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site") flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host") flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port") + flag.BoolVar(&version, "version", false, "Show version") config.AppName = "Caddy" config.AppVersion = "0.6.0" @@ -42,6 +44,11 @@ func init() { func main() { flag.Parse() + if version { + fmt.Printf("%s %s\n", config.AppName, config.AppVersion) + os.Exit(0) + } + var wg sync.WaitGroup // Set CPU cap diff --git a/middleware/internal/internal.go b/middleware/internal/internal.go new file mode 100644 index 00000000..90746b06 --- /dev/null +++ b/middleware/internal/internal.go @@ -0,0 +1,91 @@ +// The package internal provides a simple middleware that (a) prevents access +// to internal locations and (b) allows to return files from internal location +// by setting a special header, e.g. in a proxy response. +package internal + +import ( + "net/http" + + "github.com/mholt/caddy/middleware" +) + +// Internal middleware protects internal locations from external requests - +// but allows access from the inside by using a special HTTP header. +type Internal struct { + Next middleware.Handler + Paths []string +} + +const ( + redirectHeader string = "X-Accel-Redirect" + maxRedirectCount int = 10 +) + +func isInternalRedirect(w http.ResponseWriter) bool { + return w.Header().Get(redirectHeader) != "" +} + +// ServeHTTP implements the middlware.Handler interface. +func (i Internal) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + + // Internal location requested? -> Not found. + for _, prefix := range i.Paths { + if middleware.Path(r.URL.Path).Matches(prefix) { + return http.StatusNotFound, nil + } + } + + // Use internal response writer to ignore responses that will be + // redirected to internal locations + iw := internalResponseWriter{ResponseWriter: w} + status, err := i.Next.ServeHTTP(iw, r) + + for c := 0; c < maxRedirectCount && isInternalRedirect(iw); c++ { + // Redirect - adapt request URL path and send it again + // "down the chain" + r.URL.Path = iw.Header().Get(redirectHeader) + iw.ClearHeader() + + status, err = i.Next.ServeHTTP(iw, r) + } + + if isInternalRedirect(iw) { + // Too many redirect cycles + iw.ClearHeader() + return http.StatusInternalServerError, nil + } + + return status, err +} + +// internalResponseWriter wraps the underlying http.ResponseWriter and ignores +// calls to Write and WriteHeader if the response should be redirected to an +// internal location. +type internalResponseWriter struct { + http.ResponseWriter +} + +// ClearHeader removes all header fields that are already set. +func (w internalResponseWriter) ClearHeader() { + for k := range w.Header() { + w.Header().Del(k) + } +} + +// WriteHeader ignores the call if the response should be redirected to an +// internal location. +func (w internalResponseWriter) WriteHeader(code int) { + if !isInternalRedirect(w) { + w.ResponseWriter.WriteHeader(code) + } +} + +// Write ignores the call if the response should be redirected to an internal +// location. +func (w internalResponseWriter) Write(b []byte) (int, error) { + if isInternalRedirect(w) { + return 0, nil + } else { + return w.ResponseWriter.Write(b) + } +} diff --git a/middleware/rewrite/rewrite_test.go b/middleware/rewrite/rewrite_test.go new file mode 100644 index 00000000..e9793ac1 --- /dev/null +++ b/middleware/rewrite/rewrite_test.go @@ -0,0 +1,53 @@ +package rewrite + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mholt/caddy/middleware" +) + +func TestRewrite(t *testing.T) { + rw := Rewrite{ + Next: middleware.HandlerFunc(urlPrinter), + Rules: []Rule{ + {From: "/from", To: "/to"}, + {From: "/a", To: "/b"}, + }, + } + tests := []struct { + from string + expectedTo string + }{ + {"/from", "/to"}, + {"/a", "/b"}, + {"/aa", "/aa"}, + {"/", "/"}, + {"/a?foo=bar", "/b?foo=bar"}, + {"/asdf?foo=bar", "/asdf?foo=bar"}, + {"/foo#bar", "/foo#bar"}, + {"/a#foo", "/b#foo"}, + } + + for i, test := range tests { + req, err := http.NewRequest("GET", test.from, nil) + if err != nil { + t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) + } + + rec := httptest.NewRecorder() + rw.ServeHTTP(rec, req) + + if rec.Body.String() != test.expectedTo { + t.Errorf("Test %d: Expected URL to be '%s' but was '%s'", + i, test.expectedTo, rec.Body.String()) + } + } +} + +func urlPrinter(w http.ResponseWriter, r *http.Request) (int, error) { + fmt.Fprintf(w, r.URL.String()) + return 0, nil +}