package duoapi import ( "bytes" "errors" "io/ioutil" "net/http" "net/url" "strings" "testing" "time" ) func TestCanonicalize(t *testing.T) { values := url.Values{} values.Set("username", "H ell?o") values.Set("password", "H-._~i") values.Add("password", "A(!'*)") params_str := canonicalize("post", "API-XXX.duosecurity.COM", "/auth/v2/ping", values, "5") params := strings.Split(params_str, "\n") if len(params) != 5 { t.Error("Expected 5 parameters, but got " + string(len(params))) } if params[1] != string("POST") { t.Error("Expected POST, but got " + params[1]) } if params[2] != string("api-xxx.duosecurity.com") { t.Error("Expected api-xxx.duosecurity.com, but got " + params[2]) } if params[3] != string("/auth/v2/ping") { t.Error("Expected /auth/v2/ping, but got " + params[3]) } if params[4] != string("password=A%28%21%27%2A%29&password=H-._~i&username=H%20ell%3Fo") { t.Error("Expected sorted escaped params, but got " + params[4]) } } func encodeAndValidate(t *testing.T, input url.Values, output string) { values := url.Values{} for key, val := range input { values.Set(key, val[0]) } params_str := canonicalize("post", "API-XXX.duosecurity.com", "/auth/v2/ping", values, "5") params := strings.Split(params_str, "\n") if params[4] != output { t.Error("Mismatch\n" + output + "\n" + params[4]) } } func TestSimple(t *testing.T) { values := url.Values{} values.Set("realname", "First Last") values.Set("username", "root") encodeAndValidate(t, values, "realname=First%20Last&username=root") } func TestZero(t *testing.T) { values := url.Values{} encodeAndValidate(t, values, "") } func TestOne(t *testing.T) { values := url.Values{} values.Set("realname", "First Last") encodeAndValidate(t, values, "realname=First%20Last") } func TestPrintableAsciiCharaceters(t *testing.T) { values := url.Values{} values.Set("digits", "0123456789") values.Set("letters", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") values.Set("punctuation", "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") values.Set("whitespace", "\t\n\x0b\x0c\r ") encodeAndValidate(t, values, "digits=0123456789&letters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&punctuation=%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~&whitespace=%09%0A%0B%0C%0D%20") } func TestSortOrderWithCommonPrefix(t *testing.T) { values := url.Values{} values.Set("foo", "1") values.Set("foo_bar", "2") encodeAndValidate(t, values, "foo=1&foo_bar=2") } func TestUnicodeFuzzValues(t *testing.T) { values := url.Values{} values.Set("bar", "⠕ꪣ㟏䮷㛩찅暎腢슽ꇱ") values.Set("baz", "ෳ蒽噩馅뢤갺篧潩鍊뤜") values.Set("foo", "퓎훖礸僀訠輕ﴋ耤岳왕") values.Set("qux", "讗졆-芎茚쳊ꋔ谾뢲馾") encodeAndValidate(t, values, "bar=%E2%A0%95%EA%AA%A3%E3%9F%8F%E4%AE%B7%E3%9B%A9%EC%B0%85%E6%9A%8E%E8%85%A2%EC%8A%BD%EA%87%B1&baz=%E0%B7%B3%E8%92%BD%E5%99%A9%E9%A6%85%EB%A2%A4%EA%B0%BA%E7%AF%A7%E6%BD%A9%E9%8D%8A%EB%A4%9C&foo=%ED%93%8E%ED%9B%96%E7%A4%B8%E5%83%80%E8%A8%A0%E8%BC%95%EF%B4%8B%E8%80%A4%E5%B2%B3%EC%99%95&qux=%E8%AE%97%EC%A1%86-%E8%8A%8E%E8%8C%9A%EC%B3%8A%EA%8B%94%E8%B0%BE%EB%A2%B2%E9%A6%BE") } func TestUnicodeFuzzKeysAndValues(t *testing.T) { values := url.Values{} values.Set("䚚⡻㗐軳朧倪ࠐ킑È셰", "ཅ᩶㐚敌숿鬉ꯢ荃ᬧ惐") values.Set("瑉繋쳻姿﹟获귌逌쿑砓", "趷倢鋓䋯⁽蜰곾嘗ॆ丰") values.Set("瑰錔逜麮䃘䈁苘豰ᴱꁂ", "៙ந鍘꫟ꐪ䢾ﮖ濩럿㋳") values.Set("싅Ⱍ☠㘗隳F蘅⃨갡头", "ﮩ䆪붃萋☕㹮攭ꢵ핫U") encodeAndValidate(t, values, "%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU") } func TestSign(t *testing.T) { values := url.Values{} values.Set("realname", "First Last") values.Set("username", "root") res := sign("DIWJ8X6AEYOR5OMC6TQ1", "Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep", "POST", "api-XXXXXXXX.duosecurity.com", "/accounts/v1/account/list", "Tue, 21 Aug 2012 17:29:18 -0000", values) if res != "Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6MmQ5N2Q2MTY2MzE5Nzgx"+ "YjVhM2EwN2FmMzlkMzY2ZjQ5MTIzNGVkYw==" { t.Error("Signature did not produce output documented at " + "https://www.duosecurity.com/docs/authapi :(") } } func TestV2Canonicalize(t *testing.T) { values := url.Values{} values.Set("䚚⡻㗐軳朧倪ࠐ킑È셰", "ཅ᩶㐚敌숿鬉ꯢ荃ᬧ惐") values.Set("瑉繋쳻姿﹟获귌逌쿑砓", "趷倢鋓䋯⁽蜰곾嘗ॆ丰") values.Set("瑰錔逜麮䃘䈁苘豰ᴱꁂ", "៙ந鍘꫟ꐪ䢾ﮖ濩럿㋳") values.Set("싅Ⱍ☠㘗隳F蘅⃨갡头", "ﮩ䆪붃萋☕㹮攭ꢵ핫U") canon := canonicalize( "PoSt", "foO.BAr52.cOm", "/Foo/BaR2/qux", values, "Fri, 07 Dec 2012 17:18:00 -0000") expected := "Fri, 07 Dec 2012 17:18:00 -0000\nPOST\nfoo.bar52.com\n/Foo/BaR2/qux\n%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU" if canon != expected { t.Error("Mismatch!\n" + expected + "\n" + canon) } } func TestNewDuo(t *testing.T) { duo := NewDuoApi("ABC", "123", "api-XXXXXXX.duosecurity.com", "go-client") if duo == nil { t.Fatal("Failed to create a new Duo Api") } } func TestDupApiCallHttpErr(t *testing.T) { httpClient := &mockHttpClient{doError: true} sleepSvc := &mockSleepService{} duo := &DuoApi{ ikey: "ikey-foo", skey: "skey-bar", host: "host.baz", userAgent: "ua-qux", apiClient: httpClient, authClient: httpClient, sleepSvc: sleepSvc, } resp, body, err := duo.Call("GET", "/v9/hello/world", url.Values{}) if resp != nil { t.Fatal("Non nil response returned") } if len(body) != 0 { t.Fatal("Non empty body returned") } if err == nil { t.Fatal("No error returned") } if len(httpClient.actualRequests) != 1 { t.Fatal("We should not retry after an HTTP error") } } func getMockClients(httpResponses []http.Response) (*DuoApi, *mockHttpClient, *mockSleepService) { httpClient := &mockHttpClient{responses: httpResponses} sleepSvc := &mockSleepService{} return &DuoApi{ ikey: "ikey-foo", skey: "skey-bar", host: "host.baz", userAgent: "ua-qux", apiClient: httpClient, authClient: httpClient, sleepSvc: sleepSvc, }, httpClient, sleepSvc } var okResp = http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), } var rateLimitResp = http.Response{ StatusCode: 429, Body: ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), } var completeRateLimitSleepDurations = []time.Duration{ time.Millisecond * 1000, time.Millisecond * 2000, time.Millisecond * 4000, time.Millisecond * 8000, time.Millisecond * 16000, time.Millisecond * 32000, } func assertRateLimitedCall( t *testing.T, actualResponse http.Response, httpClient mockHttpClient, sleepSvc mockSleepService, expectedTotalCalls int, expectedResponse http.Response, expectedSleepDurations []time.Duration) { if actualResponse.StatusCode != expectedResponse.StatusCode { t.Fatal("returned response does not have correct status code") } if actualResponse.Body != expectedResponse.Body { t.Fatal("returned response does not have correct body") } retriedRequestCount := expectedTotalCalls - 1 if len(httpClient.actualRequests) != expectedTotalCalls { t.Fatal("Made " + string(len(httpClient.actualRequests)) + " requests instead of " + string(expectedTotalCalls)) } if len(sleepSvc.sleepCalls) != retriedRequestCount { t.Fatal("Made " + string(len(sleepSvc.sleepCalls)) + " sleep calls instead of " + string(retriedRequestCount)) } for i := range expectedSleepDurations { if sleepSvc.sleepCalls[i] != expectedSleepDurations[i] { t.Fatal("Slept for " + string(sleepSvc.sleepCalls[i]) + " instead of " + string(expectedSleepDurations[i])) } } } func TestCallRateLimitedOnce(t *testing.T) { responses := []http.Response{rateLimitResp, okResp} sleepDurations := []time.Duration{time.Millisecond * 1000} duo, mockHttp, mockSleep := getMockClients(responses) resp, _, _ := duo.Call("GET", "/v9/hello/world", url.Values{}) assertRateLimitedCall(t, *resp, *mockHttp, *mockSleep, 2, okResp, sleepDurations) } func TestCallCompletelyRateLimited(t *testing.T) { responses := make([]http.Response, 7) for i := range responses { responses[i] = rateLimitResp } duo, mockHttp, mockSleep := getMockClients(responses) resp, _, _ := duo.Call("GET", "/v9/hello/world", url.Values{}) assertRateLimitedCall(t, *resp, *mockHttp, *mockSleep, 7, rateLimitResp, completeRateLimitSleepDurations) } func TestSignedCallRateLimitedOnce(t *testing.T) { responses := []http.Response{rateLimitResp, okResp} sleepDurations := []time.Duration{time.Millisecond * 1000} duo, mockHttp, mockSleep := getMockClients(responses) resp, _, _ := duo.SignedCall("GET", "/v9/hello/world", url.Values{}) assertRateLimitedCall(t, *resp, *mockHttp, *mockSleep, 2, okResp, sleepDurations) } func TestSignedCallCompletelyRateLimited(t *testing.T) { responses := make([]http.Response, 7) for i := range responses { responses[i] = rateLimitResp } duo, mockHttp, mockSleep := getMockClients(responses) resp, _, _ := duo.SignedCall("GET", "/v9/hello/world", url.Values{}) assertRateLimitedCall(t, *resp, *mockHttp, *mockSleep, 7, rateLimitResp, completeRateLimitSleepDurations) } type mockHttpClient struct { responses []http.Response actualRequests []*http.Request doError bool } func (c *mockHttpClient) Do(req *http.Request) (*http.Response, error) { if c.actualRequests == nil { c.actualRequests = []*http.Request{} } c.actualRequests = append(c.actualRequests, req) if c.doError { return nil, errors.New("Ouch") } resp := c.responses[0] c.responses = c.responses[1:] return &resp, nil } type mockSleepService struct { sleepCalls []time.Duration } func (svc *mockSleepService) Sleep(duration time.Duration) { if svc.sleepCalls == nil { svc.sleepCalls = []time.Duration{} } svc.sleepCalls = append(svc.sleepCalls, duration) }