Skip to content

Commit ba35704

Browse files
authored
Merge pull request kubernetes#81443 from mikedanese/socks5
rest.Config: support configuring an explict proxy URL
2 parents 423c17d + 652a48d commit ba35704

File tree

19 files changed

+309
-43
lines changed

19 files changed

+309
-43
lines changed

pkg/kubelet/server/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ func TestServeExecInContainerIdleTimeout(t *testing.T) {
10341034

10351035
url := fw.testHTTPServer.URL + "/exec/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?c=ls&c=-a&" + api.ExecStdinParam + "=1"
10361036

1037-
upgradeRoundTripper := spdy.NewSpdyRoundTripper(nil, true, true)
1037+
upgradeRoundTripper := spdy.NewRoundTripper(nil, true, true)
10381038
c := &http.Client{Transport: upgradeRoundTripper}
10391039

10401040
resp, err := c.Do(makeReq(t, "POST", url, "v4.channel.k8s.io"))

staging/src/k8s.io/apimachinery/pkg/util/httpstream/spdy/roundtripper.go

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,20 @@ var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
7676
var _ httpstream.UpgradeRoundTripper = &SpdyRoundTripper{}
7777
var _ utilnet.Dialer = &SpdyRoundTripper{}
7878

79-
// NewRoundTripper creates a new SpdyRoundTripper that will use
80-
// the specified tlsConfig.
81-
func NewRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) httpstream.UpgradeRoundTripper {
82-
return NewSpdyRoundTripper(tlsConfig, followRedirects, requireSameHostRedirects)
79+
// NewRoundTripper creates a new SpdyRoundTripper that will use the specified
80+
// tlsConfig.
81+
func NewRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) *SpdyRoundTripper {
82+
return NewRoundTripperWithProxy(tlsConfig, followRedirects, requireSameHostRedirects, utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment))
8383
}
8484

85-
// NewSpdyRoundTripper creates a new SpdyRoundTripper that will use
86-
// the specified tlsConfig. This function is mostly meant for unit tests.
87-
func NewSpdyRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) *SpdyRoundTripper {
85+
// NewRoundTripperWithProxy creates a new SpdyRoundTripper that will use the
86+
// specified tlsConfig and proxy func.
87+
func NewRoundTripperWithProxy(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool, proxier func(*http.Request) (*url.URL, error)) *SpdyRoundTripper {
8888
return &SpdyRoundTripper{
8989
tlsConfig: tlsConfig,
9090
followRedirects: followRedirects,
9191
requireSameHostRedirects: requireSameHostRedirects,
92+
proxier: proxier,
9293
}
9394
}
9495

@@ -116,11 +117,7 @@ func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) {
116117
// dial dials the host specified by req, using TLS if appropriate, optionally
117118
// using a proxy server if one is configured via environment variables.
118119
func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) {
119-
proxier := s.proxier
120-
if proxier == nil {
121-
proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
122-
}
123-
proxyURL, err := proxier(req)
120+
proxyURL, err := s.proxier(req)
124121
if err != nil {
125122
return nil, err
126123
}

staging/src/k8s.io/apimachinery/pkg/util/httpstream/spdy/roundtripper_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ func TestRoundTripAndNewConnection(t *testing.T) {
282282
t.Fatalf("%s: Error creating request: %s", k, err)
283283
}
284284

285-
spdyTransport := NewSpdyRoundTripper(testCase.clientTLS, redirect, redirect)
285+
spdyTransport := NewRoundTripper(testCase.clientTLS, redirect, redirect)
286286

287287
var proxierCalled bool
288288
var proxyCalledWithHost string
@@ -425,7 +425,7 @@ func TestRoundTripRedirects(t *testing.T) {
425425
t.Fatalf("Error creating request: %s", err)
426426
}
427427

428-
spdyTransport := NewSpdyRoundTripper(nil, true, true)
428+
spdyTransport := NewRoundTripper(nil, true, true)
429429
client := &http.Client{Transport: spdyTransport}
430430

431431
resp, err := client.Do(req)

staging/src/k8s.io/client-go/rest/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ go_test(
3838
"//staging/src/k8s.io/client-go/transport:go_default_library",
3939
"//staging/src/k8s.io/client-go/util/flowcontrol:go_default_library",
4040
"//staging/src/k8s.io/client-go/util/testing:go_default_library",
41+
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
4142
"//vendor/github.com/google/gofuzz:go_default_library",
4243
"//vendor/github.com/stretchr/testify/assert:go_default_library",
4344
"//vendor/k8s.io/klog:go_default_library",

staging/src/k8s.io/client-go/rest/client_test.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ package rest
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"net/http"
2223
"net/http/httptest"
24+
"net/http/httputil"
2325
"net/url"
2426
"os"
2527
"reflect"
2628
"testing"
2729
"time"
2830

29-
"fmt"
30-
3131
v1 "k8s.io/api/core/v1"
3232
v1beta1 "k8s.io/api/extensions/v1beta1"
3333
"k8s.io/apimachinery/pkg/api/errors"
@@ -37,6 +37,8 @@ import (
3737
"k8s.io/apimachinery/pkg/util/diff"
3838
"k8s.io/client-go/kubernetes/scheme"
3939
utiltesting "k8s.io/client-go/util/testing"
40+
41+
"github.com/google/go-cmp/cmp"
4042
)
4143

4244
type TestParam struct {
@@ -252,7 +254,7 @@ func validate(testParam TestParam, t *testing.T, body []byte, fakeHandler *utilt
252254

253255
}
254256

255-
func TestHttpMethods(t *testing.T) {
257+
func TestHTTPMethods(t *testing.T) {
256258
testServer, _, _ := testServerEnv(t, 200)
257259
defer testServer.Close()
258260
c, _ := restClient(testServer)
@@ -283,6 +285,57 @@ func TestHttpMethods(t *testing.T) {
283285
}
284286
}
285287

288+
func TestHTTPProxy(t *testing.T) {
289+
ctx := context.Background()
290+
testServer, fh, _ := testServerEnv(t, 200)
291+
fh.ResponseBody = "backend data"
292+
defer testServer.Close()
293+
294+
testProxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
295+
to, err := url.Parse(req.RequestURI)
296+
if err != nil {
297+
t.Fatalf("err: %v", err)
298+
}
299+
w.Write([]byte("proxied: "))
300+
httputil.NewSingleHostReverseProxy(to).ServeHTTP(w, req)
301+
}))
302+
defer testProxyServer.Close()
303+
304+
t.Logf(testProxyServer.URL)
305+
306+
u, err := url.Parse(testProxyServer.URL)
307+
if err != nil {
308+
t.Fatalf("Failed to parse test proxy server url: %v", err)
309+
}
310+
311+
c, err := RESTClientFor(&Config{
312+
Host: testServer.URL,
313+
ContentConfig: ContentConfig{
314+
GroupVersion: &v1.SchemeGroupVersion,
315+
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
316+
},
317+
Proxy: http.ProxyURL(u),
318+
Username: "user",
319+
Password: "pass",
320+
})
321+
if err != nil {
322+
t.Fatalf("Failed to create client: %v", err)
323+
}
324+
325+
request := c.Get()
326+
if request == nil {
327+
t.Fatalf("Get: Object returned should not be nil")
328+
}
329+
330+
b, err := request.DoRaw(ctx)
331+
if err != nil {
332+
t.Fatalf("unexpected err: %v", err)
333+
}
334+
if got, want := string(b), "proxied: backend data"; !cmp.Equal(got, want) {
335+
t.Errorf("unexpected body: %v", cmp.Diff(want, got))
336+
}
337+
}
338+
286339
func TestCreateBackoffManager(t *testing.T) {
287340

288341
theUrl, _ := url.Parse("http://localhost")

staging/src/k8s.io/client-go/rest/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io/ioutil"
2424
"net"
2525
"net/http"
26+
"net/url"
2627
"os"
2728
"path/filepath"
2829
gruntime "runtime"
@@ -128,6 +129,13 @@ type Config struct {
128129
// Dial specifies the dial function for creating unencrypted TCP connections.
129130
Dial func(ctx context.Context, network, address string) (net.Conn, error)
130131

132+
// Proxy is the the proxy func to be used for all requests made by this
133+
// transport. If Proxy is nil, http.ProxyFromEnvironment is used. If Proxy
134+
// returns a nil *URL, no proxy is used.
135+
//
136+
// socks5 proxying does not currently support spdy streaming endpoints.
137+
Proxy func(*http.Request) (*url.URL, error)
138+
131139
// Version forces a specific version to be used (if registered)
132140
// Do we need this?
133141
// Version string
@@ -560,6 +568,7 @@ func AnonymousClientConfig(config *Config) *Config {
560568
Burst: config.Burst,
561569
Timeout: config.Timeout,
562570
Dial: config.Dial,
571+
Proxy: config.Proxy,
563572
}
564573
}
565574

@@ -601,5 +610,6 @@ func CopyConfig(config *Config) *Config {
601610
RateLimiter: config.RateLimiter,
602611
Timeout: config.Timeout,
603612
Dial: config.Dial,
613+
Proxy: config.Proxy,
604614
}
605615
}

staging/src/k8s.io/client-go/rest/config_test.go

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io"
2424
"net"
2525
"net/http"
26+
"net/url"
2627
"path/filepath"
2728
"reflect"
2829
"strings"
@@ -32,12 +33,12 @@ import (
3233
v1 "k8s.io/api/core/v1"
3334
"k8s.io/apimachinery/pkg/runtime"
3435
"k8s.io/apimachinery/pkg/runtime/schema"
35-
"k8s.io/apimachinery/pkg/util/diff"
3636
"k8s.io/client-go/kubernetes/scheme"
3737
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
3838
"k8s.io/client-go/transport"
3939
"k8s.io/client-go/util/flowcontrol"
4040

41+
"github.com/google/go-cmp/cmp"
4142
fuzz "github.com/google/gofuzz"
4243
"github.com/stretchr/testify/assert"
4344
)
@@ -274,8 +275,13 @@ func (n *fakeNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder,
274275
var fakeDialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
275276
return nil, fakeDialerError
276277
}
278+
277279
var fakeDialerError = errors.New("fakedialer")
278280

281+
func fakeProxyFunc(*http.Request) (*url.URL, error) {
282+
return nil, errors.New("fakeproxy")
283+
}
284+
279285
type fakeAuthProviderConfigPersister struct{}
280286

281287
func (fakeAuthProviderConfigPersister) Persist(map[string]string) error {
@@ -318,8 +324,12 @@ func TestAnonymousConfig(t *testing.T) {
318324
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
319325
r.Config = map[string]string{}
320326
},
321-
// Dial does not require fuzzer
322-
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {},
327+
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
328+
*r = fakeDialFunc
329+
},
330+
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
331+
*r = fakeProxyFunc
332+
},
323333
)
324334
for i := 0; i < 20; i++ {
325335
original := &Config{}
@@ -350,13 +360,22 @@ func TestAnonymousConfig(t *testing.T) {
350360
if !reflect.DeepEqual(expectedError, actualError) {
351361
t.Fatalf("AnonymousClientConfig dropped the Dial field")
352362
}
353-
} else {
354-
actual.Dial = nil
355-
expected.Dial = nil
356363
}
364+
actual.Dial = nil
365+
expected.Dial = nil
357366

358-
if !reflect.DeepEqual(*actual, expected) {
359-
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectGoPrintDiff(expected, actual))
367+
if actual.Proxy != nil {
368+
_, actualError := actual.Proxy(nil)
369+
_, expectedError := expected.Proxy(nil)
370+
if !reflect.DeepEqual(expectedError, actualError) {
371+
t.Fatalf("AnonymousClientConfig dropped the Proxy field")
372+
}
373+
}
374+
actual.Proxy = nil
375+
expected.Proxy = nil
376+
377+
if diff := cmp.Diff(*actual, expected); diff != "" {
378+
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not (-got, +want): %s", diff)
360379
}
361380
}
362381
}
@@ -396,6 +415,9 @@ func TestCopyConfig(t *testing.T) {
396415
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
397416
*r = fakeDialFunc
398417
},
418+
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
419+
*r = fakeProxyFunc
420+
},
399421
)
400422
for i := 0; i < 20; i++ {
401423
original := &Config{}
@@ -410,10 +432,10 @@ func TestCopyConfig(t *testing.T) {
410432
// function return the expected object.
411433
if actual.WrapTransport == nil || !reflect.DeepEqual(expected.WrapTransport(nil), &fakeRoundTripper{}) {
412434
t.Fatalf("CopyConfig dropped the WrapTransport field")
413-
} else {
414-
actual.WrapTransport = nil
415-
expected.WrapTransport = nil
416435
}
436+
actual.WrapTransport = nil
437+
expected.WrapTransport = nil
438+
417439
if actual.Dial != nil {
418440
_, actualError := actual.Dial(context.Background(), "", "")
419441
_, expectedError := expected.Dial(context.Background(), "", "")
@@ -423,6 +445,7 @@ func TestCopyConfig(t *testing.T) {
423445
}
424446
actual.Dial = nil
425447
expected.Dial = nil
448+
426449
if actual.AuthConfigPersister != nil {
427450
actualError := actual.AuthConfigPersister.Persist(nil)
428451
expectedError := expected.AuthConfigPersister.Persist(nil)
@@ -433,8 +456,18 @@ func TestCopyConfig(t *testing.T) {
433456
actual.AuthConfigPersister = nil
434457
expected.AuthConfigPersister = nil
435458

436-
if !reflect.DeepEqual(*actual, expected) {
437-
t.Fatalf("CopyConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectReflectDiff(expected, *actual))
459+
if actual.Proxy != nil {
460+
_, actualError := actual.Proxy(nil)
461+
_, expectedError := expected.Proxy(nil)
462+
if !reflect.DeepEqual(expectedError, actualError) {
463+
t.Fatalf("CopyConfig dropped the Proxy field")
464+
}
465+
}
466+
actual.Proxy = nil
467+
expected.Proxy = nil
468+
469+
if diff := cmp.Diff(*actual, expected); diff != "" {
470+
t.Fatalf("CopyConfig dropped unexpected fields, identify whether they are security related or not (-got, +want): %s", diff)
438471
}
439472
}
440473
}
@@ -564,10 +597,11 @@ func TestConfigSprint(t *testing.T) {
564597
RateLimiter: &fakeLimiter{},
565598
Timeout: 3 * time.Second,
566599
Dial: fakeDialFunc,
600+
Proxy: fakeProxyFunc,
567601
}
568602
want := fmt.Sprintf(
569-
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p)}`,
570-
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc,
603+
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`,
604+
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc,
571605
)
572606

573607
for _, f := range []string{"%s", "%v", "%+v", "%#v"} {

staging/src/k8s.io/client-go/rest/transport.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
8585
Groups: c.Impersonate.Groups,
8686
Extra: c.Impersonate.Extra,
8787
},
88-
Dial: c.Dial,
88+
Dial: c.Dial,
89+
Proxy: c.Proxy,
8990
}
9091

9192
if c.ExecProvider != nil && c.AuthProvider != nil {

staging/src/k8s.io/client-go/tools/clientcmd/api/types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ type Cluster struct {
8282
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
8383
// +optional
8484
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
85+
// ProxyURL is the URL to the proxy to be used for all requests made by this
86+
// client. URLs with "http", "https", and "socks5" schemes are supported. If
87+
// this configuration is not provided or the empty string, the client
88+
// attempts to construct a proxy configuration from http_proxy and
89+
// https_proxy environment variables. If these environment variables are not
90+
// set, the client does not attempt to proxy requests.
91+
//
92+
// socks5 proxying does not currently support spdy streaming endpoints (exec,
93+
// attach, port forward).
94+
// +optional
95+
ProxyURL string `json:"proxy-url,omitempty"`
8596
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
8697
// +optional
8798
Extensions map[string]runtime.Object `json:"extensions,omitempty"`

staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ type Cluster struct {
7575
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
7676
// +optional
7777
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
78+
// ProxyURL is the URL to the proxy to be used for all requests made by this
79+
// client. URLs with "http", "https", and "socks5" schemes are supported. If
80+
// this configuration is not provided or the empty string, the client
81+
// attempts to construct a proxy configuration from http_proxy and
82+
// https_proxy environment variables. If these environment variables are not
83+
// set, the client does not attempt to proxy requests.
84+
//
85+
// socks5 proxying does not currently support spdy streaming endpoints (exec,
86+
// attach, port forward).
87+
// +optional
88+
ProxyURL string `json:"proxy-url,omitempty"`
7889
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
7990
// +optional
8091
Extensions []NamedExtension `json:"extensions,omitempty"`

0 commit comments

Comments
 (0)