remove client option
rabbitprincess committed
Mar 7, 2026 at 21:51 UTC
c7a84388041f52fb0d058351712d935374682460
4 files changed
+180
-39
cmd/demo-app/main.go
+1
-1
@@ -53,7 +53,7 @@ func runDemo() error {
53
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
54
defer stop()
55
56
- sdkClient, err := sdk.NewClient(sdk.ClientConfig{RelayURL: flagServerURL})
56
+ sdkClient, err := sdk.NewClient(flagServerURL)
57
if err != nil {
58
return fmt.Errorf("new client: %w", err)
59
}
cmd/portal-tunnel/relays.go
+1
-1
@@ -75,7 +75,7 @@ func (r *relayRuntime) run(ctx context.Context, localAddr string, connWG *sync.W
75
func startRelayRuntimes(ctx context.Context, relayURLs []string, req sdk.ListenRequest) ([]*relayRuntime, error) {
76
runtimes := make([]*relayRuntime, 0, len(relayURLs))
77
for _, relayURL := range relayURLs {
78
- client, err := sdk.NewClient(sdk.ClientConfig{RelayURL: relayURL})
78
+ client, err := sdk.NewClient(relayURL)
79
if err != nil {
80
_ = closeRelayRuntimes(runtimes)
81
return nil, fmt.Errorf("create relay client %s: %w", relayURL, err)
sdk/client.go
+105
-36
@@ -32,39 +32,114 @@ const (
32
defaultHTTPShutdownTimeout = 5 * time.Second
33
)
34
35
-// ClientConfig configures the SDK client.
36
-type ClientConfig struct {
37
- RelayURL string
38
- RootCAPEM []byte
35
+type ClientOption func(*Client)
36
+
37
+func WithRootCAPEM(rootCAPEM []byte) ClientOption {
38
+ rootCAPEM = append([]byte(nil), rootCAPEM...)
39
+ return func(client *Client) {
40
+ client.rootCAPEM = append([]byte(nil), rootCAPEM...)
41
+ }
42
+}
43
+
44
+func WithInsecureSkipVerify(skip bool) ClientOption {
45
+ return func(client *Client) {
46
+ client.insecureSkipVerify = skip
47
+ }
48
+}
49
+
50
+func WithDialTimeout(timeout time.Duration) ClientOption {
51
+ return func(client *Client) {
52
+ if timeout > 0 {
53
+ client.dialTimeout = timeout
54
+ }
55
+ }
56
+}
57
+
58
+func WithRequestTimeout(timeout time.Duration) ClientOption {
59
+ return func(client *Client) {
60
+ if timeout > 0 {
61
+ client.requestTimeout = timeout
62
+ }
63
+ }
64
+}
65
+
66
+func WithHandshakeTimeout(timeout time.Duration) ClientOption {
67
+ return func(client *Client) {
68
+ if timeout > 0 {
69
+ client.handshakeTimeout = timeout
70
+ }
71
+ }
72
+}
73
+
74
+func WithLeaseTTL(ttl time.Duration) ClientOption {
75
+ return func(client *Client) {
76
+ if ttl > 0 {
77
+ client.leaseTTL = ttl
78
+ }
79
+ }
80
+}
81
+
82
+func WithRenewBefore(d time.Duration) ClientOption {
83
+ return func(client *Client) {
84
+ if d > 0 {
85
+ client.renewBefore = d
86
+ }
87
+ }
88
+}
89
+
90
+func WithReadyTarget(n int) ClientOption {
91
+ return func(client *Client) {
92
+ if n > 0 {
93
+ client.readyTarget = n
94
+ }
95
+ }
96
}
97
98
type Client struct {
42
- baseURL *url.URL
43
- httpClient *http.Client
44
- rawTLSConfig *tls.Config
45
- dialTimeout time.Duration
46
- handshakeTimeout time.Duration
47
- leaseTTL time.Duration
48
- renewBefore time.Duration
49
- readyTarget int
99
+ baseURL *url.URL
100
+ httpClient *http.Client
101
+ rawTLSConfig *tls.Config
102
+ rootCAPEM []byte
103
+ insecureSkipVerify bool
104
+ dialTimeout time.Duration
105
+ requestTimeout time.Duration
106
+ handshakeTimeout time.Duration
107
+ leaseTTL time.Duration
108
+ renewBefore time.Duration
109
+ readyTarget int
110
}
111
52
-func NewClient(cfg ClientConfig) (*Client, error) {
53
- baseURL, err := url.Parse(strings.TrimSpace(cfg.RelayURL))
112
+func NewClient(relayURL string, options ...ClientOption) (*Client, error) {
113
+ baseURL, err := url.Parse(strings.TrimSpace(relayURL))
114
if err != nil {
115
return nil, fmt.Errorf("parse relay url: %w", err)
116
}
117
if !strings.EqualFold(baseURL.Scheme, "https") {
58
- return nil, fmt.Errorf("relay url must use https: %q", cfg.RelayURL)
118
+ return nil, fmt.Errorf("relay url must use https: %q", relayURL)
119
}
120
if baseURL.Host == "" {
61
- return nil, fmt.Errorf("relay url host is empty: %q", cfg.RelayURL)
121
+ return nil, fmt.Errorf("relay url host is empty: %q", relayURL)
122
}
123
baseURL.Path = strings.TrimRight(baseURL.Path, "/")
124
baseURL.RawQuery = ""
125
baseURL.Fragment = ""
126
67
- if len(cfg.RootCAPEM) == 0 && isLocalRelayHost(baseURL.Hostname()) {
127
+ client := &Client{
128
+ baseURL: baseURL,
129
+ dialTimeout: defaultDialTimeout,
130
+ requestTimeout: defaultRequestTimeout,
131
+ handshakeTimeout: defaultHandshakeTimeout,
132
+ leaseTTL: defaultLeaseTTL,
133
+ renewBefore: defaultRenewBefore,
134
+ readyTarget: defaultReadyTarget,
135
+ }
136
+ for _, option := range options {
137
+ if option != nil {
138
+ option(client)
139
+ }
140
+ }
141
+
142
+ if len(client.rootCAPEM) == 0 && !client.insecureSkipVerify && isLocalRelayHost(baseURL.Hostname()) {
143
bootstrapCtx, cancel := context.WithTimeout(context.Background(), defaultDialTimeout+defaultHandshakeTimeout)
144
defer cancel()
145
@@ -72,19 +147,20 @@ func NewClient(cfg ClientConfig) (*Client, error) {
147
if bootstrapErr != nil {
148
return nil, fmt.Errorf("bootstrap localhost relay trust: %w", bootstrapErr)
149
}
75
- cfg.RootCAPEM = rootCAPEM
150
+ client.rootCAPEM = rootCAPEM
151
}
152
78
- rootCAs, err := buildRootCAs(cfg.RootCAPEM)
153
+ rootCAs, err := buildRootCAs(client.rootCAPEM)
154
if err != nil {
155
return nil, err
156
}
157
158
baseTLS := &tls.Config{
84
- MinVersion: tls.VersionTLS12,
85
- ServerName: baseURL.Hostname(),
86
- RootCAs: rootCAs,
87
- NextProtos: []string{"http/1.1"},
159
+ MinVersion: tls.VersionTLS12,
160
+ ServerName: baseURL.Hostname(),
161
+ RootCAs: rootCAs,
162
+ InsecureSkipVerify: client.insecureSkipVerify,
163
+ NextProtos: []string{"http/1.1"},
164
}
165
166
transport := &http.Transport{
@@ -92,19 +168,12 @@ func NewClient(cfg ClientConfig) (*Client, error) {
168
ForceAttemptHTTP2: false,
169
}
170
95
- return &Client{
96
- baseURL: baseURL,
97
- httpClient: &http.Client{
98
- Transport: transport,
99
- Timeout: defaultRequestTimeout,
100
- },
101
- rawTLSConfig: baseTLS,
102
- dialTimeout: defaultDialTimeout,
103
- handshakeTimeout: defaultHandshakeTimeout,
104
- leaseTTL: defaultLeaseTTL,
105
- renewBefore: defaultRenewBefore,
106
- readyTarget: defaultReadyTarget,
107
- }, nil
171
+ client.httpClient = &http.Client{
172
+ Transport: transport,
173
+ Timeout: client.requestTimeout,
174
+ }
175
+ client.rawTLSConfig = baseTLS
176
+ return client, nil
177
}
178
179
func (c *Client) Close() {
sdk/client_test.go
+73
-1
@@ -4,6 +4,7 @@ import (
4
"context"
5
"crypto/tls"
6
"encoding/json"
7
+ "encoding/pem"
8
"errors"
9
"net/http"
10
"net/http/httptest"
@@ -22,7 +23,7 @@ func TestNewClientAutoTrustsLocalhostRelayCertificate(t *testing.T) {
23
}))
24
defer server.Close()
25
25
- client, err := NewClient(ClientConfig{RelayURL: server.URL})
26
+ client, err := NewClient(server.URL)
27
if err != nil {
28
t.Fatalf("NewClient() error = %v", err)
29
}
@@ -39,6 +40,77 @@ func TestNewClientAutoTrustsLocalhostRelayCertificate(t *testing.T) {
40
}
41
}
42
43
+func TestNewClientAppliesOptions(t *testing.T) {
44
+ t.Parallel()
45
+
46
+ server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
47
+ w.WriteHeader(http.StatusOK)
48
+ }))
49
+ defer server.Close()
50
+
51
+ rootCAPEM := pem.EncodeToMemory(&pem.Block{
52
+ Type: "CERTIFICATE",
53
+ Bytes: server.Certificate().Raw,
54
+ })
55
+ client, err := NewClient(
56
+ "https://relay.example.com/base/",
57
+ WithRootCAPEM(rootCAPEM),
58
+ WithInsecureSkipVerify(true),
59
+ WithDialTimeout(2*time.Second),
60
+ WithRequestTimeout(3*time.Second),
61
+ WithHandshakeTimeout(4*time.Second),
62
+ WithLeaseTTL(5*time.Minute),
63
+ WithRenewBefore(45*time.Second),
64
+ WithReadyTarget(3),
65
+ )
66
+ if err != nil {
67
+ t.Fatalf("NewClient() error = %v", err)
68
+ }
69
+ defer client.Close()
70
+
71
+ if got := client.baseURL.String(); got != "https://relay.example.com/base" {
72
+ t.Fatalf("baseURL.String() = %q, want %q", got, "https://relay.example.com/base")
73
+ }
74
+ if !client.insecureSkipVerify {
75
+ t.Fatal("insecureSkipVerify = false, want true")
76
+ }
77
+ if client.dialTimeout != 2*time.Second {
78
+ t.Fatalf("dialTimeout = %v, want %v", client.dialTimeout, 2*time.Second)
79
+ }
80
+ if client.requestTimeout != 3*time.Second {
81
+ t.Fatalf("requestTimeout = %v, want %v", client.requestTimeout, 3*time.Second)
82
+ }
83
+ if client.handshakeTimeout != 4*time.Second {
84
+ t.Fatalf("handshakeTimeout = %v, want %v", client.handshakeTimeout, 4*time.Second)
85
+ }
86
+ if client.leaseTTL != 5*time.Minute {
87
+ t.Fatalf("leaseTTL = %v, want %v", client.leaseTTL, 5*time.Minute)
88
+ }
89
+ if client.renewBefore != 45*time.Second {
90
+ t.Fatalf("renewBefore = %v, want %v", client.renewBefore, 45*time.Second)
91
+ }
92
+ if client.readyTarget != 3 {
93
+ t.Fatalf("readyTarget = %d, want %d", client.readyTarget, 3)
94
+ }
95
+ if client.httpClient.Timeout != 3*time.Second {
96
+ t.Fatalf("httpClient.Timeout = %v, want %v", client.httpClient.Timeout, 3*time.Second)
97
+ }
98
+ if !client.rawTLSConfig.InsecureSkipVerify {
99
+ t.Fatal("rawTLSConfig.InsecureSkipVerify = false, want true")
100
+ }
101
+ if string(client.rootCAPEM) != string(rootCAPEM) {
102
+ t.Fatalf("rootCAPEM = %q, want copied PEM input", string(client.rootCAPEM))
103
+ }
104
+
105
+ rootCAPEM[0] = 'X'
106
+ if string(client.rootCAPEM) == string(rootCAPEM) {
107
+ t.Fatalf("rootCAPEM changed with caller slice mutation: %q", string(client.rootCAPEM))
108
+ }
109
+ if len(client.rootCAPEM) == 0 || client.rootCAPEM[0] != '-' {
110
+ t.Fatalf("rootCAPEM changed with caller slice mutation: %q", string(client.rootCAPEM))
111
+ }
112
+}
113
+
114
func TestOpenReverseSessionPreservesAPIErrorCode(t *testing.T) {
115
t.Parallel()
116