github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/auth/session_test.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/docker/distribution/registry/client/transport"
    13  	"github.com/docker/distribution/testutil"
    14  )
    15  
    16  // An implementation of clock for providing fake time data.
    17  type fakeClock struct {
    18  	current time.Time
    19  }
    20  
    21  // Now implements clock
    22  func (fc *fakeClock) Now() time.Time { return fc.current }
    23  
    24  func testServer(rrm testutil.RequestResponseMap) (string, func()) {
    25  	h := testutil.NewHandler(rrm)
    26  	s := httptest.NewServer(h)
    27  	return s.URL, s.Close
    28  }
    29  
    30  type testAuthenticationWrapper struct {
    31  	headers   http.Header
    32  	authCheck func(string) bool
    33  	next      http.Handler
    34  }
    35  
    36  func (w *testAuthenticationWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    37  	auth := r.Header.Get("Authorization")
    38  	if auth == "" || !w.authCheck(auth) {
    39  		h := rw.Header()
    40  		for k, values := range w.headers {
    41  			h[k] = values
    42  		}
    43  		rw.WriteHeader(http.StatusUnauthorized)
    44  		return
    45  	}
    46  	w.next.ServeHTTP(rw, r)
    47  }
    48  
    49  func testServerWithAuth(rrm testutil.RequestResponseMap, authenticate string, authCheck func(string) bool) (string, func()) {
    50  	h := testutil.NewHandler(rrm)
    51  	wrapper := &testAuthenticationWrapper{
    52  
    53  		headers: http.Header(map[string][]string{
    54  			"X-API-Version":       {"registry/2.0"},
    55  			"X-Multi-API-Version": {"registry/2.0", "registry/2.1", "trust/1.0"},
    56  			"WWW-Authenticate":    {authenticate},
    57  		}),
    58  		authCheck: authCheck,
    59  		next:      h,
    60  	}
    61  
    62  	s := httptest.NewServer(wrapper)
    63  	return s.URL, s.Close
    64  }
    65  
    66  // ping pings the provided endpoint to determine its required authorization challenges.
    67  // If a version header is provided, the versions will be returned.
    68  func ping(manager ChallengeManager, endpoint, versionHeader string) ([]APIVersion, error) {
    69  	resp, err := http.Get(endpoint)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	defer resp.Body.Close()
    74  
    75  	if err := manager.AddResponse(resp); err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	return APIVersions(resp, versionHeader), err
    80  }
    81  
    82  type testCredentialStore struct {
    83  	username string
    84  	password string
    85  }
    86  
    87  func (tcs *testCredentialStore) Basic(*url.URL) (string, string) {
    88  	return tcs.username, tcs.password
    89  }
    90  
    91  func TestEndpointAuthorizeToken(t *testing.T) {
    92  	service := "localhost.localdomain"
    93  	repo1 := "some/registry"
    94  	repo2 := "other/registry"
    95  	scope1 := fmt.Sprintf("repository:%s:pull,push", repo1)
    96  	scope2 := fmt.Sprintf("repository:%s:pull,push", repo2)
    97  	tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
    98  		{
    99  			Request: testutil.Request{
   100  				Method: "GET",
   101  				Route:  fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope1), service),
   102  			},
   103  			Response: testutil.Response{
   104  				StatusCode: http.StatusOK,
   105  				Body:       []byte(`{"token":"statictoken"}`),
   106  			},
   107  		},
   108  		{
   109  			Request: testutil.Request{
   110  				Method: "GET",
   111  				Route:  fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope2), service),
   112  			},
   113  			Response: testutil.Response{
   114  				StatusCode: http.StatusOK,
   115  				Body:       []byte(`{"token":"badtoken"}`),
   116  			},
   117  		},
   118  	})
   119  	te, tc := testServer(tokenMap)
   120  	defer tc()
   121  
   122  	m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   123  		{
   124  			Request: testutil.Request{
   125  				Method: "GET",
   126  				Route:  "/v2/hello",
   127  			},
   128  			Response: testutil.Response{
   129  				StatusCode: http.StatusAccepted,
   130  			},
   131  		},
   132  	})
   133  
   134  	authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
   135  	validCheck := func(a string) bool {
   136  		return a == "Bearer statictoken"
   137  	}
   138  	e, c := testServerWithAuth(m, authenicate, validCheck)
   139  	defer c()
   140  
   141  	challengeManager1 := NewSimpleChallengeManager()
   142  	versions, err := ping(challengeManager1, e+"/v2/", "x-api-version")
   143  	if err != nil {
   144  		t.Fatal(err)
   145  	}
   146  	if len(versions) != 1 {
   147  		t.Fatalf("Unexpected version count: %d, expected 1", len(versions))
   148  	}
   149  	if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
   150  		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
   151  	}
   152  	transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, nil, repo1, "pull", "push")))
   153  	client := &http.Client{Transport: transport1}
   154  
   155  	req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   156  	resp, err := client.Do(req)
   157  	if err != nil {
   158  		t.Fatalf("Error sending get request: %s", err)
   159  	}
   160  
   161  	if resp.StatusCode != http.StatusAccepted {
   162  		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   163  	}
   164  
   165  	badCheck := func(a string) bool {
   166  		return a == "Bearer statictoken"
   167  	}
   168  	e2, c2 := testServerWithAuth(m, authenicate, badCheck)
   169  	defer c2()
   170  
   171  	challengeManager2 := NewSimpleChallengeManager()
   172  	versions, err = ping(challengeManager2, e+"/v2/", "x-multi-api-version")
   173  	if err != nil {
   174  		t.Fatal(err)
   175  	}
   176  	if len(versions) != 3 {
   177  		t.Fatalf("Unexpected version count: %d, expected 3", len(versions))
   178  	}
   179  	if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
   180  		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
   181  	}
   182  	if check := (APIVersion{Type: "registry", Version: "2.1"}); versions[1] != check {
   183  		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[1], check)
   184  	}
   185  	if check := (APIVersion{Type: "trust", Version: "1.0"}); versions[2] != check {
   186  		t.Fatalf("Unexpected api version: %#v, expected %#v", versions[2], check)
   187  	}
   188  	transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, nil, repo2, "pull", "push")))
   189  	client2 := &http.Client{Transport: transport2}
   190  
   191  	req, _ = http.NewRequest("GET", e2+"/v2/hello", nil)
   192  	resp, err = client2.Do(req)
   193  	if err != nil {
   194  		t.Fatalf("Error sending get request: %s", err)
   195  	}
   196  
   197  	if resp.StatusCode != http.StatusUnauthorized {
   198  		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized)
   199  	}
   200  }
   201  
   202  func basicAuth(username, password string) string {
   203  	auth := username + ":" + password
   204  	return base64.StdEncoding.EncodeToString([]byte(auth))
   205  }
   206  
   207  func TestEndpointAuthorizeTokenBasic(t *testing.T) {
   208  	service := "localhost.localdomain"
   209  	repo := "some/fun/registry"
   210  	scope := fmt.Sprintf("repository:%s:pull,push", repo)
   211  	username := "tokenuser"
   212  	password := "superSecretPa$$word"
   213  
   214  	tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   215  		{
   216  			Request: testutil.Request{
   217  				Method: "GET",
   218  				Route:  fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
   219  			},
   220  			Response: testutil.Response{
   221  				StatusCode: http.StatusOK,
   222  				Body:       []byte(`{"access_token":"statictoken"}`),
   223  			},
   224  		},
   225  	})
   226  
   227  	authenicate1 := fmt.Sprintf("Basic realm=localhost")
   228  	basicCheck := func(a string) bool {
   229  		return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
   230  	}
   231  	te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
   232  	defer tc()
   233  
   234  	m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   235  		{
   236  			Request: testutil.Request{
   237  				Method: "GET",
   238  				Route:  "/v2/hello",
   239  			},
   240  			Response: testutil.Response{
   241  				StatusCode: http.StatusAccepted,
   242  			},
   243  		},
   244  	})
   245  
   246  	authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
   247  	bearerCheck := func(a string) bool {
   248  		return a == "Bearer statictoken"
   249  	}
   250  	e, c := testServerWithAuth(m, authenicate2, bearerCheck)
   251  	defer c()
   252  
   253  	creds := &testCredentialStore{
   254  		username: username,
   255  		password: password,
   256  	}
   257  
   258  	challengeManager := NewSimpleChallengeManager()
   259  	_, err := ping(challengeManager, e+"/v2/", "")
   260  	if err != nil {
   261  		t.Fatal(err)
   262  	}
   263  	transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewTokenHandler(nil, creds, repo, "pull", "push"), NewBasicHandler(creds)))
   264  	client := &http.Client{Transport: transport1}
   265  
   266  	req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   267  	resp, err := client.Do(req)
   268  	if err != nil {
   269  		t.Fatalf("Error sending get request: %s", err)
   270  	}
   271  
   272  	if resp.StatusCode != http.StatusAccepted {
   273  		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   274  	}
   275  }
   276  
   277  func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) {
   278  	service := "localhost.localdomain"
   279  	repo := "some/fun/registry"
   280  	scope := fmt.Sprintf("repository:%s:pull,push", repo)
   281  	username := "tokenuser"
   282  	password := "superSecretPa$$word"
   283  
   284  	tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   285  		{
   286  			Request: testutil.Request{
   287  				Method: "GET",
   288  				Route:  fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
   289  			},
   290  			Response: testutil.Response{
   291  				StatusCode: http.StatusOK,
   292  				Body:       []byte(`{"token":"statictoken", "expires_in": 3001}`),
   293  			},
   294  		},
   295  		{
   296  			Request: testutil.Request{
   297  				Method: "GET",
   298  				Route:  fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
   299  			},
   300  			Response: testutil.Response{
   301  				StatusCode: http.StatusOK,
   302  				Body:       []byte(`{"access_token":"statictoken", "expires_in": 3001}`),
   303  			},
   304  		},
   305  	})
   306  
   307  	authenicate1 := fmt.Sprintf("Basic realm=localhost")
   308  	tokenExchanges := 0
   309  	basicCheck := func(a string) bool {
   310  		tokenExchanges = tokenExchanges + 1
   311  		return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
   312  	}
   313  	te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
   314  	defer tc()
   315  
   316  	m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   317  		{
   318  			Request: testutil.Request{
   319  				Method: "GET",
   320  				Route:  "/v2/hello",
   321  			},
   322  			Response: testutil.Response{
   323  				StatusCode: http.StatusAccepted,
   324  			},
   325  		},
   326  		{
   327  			Request: testutil.Request{
   328  				Method: "GET",
   329  				Route:  "/v2/hello",
   330  			},
   331  			Response: testutil.Response{
   332  				StatusCode: http.StatusAccepted,
   333  			},
   334  		},
   335  		{
   336  			Request: testutil.Request{
   337  				Method: "GET",
   338  				Route:  "/v2/hello",
   339  			},
   340  			Response: testutil.Response{
   341  				StatusCode: http.StatusAccepted,
   342  			},
   343  		},
   344  		{
   345  			Request: testutil.Request{
   346  				Method: "GET",
   347  				Route:  "/v2/hello",
   348  			},
   349  			Response: testutil.Response{
   350  				StatusCode: http.StatusAccepted,
   351  			},
   352  		},
   353  		{
   354  			Request: testutil.Request{
   355  				Method: "GET",
   356  				Route:  "/v2/hello",
   357  			},
   358  			Response: testutil.Response{
   359  				StatusCode: http.StatusAccepted,
   360  			},
   361  		},
   362  	})
   363  
   364  	authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
   365  	bearerCheck := func(a string) bool {
   366  		return a == "Bearer statictoken"
   367  	}
   368  	e, c := testServerWithAuth(m, authenicate2, bearerCheck)
   369  	defer c()
   370  
   371  	creds := &testCredentialStore{
   372  		username: username,
   373  		password: password,
   374  	}
   375  
   376  	challengeManager := NewSimpleChallengeManager()
   377  	_, err := ping(challengeManager, e+"/v2/", "")
   378  	if err != nil {
   379  		t.Fatal(err)
   380  	}
   381  	clock := &fakeClock{current: time.Now()}
   382  	transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds)))
   383  	client := &http.Client{Transport: transport1}
   384  
   385  	// First call should result in a token exchange
   386  	// Subsequent calls should recycle the token from the first request, until the expiration has lapsed.
   387  	timeIncrement := 1000 * time.Second
   388  	for i := 0; i < 4; i++ {
   389  		req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   390  		resp, err := client.Do(req)
   391  		if err != nil {
   392  			t.Fatalf("Error sending get request: %s", err)
   393  		}
   394  		if resp.StatusCode != http.StatusAccepted {
   395  			t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   396  		}
   397  		if tokenExchanges != 1 {
   398  			t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i)
   399  		}
   400  		clock.current = clock.current.Add(timeIncrement)
   401  	}
   402  
   403  	// After we've exceeded the expiration, we should see a second token exchange.
   404  	req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   405  	resp, err := client.Do(req)
   406  	if err != nil {
   407  		t.Fatalf("Error sending get request: %s", err)
   408  	}
   409  	if resp.StatusCode != http.StatusAccepted {
   410  		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   411  	}
   412  	if tokenExchanges != 2 {
   413  		t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges)
   414  	}
   415  }
   416  
   417  func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) {
   418  	service := "localhost.localdomain"
   419  	repo := "some/fun/registry"
   420  	scope := fmt.Sprintf("repository:%s:pull,push", repo)
   421  	username := "tokenuser"
   422  	password := "superSecretPa$$word"
   423  
   424  	// This test sets things up such that the token was issued one increment
   425  	// earlier than its sibling in TestEndpointAuthorizeTokenBasicWithExpiresIn.
   426  	// This will mean that the token expires after 3 increments instead of 4.
   427  	clock := &fakeClock{current: time.Now()}
   428  	timeIncrement := 1000 * time.Second
   429  	firstIssuedAt := clock.Now()
   430  	clock.current = clock.current.Add(timeIncrement)
   431  	secondIssuedAt := clock.current.Add(2 * timeIncrement)
   432  	tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   433  		{
   434  			Request: testutil.Request{
   435  				Method: "GET",
   436  				Route:  fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
   437  			},
   438  			Response: testutil.Response{
   439  				StatusCode: http.StatusOK,
   440  				Body:       []byte(`{"token":"statictoken", "issued_at": "` + firstIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`),
   441  			},
   442  		},
   443  		{
   444  			Request: testutil.Request{
   445  				Method: "GET",
   446  				Route:  fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
   447  			},
   448  			Response: testutil.Response{
   449  				StatusCode: http.StatusOK,
   450  				Body:       []byte(`{"access_token":"statictoken", "issued_at": "` + secondIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`),
   451  			},
   452  		},
   453  	})
   454  
   455  	authenicate1 := fmt.Sprintf("Basic realm=localhost")
   456  	tokenExchanges := 0
   457  	basicCheck := func(a string) bool {
   458  		tokenExchanges = tokenExchanges + 1
   459  		return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
   460  	}
   461  	te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
   462  	defer tc()
   463  
   464  	m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   465  		{
   466  			Request: testutil.Request{
   467  				Method: "GET",
   468  				Route:  "/v2/hello",
   469  			},
   470  			Response: testutil.Response{
   471  				StatusCode: http.StatusAccepted,
   472  			},
   473  		},
   474  		{
   475  			Request: testutil.Request{
   476  				Method: "GET",
   477  				Route:  "/v2/hello",
   478  			},
   479  			Response: testutil.Response{
   480  				StatusCode: http.StatusAccepted,
   481  			},
   482  		},
   483  		{
   484  			Request: testutil.Request{
   485  				Method: "GET",
   486  				Route:  "/v2/hello",
   487  			},
   488  			Response: testutil.Response{
   489  				StatusCode: http.StatusAccepted,
   490  			},
   491  		},
   492  		{
   493  			Request: testutil.Request{
   494  				Method: "GET",
   495  				Route:  "/v2/hello",
   496  			},
   497  			Response: testutil.Response{
   498  				StatusCode: http.StatusAccepted,
   499  			},
   500  		},
   501  	})
   502  
   503  	authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
   504  	bearerCheck := func(a string) bool {
   505  		return a == "Bearer statictoken"
   506  	}
   507  	e, c := testServerWithAuth(m, authenicate2, bearerCheck)
   508  	defer c()
   509  
   510  	creds := &testCredentialStore{
   511  		username: username,
   512  		password: password,
   513  	}
   514  
   515  	challengeManager := NewSimpleChallengeManager()
   516  	_, err := ping(challengeManager, e+"/v2/", "")
   517  	if err != nil {
   518  		t.Fatal(err)
   519  	}
   520  	transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds)))
   521  	client := &http.Client{Transport: transport1}
   522  
   523  	// First call should result in a token exchange
   524  	// Subsequent calls should recycle the token from the first request, until the expiration has lapsed.
   525  	// We shaved one increment off of the equivalent logic in TestEndpointAuthorizeTokenBasicWithExpiresIn
   526  	// so this loop should have one fewer iteration.
   527  	for i := 0; i < 3; i++ {
   528  		req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   529  		resp, err := client.Do(req)
   530  		if err != nil {
   531  			t.Fatalf("Error sending get request: %s", err)
   532  		}
   533  		if resp.StatusCode != http.StatusAccepted {
   534  			t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   535  		}
   536  		if tokenExchanges != 1 {
   537  			t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i)
   538  		}
   539  		clock.current = clock.current.Add(timeIncrement)
   540  	}
   541  
   542  	// After we've exceeded the expiration, we should see a second token exchange.
   543  	req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   544  	resp, err := client.Do(req)
   545  	if err != nil {
   546  		t.Fatalf("Error sending get request: %s", err)
   547  	}
   548  	if resp.StatusCode != http.StatusAccepted {
   549  		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   550  	}
   551  	if tokenExchanges != 2 {
   552  		t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges)
   553  	}
   554  }
   555  
   556  func TestEndpointAuthorizeBasic(t *testing.T) {
   557  	m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
   558  		{
   559  			Request: testutil.Request{
   560  				Method: "GET",
   561  				Route:  "/v2/hello",
   562  			},
   563  			Response: testutil.Response{
   564  				StatusCode: http.StatusAccepted,
   565  			},
   566  		},
   567  	})
   568  
   569  	username := "user1"
   570  	password := "funSecretPa$$word"
   571  	authenicate := fmt.Sprintf("Basic realm=localhost")
   572  	validCheck := func(a string) bool {
   573  		return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
   574  	}
   575  	e, c := testServerWithAuth(m, authenicate, validCheck)
   576  	defer c()
   577  	creds := &testCredentialStore{
   578  		username: username,
   579  		password: password,
   580  	}
   581  
   582  	challengeManager := NewSimpleChallengeManager()
   583  	_, err := ping(challengeManager, e+"/v2/", "")
   584  	if err != nil {
   585  		t.Fatal(err)
   586  	}
   587  	transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewBasicHandler(creds)))
   588  	client := &http.Client{Transport: transport1}
   589  
   590  	req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
   591  	resp, err := client.Do(req)
   592  	if err != nil {
   593  		t.Fatalf("Error sending get request: %s", err)
   594  	}
   595  
   596  	if resp.StatusCode != http.StatusAccepted {
   597  		t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
   598  	}
   599  }