mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-20 21:01:17 +00:00
330 lines
11 KiB
Go
330 lines
11 KiB
Go
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)
|
||
}
|