Skip to content

Commit ea51984

Browse files
authored
Merge branch 'main' into feat/STACKITCLI-383-onboard-telemetry-link
2 parents bd67b2f + 90ee651 commit ea51984

6 files changed

Lines changed: 139 additions & 47 deletions

File tree

‎internal/cmd/curl/curl_test.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ func TestParseInput(t *testing.T) {
139139
model.URL = "https://www.example.website.com/"
140140
}),
141141
},
142+
{
143+
description: "hostname suffix without domain boundary",
144+
argValues: []string{
145+
"https://suspiciousstackit.cloud/",
146+
},
147+
flagValues: fixtureFlagValues(),
148+
allowedURLDomain: "stackit.cloud",
149+
isValid: false,
150+
},
142151
{
143152
description: "invalid method 1",
144153
argValues: fixtureArgValues(),

‎internal/pkg/generic-client/generic_client.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func ConfigureClientGeneric[T any](p *print.Printer, cliVersion, customEndpoint
3737

3838
if p.IsVerbosityDebug() {
3939
cfgOptions = append(cfgOptions,
40-
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)),
40+
sdkConfig.WithMiddleware(print.RequestResponseCapturer(p)),
4141
)
4242
}
4343

‎internal/pkg/print/debug.go‎

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -89,23 +89,14 @@ func BuildDebugStrFromSlice(inputSlice []string) string {
8989

9090
// buildHeaderMap converts a map to a user-friendly string representation.
9191
// This function also filters the headers based on the includeHeaders parameter.
92-
// If includeHeaders is empty, the default header filters are used.
92+
// If includeHeaders is empty, all headers will be printed.
9393
func buildHeaderMap(headers http.Header, includeHeaders []string) map[string]any {
9494
headersMap := make(map[string]any)
9595
for key, values := range headers {
96-
headersMap[key] = strings.Join(values, ", ")
97-
}
98-
99-
headersToInclude := defaultHTTPHeaders
100-
if len(includeHeaders) != 0 {
101-
headersToInclude = includeHeaders
102-
}
103-
for key := range headersMap {
104-
if !slices.Contains(headersToInclude, key) {
105-
delete(headersMap, key)
96+
if len(includeHeaders) == 0 || slices.Contains(includeHeaders, key) {
97+
headersMap[key] = strings.Join(values, ", ")
10698
}
10799
}
108-
109100
return headersMap
110101
}
111102

@@ -132,9 +123,9 @@ func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
132123
}
133124

134125
// BuildDebugStrFromHTTPRequest converts an HTTP request to a user-friendly string representation.
135-
// This function also receives a list of headers to include in the output, if empty, the default headers are used.
126+
// Only the headers specified in defaultHTTPHeaders will be printed.
136127
// The return value is a list of strings that should be printed separately.
137-
func BuildDebugStrFromHTTPRequest(req *http.Request, includeHeaders []string) ([]string, error) {
128+
func BuildDebugStrFromHTTPRequest(req *http.Request) ([]string, error) {
138129
if req == nil {
139130
return nil, fmt.Errorf("request is nil")
140131
}
@@ -150,7 +141,7 @@ func BuildDebugStrFromHTTPRequest(req *http.Request, includeHeaders []string) ([
150141

151142
status := fmt.Sprintf("request to %s: %s %s", unescapedURL, req.Method, req.Proto)
152143

153-
headersMap := buildHeaderMap(req.Header, includeHeaders)
144+
headersMap := buildHeaderMap(req.Header, defaultHTTPHeaders)
154145
headers := fmt.Sprintf("request headers: %v", BuildDebugStrFromMap(headersMap))
155146

156147
var save io.ReadCloser
@@ -179,9 +170,9 @@ func BuildDebugStrFromHTTPRequest(req *http.Request, includeHeaders []string) ([
179170
}
180171

181172
// BuildDebugStrFromHTTPResponse converts an HTTP response to a user-friendly string representation.
182-
// This function also receives a list of headers to include in the output, if empty, the default headers are used.
173+
// All headers will be printed.
183174
// The return value is a list of strings that should be printed separately.
184-
func BuildDebugStrFromHTTPResponse(resp *http.Response, includeHeaders []string) ([]string, error) {
175+
func BuildDebugStrFromHTTPResponse(resp *http.Response) ([]string, error) {
185176
if resp == nil {
186177
return nil, fmt.Errorf("response is nil")
187178
}
@@ -199,7 +190,7 @@ func BuildDebugStrFromHTTPResponse(resp *http.Response, includeHeaders []string)
199190

200191
status := fmt.Sprintf("response from %s: %s %s", unescapedURL, resp.Proto, resp.Status)
201192

202-
headersMap := buildHeaderMap(resp.Header, includeHeaders)
193+
headersMap := buildHeaderMap(resp.Header, nil)
203194
headers := fmt.Sprintf("response headers: %v", BuildDebugStrFromMap(headersMap))
204195

205196
var save io.ReadCloser
@@ -228,23 +219,20 @@ func BuildDebugStrFromHTTPResponse(resp *http.Response, includeHeaders []string)
228219
}
229220

230221
// RequestResponseCapturer is a middleware that captures the request and response of an HTTP request.
231-
// Receives a printer and a list of headers to include in the output
232-
// If the list of headers is empty, the default headers are used.
233-
// The printer is used to print the captured data.
234-
func RequestResponseCapturer(p *Printer, includeHeaders []string) config.Middleware {
222+
// Receives a printer used to print the captured data.
223+
func RequestResponseCapturer(p *Printer) config.Middleware {
235224
return func(rt http.RoundTripper) http.RoundTripper {
236-
return &roundTripperWithCapture{rt, p, includeHeaders}
225+
return &roundTripperWithCapture{rt, p}
237226
}
238227
}
239228

240229
type roundTripperWithCapture struct {
241-
transport http.RoundTripper
242-
p *Printer
243-
debugHttpHeaders []string
230+
transport http.RoundTripper
231+
p *Printer
244232
}
245233

246234
func (rt roundTripperWithCapture) RoundTrip(req *http.Request) (*http.Response, error) {
247-
reqStr, err := BuildDebugStrFromHTTPRequest(req, rt.debugHttpHeaders)
235+
reqStr, err := BuildDebugStrFromHTTPRequest(req)
248236
if err != nil {
249237
rt.p.Debug(ErrorLevel, "printing request to debug logs: %v", err)
250238
}
@@ -254,7 +242,7 @@ func (rt roundTripperWithCapture) RoundTrip(req *http.Request) (*http.Response,
254242
resp, err := rt.transport.RoundTrip(req)
255243
defer func() {
256244
if err == nil {
257-
respStrSlice, tempErr := BuildDebugStrFromHTTPResponse(resp, rt.debugHttpHeaders)
245+
respStrSlice, tempErr := BuildDebugStrFromHTTPResponse(resp)
258246
if tempErr != nil {
259247
rt.p.Debug(ErrorLevel, "printing HTTP response to debug logs: %v", tempErr)
260248
}

‎internal/pkg/print/debug_test.go‎

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func TestBuildHeaderMap(t *testing.T) {
322322
},
323323
},
324324
{
325-
description: "no include headers",
325+
description: "non default HTTP headers",
326326
inputHeader: http.Header{
327327
"Accept": []string{"value1"},
328328
"key2": []string{"value2"},
@@ -332,6 +332,7 @@ func TestBuildHeaderMap(t *testing.T) {
332332
expected: map[string]any{
333333
"Accept": "value1",
334334
"Date": "value3",
335+
"key2": "value2",
335336
},
336337
},
337338
{
@@ -364,11 +365,10 @@ func TestBuildHeaderMap(t *testing.T) {
364365

365366
func TestBuildDebugStrFromHTTPRequest(t *testing.T) {
366367
tests := []struct {
367-
description string
368-
inputReq *http.Request
369-
inputIncludeHeaders []string
370-
expected []string
371-
isValid bool
368+
description string
369+
inputReq *http.Request
370+
expected []string
371+
isValid bool
372372
}{
373373
{
374374
description: "base",
@@ -381,12 +381,13 @@ func TestBuildDebugStrFromHTTPRequest(t *testing.T) {
381381
isValid: true,
382382
},
383383
{
384-
description: "include headers",
385-
inputReq: fixtureHTTPRequest(),
386-
inputIncludeHeaders: []string{"Content-Type", "Accept"},
384+
description: "includes only default headers",
385+
inputReq: fixtureHTTPRequest(func(req *http.Request) {
386+
req.Header["Authorization"] = []string{"Bearer: ey"}
387+
}),
387388
expected: []string{
388389
"request to http://example.com: GET HTTP/1.1",
389-
"request headers: [Accept: application/json, Content-Type: application/json]",
390+
"request headers: [Accept: application/json, Content-Length: 15, Content-Type: application/json]",
390391
"request body: [key: value]",
391392
},
392393
isValid: true,
@@ -438,7 +439,7 @@ func TestBuildDebugStrFromHTTPRequest(t *testing.T) {
438439

439440
for _, tt := range tests {
440441
t.Run(tt.description, func(t *testing.T) {
441-
actual, err := BuildDebugStrFromHTTPRequest(tt.inputReq, tt.inputIncludeHeaders)
442+
actual, err := BuildDebugStrFromHTTPRequest(tt.inputReq)
442443
if err != nil {
443444
if !tt.isValid {
444445
return
@@ -458,11 +459,10 @@ func TestBuildDebugStrFromHTTPRequest(t *testing.T) {
458459

459460
func TestBuildDebugStrFromHTTPResponse(t *testing.T) {
460461
tests := []struct {
461-
description string
462-
inputResp *http.Response
463-
inputIncludeHeaders []string
464-
expected []string
465-
isValid bool
462+
description string
463+
inputResp *http.Response
464+
expected []string
465+
isValid bool
466466
}{
467467
{
468468
description: "base",
@@ -496,6 +496,18 @@ func TestBuildDebugStrFromHTTPResponse(t *testing.T) {
496496
},
497497
isValid: true,
498498
},
499+
{
500+
description: "more headers",
501+
inputResp: fixtureHTTPResponse(func(resp *http.Response) { // nolint:bodyclose // false positive, body is closed in the test
502+
resp.Header.Add("X-Trace-Id", "trace-id")
503+
}),
504+
expected: []string{
505+
"response from http://example.com: HTTP/1.1 200 OK",
506+
"response headers: [Accept: application/json, Content-Length: 15, Content-Type: application/json, X-Trace-Id: trace-id]",
507+
"response body: [key: value]",
508+
},
509+
isValid: true,
510+
},
499511
{
500512
description: "empty body",
501513
inputResp: fixtureHTTPResponse(func(resp *http.Response) { // nolint:bodyclose // false positive, body is closed in the test
@@ -517,7 +529,7 @@ func TestBuildDebugStrFromHTTPResponse(t *testing.T) {
517529
err = tt.inputResp.Body.Close()
518530
}()
519531
}
520-
actual, err := BuildDebugStrFromHTTPResponse(tt.inputResp, tt.inputIncludeHeaders)
532+
actual, err := BuildDebugStrFromHTTPResponse(tt.inputResp)
521533
if err != nil {
522534
if !tt.isValid {
523535
return

‎internal/pkg/utils/utils.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,13 @@ func ValidateURLDomain(value string) error {
108108
}
109109

110110
allowedUrlDomain := viper.GetString(config.AllowedUrlDomainKey)
111+
if allowedUrlDomain == "" {
112+
return nil
113+
}
111114

112-
if !strings.HasSuffix(urlHost, allowedUrlDomain) {
115+
urlHost = strings.ToLower(urlHost)
116+
allowedUrlDomain = strings.ToLower(allowedUrlDomain)
117+
if urlHost != allowedUrlDomain && !strings.HasSuffix(urlHost, "."+allowedUrlDomain) {
113118
return fmt.Errorf(`only urls belonging to domain %s are allowed`, allowedUrlDomain)
114119
}
115120
return nil

‎internal/pkg/utils/utils_test.go‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,60 @@ func TestValidateURLDomain(t *testing.T) {
119119
input: "https://example.stackit.cloud",
120120
isValid: true,
121121
},
122+
{
123+
name: "apex domain",
124+
allowedUrlDomain: "stackit.cloud",
125+
input: "https://stackit.cloud/path",
126+
isValid: true,
127+
},
128+
{
129+
name: "multiple subdomains",
130+
allowedUrlDomain: "stackit.cloud",
131+
input: "https://dns.api.stackit.cloud/v1",
132+
isValid: true,
133+
},
134+
{
135+
name: "hostname suffix without label boundary",
136+
allowedUrlDomain: "stackit.cloud",
137+
input: "https://suspiciousstackit.cloud",
138+
isValid: false,
139+
},
140+
{
141+
name: "lookalike subdomain",
142+
allowedUrlDomain: "stackit.cloud",
143+
input: "https://api.suspiciousstackit.cloud",
144+
isValid: false,
145+
},
146+
{
147+
name: "domain followed by extra labels",
148+
allowedUrlDomain: "stackit.cloud",
149+
input: "https://api.stackit.cloud.evil.example",
150+
isValid: false,
151+
},
152+
{
153+
name: "port is not hostname",
154+
allowedUrlDomain: "stackit.cloud",
155+
input: "https://stackit.cloud:443/v1",
156+
isValid: true,
157+
},
158+
{
159+
name: "userinfo does not affect hostname",
160+
allowedUrlDomain: "stackit.cloud",
161+
input: "https://stackit.cloud@evil.example/path",
162+
isValid: false,
163+
},
164+
{
165+
name: "path does not affect hostname",
166+
allowedUrlDomain: "stackit.cloud",
167+
input: "https://evil.example/path/stackit.cloud",
168+
isValid: false,
169+
},
170+
{
171+
name: "hostname is case insensitive",
172+
allowedUrlDomain: "stackit.cloud",
173+
input: "https://API.STACKIT.CLOUD/path",
174+
isValid: true,
175+
},
122176
{
123177
name: "STACKIT URL invalid",
124178
allowedUrlDomain: "example.com",
@@ -137,6 +191,12 @@ func TestValidateURLDomain(t *testing.T) {
137191
input: "https://www.test.example.com/",
138192
isValid: true,
139193
},
194+
{
195+
name: "custom domain boundary rejected",
196+
allowedUrlDomain: "example.com",
197+
input: "https://badexample.com",
198+
isValid: false,
199+
},
140200
{
141201
name: "every URL valid",
142202
allowedUrlDomain: "",
@@ -153,6 +213,24 @@ func TestValidateURLDomain(t *testing.T) {
153213
input: "http://example.stackit.cloud",
154214
isValid: false,
155215
},
216+
{
217+
name: "invalid protocol with allowed domain",
218+
allowedUrlDomain: "stackit.cloud",
219+
input: "http://api.stackit.cloud",
220+
isValid: false,
221+
},
222+
{
223+
name: "missing host",
224+
allowedUrlDomain: "stackit.cloud",
225+
input: "https:///path",
226+
isValid: false,
227+
},
228+
{
229+
name: "malformed URL",
230+
allowedUrlDomain: "stackit.cloud",
231+
input: "https://%zz",
232+
isValid: false,
233+
},
156234
{
157235
name: "no protocol",
158236
input: "example.stackit.cloud",

0 commit comments

Comments
 (0)