diff --git a/.travis.yml b/.travis.yml index 37656b0b..47e2d4f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: go go: - 1.4 + - 1.5 - tip script: go test ./... diff --git a/README.md b/README.md index 76645389..44fd023b 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 diff --git a/config/parse/dispenser.go b/config/parse/dispenser.go index e8a0c847..a7457f56 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/basicauth.go b/config/setup/basicauth.go index 6d1ece10..bc57d1c6 100644 --- a/config/setup/basicauth.go +++ b/config/setup/basicauth.go @@ -1,12 +1,16 @@ package setup import ( + "strings" + "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/basicauth" ) // 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 @@ -16,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 } @@ -23,6 +28,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 +37,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], c.Root); 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 +50,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], c.Root); err != nil { + return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) + } default: return rules, c.ArgErr() } @@ -51,3 +62,11 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { return rules, nil } + +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, siteRoot) +} diff --git a/config/setup/basicauth_test.go b/config/setup/basicauth_test.go index f5cb8067..7b0ba3d9 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/config/setup/errors.go b/config/setup/errors.go index 58f22dfa..ae42f381 100644 --- a/config/setup/errors.go +++ b/config/setup/errors.go @@ -23,25 +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 != "" { + 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 } + 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 }) @@ -71,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) @@ -91,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 00000000..f161eaf3 --- /dev/null +++ b/config/setup/errors_test.go @@ -0,0 +1,139 @@ +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, + LocalTime: true, + }, + }}, + {`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, + LocalTime: true, + }, + }}, + } + 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 0c15c3d0..8bb4788a 100644 --- a/config/setup/log.go +++ b/config/setup/log.go @@ -22,25 +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 { + 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 } + 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 @@ -57,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 @@ -70,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 @@ -91,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 6f8e1cce..ae7a96e3 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,17 @@ 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, + LocalTime: true, + }, + }}}, } for i, test := range tests { c := NewTestController(test.inputLogRules) @@ -128,6 +143,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/markdown_test.go b/config/setup/markdown_test.go index c8b2f315..1ada8a1f 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/config/setup/roller.go b/config/setup/roller.go new file mode 100644 index 00000000..fedc52c5 --- /dev/null +++ b/config/setup/roller.go @@ -0,0 +1,40 @@ +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, + LocalTime: true, + }, nil +} diff --git a/dist/CHANGES.txt b/dist/CHANGES.txt index dced4bd4..f21eeda9 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/main.go b/main.go index 947548a5..c1c0d143 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, 3. command line argument 4. 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 { diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 221446a2..eeeb5476 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" ) @@ -14,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. @@ -37,7 +46,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 +74,71 @@ 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, siteRoot string) (PasswordMatcher, error) { + filename = filepath.Join(siteRoot, filename) + 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 51f944bd..aa1fc244 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) + } + } +} diff --git a/middleware/browse/browse_test.go b/middleware/browse/browse_test.go index b396fda1..05ccff14 100644 --- a/middleware/browse/browse_test.go +++ b/middleware/browse/browse_test.go @@ -236,7 +236,6 @@ func TestBrowseJson(t *testing.T) { if err != nil { t.Fatalf("Unable to Marshal the listing ") } - expectedJsonString := string(marsh) if actualJsonResponseString != expectedJsonString { t.Errorf("Json response string doesnt match the expected Json response ") diff --git a/middleware/errors/errors.go b/middleware/errors/errors.go index 8f1e04b4..44451ab1 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 45a3e9a2..f2fbb421 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/recorder_test.go b/middleware/recorder_test.go index 9d4e5b83..ed6c6abd 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()) } } diff --git a/middleware/replacer_test.go b/middleware/replacer_test.go index ad8a9489..7986a735 100644 --- a/middleware/replacer_test.go +++ b/middleware/replacer_test.go @@ -22,6 +22,7 @@ func TestNewReplacer(t *testing.T) { switch v := replaceValues.(type) { case replacer: + if v.replacements["{host}"] != "caddyserver.com" { t.Errorf("Expected host to be caddyserver.com") } @@ -36,3 +37,35 @@ func TestNewReplacer(t *testing.T) { t.Fatalf("Return Value from New Replacer expected pass type assertion into a replacer type \n") } } + +func TestReplace(t *testing.T) { + w := httptest.NewRecorder() + recordRequest := NewResponseRecorder(w) + userJson := `{"username": "dennis"}` + + reader := strings.NewReader(userJson) //Convert string to reader + + request, err := http.NewRequest("POST", "http://caddyserver.com", reader) //Create request with JSON body + if err != nil { + t.Fatalf("Request Formation Failed \n") + } + replaceValues := NewReplacer(request, recordRequest, "") + + switch v := replaceValues.(type) { + case replacer: + + if v.Replace("This host is {host}") != "This host is caddyserver.com" { + t.Errorf("Expected host replacement failed") + } + if v.Replace("This request method is {method}") != "This request method is POST" { + t.Errorf("Expected method replacement failed") + } + if v.Replace("The response status is {status}") != "The response status is 200" { + t.Errorf("Expected status replacement failed") + } + + default: + t.Fatalf("Return Value from New Replacer expected pass type assertion into a replacer type \n") + } + +} diff --git a/middleware/roller.go b/middleware/roller.go new file mode 100644 index 00000000..0f8a2b2e --- /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, + } +}