diff --git a/caddyhttp/basicauth/basicauth.go b/caddyhttp/basicauth/basicauth.go index a55ef17ec..78a9192e2 100644 --- a/caddyhttp/basicauth/basicauth.go +++ b/caddyhttp/basicauth/basicauth.go @@ -37,6 +37,7 @@ type BasicAuth struct { // ServeHTTP implements the httpserver.Handler interface. func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { var protected, isAuthenticated bool + var realm string for _, rule := range a.Rules { for _, res := range rule.Resources { @@ -46,6 +47,7 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // path matches; this endpoint is protected protected = true + realm = rule.Realm // parse auth header username, password, ok := r.BasicAuth() @@ -74,7 +76,10 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // browsers show a message that says something like: // "The website says: " // which is kinda dumb, but whatever. - w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"") + if realm == "" { + realm = "Restricted" + } + w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"") return http.StatusUnauthorized, nil } @@ -89,6 +94,7 @@ type Rule struct { Username string Password func(string) bool Resources []string + Realm string // See RFC 1945 and RFC 2617, default: "Restricted" } // PasswordMatcher determines whether a password matches a rule. diff --git a/caddyhttp/basicauth/basicauth_test.go b/caddyhttp/basicauth/basicauth_test.go index 64dd4592a..15003866e 100644 --- a/caddyhttp/basicauth/basicauth_test.go +++ b/caddyhttp/basicauth/basicauth_test.go @@ -25,10 +25,20 @@ func TestBasicAuth(t *testing.T) { } return http.StatusOK, nil } - rw := BasicAuth{ - Next: httpserver.HandlerFunc(upstreamHandler), - Rules: []Rule{ - {Username: "okuser", Password: PlainMatcher("okpass"), Resources: []string{"/testing"}}, + rws := []BasicAuth{ + { + Next: httpserver.HandlerFunc(upstreamHandler), + Rules: []Rule{ + {Username: "okuser", Password: PlainMatcher("okpass"), + Resources: []string{"/testing"}, Realm: "Resources"}, + }, + }, + { + Next: httpserver.HandlerFunc(upstreamHandler), + Rules: []Rule{ + {Username: "okuser", Password: PlainMatcher("okpass"), + Resources: []string{"/testing"}}, + }, }, } @@ -51,34 +61,40 @@ func TestBasicAuth(t *testing.T) { } var test testType - 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) + for _, rw := range rws { + expectRealm := rw.Rules[0].Realm + if expectRealm == "" { + expectRealm = "Restricted" // Default if Realm not specified in rule } - req.SetBasicAuth(test.user, test.password) + 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) + } + req.SetBasicAuth(test.user, test.password) - rec := httptest.NewRecorder() - result, err := rw.ServeHTTP(rec, req) - if err != nil { - t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err) - } - if result != test.result { - t.Errorf("Test %d: Expected status code %d but was %d", - i, test.result, result) - } - if test.result == http.StatusUnauthorized { - headers := rec.Header() - if val, ok := headers["Www-Authenticate"]; ok { - if got, want := val[0], "Basic realm=\"Restricted\""; got != want { - t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got) + rec := httptest.NewRecorder() + result, err := rw.ServeHTTP(rec, req) + if err != nil { + t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err) + } + if result != test.result { + t.Errorf("Test %d: Expected status code %d but was %d", + i, test.result, result) + } + if test.result == http.StatusUnauthorized { + headers := rec.Header() + if val, ok := headers["Www-Authenticate"]; ok { + if got, want := val[0], "Basic realm=\""+expectRealm+"\""; got != want { + t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got) + } + } else { + t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i) } } else { - t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i) - } - } else { - if got, want := req.Header.Get("Authorization"), ""; got != want { - t.Errorf("Test %d: Expected Authorization header to be stripped from request after successful authentication, but is: %s", i, got) + if got, want := req.Header.Get("Authorization"), ""; got != want { + t.Errorf("Test %d: Expected Authorization header to be stripped from request after successful authentication, but is: %s", i, got) + } } } } diff --git a/caddyhttp/basicauth/setup.go b/caddyhttp/basicauth/setup.go index 7dc4e8f2c..0f2c688c9 100644 --- a/caddyhttp/basicauth/setup.go +++ b/caddyhttp/basicauth/setup.go @@ -51,13 +51,6 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) { if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.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() { - return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val()) - } - } case 3: rule.Resources = append(rule.Resources, args[0]) rule.Username = args[1] @@ -68,6 +61,29 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) { return rules, c.ArgErr() } + // If nested block is present, process it here + for c.NextBlock() { + val := c.Val() + args = c.RemainingArgs() + switch len(args) { + case 0: + // Assume single argument is path resource + rule.Resources = append(rule.Resources, val) + case 1: + if val == "realm" { + if rule.Realm == "" { + rule.Realm = strings.Replace(args[0], `"`, `\"`, -1) + } else { + return rules, c.Errf("\"realm\" subdirective can only be specified once") + } + } else { + return rules, c.Errf("expecting \"realm\", got \"%s\"", val) + } + default: + return rules, c.ArgErr() + } + } + rules = append(rules, rule) } diff --git a/caddyhttp/basicauth/setup_test.go b/caddyhttp/basicauth/setup_test.go index 80b4db39b..1075b2bc0 100644 --- a/caddyhttp/basicauth/setup_test.go +++ b/caddyhttp/basicauth/setup_test.go @@ -64,12 +64,39 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` }`, false, "pwd", []Rule{ {Username: "user"}, }}, + {`basicauth /resource1 user pwd { + }`, false, "pwd", []Rule{ + {Username: "user", Resources: []string{"/resource1"}}, + }}, + {`basicauth /resource1 user pwd { + realm Resources + }`, false, "pwd", []Rule{ + {Username: "user", Resources: []string{"/resource1"}, Realm: "Resources"}, + }}, {`basicauth user pwd { /resource1 /resource2 }`, false, "pwd", []Rule{ {Username: "user", Resources: []string{"/resource1", "/resource2"}}, }}, + {`basicauth user pwd { + /resource1 + /resource2 + realm "Secure resources" + }`, false, "pwd", []Rule{ + {Username: "user", Resources: []string{"/resource1", "/resource2"}, Realm: "Secure resources"}, + }}, + {`basicauth user pwd { + /resource1 + realm "Secure resources" + realm Extra + /resource2 + }`, true, "pwd", []Rule{}}, + {`basicauth user pwd { + /resource1 + foo "Resources" + /resource2 + }`, true, "pwd", []Rule{}}, {`basicauth /resource user pwd`, false, "pwd", []Rule{ {Username: "user", Resources: []string{"/resource"}}, }}, @@ -109,6 +136,11 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` i, j, expectedRule.Username, actualRule.Username) } + if actualRule.Realm != expectedRule.Realm { + t.Errorf("Test %d, rule %d: Expected realm '%s', got '%s'", + i, j, expectedRule.Realm, actualRule.Realm) + } + if strings.Contains(test.input, "htpasswd=") && skipHtpassword { continue }