2015-05-21 20:14:46 +02:00
|
|
|
package auth
|
2015-05-07 22:16:52 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"net/url"
|
|
|
|
"testing"
|
2015-09-30 17:47:01 +02:00
|
|
|
"time"
|
2015-05-07 22:16:52 +02:00
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
"github.com/docker/distribution/registry/client/auth/challenge"
|
2015-05-21 20:14:46 +02:00
|
|
|
"github.com/docker/distribution/registry/client/transport"
|
2015-05-07 22:16:52 +02:00
|
|
|
"github.com/docker/distribution/testutil"
|
|
|
|
)
|
|
|
|
|
2015-09-30 17:47:01 +02:00
|
|
|
// An implementation of clock for providing fake time data.
|
|
|
|
type fakeClock struct {
|
|
|
|
current time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now implements clock
|
|
|
|
func (fc *fakeClock) Now() time.Time { return fc.current }
|
|
|
|
|
2015-05-16 01:50:17 +02:00
|
|
|
func testServer(rrm testutil.RequestResponseMap) (string, func()) {
|
|
|
|
h := testutil.NewHandler(rrm)
|
|
|
|
s := httptest.NewServer(h)
|
|
|
|
return s.URL, s.Close
|
|
|
|
}
|
|
|
|
|
2015-05-07 22:16:52 +02:00
|
|
|
type testAuthenticationWrapper struct {
|
|
|
|
headers http.Header
|
|
|
|
authCheck func(string) bool
|
|
|
|
next http.Handler
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *testAuthenticationWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
auth := r.Header.Get("Authorization")
|
|
|
|
if auth == "" || !w.authCheck(auth) {
|
|
|
|
h := rw.Header()
|
|
|
|
for k, values := range w.headers {
|
|
|
|
h[k] = values
|
|
|
|
}
|
|
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.next.ServeHTTP(rw, r)
|
|
|
|
}
|
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
func testServerWithAuth(rrm testutil.RequestResponseMap, authenticate string, authCheck func(string) bool) (string, func()) {
|
2015-05-07 22:16:52 +02:00
|
|
|
h := testutil.NewHandler(rrm)
|
|
|
|
wrapper := &testAuthenticationWrapper{
|
|
|
|
|
|
|
|
headers: http.Header(map[string][]string{
|
2015-06-16 01:10:48 +02:00
|
|
|
"X-API-Version": {"registry/2.0"},
|
|
|
|
"X-Multi-API-Version": {"registry/2.0", "registry/2.1", "trust/1.0"},
|
|
|
|
"WWW-Authenticate": {authenticate},
|
2015-05-07 22:16:52 +02:00
|
|
|
}),
|
|
|
|
authCheck: authCheck,
|
|
|
|
next: h,
|
|
|
|
}
|
|
|
|
|
|
|
|
s := httptest.NewServer(wrapper)
|
2015-05-09 01:29:23 +02:00
|
|
|
return s.URL, s.Close
|
2015-05-07 22:16:52 +02:00
|
|
|
}
|
|
|
|
|
2015-06-16 01:10:48 +02:00
|
|
|
// ping pings the provided endpoint to determine its required authorization challenges.
|
|
|
|
// If a version header is provided, the versions will be returned.
|
2016-11-08 02:13:56 +01:00
|
|
|
func ping(manager challenge.Manager, endpoint, versionHeader string) ([]APIVersion, error) {
|
2015-06-16 01:10:48 +02:00
|
|
|
resp, err := http.Get(endpoint)
|
|
|
|
if err != nil {
|
2015-06-30 19:56:29 +02:00
|
|
|
return nil, err
|
2015-06-16 01:10:48 +02:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2015-06-30 19:56:29 +02:00
|
|
|
if err := manager.AddResponse(resp); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return APIVersions(resp, versionHeader), err
|
2015-06-16 01:10:48 +02:00
|
|
|
}
|
|
|
|
|
2015-05-07 22:16:52 +02:00
|
|
|
type testCredentialStore struct {
|
2016-03-04 09:34:17 +01:00
|
|
|
username string
|
|
|
|
password string
|
|
|
|
refreshTokens map[string]string
|
2015-05-07 22:16:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (tcs *testCredentialStore) Basic(*url.URL) (string, string) {
|
|
|
|
return tcs.username, tcs.password
|
|
|
|
}
|
|
|
|
|
2016-03-04 09:34:17 +01:00
|
|
|
func (tcs *testCredentialStore) RefreshToken(u *url.URL, service string) string {
|
|
|
|
return tcs.refreshTokens[service]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tcs *testCredentialStore) SetRefreshToken(u *url.URL, service string, token string) {
|
|
|
|
if tcs.refreshTokens != nil {
|
|
|
|
tcs.refreshTokens[service] = token
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-07 22:16:52 +02:00
|
|
|
func TestEndpointAuthorizeToken(t *testing.T) {
|
|
|
|
service := "localhost.localdomain"
|
|
|
|
repo1 := "some/registry"
|
|
|
|
repo2 := "other/registry"
|
|
|
|
scope1 := fmt.Sprintf("repository:%s:pull,push", repo1)
|
|
|
|
scope2 := fmt.Sprintf("repository:%s:pull,push", repo2)
|
|
|
|
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope1), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"token":"statictoken"}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope2), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"token":"badtoken"}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
te, tc := testServer(tokenMap)
|
|
|
|
defer tc()
|
|
|
|
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
2015-05-09 01:29:23 +02:00
|
|
|
Route: "/v2/hello",
|
2015-05-07 22:16:52 +02:00
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
2015-05-07 22:16:52 +02:00
|
|
|
validCheck := func(a string) bool {
|
|
|
|
return a == "Bearer statictoken"
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate, validCheck)
|
|
|
|
defer c()
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager1 := challenge.NewSimpleManager()
|
2015-06-30 19:56:29 +02:00
|
|
|
versions, err := ping(challengeManager1, e+"/v2/", "x-api-version")
|
2015-05-21 20:14:46 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-06-16 01:10:48 +02:00
|
|
|
if len(versions) != 1 {
|
|
|
|
t.Fatalf("Unexpected version count: %d, expected 1", len(versions))
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
|
|
|
}
|
2015-06-30 19:56:29 +02:00
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, nil, repo1, "pull", "push")))
|
2015-05-14 18:54:23 +02:00
|
|
|
client := &http.Client{Transport: transport1}
|
2015-05-07 22:16:52 +02:00
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
2015-05-07 22:16:52 +02:00
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
|
2016-03-04 09:34:17 +01:00
|
|
|
e2, c2 := testServerWithAuth(m, authenicate, validCheck)
|
2015-05-07 22:16:52 +02:00
|
|
|
defer c2()
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager2 := challenge.NewSimpleManager()
|
2016-03-04 09:34:17 +01:00
|
|
|
versions, err = ping(challengeManager2, e2+"/v2/", "x-multi-api-version")
|
2015-05-21 20:14:46 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-06-16 01:10:48 +02:00
|
|
|
if len(versions) != 3 {
|
|
|
|
t.Fatalf("Unexpected version count: %d, expected 3", len(versions))
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.1"}); versions[1] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[1], check)
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "trust", Version: "1.0"}); versions[2] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[2], check)
|
|
|
|
}
|
2015-06-30 19:56:29 +02:00
|
|
|
transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, nil, repo2, "pull", "push")))
|
2015-05-14 18:54:23 +02:00
|
|
|
client2 := &http.Client{Transport: transport2}
|
2015-05-07 22:16:52 +02:00
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
req, _ = http.NewRequest("GET", e2+"/v2/hello", nil)
|
2015-05-07 22:16:52 +02:00
|
|
|
resp, err = client2.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-04 09:34:17 +01:00
|
|
|
func TestEndpointAuthorizeRefreshToken(t *testing.T) {
|
|
|
|
service := "localhost.localdomain"
|
|
|
|
repo1 := "some/registry"
|
|
|
|
repo2 := "other/registry"
|
|
|
|
scope1 := fmt.Sprintf("repository:%s:pull,push", repo1)
|
|
|
|
scope2 := fmt.Sprintf("repository:%s:pull,push", repo2)
|
|
|
|
refreshToken1 := "0123456790abcdef"
|
|
|
|
refreshToken2 := "0123456790fedcba"
|
|
|
|
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "POST",
|
|
|
|
Route: "/token",
|
2016-03-04 20:32:48 +01:00
|
|
|
Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)),
|
2016-03-04 09:34:17 +01:00
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken1)),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// In the future this test may fail and require using basic auth to get a different refresh token
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "POST",
|
|
|
|
Route: "/token",
|
2016-03-04 20:32:48 +01:00
|
|
|
Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope2), service)),
|
2016-03-04 09:34:17 +01:00
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken2)),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "POST",
|
|
|
|
Route: "/token",
|
2016-03-04 20:32:48 +01:00
|
|
|
Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken2, url.QueryEscape(scope2), service)),
|
2016-03-04 09:34:17 +01:00
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"access_token":"badtoken","refresh_token":"%s"}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
te, tc := testServer(tokenMap)
|
|
|
|
defer tc()
|
|
|
|
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
|
|
|
validCheck := func(a string) bool {
|
|
|
|
return a == "Bearer statictoken"
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate, validCheck)
|
|
|
|
defer c()
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager1 := challenge.NewSimpleManager()
|
2016-03-04 09:34:17 +01:00
|
|
|
versions, err := ping(challengeManager1, e+"/v2/", "x-api-version")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if len(versions) != 1 {
|
|
|
|
t.Fatalf("Unexpected version count: %d, expected 1", len(versions))
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
|
|
|
}
|
|
|
|
creds := &testCredentialStore{
|
|
|
|
refreshTokens: map[string]string{
|
|
|
|
service: refreshToken1,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, creds, repo1, "pull", "push")))
|
|
|
|
client := &http.Client{Transport: transport1}
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try with refresh token setting
|
|
|
|
e2, c2 := testServerWithAuth(m, authenicate, validCheck)
|
|
|
|
defer c2()
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager2 := challenge.NewSimpleManager()
|
2016-03-04 09:34:17 +01:00
|
|
|
versions, err = ping(challengeManager2, e2+"/v2/", "x-api-version")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if len(versions) != 1 {
|
|
|
|
t.Fatalf("Unexpected version count: %d, expected 1", len(versions))
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
|
|
|
}
|
|
|
|
|
|
|
|
transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, creds, repo2, "pull", "push")))
|
|
|
|
client2 := &http.Client{Transport: transport2}
|
|
|
|
|
|
|
|
req, _ = http.NewRequest("GET", e2+"/v2/hello", nil)
|
|
|
|
resp, err = client2.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized)
|
|
|
|
}
|
|
|
|
|
|
|
|
if creds.refreshTokens[service] != refreshToken2 {
|
|
|
|
t.Fatalf("Refresh token not set after change")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try with bad token
|
|
|
|
e3, c3 := testServerWithAuth(m, authenicate, validCheck)
|
|
|
|
defer c3()
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager3 := challenge.NewSimpleManager()
|
2016-03-04 09:34:17 +01:00
|
|
|
versions, err = ping(challengeManager3, e3+"/v2/", "x-api-version")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
|
|
|
}
|
|
|
|
|
|
|
|
transport3 := transport.NewTransport(nil, NewAuthorizer(challengeManager3, NewTokenHandler(nil, creds, repo2, "pull", "push")))
|
|
|
|
client3 := &http.Client{Transport: transport3}
|
|
|
|
|
|
|
|
req, _ = http.NewRequest("GET", e3+"/v2/hello", nil)
|
|
|
|
resp, err = client3.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-13 02:15:56 +02:00
|
|
|
func TestEndpointAuthorizeV2RefreshToken(t *testing.T) {
|
|
|
|
service := "localhost.localdomain"
|
|
|
|
scope1 := "registry:catalog:search"
|
|
|
|
refreshToken1 := "0123456790abcdef"
|
|
|
|
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "POST",
|
|
|
|
Route: "/token",
|
|
|
|
Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken1)),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
te, tc := testServer(tokenMap)
|
|
|
|
defer tc()
|
|
|
|
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v1/search",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
|
|
|
validCheck := func(a string) bool {
|
|
|
|
return a == "Bearer statictoken"
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate, validCheck)
|
|
|
|
defer c()
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager1 := challenge.NewSimpleManager()
|
2016-07-13 02:15:56 +02:00
|
|
|
versions, err := ping(challengeManager1, e+"/v2/", "x-api-version")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if len(versions) != 1 {
|
|
|
|
t.Fatalf("Unexpected version count: %d, expected 1", len(versions))
|
|
|
|
}
|
|
|
|
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
|
|
|
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
|
|
|
}
|
|
|
|
tho := TokenHandlerOptions{
|
|
|
|
Credentials: &testCredentialStore{
|
|
|
|
refreshTokens: map[string]string{
|
|
|
|
service: refreshToken1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Scopes: []Scope{
|
|
|
|
RegistryScope{
|
|
|
|
Name: "catalog",
|
|
|
|
Actions: []string{"search"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandlerWithOptions(tho)))
|
|
|
|
client := &http.Client{Transport: transport1}
|
|
|
|
|
|
|
|
req, _ := http.NewRequest("GET", e+"/v1/search", nil)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-07 22:16:52 +02:00
|
|
|
func basicAuth(username, password string) string {
|
|
|
|
auth := username + ":" + password
|
|
|
|
return base64.StdEncoding.EncodeToString([]byte(auth))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestEndpointAuthorizeTokenBasic(t *testing.T) {
|
|
|
|
service := "localhost.localdomain"
|
|
|
|
repo := "some/fun/registry"
|
|
|
|
scope := fmt.Sprintf("repository:%s:pull,push", repo)
|
|
|
|
username := "tokenuser"
|
|
|
|
password := "superSecretPa$$word"
|
|
|
|
|
|
|
|
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
2015-09-30 17:47:01 +02:00
|
|
|
Body: []byte(`{"access_token":"statictoken"}`),
|
2015-05-07 22:16:52 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2020-03-18 23:02:24 +01:00
|
|
|
authenicate1 := "Basic realm=localhost"
|
2015-05-07 22:16:52 +02:00
|
|
|
basicCheck := func(a string) bool {
|
|
|
|
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
|
|
|
}
|
|
|
|
te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
|
|
|
|
defer tc()
|
|
|
|
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
2015-05-09 01:29:23 +02:00
|
|
|
Route: "/v2/hello",
|
2015-05-07 22:16:52 +02:00
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
2015-05-07 22:16:52 +02:00
|
|
|
bearerCheck := func(a string) bool {
|
|
|
|
return a == "Bearer statictoken"
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate2, bearerCheck)
|
|
|
|
defer c()
|
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
creds := &testCredentialStore{
|
2015-05-07 22:16:52 +02:00
|
|
|
username: username,
|
|
|
|
password: password,
|
|
|
|
}
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager := challenge.NewSimpleManager()
|
2015-06-30 19:56:29 +02:00
|
|
|
_, err := ping(challengeManager, e+"/v2/", "")
|
2015-05-21 20:14:46 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-06-30 19:56:29 +02:00
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewTokenHandler(nil, creds, repo, "pull", "push"), NewBasicHandler(creds)))
|
2015-05-14 18:54:23 +02:00
|
|
|
client := &http.Client{Transport: transport1}
|
2015-05-07 22:16:52 +02:00
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
2015-05-07 22:16:52 +02:00
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-30 17:47:01 +02:00
|
|
|
func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) {
|
|
|
|
service := "localhost.localdomain"
|
|
|
|
repo := "some/fun/registry"
|
|
|
|
scope := fmt.Sprintf("repository:%s:pull,push", repo)
|
|
|
|
username := "tokenuser"
|
|
|
|
password := "superSecretPa$$word"
|
|
|
|
|
|
|
|
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"token":"statictoken", "expires_in": 3001}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"access_token":"statictoken", "expires_in": 3001}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2020-03-18 23:02:24 +01:00
|
|
|
authenicate1 := "Basic realm=localhost"
|
2015-09-30 17:47:01 +02:00
|
|
|
tokenExchanges := 0
|
|
|
|
basicCheck := func(a string) bool {
|
|
|
|
tokenExchanges = tokenExchanges + 1
|
|
|
|
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
|
|
|
}
|
|
|
|
te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
|
|
|
|
defer tc()
|
|
|
|
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
|
|
|
bearerCheck := func(a string) bool {
|
|
|
|
return a == "Bearer statictoken"
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate2, bearerCheck)
|
|
|
|
defer c()
|
|
|
|
|
|
|
|
creds := &testCredentialStore{
|
|
|
|
username: username,
|
|
|
|
password: password,
|
|
|
|
}
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager := challenge.NewSimpleManager()
|
2015-09-30 17:47:01 +02:00
|
|
|
_, err := ping(challengeManager, e+"/v2/", "")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
clock := &fakeClock{current: time.Now()}
|
2016-03-04 20:32:48 +01:00
|
|
|
options := TokenHandlerOptions{
|
|
|
|
Transport: nil,
|
|
|
|
Credentials: creds,
|
|
|
|
Scopes: []Scope{
|
|
|
|
RepositoryScope{
|
|
|
|
Repository: repo,
|
|
|
|
Actions: []string{"pull", "push"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
tHandler := NewTokenHandlerWithOptions(options)
|
|
|
|
tHandler.(*tokenHandler).clock = clock
|
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, tHandler, NewBasicHandler(creds)))
|
2015-09-30 17:47:01 +02:00
|
|
|
client := &http.Client{Transport: transport1}
|
|
|
|
|
|
|
|
// First call should result in a token exchange
|
|
|
|
// Subsequent calls should recycle the token from the first request, until the expiration has lapsed.
|
|
|
|
timeIncrement := 1000 * time.Second
|
|
|
|
for i := 0; i < 4; i++ {
|
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
if tokenExchanges != 1 {
|
|
|
|
t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i)
|
|
|
|
}
|
|
|
|
clock.current = clock.current.Add(timeIncrement)
|
|
|
|
}
|
|
|
|
|
|
|
|
// After we've exceeded the expiration, we should see a second token exchange.
|
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
if tokenExchanges != 2 {
|
|
|
|
t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) {
|
|
|
|
service := "localhost.localdomain"
|
|
|
|
repo := "some/fun/registry"
|
|
|
|
scope := fmt.Sprintf("repository:%s:pull,push", repo)
|
|
|
|
username := "tokenuser"
|
|
|
|
password := "superSecretPa$$word"
|
|
|
|
|
|
|
|
// This test sets things up such that the token was issued one increment
|
|
|
|
// earlier than its sibling in TestEndpointAuthorizeTokenBasicWithExpiresIn.
|
|
|
|
// This will mean that the token expires after 3 increments instead of 4.
|
|
|
|
clock := &fakeClock{current: time.Now()}
|
|
|
|
timeIncrement := 1000 * time.Second
|
|
|
|
firstIssuedAt := clock.Now()
|
|
|
|
clock.current = clock.current.Add(timeIncrement)
|
|
|
|
secondIssuedAt := clock.current.Add(2 * timeIncrement)
|
|
|
|
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"token":"statictoken", "issued_at": "` + firstIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
Body: []byte(`{"access_token":"statictoken", "issued_at": "` + secondIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2020-03-18 23:02:24 +01:00
|
|
|
authenicate1 := "Basic realm=localhost"
|
2015-09-30 17:47:01 +02:00
|
|
|
tokenExchanges := 0
|
|
|
|
basicCheck := func(a string) bool {
|
|
|
|
tokenExchanges = tokenExchanges + 1
|
|
|
|
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
|
|
|
}
|
|
|
|
te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
|
|
|
|
defer tc()
|
|
|
|
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
|
|
|
Route: "/v2/hello",
|
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
|
|
|
bearerCheck := func(a string) bool {
|
|
|
|
return a == "Bearer statictoken"
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate2, bearerCheck)
|
|
|
|
defer c()
|
|
|
|
|
|
|
|
creds := &testCredentialStore{
|
|
|
|
username: username,
|
|
|
|
password: password,
|
|
|
|
}
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager := challenge.NewSimpleManager()
|
2015-09-30 17:47:01 +02:00
|
|
|
_, err := ping(challengeManager, e+"/v2/", "")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2016-03-04 20:32:48 +01:00
|
|
|
|
|
|
|
options := TokenHandlerOptions{
|
|
|
|
Transport: nil,
|
|
|
|
Credentials: creds,
|
|
|
|
Scopes: []Scope{
|
|
|
|
RepositoryScope{
|
|
|
|
Repository: repo,
|
|
|
|
Actions: []string{"pull", "push"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
tHandler := NewTokenHandlerWithOptions(options)
|
|
|
|
tHandler.(*tokenHandler).clock = clock
|
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, tHandler, NewBasicHandler(creds)))
|
2015-09-30 17:47:01 +02:00
|
|
|
client := &http.Client{Transport: transport1}
|
|
|
|
|
|
|
|
// First call should result in a token exchange
|
|
|
|
// Subsequent calls should recycle the token from the first request, until the expiration has lapsed.
|
|
|
|
// We shaved one increment off of the equivalent logic in TestEndpointAuthorizeTokenBasicWithExpiresIn
|
|
|
|
// so this loop should have one fewer iteration.
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
if tokenExchanges != 1 {
|
|
|
|
t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i)
|
|
|
|
}
|
|
|
|
clock.current = clock.current.Add(timeIncrement)
|
|
|
|
}
|
|
|
|
|
|
|
|
// After we've exceeded the expiration, we should see a second token exchange.
|
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
if tokenExchanges != 2 {
|
|
|
|
t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-07 22:16:52 +02:00
|
|
|
func TestEndpointAuthorizeBasic(t *testing.T) {
|
|
|
|
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
|
|
|
{
|
|
|
|
Request: testutil.Request{
|
|
|
|
Method: "GET",
|
2015-05-09 01:29:23 +02:00
|
|
|
Route: "/v2/hello",
|
2015-05-07 22:16:52 +02:00
|
|
|
},
|
|
|
|
Response: testutil.Response{
|
|
|
|
StatusCode: http.StatusAccepted,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
username := "user1"
|
|
|
|
password := "funSecretPa$$word"
|
2020-03-18 23:02:24 +01:00
|
|
|
authenicate := "Basic realm=localhost"
|
2015-05-07 22:16:52 +02:00
|
|
|
validCheck := func(a string) bool {
|
|
|
|
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
|
|
|
}
|
|
|
|
e, c := testServerWithAuth(m, authenicate, validCheck)
|
|
|
|
defer c()
|
2015-05-09 01:29:23 +02:00
|
|
|
creds := &testCredentialStore{
|
2015-05-07 22:16:52 +02:00
|
|
|
username: username,
|
|
|
|
password: password,
|
|
|
|
}
|
|
|
|
|
2016-11-08 02:13:56 +01:00
|
|
|
challengeManager := challenge.NewSimpleManager()
|
2015-06-30 19:56:29 +02:00
|
|
|
_, err := ping(challengeManager, e+"/v2/", "")
|
2015-05-21 20:14:46 +02:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2015-06-30 19:56:29 +02:00
|
|
|
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewBasicHandler(creds)))
|
2015-05-14 18:54:23 +02:00
|
|
|
client := &http.Client{Transport: transport1}
|
2015-05-07 22:16:52 +02:00
|
|
|
|
2015-05-09 01:29:23 +02:00
|
|
|
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
2015-05-07 22:16:52 +02:00
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error sending get request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusAccepted {
|
|
|
|
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
|
|
|
}
|
|
|
|
}
|