diff --git a/pkg/client/opts.go b/pkg/client/opts.go index 97132f8f..8a998ed1 100644 --- a/pkg/client/opts.go +++ b/pkg/client/opts.go @@ -116,19 +116,19 @@ func defaultClientOptions() *clientOptions { } } +// WithAddress returns option to specify +// network address of the remote server. +// +// Ignored if WithGRPCConnection is provided. func WithAddress(addr string) Option { return func(opts *clientOptions) { opts.rawOpts = append(opts.rawOpts, client.WithNetworkAddress(addr)) } } -func WithGRPCConnection(grpcConn *grpc.ClientConn) Option { - return func(opts *clientOptions) { - opts.rawOpts = append(opts.rawOpts, client.WithGRPCConn(grpcConn)) - } -} - // WithDialTimeout returns option to set connection timeout to the remote node. +// +// Ignored if WithGRPCConn is provided. func WithDialTimeout(dur time.Duration) Option { return func(opts *clientOptions) { opts.rawOpts = append(opts.rawOpts, client.WithDialTimeout(dur)) @@ -136,6 +136,8 @@ func WithDialTimeout(dur time.Duration) Option { } // WithTLSConfig returns option to set connection's TLS config to the remote node. +// +// Ignored if WithGRPCConnection is provided. func WithTLSConfig(cfg *tls.Config) Option { return func(opts *clientOptions) { opts.rawOpts = append(opts.rawOpts, client.WithTLSCfg(cfg)) @@ -149,3 +151,37 @@ func WithDefaultPrivateKey(key *ecdsa.PrivateKey) Option { opts.key = key } } + +// WithURIAddress returns option to specify +// network address of a remote server and connection +// scheme for it. +// +// Format of the URI: +// +// [scheme://]host:port +// +// Supported schemes: +// - grpc; +// - grpcs. +// +// tls.Cfg second argument is optional and is taken into +// account only in case of `grpcs` scheme. +// +// Falls back to WithNetworkAddress if address is not a valid URI. +// +// Do not use along with WithAddress and WithTLSConfig. +// +// Ignored if WithGRPCConnection is provided. +func WithURIAddress(addr string, tlsCfg *tls.Config) Option { + return func(opts *clientOptions) { + opts.rawOpts = append(opts.rawOpts, client.WithNetworkURIAddress(addr, tlsCfg)...) + } +} + +// WithGRPCConnection returns option to set GRPC connection to +// the remote node. +func WithGRPCConnection(grpcConn *grpc.ClientConn) Option { + return func(opts *clientOptions) { + opts.rawOpts = append(opts.rawOpts, client.WithGRPCConn(grpcConn)) + } +} diff --git a/rpc/client/options.go b/rpc/client/options.go index 0dcabfb6..dd89f513 100644 --- a/rpc/client/options.go +++ b/rpc/client/options.go @@ -2,11 +2,17 @@ package client import ( "crypto/tls" + "net/url" "time" "google.golang.org/grpc" ) +const ( + grpcScheme = "grpc" + grpcTLSScheme = "grpcs" +) + // Option is a Client's option. type Option func(*cfg) @@ -40,6 +46,44 @@ func WithNetworkAddress(v string) Option { } } +// WithNetworkURIAddress combines WithNetworkAddress and WithTLSCfg options +// based on arguments. +// +// Do not use along with WithNetworkAddress and WithTLSCfg. +// +// Ignored if WithGRPCConn is provided. +func WithNetworkURIAddress(addr string, tlsCfg *tls.Config) []Option { + uri, err := url.ParseRequestURI(addr) + if err != nil { + return []Option{WithNetworkAddress(addr)} + } + + // check if passed string was parsed correctly + // URIs that do not start with a slash after the scheme are interpreted as: + // `scheme:opaque` => if `opaque` is not empty, then it is supposed that URI + // is in `host:port` format + if uri.Opaque != "" { + return []Option{WithNetworkAddress(addr)} + } + + switch uri.Scheme { + case grpcScheme: + tlsCfg = nil + case grpcTLSScheme: + if tlsCfg == nil { + tlsCfg = &tls.Config{} + } + default: + // not supported scheme + return nil + } + + return []Option{ + WithNetworkAddress(uri.Host), + WithTLSCfg(tlsCfg), + } +} + // WithDialTimeout returns option to specify // dial timeout of the remote server connection. // @@ -58,9 +102,7 @@ func WithDialTimeout(v time.Duration) Option { // Ignored if WithGRPCConn is provided. func WithTLSCfg(v *tls.Config) Option { return func(c *cfg) { - if v != nil { - c.tlsCfg = v - } + c.tlsCfg = v } } diff --git a/rpc/client/options_test.go b/rpc/client/options_test.go new file mode 100644 index 00000000..eeb5a135 --- /dev/null +++ b/rpc/client/options_test.go @@ -0,0 +1,197 @@ +package client + +import ( + "crypto/tls" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWithNetworkURIAddress(t *testing.T) { + hostPort := "neofs.example.com:8080" + apiPort := "127.0.0.1:8080" + serverName := "testServer" + + testCases := []struct { + uri string + tlsConfig *tls.Config + + wantHost string + wantTLS bool + }{ + { + uri: grpcScheme + "://" + hostPort, + tlsConfig: nil, + wantHost: "neofs.example.com:8080", + wantTLS: false, + }, + { + uri: grpcScheme + "://" + hostPort, + tlsConfig: &tls.Config{}, + wantHost: "neofs.example.com:8080", + wantTLS: false, + }, + { + uri: grpcTLSScheme + "://" + hostPort, + tlsConfig: nil, + wantHost: "neofs.example.com:8080", + wantTLS: true, + }, + { + uri: grpcTLSScheme + "://" + hostPort, + tlsConfig: &tls.Config{ServerName: serverName}, + wantHost: "neofs.example.com:8080", + wantTLS: true, + }, + { + uri: "wrongScheme://" + hostPort, + tlsConfig: nil, + wantHost: "", + wantTLS: false, + }, + { + uri: "impossibleToParseIt", + tlsConfig: nil, + wantHost: "impossibleToParseIt", + wantTLS: false, + }, + { + uri: hostPort, + tlsConfig: nil, + wantHost: hostPort, + wantTLS: false, + }, + { + uri: apiPort, + tlsConfig: nil, + wantHost: apiPort, + wantTLS: false, + }, + } + + for _, test := range testCases { + cfg := &cfg{} + opts := WithNetworkURIAddress(test.uri, test.tlsConfig) + + for _, opt := range opts { + opt(cfg) + } + + require.Equal(t, test.wantHost, cfg.addr, test.uri) + require.Equal(t, test.wantTLS, cfg.tlsCfg != nil, test.uri) + // check if custom tlsConfig was applied + if test.tlsConfig != nil && test.wantTLS { + require.Equal(t, test.tlsConfig.ServerName, cfg.tlsCfg.ServerName, test.uri) + } + } +} + +func Test_WithNetworkAddress_WithTLS_WithNetworkURIAddress(t *testing.T) { + addr1, addr2 := "example1.com:8080", "example2.com:8080" + + testCases := []struct { + addr string + withTLS bool + + uri string + + wantHost string + wantTLS bool + }{ + { + addr: addr1, + withTLS: true, + + uri: grpcScheme + "://" + addr2, + + wantHost: addr2, + wantTLS: false, + }, + { + addr: addr1, + withTLS: false, + + uri: grpcTLSScheme + "://" + addr2, + + wantHost: addr2, + wantTLS: true, + }, + } + + for _, test := range testCases { + // order: + // 1. WithNetworkAddress + // 2. WithTLSCfg(if test.withTLS == true) + // 3. WithNetworkURIAddress + config := &cfg{} + opts := []Option{WithNetworkAddress(test.addr)} + + if test.withTLS { + opts = append(opts, WithTLSCfg(&tls.Config{})) + } + + opts = append(opts, WithNetworkURIAddress(test.uri, nil)...) + + for _, opt := range opts { + opt(config) + } + + require.Equal(t, test.wantHost, config.addr, test.addr) + require.Equal(t, test.wantTLS, config.tlsCfg != nil, test.addr) + } +} + +func Test_WithNetworkURIAddress_WithTLS_WithNetworkAddress(t *testing.T) { + addr1, addr2 := "example1.com:8080", "example2.com:8080" + + testCases := []struct { + addr string + withTLS bool + + uri string + + wantHost string + wantTLS bool + }{ + { + uri: grpcScheme + "://" + addr1, + + addr: addr2, + withTLS: true, + + wantHost: addr2, + wantTLS: true, + }, + { + uri: grpcTLSScheme + "://" + addr1, + + addr: addr2, + withTLS: false, + + wantHost: addr2, + wantTLS: true, + }, + } + + for _, test := range testCases { + // order: + // 1. WithNetworkURIAddress + // 2. WithNetworkAddress + // 3. WithTLSCfg(if test.withTLS == true) + config := &cfg{} + opts := WithNetworkURIAddress(test.uri, nil) + + opts = append(opts, WithNetworkAddress(test.addr)) + + if test.withTLS { + opts = append(opts, WithTLSCfg(&tls.Config{})) + } + + for _, opt := range opts { + opt(config) + } + + require.Equal(t, test.wantHost, config.addr, test.uri) + require.Equal(t, test.wantTLS, config.tlsCfg != nil, test.uri) + } +}