diff --git a/Makefile b/Makefile index 037a26d2c266659833da760943773512acf701c4..a8a4d7f77b3ecf708f8b7d2dad9f8b3b21ca47ea 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ assets: httpbin/assets/* go-bindata -o httpbin/assets.go -pkg=httpbin -prefix=httpbin/assets httpbin/assets test: assets - go test github.com/mccutchen/go-httpbin/httpbin + go test ./... testcover: assets mkdir -p dist diff --git a/httpbin/digest/digest.go b/httpbin/digest/digest.go index 7baffc1a30108e820de80af585c0540d06a2bc11..13c09e1306333184a1049955cafe4c2ef82cb8d7 100644 --- a/httpbin/digest/digest.go +++ b/httpbin/digest/digest.go @@ -24,11 +24,9 @@ import ( // Check returns a bool indicating whether the request is correctly // authenticated for the given username and password. -// -// TODO: use constant-time equality comparison. func Check(req *http.Request, username, password string) bool { auth := parseAuthorizationHeader(req.Header.Get("Authorization")) - if auth == nil { + if auth == nil || auth.username != username { return false } expectedResponse := response(auth, password, req.Method, req.RequestURI) @@ -116,19 +114,22 @@ func parseAuthorizationHeader(value string) *authorization { // parseDictHeader is a simplistic, buggy, and incomplete implementation of // parsing key-value pairs from a header value into a map. func parseDictHeader(value string) map[string]string { - res := make(map[string]string) - for _, pair := range strings.Split(value, ",") { - parts := strings.SplitN(pair, "=", 2) - if len(parts) == 1 { - res[parts[0]] = "" + pairs := strings.Split(value, ",") + res := make(map[string]string, len(pairs)) + for _, pair := range pairs { + parts := strings.SplitN(strings.TrimSpace(pair), "=", 2) + key := strings.TrimSpace(parts[0]) + if len(key) == 0 { continue } - key := parts[0] - val := parts[1] - if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) { - val = val[1 : len(val)-1] + val := "" + if len(parts) > 1 { + val = strings.TrimSpace(parts[1]) + if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) { + val = val[1 : len(val)-1] + } } - res[strings.TrimSpace(key)] = strings.TrimSpace(val) + res[key] = val } return res } diff --git a/httpbin/digest/digest_test.go b/httpbin/digest/digest_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a359f1ac0fd4e34455a8bd72470bf62853ea3e14 --- /dev/null +++ b/httpbin/digest/digest_test.go @@ -0,0 +1,253 @@ +package digest + +import ( + "crypto" + "fmt" + "net/http" + "reflect" + "testing" +) + +// Well-formed examples from Wikipedia: +// https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation +const ( + exampleUsername = "Mufasa" + examplePassword = "Circle Of Life" + + exampleChallenge string = `Digest realm="testrealm@host.com", + qop="auth,auth-int", + nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + opaque="5ccc069c403ebaf9f0171e9517f40e41"` + + exampleAuthorization string = `Digest username="Mufasa", + realm="testrealm@host.com", + nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + uri="/dir/index.html", + qop=auth, + nc=00000001, + cnonce="0a4f113b", + response="6629fae49393a05397450978507c4ef1", + opaque="5ccc069c403ebaf9f0171e9517f40e41"` +) + +func assertStringEquals(t *testing.T, expected, got string) { + if expected != got { + t.Errorf("Expected %#v, got %#v", expected, got) + } +} + +func buildRequest(method, uri, authHeader string) *http.Request { + req, _ := http.NewRequest(method, uri, nil) + req.RequestURI = uri + if authHeader != "" { + req.Header.Set("Authorization", authHeader) + } + return req +} + +func TestCheck(t *testing.T) { + t.Run("missing authorization", func(t *testing.T) { + req := buildRequest("GET", "/dir/index.html", "") + if Check(req, exampleUsername, examplePassword) != false { + t.Error("Missing Authorization header should fail") + } + }) + + t.Run("wrong username", func(t *testing.T) { + req := buildRequest("GET", "/dir/index.html", exampleAuthorization) + if Check(req, "Simba", examplePassword) != false { + t.Error("Incorrect username should fail") + } + }) + + t.Run("wrong password", func(t *testing.T) { + req := buildRequest("GET", "/dir/index.html", exampleAuthorization) + if Check(req, examplePassword, "foobar") != false { + t.Error("Incorrect password should fail") + } + }) + + t.Run("ok", func(t *testing.T) { + req := buildRequest("GET", "/dir/index.html", exampleAuthorization) + if Check(req, exampleUsername, examplePassword) != true { + t.Error("Correct credentials should pass") + } + }) +} + +func TestResponse(t *testing.T) { + auth := parseAuthorizationHeader(exampleAuthorization) + expected := auth.response + got := response(auth, examplePassword, "GET", "/dir/index.html") + assertStringEquals(t, expected, got) +} + +func TestHash(t *testing.T) { + var tests = []struct { + algorithm crypto.Hash + data []byte + expected string + }{ + {crypto.SHA256, []byte("hello, world!\n"), "4dca0fd5f424a31b03ab807cbae77eb32bf2d089eed1cee154b3afed458de0dc"}, + {crypto.MD5, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"}, + + // Any unhandled hash results in MD5 being used + {crypto.MD4, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"}, + {crypto.SHA512, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"}, + } + for _, test := range tests { + t.Run(fmt.Sprintf("hash/%v", test.algorithm), func(t *testing.T) { + result := hash(test.data, test.algorithm) + assertStringEquals(t, test.expected, result) + }) + } +} + +func TestCompare(t *testing.T) { + if compare("foo", "bar") != false { + t.Error("Expected foo != bar") + } + + if compare("foo", "foo") != true { + t.Error("Expected foo == foo") + } +} + +func TestParseDictHeader(t *testing.T) { + var tests = []struct { + input string + expected map[string]string + }{ + {"foo=bar", map[string]string{"foo": "bar"}}, + + // keys without values get the empty string + {"foo", map[string]string{"foo": ""}}, + {"foo=bar, baz", map[string]string{"foo": "bar", "baz": ""}}, + + // no spaces required + {"foo=bar,baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, + + // spaces are stripped + {"foo=bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, + {"foo= bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, + {"foo=bar, baz = quux", map[string]string{"foo": "bar", "baz": "quux"}}, + {" foo =bar, baz=quux", map[string]string{"foo": "bar", "baz": "quux"}}, + {"foo=bar,baz = quux ", map[string]string{"foo": "bar", "baz": "quux"}}, + + // quotes around values are stripped + {`foo="bar two three four", baz=quux`, map[string]string{"foo": "bar two three four", "baz": "quux"}}, + {`foo=bar, baz=""`, map[string]string{"foo": "bar", "baz": ""}}, + + // quotes around keys are not stripped + {`"foo"="bar", "baz two"=quux`, map[string]string{`"foo"`: "bar", `"baz two"`: "quux"}}, + + // spaces within quotes around values are preserved + {`foo=bar, baz=" quux "`, map[string]string{"foo": "bar", "baz": " quux "}}, + + // commas values are NOT handled correctly + {`foo="one, two, three", baz=quux`, map[string]string{"foo": `"one`, "two": "", `three"`: "", "baz": "quux"}}, + {",,,", make(map[string]string)}, + + // trailing comma is okay + {"foo=bar,", map[string]string{"foo": "bar"}}, + {"foo=bar, ", map[string]string{"foo": "bar"}}, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + results := parseDictHeader(test.input) + if !reflect.DeepEqual(test.expected, results) { + t.Errorf("expected %#v, got %#v", test.expected, results) + } + }) + } +} + +func TestParseAuthorizationHeader(t *testing.T) { + var tests = []struct { + input string + expected *authorization + }{ + {"", nil}, + {"Digest", nil}, + {"Basic QWxhZGRpbjpPcGVuU2VzYW1l", nil}, + + // case sensitive on Digest + {"digest username=u, realm=r, nonce=n", nil}, + + // incomplete headers are fine + {"Digest username=u, realm=r, nonce=n", &authorization{ + algorithm: crypto.MD5, + username: "u", + realm: "r", + nonce: "n", + }}, + + // algorithm can be either MD5 or SHA-256, with MD5 as default + {"Digest username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + {"Digest algorithm=MD5, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + {"Digest algorithm=md5, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + {"Digest algorithm=SHA-256, username=u", &authorization{ + algorithm: crypto.SHA256, + username: "u", + }}, + {"Digest algorithm=foo, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + {"Digest algorithm=SHA-512, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + // algorithm not case sensitive + {"Digest algorithm=sha-256, username=u", &authorization{ + algorithm: crypto.SHA256, + username: "u", + }}, + // but dash is required in SHA-256 is not recognized + {"Digest algorithm=SHA256, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + // session variants not recognized + {"Digest algorithm=SHA-256-sess, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + {"Digest algorithm=MD5-sess, username=u", &authorization{ + algorithm: crypto.MD5, + username: "u", + }}, + + {exampleAuthorization, &authorization{ + algorithm: crypto.MD5, + cnonce: "0a4f113b", + nc: "00000001", + nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093", + opaque: "5ccc069c403ebaf9f0171e9517f40e41", + qop: "auth", + realm: "testrealm@host.com", + response: "6629fae49393a05397450978507c4ef1", + uri: "/dir/index.html", + username: exampleUsername, + }}, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + got := parseAuthorizationHeader(test.input) + if !reflect.DeepEqual(test.expected, got) { + t.Errorf("expected %#v, got %#v", test.expected, got) + } + }) + } +}