2018-11-14 09:18:03 +00:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2019-01-18 14:18:04 +00:00
|
|
|
"errors"
|
2018-11-14 09:18:03 +00:00
|
|
|
"net/http"
|
2019-01-15 13:28:04 +00:00
|
|
|
"net/http/httptest"
|
2018-11-14 09:18:03 +00:00
|
|
|
"testing"
|
|
|
|
|
2019-08-03 01:58:23 +00:00
|
|
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
|
|
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
2020-01-27 09:40:05 +00:00
|
|
|
"github.com/containous/traefik/v2/pkg/server/provider"
|
2019-01-15 13:28:04 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2018-11-14 09:18:03 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
2019-01-18 14:18:04 +00:00
|
|
|
func TestBuilder_BuildChainNilConfig(t *testing.T) {
|
2019-07-15 15:04:04 +00:00
|
|
|
testConfig := map[string]*runtime.MiddlewareInfo{
|
2018-11-14 09:18:03 +00:00
|
|
|
"empty": {},
|
|
|
|
}
|
2020-04-20 16:36:34 +00:00
|
|
|
middlewaresBuilder := NewBuilder(testConfig, nil, nil)
|
2018-11-14 09:18:03 +00:00
|
|
|
|
2019-01-15 13:28:04 +00:00
|
|
|
chain := middlewaresBuilder.BuildChain(context.Background(), []string{"empty"})
|
|
|
|
_, err := chain.Then(nil)
|
2019-01-18 14:18:04 +00:00
|
|
|
require.Error(t, err)
|
2018-11-14 09:18:03 +00:00
|
|
|
}
|
|
|
|
|
2019-01-18 14:18:04 +00:00
|
|
|
func TestBuilder_BuildChainNonExistentChain(t *testing.T) {
|
2019-07-15 15:04:04 +00:00
|
|
|
testConfig := map[string]*runtime.MiddlewareInfo{
|
2019-01-18 14:18:04 +00:00
|
|
|
"foobar": {},
|
|
|
|
}
|
2020-04-20 16:36:34 +00:00
|
|
|
middlewaresBuilder := NewBuilder(testConfig, nil, nil)
|
2019-01-18 14:18:04 +00:00
|
|
|
|
|
|
|
chain := middlewaresBuilder.BuildChain(context.Background(), []string{"empty"})
|
|
|
|
_, err := chain.Then(nil)
|
|
|
|
require.Error(t, err)
|
|
|
|
}
|
|
|
|
|
2019-04-01 13:30:07 +00:00
|
|
|
func TestBuilder_BuildChainWithContext(t *testing.T) {
|
2019-01-15 13:28:04 +00:00
|
|
|
testCases := []struct {
|
|
|
|
desc string
|
|
|
|
buildChain []string
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration map[string]*dynamic.Middleware
|
2019-01-15 13:28:04 +00:00
|
|
|
expected map[string]string
|
|
|
|
contextProvider string
|
|
|
|
expectedError error
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
desc: "Simple middleware",
|
|
|
|
buildChain: []string{"middleware-1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-01-15 13:28:04 +00:00
|
|
|
"middleware-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-01-15 13:28:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expected: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Middleware that references a chain",
|
|
|
|
buildChain: []string{"middleware-chain-1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-01-15 13:28:04 +00:00
|
|
|
"middleware-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-01-15 13:28:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"middleware-chain-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expected: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
{
|
2019-06-21 07:54:04 +00:00
|
|
|
desc: "Should suffix the middlewareName with the provider in the context",
|
2019-01-15 13:28:04 +00:00
|
|
|
buildChain: []string{"middleware-1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-1@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-06-21 07:54:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-1@provider-1": "value-middleware-1"},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
expected: map[string]string{"middleware-1@provider-1": "value-middleware-1"},
|
2019-01-15 13:28:04 +00:00
|
|
|
contextProvider: "provider-1",
|
|
|
|
},
|
|
|
|
{
|
2019-06-21 07:54:04 +00:00
|
|
|
desc: "Should not suffix a qualified middlewareName with the provider in the context",
|
|
|
|
buildChain: []string{"middleware-1@provider-1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-1@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-06-21 07:54:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-1@provider-1": "value-middleware-1"},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
expected: map[string]string{"middleware-1@provider-1": "value-middleware-1"},
|
2019-01-15 13:28:04 +00:00
|
|
|
contextProvider: "provider-1",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Should be context aware if a chain references another middleware",
|
2019-06-21 07:54:04 +00:00
|
|
|
buildChain: []string{"middleware-chain-1@provider-1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-1@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-01-15 13:28:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-chain-1@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expected: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Should handle nested chains with different context",
|
2019-06-21 07:54:04 +00:00
|
|
|
buildChain: []string{"middleware-chain-1@provider-1", "middleware-chain-1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-1@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-01-15 13:28:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-1": "value-middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-2@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Headers: &dynamic.Headers{
|
2019-01-15 13:28:04 +00:00
|
|
|
CustomRequestHeaders: map[string]string{"middleware-2": "value-middleware-2"},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-chain-1@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"middleware-1"},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-chain-2@provider-1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"middleware-2"},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"middleware-chain-1@provider-2": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-06-21 07:54:04 +00:00
|
|
|
Middlewares: []string{"middleware-2@provider-1", "middleware-chain-2@provider-1"},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expected: map[string]string{"middleware-1": "value-middleware-1", "middleware-2": "value-middleware-2"},
|
|
|
|
contextProvider: "provider-2",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Detects recursion in Middleware chain",
|
|
|
|
buildChain: []string{"m1"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-01-15 13:28:04 +00:00
|
|
|
"ok": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Retry: &dynamic.Retry{},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
"m1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m2"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"m2": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"ok", "m3"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"m3": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expectedError: errors.New("could not instantiate middleware m1: recursion detected in m1->m2->m3->m1"),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Detects recursion in Middleware chain",
|
2019-06-21 07:54:04 +00:00
|
|
|
buildChain: []string{"m1@provider"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-06-21 07:54:04 +00:00
|
|
|
"ok@provider2": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Retry: &dynamic.Retry{},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"m1@provider": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-06-21 07:54:04 +00:00
|
|
|
Middlewares: []string{"m2@provider2"},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"m2@provider2": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-06-21 07:54:04 +00:00
|
|
|
Middlewares: []string{"ok", "m3@provider"},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
"m3@provider": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2019-06-21 07:54:04 +00:00
|
|
|
expectedError: errors.New("could not instantiate middleware m1@provider: recursion detected in m1@provider->m2@provider2->m3@provider->m1@provider"),
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
buildChain: []string{"ok", "m0"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-01-15 13:28:04 +00:00
|
|
|
"ok": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Retry: &dynamic.Retry{},
|
2019-01-15 13:28:04 +00:00
|
|
|
},
|
|
|
|
"m0": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m0"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Detects MiddlewareChain that references a Chain that references a Chain with a missing middleware",
|
|
|
|
buildChain: []string{"m0"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-01-15 13:28:04 +00:00
|
|
|
"m0": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m1"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"m1": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m2"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"m2": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m3"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"m3": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m2"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expectedError: errors.New("could not instantiate middleware m2: recursion detected in m0->m1->m2->m3->m2"),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "--",
|
|
|
|
buildChain: []string{"m0"},
|
2019-07-10 07:26:04 +00:00
|
|
|
configuration: map[string]*dynamic.Middleware{
|
2019-01-15 13:28:04 +00:00
|
|
|
"m0": {
|
2019-07-10 07:26:04 +00:00
|
|
|
Chain: &dynamic.Chain{
|
2019-01-15 13:28:04 +00:00
|
|
|
Middlewares: []string{"m0"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expectedError: errors.New("could not instantiate middleware m0: recursion detected in m0->m0"),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range testCases {
|
|
|
|
test := test
|
|
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
if len(test.contextProvider) > 0 {
|
2020-01-27 09:40:05 +00:00
|
|
|
ctx = provider.AddInContext(ctx, "foobar@"+test.contextProvider)
|
2019-01-15 13:28:04 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 15:04:04 +00:00
|
|
|
rtConf := runtime.NewConfig(dynamic.Configuration{
|
2019-07-10 07:26:04 +00:00
|
|
|
HTTP: &dynamic.HTTPConfiguration{
|
2019-05-16 08:58:06 +00:00
|
|
|
Middlewares: test.configuration,
|
|
|
|
},
|
|
|
|
})
|
2020-04-20 16:36:34 +00:00
|
|
|
builder := NewBuilder(rtConf.Middlewares, nil, nil)
|
2019-01-15 13:28:04 +00:00
|
|
|
|
|
|
|
result := builder.BuildChain(ctx, test.buildChain)
|
|
|
|
|
|
|
|
handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
|
|
|
|
if test.expectedError != nil {
|
|
|
|
require.NotNil(t, err)
|
|
|
|
require.Equal(t, test.expectedError.Error(), err.Error())
|
|
|
|
} else {
|
|
|
|
require.NoError(t, err)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
request, _ := http.NewRequest(http.MethodGet, "http://foo/", nil)
|
|
|
|
handlers.ServeHTTP(recorder, request)
|
|
|
|
|
|
|
|
for key, value := range test.expected {
|
|
|
|
assert.Equal(t, value, request.Header.Get(key))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2019-04-01 13:30:07 +00:00
|
|
|
|
|
|
|
func TestBuilder_buildConstructor(t *testing.T) {
|
2019-07-10 07:26:04 +00:00
|
|
|
testConfig := map[string]*dynamic.Middleware{
|
2019-04-01 13:30:07 +00:00
|
|
|
"cb-empty": {
|
2019-07-10 07:26:04 +00:00
|
|
|
CircuitBreaker: &dynamic.CircuitBreaker{
|
2019-04-01 13:30:07 +00:00
|
|
|
Expression: "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"cb-foo": {
|
2019-07-10 07:26:04 +00:00
|
|
|
CircuitBreaker: &dynamic.CircuitBreaker{
|
2019-04-01 13:30:07 +00:00
|
|
|
Expression: "NetworkErrorRatio() > 0.5",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"ap-empty": {
|
2019-07-10 07:26:04 +00:00
|
|
|
AddPrefix: &dynamic.AddPrefix{
|
2019-04-01 13:30:07 +00:00
|
|
|
Prefix: "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
"ap-foo": {
|
2019-07-10 07:26:04 +00:00
|
|
|
AddPrefix: &dynamic.AddPrefix{
|
2019-04-01 13:30:07 +00:00
|
|
|
Prefix: "foo/",
|
|
|
|
},
|
|
|
|
},
|
2019-09-03 13:02:05 +00:00
|
|
|
"buff-foo": {
|
|
|
|
Buffering: &dynamic.Buffering{
|
|
|
|
MaxRequestBodyBytes: 1,
|
|
|
|
MemRequestBodyBytes: 2,
|
|
|
|
MaxResponseBodyBytes: 3,
|
|
|
|
MemResponseBodyBytes: 5,
|
|
|
|
},
|
|
|
|
},
|
2019-04-01 13:30:07 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 15:04:04 +00:00
|
|
|
rtConf := runtime.NewConfig(dynamic.Configuration{
|
2019-07-10 07:26:04 +00:00
|
|
|
HTTP: &dynamic.HTTPConfiguration{
|
2019-05-16 08:58:06 +00:00
|
|
|
Middlewares: testConfig,
|
|
|
|
},
|
|
|
|
})
|
2020-04-20 16:36:34 +00:00
|
|
|
middlewaresBuilder := NewBuilder(rtConf.Middlewares, nil, nil)
|
2019-04-01 13:30:07 +00:00
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
desc string
|
|
|
|
middlewareID string
|
|
|
|
expectedError bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
desc: "Should fail at creating a circuit breaker with an empty expression",
|
|
|
|
middlewareID: "cb-empty",
|
|
|
|
expectedError: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Should create a circuit breaker with a valid expression",
|
|
|
|
middlewareID: "cb-foo",
|
|
|
|
expectedError: false,
|
|
|
|
},
|
2019-09-03 13:02:05 +00:00
|
|
|
{
|
|
|
|
desc: "Should create a buffering middleware",
|
|
|
|
middlewareID: "buff-foo",
|
|
|
|
expectedError: false,
|
|
|
|
},
|
2019-04-01 13:30:07 +00:00
|
|
|
{
|
|
|
|
desc: "Should not create an empty AddPrefix middleware when given an empty prefix",
|
|
|
|
middlewareID: "ap-empty",
|
|
|
|
expectedError: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "Should create an AddPrefix middleware when given a valid configuration",
|
|
|
|
middlewareID: "ap-foo",
|
|
|
|
expectedError: false,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range testCases {
|
|
|
|
test := test
|
|
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2019-05-16 08:58:06 +00:00
|
|
|
constructor, err := middlewaresBuilder.buildConstructor(context.Background(), test.middlewareID)
|
2019-04-01 13:30:07 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
middleware, err2 := constructor(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
|
|
|
|
|
|
|
if test.expectedError {
|
|
|
|
require.Error(t, err2)
|
|
|
|
} else {
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, middleware)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|