From bb5a322ce27a95965c081c490db42568c7555d7e Mon Sep 17 00:00:00 2001 From: Maxime Date: Sat, 8 Aug 2015 16:13:10 +0200 Subject: [PATCH 01/11] Added lumberjack library for log rolling --- config/setup/errors.go | 8 +++++++- config/setup/log.go | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/config/setup/errors.go b/config/setup/errors.go index 58f22dfa5..9221ee92c 100644 --- a/config/setup/errors.go +++ b/config/setup/errors.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/go-syslog" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/errors" + "gopkg.in/natefinch/lumberjack.v2" ) // Errors configures a new gzip middleware instance. @@ -35,10 +36,15 @@ func Errors(c *Controller) (middleware.Middleware, error) { return err } } else if handler.LogFile != "" { - file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + _, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return err } + file = &lumberjack.Logger{ + Filename: handler.LogFile, + MaxSize: 20, + MaxBackups: 10, + } } handler.Log = log.New(file, "", 0) diff --git a/config/setup/log.go b/config/setup/log.go index 0c15c3d0f..6bf9f3120 100644 --- a/config/setup/log.go +++ b/config/setup/log.go @@ -9,6 +9,7 @@ import ( "github.com/mholt/caddy/middleware" caddylog "github.com/mholt/caddy/middleware/log" "github.com/mholt/caddy/server" + "gopkg.in/natefinch/lumberjack.v2" ) // Log sets up the logging middleware. @@ -34,10 +35,15 @@ func Log(c *Controller) (middleware.Middleware, error) { return err } } else { - file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + _, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return err } + file = &lumberjack.Logger{ + Filename: rules[i].OutputFile, + MaxSize: 20, + MaxBackups: 10, + } } rules[i].Log = log.New(file, "", 0) From e3cea042d63a7f4eb8fb7151c8634b812eea917a Mon Sep 17 00:00:00 2001 From: karthic rao Date: Sun, 30 Aug 2015 19:00:35 +0530 Subject: [PATCH 02/11] Left over comments removed Redundant comments in the code removed --- middleware/browse/browse_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/middleware/browse/browse_test.go b/middleware/browse/browse_test.go index 13894a8dc..9f91f34ef 100644 --- a/middleware/browse/browse_test.go +++ b/middleware/browse/browse_test.go @@ -1,8 +1,7 @@ package browse import ( - //"bytes" - "encoding/json" + "encoding/json" "github.com/mholt/caddy/middleware" "net/http" "net/http/httptest" @@ -191,7 +190,7 @@ func TestBrowseJson(t *testing.T) { } actualJsonResponseString := rec.Body.String() - //t.Logf("json response body: %s\n", respBody) + //generating the listing to compare it with the response body file, err := os.Open(b.Root + req.URL.Path) if err != nil { @@ -233,12 +232,10 @@ func TestBrowseJson(t *testing.T) { listing.Order = "asc" listing.applySort() - //var buf bytes.Buffer marsh, err := json.Marshal(listing.Items) if err != nil { t.Fatalf("Unable to Marshal the listing ") } - //t.Logf("json value: %s\n", string(marsh)) expectedJsonString := string(marsh) if actualJsonResponseString != expectedJsonString { t.Errorf("Json response string doesnt match the expected Json response ") From d79d2611ca592068e5fcc85eecbca5c1a3a1fbae Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 30 Aug 2015 10:55:30 -0600 Subject: [PATCH 03/11] Mention setcap in readme so it's more prominent --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 766453899..44fd023b4 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ By default, Caddy serves the current directory at [localhost:2015](http://localh Caddy accepts some flags from the command line. Run `caddy -h` to view the help for flags. You can also pipe a Caddyfile into the caddy command. +**Running as root:** We advise against this; use setcap instead, like so: `setcap cap_net_bind_service=+ep ./caddy` This will allow you to listen on ports below 1024 (like 80 and 443). #### Docker Container From 392f1d70eb29ebfc2d8b155a3707c81f2fcee529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Gul=C3=A1csi?= Date: Sun, 30 Aug 2015 20:07:43 +0200 Subject: [PATCH 04/11] Add htpasswd support for basic auth If the password arg starts with htpasswd=, then the rest is treated as the file name of the htpasswd file, and used for md5 and sha1 hashes. --- config/setup/basicauth.go | 20 ++++++- config/setup/basicauth_test.go | 64 +++++++++++++++----- middleware/basicauth/basicauth.go | 81 +++++++++++++++++++++++++- middleware/basicauth/basicauth_test.go | 36 +++++++++++- 4 files changed, 178 insertions(+), 23 deletions(-) diff --git a/config/setup/basicauth.go b/config/setup/basicauth.go index 6d1ece108..a59e13497 100644 --- a/config/setup/basicauth.go +++ b/config/setup/basicauth.go @@ -1,6 +1,8 @@ package setup import ( + "strings" + "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/basicauth" ) @@ -23,6 +25,7 @@ func BasicAuth(c *Controller) (middleware.Middleware, error) { func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { var rules []basicauth.Rule + var err error for c.Next() { var rule basicauth.Rule @@ -31,7 +34,10 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { switch len(args) { case 2: rule.Username = args[0] - rule.Password = args[1] + if rule.Password, err = passwordMatcher(rule.Username, args[1]); err != nil { + return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) + } + for c.NextBlock() { rule.Resources = append(rule.Resources, c.Val()) if c.NextArg() { @@ -41,7 +47,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { case 3: rule.Resources = append(rule.Resources, args[0]) rule.Username = args[1] - rule.Password = args[2] + if rule.Password, err = passwordMatcher(rule.Username, args[2]); err != nil { + return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) + } default: return rules, c.ArgErr() } @@ -51,3 +59,11 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { return rules, nil } + +func passwordMatcher(username, passw string) (basicauth.PasswordMatcher, error) { + if !strings.HasPrefix(passw, "htpasswd=") { + return basicauth.PlainMatcher(passw), nil + } + + return basicauth.GetHtpasswdMatcher(passw[9:], username) +} diff --git a/config/setup/basicauth_test.go b/config/setup/basicauth_test.go index f5cb80672..7b0ba3d96 100644 --- a/config/setup/basicauth_test.go +++ b/config/setup/basicauth_test.go @@ -2,6 +2,9 @@ package setup import ( "fmt" + "io/ioutil" + "os" + "strings" "testing" "github.com/mholt/caddy/middleware/basicauth" @@ -30,35 +33,57 @@ func TestBasicAuth(t *testing.T) { } func TestBasicAuthParse(t *testing.T) { + htpasswdPasswd := "IedFOuGmTpT8" + htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww= +md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` + + var skipHtpassword bool + htfh, err := ioutil.TempFile("", "basicauth-") + if err != nil { + t.Logf("Error creating temp file (%v), will skip htpassword test") + skipHtpassword = true + } else { + if _, err = htfh.Write([]byte(htpasswdFile)); err != nil { + t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err) + } + htfh.Close() + defer os.Remove(htfh.Name()) + } + tests := []struct { input string shouldErr bool + password string expected []basicauth.Rule }{ - {`basicauth user pwd`, false, []basicauth.Rule{ - {Username: "user", Password: "pwd"}, + {`basicauth user pwd`, false, "pwd", []basicauth.Rule{ + {Username: "user"}, }}, {`basicauth user pwd { - }`, false, []basicauth.Rule{ - {Username: "user", Password: "pwd"}, + }`, false, "pwd", []basicauth.Rule{ + {Username: "user"}, }}, {`basicauth user pwd { /resource1 /resource2 - }`, false, []basicauth.Rule{ - {Username: "user", Password: "pwd", Resources: []string{"/resource1", "/resource2"}}, + }`, false, "pwd", []basicauth.Rule{ + {Username: "user", Resources: []string{"/resource1", "/resource2"}}, }}, - {`basicauth /resource user pwd`, false, []basicauth.Rule{ - {Username: "user", Password: "pwd", Resources: []string{"/resource"}}, + {`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{ + {Username: "user", 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 /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{ + {Username: "user1", Resources: []string{"/res1"}}, + {Username: "user2", Resources: []string{"/res2"}}, + }}, + {`basicauth user`, true, "", []basicauth.Rule{}}, + {`basicauth`, true, "", []basicauth.Rule{}}, + {`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}}, + + {`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{ + {Username: "sha1"}, }}, - {`basicauth user`, true, []basicauth.Rule{}}, - {`basicauth`, true, []basicauth.Rule{}}, - {`basicauth /resource user pwd asdf`, true, []basicauth.Rule{}}, } for i, test := range tests { @@ -84,9 +109,16 @@ func TestBasicAuthParse(t *testing.T) { i, j, expectedRule.Username, actualRule.Username) } - if actualRule.Password != expectedRule.Password { + if strings.Contains(test.input, "htpasswd=") && skipHtpassword { + continue + } + pwd := test.password + if len(actual) > 1 { + pwd = fmt.Sprintf("%s%d", pwd, j+1) + } + if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") { t.Errorf("Test %d, rule %d: Expected password '%s', got '%s'", - i, j, expectedRule.Password, actualRule.Password) + i, j, test.password, actualRule.Password) } expectedRes := fmt.Sprintf("%v", expectedRule.Resources) diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 221446a2f..02ad36c55 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -2,9 +2,17 @@ package basicauth import ( + "bufio" "crypto/subtle" + "fmt" + "io" "net/http" + "os" + "path/filepath" + "strings" + "sync" + "github.com/jimstudt/http-authentication/basic" "github.com/mholt/caddy/middleware" ) @@ -37,7 +45,8 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // Check credentials if !ok || username != rule.Username || - subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 { + !rule.Password(password) { + //subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 { continue } @@ -64,6 +73,74 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // file or directory paths. type Rule struct { Username string - Password string + Password func(string) bool Resources []string } + +type PasswordMatcher func(pw string) bool + +var ( + htpasswords map[string]map[string]PasswordMatcher + htpasswordsMu sync.Mutex +) + +func GetHtpasswdMatcher(filename, username string) (PasswordMatcher, error) { + filename, err := filepath.Abs(filename) + if err != nil { + return nil, err + } + htpasswordsMu.Lock() + if htpasswords == nil { + htpasswords = make(map[string]map[string]PasswordMatcher) + } + pm := htpasswords[filename] + if pm == nil { + fh, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("open %q: %v", filename, err) + } + defer fh.Close() + pm = make(map[string]PasswordMatcher) + if err = parseHtpasswd(pm, fh); err != nil { + return nil, fmt.Errorf("parsing htpasswd %q: %v", fh.Name(), err) + } + htpasswords[filename] = pm + } + htpasswordsMu.Unlock() + if pm[username] == nil { + return nil, fmt.Errorf("username %q not found in %q", username, filename) + } + return pm[username], nil +} + +func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.IndexByte(line, '#') == 0 { + continue + } + i := strings.IndexByte(line, ':') + if i <= 0 { + return fmt.Errorf("malformed line, no color: %q", line) + } + user, encoded := line[:i], line[i+1:] + for _, p := range basic.DefaultSystems { + matcher, err := p(encoded) + if err != nil { + return err + } + if matcher != nil { + pm[user] = matcher.MatchesPassword + break + } + } + } + return scanner.Err() +} + +func PlainMatcher(passw string) PasswordMatcher { + return func(pw string) bool { + return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1 + } +} diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index 51f944bd6..393f2e4ed 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -3,8 +3,10 @@ package basicauth import ( "encoding/base64" "fmt" + "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" "github.com/mholt/caddy/middleware" @@ -15,7 +17,7 @@ func TestBasicAuth(t *testing.T) { rw := BasicAuth{ Next: middleware.HandlerFunc(contentHandler), Rules: []Rule{ - {Username: "test", Password: "ttest", Resources: []string{"/testing"}}, + {Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}}, }, } @@ -66,8 +68,8 @@ func TestMultipleOverlappingRules(t *testing.T) { rw := BasicAuth{ Next: middleware.HandlerFunc(contentHandler), Rules: []Rule{ - {Username: "t", Password: "p1", Resources: []string{"/t"}}, - {Username: "t1", Password: "p2", Resources: []string{"/t/t"}}, + {Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}}, + {Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}}, }, } @@ -111,3 +113,31 @@ func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) { fmt.Fprintf(w, r.URL.String()) return http.StatusOK, nil } + +func TestHtpasswd(t *testing.T) { + htpasswdPasswd := "IedFOuGmTpT8" + htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww= +md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` + + htfh, err := ioutil.TempFile("", "basicauth-") + if err != nil { + t.Skipf("Error creating temp file (%v), will skip htpassword test") + return + } + if _, err = htfh.Write([]byte(htpasswdFile)); err != nil { + t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err) + } + htfh.Close() + defer os.Remove(htfh.Name()) + + for i, username := range []string{"sha1", "md5"} { + rule := Rule{Username: username, Resources: []string{"/testing"}} + if rule.Password, err = GetHtpasswdMatcher(htfh.Name(), rule.Username); err != nil { + t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err) + } + t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password) + if !rule.Password(htpasswdPasswd) || rule.Password(htpasswdPasswd+"!") { + t.Errorf("%d (%s) password does not match.", i, rule.Username) + } + } +} From 008160998abd17e4d56fd1f4bb26581e3744c020 Mon Sep 17 00:00:00 2001 From: Maxime Date: Wed, 2 Sep 2015 15:13:31 +0200 Subject: [PATCH 05/11] Added LogRoller parser and entity. The errors and logs can now have log rolling if provided by the user. The current customisable parameter of it are: The maximal size of the file before rolling. The maximal age/time of the file before rolling. The number of backups to keep. --- config/parse/dispenser.go | 5 ++ config/setup/errors.go | 40 ++++++++--- config/setup/errors_test.go | 137 ++++++++++++++++++++++++++++++++++++ config/setup/log.go | 47 ++++++++++--- config/setup/log_test.go | 42 ++++++++++- config/setup/roller.go | 39 ++++++++++ middleware/errors/errors.go | 1 + middleware/log/log.go | 1 + middleware/roller.go | 25 +++++++ 9 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 config/setup/errors_test.go create mode 100644 config/setup/roller.go create mode 100644 middleware/roller.go diff --git a/config/parse/dispenser.go b/config/parse/dispenser.go index e8a0c8477..a7457f561 100644 --- a/config/parse/dispenser.go +++ b/config/parse/dispenser.go @@ -119,6 +119,11 @@ func (d *Dispenser) NextBlock() bool { return true } +func (d *Dispenser) IncrNest() { + d.nesting++ + return +} + // Val gets the text of the current token. If there is no token // loaded, it returns empty string. func (d *Dispenser) Val() string { diff --git a/config/setup/errors.go b/config/setup/errors.go index 9221ee92c..ae42f381b 100644 --- a/config/setup/errors.go +++ b/config/setup/errors.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/go-syslog" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/errors" - "gopkg.in/natefinch/lumberjack.v2" ) // Errors configures a new gzip middleware instance. @@ -24,30 +23,35 @@ func Errors(c *Controller) (middleware.Middleware, error) { // Open the log file for writing when the server starts c.Startup = append(c.Startup, func() error { var err error - var file io.Writer + var writer io.Writer if handler.LogFile == "stdout" { - file = os.Stdout + writer = os.Stdout } else if handler.LogFile == "stderr" { - file = os.Stderr + writer = os.Stderr } else if handler.LogFile == "syslog" { - file, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy") + writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy") if err != nil { return err } } else if handler.LogFile != "" { - _, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + var file *os.File + file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return err } - file = &lumberjack.Logger{ - Filename: handler.LogFile, - MaxSize: 20, - MaxBackups: 10, + if handler.LogRoller != nil { + file.Close() + + handler.LogRoller.Filename = handler.LogFile + + writer = handler.LogRoller.GetLogWriter() + } else { + writer = file } } - handler.Log = log.New(file, "", 0) + handler.Log = log.New(writer, "", 0) return nil }) @@ -77,6 +81,16 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { if what == "log" { handler.LogFile = where + if c.NextArg() { + if c.Val() == "{" { + c.IncrNest() + logRoller, err := parseRoller(c) + if err != nil { + return hadBlock, err + } + handler.LogRoller = logRoller + } + } } else { // Error page; ensure it exists where = path.Join(c.Root, where) @@ -97,6 +111,10 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { } for c.Next() { + // weird hack to avoid having the handler values overwritten. + if c.Val() == "}" { + continue + } // Configuration may be in a block hadBlock, err := optionalBlock() if err != nil { diff --git a/config/setup/errors_test.go b/config/setup/errors_test.go new file mode 100644 index 000000000..5ece288d0 --- /dev/null +++ b/config/setup/errors_test.go @@ -0,0 +1,137 @@ +package setup + +import ( + "testing" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/errors" +) + +func TestErrors(t *testing.T) { + + c := NewTestController(`errors`) + + mid, err := Errors(c) + + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } + + if mid == nil { + t.Fatal("Expected middleware, was nil instead") + } + + handler := mid(EmptyNext) + myHandler, ok := handler.(*errors.ErrorHandler) + + if !ok { + t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler) + } + + if myHandler.LogFile != errors.DefaultLogFilename { + t.Errorf("Expected %s as the default LogFile", errors.DefaultLogFilename) + } + if myHandler.LogRoller != nil { + t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller) + } + if !SameNext(myHandler.Next, EmptyNext) { + t.Error("'Next' field of handler was not set properly") + } +} + +func TestErrorsParse(t *testing.T) { + tests := []struct { + inputErrorsRules string + shouldErr bool + expectedErrorHandler errors.ErrorHandler + }{ + {`errors`, false, errors.ErrorHandler{ + LogFile: errors.DefaultLogFilename, + }}, + {`errors errors.txt`, false, errors.ErrorHandler{ + LogFile: "errors.txt", + }}, + {`errors { log errors.txt + 404 404.html + 500 500.html +}`, false, errors.ErrorHandler{ + LogFile: "errors.txt", + ErrorPages: map[int]string{ + 404: "404.html", + 500: "500.html", + }, + }}, + {`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{ + LogFile: "errors.txt", + LogRoller: &middleware.LogRoller{ + MaxSize: 2, + MaxAge: 10, + MaxBackups: 3, + }, + }}, + {`errors { log errors.txt { + size 3 + age 11 + keep 5 + } + 404 404.html + 503 503.html +}`, false, errors.ErrorHandler{ + LogFile: "errors.txt", + ErrorPages: map[int]string{ + 404: "404.html", + 503: "503.html", + }, + LogRoller: &middleware.LogRoller{ + MaxSize: 3, + MaxAge: 11, + MaxBackups: 5, + }, + }}, + } + for i, test := range tests { + c := NewTestController(test.inputErrorsRules) + actualErrorsRule, err := errorsParse(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 actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile { + t.Errorf("Test %d expected LogFile to be %s , but got %s", + i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile) + } + if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil { + t.Fatalf("Test %d expected LogRoller to be %v, but got %v", + i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller) + } + if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) { + t.Fatalf("Test %d expected %d no of Error pages, but got %d ", + i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages)) + } + if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil { + if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename { + t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s", + i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename) + } + if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge { + t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d", + i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge) + } + if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups { + t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d", + i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups) + } + if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize { + t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d", + i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize) + } + if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime { + t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t", + i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime) + } + } + } + +} diff --git a/config/setup/log.go b/config/setup/log.go index 6bf9f3120..8bb4788a1 100644 --- a/config/setup/log.go +++ b/config/setup/log.go @@ -9,7 +9,6 @@ import ( "github.com/mholt/caddy/middleware" caddylog "github.com/mholt/caddy/middleware/log" "github.com/mholt/caddy/server" - "gopkg.in/natefinch/lumberjack.v2" ) // Log sets up the logging middleware. @@ -23,30 +22,33 @@ func Log(c *Controller) (middleware.Middleware, error) { c.Startup = append(c.Startup, func() error { for i := 0; i < len(rules); i++ { var err error - var file io.Writer + var writer io.Writer if rules[i].OutputFile == "stdout" { - file = os.Stdout + writer = os.Stdout } else if rules[i].OutputFile == "stderr" { - file = os.Stderr + writer = os.Stderr } else if rules[i].OutputFile == "syslog" { - file, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy") + writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy") if err != nil { return err } } else { - _, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + var file *os.File + file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return err } - file = &lumberjack.Logger{ - Filename: rules[i].OutputFile, - MaxSize: 20, - MaxBackups: 10, + if rules[i].Roller != nil { + file.Close() + rules[i].Roller.Filename = rules[i].OutputFile + writer = rules[i].Roller.GetLogWriter() + } else { + writer = file } } - rules[i].Log = log.New(file, "", 0) + rules[i].Log = log.New(writer, "", 0) } return nil @@ -63,12 +65,33 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { for c.Next() { args := c.RemainingArgs() + var logRoller *middleware.LogRoller + if c.NextBlock() { + if c.Val() == "rotate" { + if c.NextArg() { + if c.Val() == "{" { + var err error + logRoller, err = parseRoller(c) + if err != nil { + return nil, err + } + // This part doesn't allow having something after the rotate block + if c.Next() { + if c.Val() != "}" { + return nil, c.ArgErr() + } + } + } + } + } + } if len(args) == 0 { // Nothing specified; use defaults rules = append(rules, caddylog.Rule{ PathScope: "/", OutputFile: caddylog.DefaultLogFilename, Format: caddylog.DefaultLogFormat, + Roller: logRoller, }) } else if len(args) == 1 { // Only an output file specified @@ -76,6 +99,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { PathScope: "/", OutputFile: args[0], Format: caddylog.DefaultLogFormat, + Roller: logRoller, }) } else { // Path scope, output file, and maybe a format specified @@ -97,6 +121,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) { PathScope: args[0], OutputFile: args[1], Format: format, + Roller: logRoller, }) } } diff --git a/config/setup/log_test.go b/config/setup/log_test.go index 6f8e1cce5..a9693a699 100644 --- a/config/setup/log_test.go +++ b/config/setup/log_test.go @@ -3,6 +3,7 @@ package setup import ( "testing" + "github.com/mholt/caddy/middleware" caddylog "github.com/mholt/caddy/middleware/log" ) @@ -36,6 +37,9 @@ func TestLog(t *testing.T) { if myHandler.Rules[0].Format != caddylog.DefaultLogFormat { t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat) } + if myHandler.Rules[0].Roller != nil { + t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller) + } if !SameNext(myHandler.Next, EmptyNext) { t.Error("'Next' field of handler was not set properly") } @@ -78,7 +82,7 @@ func TestLogParse(t *testing.T) { OutputFile: "accesslog.txt", Format: caddylog.CombinedLogFormat, }}}, - {`log /api1 log.txt + {`log /api1 log.txt log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{ PathScope: "/api1", OutputFile: "log.txt", @@ -98,6 +102,16 @@ func TestLogParse(t *testing.T) { OutputFile: "log.txt", Format: "{when}", }}}, + {`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{ + PathScope: "/", + OutputFile: "access.log", + Format: caddylog.DefaultLogFormat, + Roller: &middleware.LogRoller{ + MaxSize: 2, + MaxAge: 10, + MaxBackups: 3, + }, + }}}, } for i, test := range tests { c := NewTestController(test.inputLogRules) @@ -128,6 +142,32 @@ func TestLogParse(t *testing.T) { t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s", i, j, test.expectedLogRules[j].Format, actualLogRule.Format) } + if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil { + t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v", + i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller) + } + if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil { + if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename { + t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s", + i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename) + } + if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge { + t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d", + i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge) + } + if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups { + t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d", + i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups) + } + if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize { + t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d", + i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize) + } + if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime { + t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t", + i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime) + } + } } } diff --git a/config/setup/roller.go b/config/setup/roller.go new file mode 100644 index 000000000..3a5b8a8d7 --- /dev/null +++ b/config/setup/roller.go @@ -0,0 +1,39 @@ +package setup + +import ( + "strconv" + + "github.com/mholt/caddy/middleware" +) + +func parseRoller(c *Controller) (*middleware.LogRoller, error) { + var size, age, keep int + // This is kind of a hack to support nested blocks: + // As we are already in a block: either log or errors, + // c.nesting > 0 but, as soon as c meets a }, it thinks + // the block is over and return false for c.NextBlock. + for c.NextBlock() { + what := c.Val() + if !c.NextArg() { + return nil, c.ArgErr() + } + value := c.Val() + var err error + switch what { + case "size": + size, err = strconv.Atoi(value) + case "age": + age, err = strconv.Atoi(value) + case "keep": + keep, err = strconv.Atoi(value) + } + if err != nil { + return nil, err + } + } + return &middleware.LogRoller{ + MaxSize: size, + MaxAge: age, + MaxBackups: keep, + }, nil +} diff --git a/middleware/errors/errors.go b/middleware/errors/errors.go index 8f1e04b46..44451ab12 100644 --- a/middleware/errors/errors.go +++ b/middleware/errors/errors.go @@ -20,6 +20,7 @@ type ErrorHandler struct { ErrorPages map[int]string // map of status code to filename LogFile string Log *log.Logger + LogRoller *middleware.LogRoller } func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { diff --git a/middleware/log/log.go b/middleware/log/log.go index 45a3e9a20..f2fbb4217 100644 --- a/middleware/log/log.go +++ b/middleware/log/log.go @@ -47,6 +47,7 @@ type Rule struct { OutputFile string Format string Log *log.Logger + Roller *middleware.LogRoller } const ( diff --git a/middleware/roller.go b/middleware/roller.go new file mode 100644 index 000000000..0f8a2b2e3 --- /dev/null +++ b/middleware/roller.go @@ -0,0 +1,25 @@ +package middleware + +import ( + "io" + + "gopkg.in/natefinch/lumberjack.v2" +) + +type LogRoller struct { + Filename string + MaxSize int + MaxAge int + MaxBackups int + LocalTime bool +} + +func (l LogRoller) GetLogWriter() io.Writer { + return &lumberjack.Logger{ + Filename: l.Filename, + MaxSize: l.MaxSize, + MaxAge: l.MaxAge, + MaxBackups: l.MaxBackups, + LocalTime: l.LocalTime, + } +} From 94becb89f60c3a519ea170c4f2da94472ad02e9c Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 2 Sep 2015 18:28:03 -0600 Subject: [PATCH 06/11] Add Go 1.5 to the Travis CI manifest --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 37656b0b9..47e2d4f58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: go go: - 1.4 + - 1.5 - tip script: go test ./... From b199825c3b90707f1dbc1a2156d02de44c2d6c3d Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Fri, 4 Sep 2015 08:34:58 -0700 Subject: [PATCH 07/11] Fix formatting directives in tests Signed-off-by: Alexander Morozov --- config/setup/markdown_test.go | 2 +- middleware/recorder_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/setup/markdown_test.go b/config/setup/markdown_test.go index c8b2f3154..1ada8a1f1 100644 --- a/config/setup/markdown_test.go +++ b/config/setup/markdown_test.go @@ -111,7 +111,7 @@ func TestMarkdownStaticGen(t *testing.T) { fp := filepath.Join(c.Root, markdown.DefaultStaticDir) if err = os.RemoveAll(fp); err != nil { - t.Errorf("Error while removing the generated static files: ", err) + t.Errorf("Error while removing the generated static files: %v", err) } } diff --git a/middleware/recorder_test.go b/middleware/recorder_test.go index 9d4e5b838..ed6c6abdd 100644 --- a/middleware/recorder_test.go +++ b/middleware/recorder_test.go @@ -13,7 +13,7 @@ func TestNewResponseRecorder(t *testing.T) { t.Fatalf("Expected Response writer in the Recording to be same as the one sent\n") } if recordRequest.status != http.StatusOK { - t.Fatalf("Expected recorded status to be http.StatusOK (%d) , but found %d\n ", recordRequest.status) + t.Fatalf("Expected recorded status to be http.StatusOK (%d) , but found %d\n ", http.StatusOK, recordRequest.status) } } func TestWriteHeader(t *testing.T) { @@ -35,6 +35,6 @@ func TestWrite(t *testing.T) { t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size) } if w.Body.String() != responseTestString { - t.Fatalf("Expected Response Body to be %s , but found %s\n", w.Body.String()) + t.Fatalf("Expected Response Body to be %s , but found %s\n", responseTestString, w.Body.String()) } } From 69950e57f0c7466c45daff6b326d52f19c0481ba Mon Sep 17 00:00:00 2001 From: Maxime Date: Fri, 4 Sep 2015 19:18:01 +0200 Subject: [PATCH 08/11] Use localtime for the log roller timestamp --- config/setup/errors_test.go | 2 ++ config/setup/log_test.go | 1 + config/setup/roller.go | 1 + 3 files changed, 4 insertions(+) diff --git a/config/setup/errors_test.go b/config/setup/errors_test.go index 5ece288d0..f161eaf3c 100644 --- a/config/setup/errors_test.go +++ b/config/setup/errors_test.go @@ -67,6 +67,7 @@ func TestErrorsParse(t *testing.T) { MaxSize: 2, MaxAge: 10, MaxBackups: 3, + LocalTime: true, }, }}, {`errors { log errors.txt { @@ -86,6 +87,7 @@ func TestErrorsParse(t *testing.T) { MaxSize: 3, MaxAge: 11, MaxBackups: 5, + LocalTime: true, }, }}, } diff --git a/config/setup/log_test.go b/config/setup/log_test.go index a9693a699..ae7a96e31 100644 --- a/config/setup/log_test.go +++ b/config/setup/log_test.go @@ -110,6 +110,7 @@ func TestLogParse(t *testing.T) { MaxSize: 2, MaxAge: 10, MaxBackups: 3, + LocalTime: true, }, }}}, } diff --git a/config/setup/roller.go b/config/setup/roller.go index 3a5b8a8d7..fedc52c58 100644 --- a/config/setup/roller.go +++ b/config/setup/roller.go @@ -35,5 +35,6 @@ func parseRoller(c *Controller) (*middleware.LogRoller, error) { MaxSize: size, MaxAge: age, MaxBackups: keep, + LocalTime: true, }, nil } From 4e1717db4c9b44c9255e5c58fce7d3f49af010a1 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 5 Sep 2015 16:04:30 -0600 Subject: [PATCH 09/11] basicauth: htpasswd path now relative to site root --- config/setup/basicauth.go | 11 +++++++---- dist/CHANGES.txt | 5 +++++ middleware/basicauth/basicauth.go | 12 +++++------- middleware/basicauth/basicauth_test.go | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/config/setup/basicauth.go b/config/setup/basicauth.go index a59e13497..bc57d1c6e 100644 --- a/config/setup/basicauth.go +++ b/config/setup/basicauth.go @@ -9,6 +9,8 @@ import ( // BasicAuth configures a new BasicAuth middleware instance. func BasicAuth(c *Controller) (middleware.Middleware, error) { + root := c.Root + rules, err := basicAuthParse(c) if err != nil { return nil, err @@ -18,6 +20,7 @@ func BasicAuth(c *Controller) (middleware.Middleware, error) { return func(next middleware.Handler) middleware.Handler { basic.Next = next + basic.SiteRoot = root return basic }, nil } @@ -34,7 +37,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { switch len(args) { case 2: rule.Username = args[0] - if rule.Password, err = passwordMatcher(rule.Username, args[1]); err != nil { + if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil { return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) } @@ -47,7 +50,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { case 3: rule.Resources = append(rule.Resources, args[0]) rule.Username = args[1] - if rule.Password, err = passwordMatcher(rule.Username, args[2]); err != nil { + if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil { return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) } default: @@ -60,10 +63,10 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { return rules, nil } -func passwordMatcher(username, passw string) (basicauth.PasswordMatcher, error) { +func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) { if !strings.HasPrefix(passw, "htpasswd=") { return basicauth.PlainMatcher(passw), nil } - return basicauth.GetHtpasswdMatcher(passw[9:], username) + return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot) } diff --git a/dist/CHANGES.txt b/dist/CHANGES.txt index dced4bd4e..f21eeda92 100644 --- a/dist/CHANGES.txt +++ b/dist/CHANGES.txt @@ -1,5 +1,10 @@ CHANGES + +- basicauth: Support for legacy htpasswd files +- browse: JSON response with file listing given Accept header + + 0.7.5 (August 5, 2015) - core: All listeners bind to 0.0.0.0 unless 'bind' directive is used - fastcgi: Set HTTPS env variable if connection is secure diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 02ad36c55..eeeb54761 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -22,8 +22,9 @@ import ( // security of HTTP Basic Auth is disputed. Use discretion when deciding // what to protect with BasicAuth. type BasicAuth struct { - Next middleware.Handler - Rules []Rule + Next middleware.Handler + SiteRoot string + Rules []Rule } // ServeHTTP implements the middleware.Handler interface. @@ -84,11 +85,8 @@ var ( htpasswordsMu sync.Mutex ) -func GetHtpasswdMatcher(filename, username string) (PasswordMatcher, error) { - filename, err := filepath.Abs(filename) - if err != nil { - return nil, err - } +func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) { + filename = filepath.Join(siteRoot, filename) htpasswordsMu.Lock() if htpasswords == nil { htpasswords = make(map[string]map[string]PasswordMatcher) diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index 393f2e4ed..aa1fc2443 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -132,7 +132,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` for i, username := range []string{"sha1", "md5"} { rule := Rule{Username: username, Resources: []string{"/testing"}} - if rule.Password, err = GetHtpasswdMatcher(htfh.Name(), rule.Username); err != nil { + if rule.Password, err = GetHtpasswdMatcher(htfh.Name(), rule.Username, "/"); err != nil { t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err) } t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password) From ed10863494dfc9985d95b8df660b7ae66006fc5b Mon Sep 17 00:00:00 2001 From: Benoit Benedetti Date: Tue, 8 Sep 2015 20:14:23 +0200 Subject: [PATCH 10/11] Configuration as command line arg #222 --- main.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 947548a56..3342ae7c4 100644 --- a/main.go +++ b/main.go @@ -124,7 +124,7 @@ func isLocalhost(s string) bool { // loadConfigs loads configuration from a file or stdin (piped). // The configurations are grouped by bind address. // Configuration is obtained from one of three sources, tried -// in this order: 1. -conf flag, 2. stdin, 3. Caddyfile. +// in this order: 1. -conf flag, 2. stdin, 4. command line argument 3. Caddyfile. // If none of those are available, a default configuration is // loaded. func loadConfigs() (config.Group, error) { @@ -155,6 +155,12 @@ func loadConfigs() (config.Group, error) { } } + // Command line Arg + if flag.NArg() > 0 { + confBody := ":" + config.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") + return config.Load("args", bytes.NewBufferString(confBody)) + } + // Caddyfile file, err := os.Open(config.DefaultConfigFile) if err != nil { From 5d32af8a6b8e48fa97de93eeac84cedc3b68bd7f Mon Sep 17 00:00:00 2001 From: Benoit Benedetti Date: Tue, 8 Sep 2015 22:38:30 +0200 Subject: [PATCH 11/11] Fix typo in loadConfigs comment --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 3342ae7c4..c1c0d1430 100644 --- a/main.go +++ b/main.go @@ -124,7 +124,7 @@ func isLocalhost(s string) bool { // loadConfigs loads configuration from a file or stdin (piped). // The configurations are grouped by bind address. // Configuration is obtained from one of three sources, tried -// in this order: 1. -conf flag, 2. stdin, 4. command line argument 3. Caddyfile. +// in this order: 1. -conf flag, 2. stdin, 3. command line argument 4. Caddyfile. // If none of those are available, a default configuration is // loaded. func loadConfigs() (config.Group, error) {