Skip to content
Snippets Groups Projects
Commit 71a6a044 authored by Will McCutchen's avatar Will McCutchen
Browse files

Add tests for digest package

parent 3ed4a706
No related branches found
No related tags found
No related merge requests found
...@@ -8,7 +8,7 @@ assets: httpbin/assets/* ...@@ -8,7 +8,7 @@ assets: httpbin/assets/*
go-bindata -o httpbin/assets.go -pkg=httpbin -prefix=httpbin/assets httpbin/assets go-bindata -o httpbin/assets.go -pkg=httpbin -prefix=httpbin/assets httpbin/assets
test: assets test: assets
go test github.com/mccutchen/go-httpbin/httpbin go test ./...
testcover: assets testcover: assets
mkdir -p dist mkdir -p dist
......
...@@ -24,11 +24,9 @@ import ( ...@@ -24,11 +24,9 @@ import (
// Check returns a bool indicating whether the request is correctly // Check returns a bool indicating whether the request is correctly
// authenticated for the given username and password. // authenticated for the given username and password.
//
// TODO: use constant-time equality comparison.
func Check(req *http.Request, username, password string) bool { func Check(req *http.Request, username, password string) bool {
auth := parseAuthorizationHeader(req.Header.Get("Authorization")) auth := parseAuthorizationHeader(req.Header.Get("Authorization"))
if auth == nil { if auth == nil || auth.username != username {
return false return false
} }
expectedResponse := response(auth, password, req.Method, req.RequestURI) expectedResponse := response(auth, password, req.Method, req.RequestURI)
...@@ -116,19 +114,22 @@ func parseAuthorizationHeader(value string) *authorization { ...@@ -116,19 +114,22 @@ func parseAuthorizationHeader(value string) *authorization {
// parseDictHeader is a simplistic, buggy, and incomplete implementation of // parseDictHeader is a simplistic, buggy, and incomplete implementation of
// parsing key-value pairs from a header value into a map. // parsing key-value pairs from a header value into a map.
func parseDictHeader(value string) map[string]string { func parseDictHeader(value string) map[string]string {
res := make(map[string]string) pairs := strings.Split(value, ",")
for _, pair := range strings.Split(value, ",") { res := make(map[string]string, len(pairs))
parts := strings.SplitN(pair, "=", 2) for _, pair := range pairs {
if len(parts) == 1 { parts := strings.SplitN(strings.TrimSpace(pair), "=", 2)
res[parts[0]] = "" key := strings.TrimSpace(parts[0])
if len(key) == 0 {
continue continue
} }
key := parts[0] val := ""
val := parts[1] if len(parts) > 1 {
if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) { val = strings.TrimSpace(parts[1])
val = val[1 : len(val)-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 return res
} }
......
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)
}
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment