Skip to content

Commit a51e1b8

Browse files
committed
OCM-18900 | fix: Improve error message for proxy passwords with special characters
1 parent 88c5125 commit a51e1b8

File tree

7 files changed

+342
-38
lines changed

7 files changed

+342
-38
lines changed

cmd/create/cluster/cmd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3047,15 +3047,15 @@ func run(cmd *cobra.Command, _ []string) {
30473047
Help: cmd.Flags().Lookup("https-proxy").Usage,
30483048
Default: httpsProxy,
30493049
Validators: []interactive.Validator{
3050-
interactive.IsURL,
3050+
ocm.ValidateHTTPSProxy,
30513051
},
30523052
})
30533053
if err != nil {
30543054
r.Reporter.Errorf("Expected a valid https proxy: %s", err)
30553055
os.Exit(1)
30563056
}
30573057
}
3058-
err = interactive.IsURL(httpsProxy)
3058+
err = ocm.ValidateHTTPSProxy(httpsProxy)
30593059
if err != nil {
30603060
r.Reporter.Errorf("%s", err)
30613061
os.Exit(1)

cmd/edit/cluster/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ func run(cmd *cobra.Command, _ []string) {
508508
}
509509
}
510510
if httpsProxy != nil && *httpsProxy != input.DoubleQuotesToRemove {
511-
err = interactive.IsURL(*httpsProxy)
511+
err = ocm.ValidateHTTPSProxy(*httpsProxy)
512512
if err != nil {
513513
r.Reporter.Errorf("%s", err)
514514
os.Exit(1)

pkg/helper/helpers.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,65 @@ func FilterEmptyStrings(strings []string) []string {
268268
}
269269
return filteredResult
270270
}
271+
272+
type URLCredentialValidation struct {
273+
Field string
274+
InvalidChar rune
275+
HasInvalid bool
276+
}
277+
278+
func ValidateURLCredentials(urlString string) *URLCredentialValidation {
279+
if !strings.Contains(urlString, "@") || !strings.Contains(urlString, "://") {
280+
return &URLCredentialValidation{HasInvalid: false}
281+
}
282+
283+
schemeIdx := strings.Index(urlString, "://")
284+
if schemeIdx == -1 {
285+
return &URLCredentialValidation{HasInvalid: false}
286+
}
287+
288+
rest := urlString[schemeIdx+3:]
289+
atIdx := strings.LastIndex(rest, "@")
290+
if atIdx == -1 {
291+
return &URLCredentialValidation{HasInvalid: false}
292+
}
293+
294+
userinfo := rest[:atIdx]
295+
296+
if strings.Count(urlString, "@") > 1 {
297+
return &URLCredentialValidation{
298+
Field: "password",
299+
InvalidChar: '@',
300+
HasInvalid: true,
301+
}
302+
}
303+
304+
colonIdx := strings.Index(userinfo, ":")
305+
if colonIdx == -1 {
306+
return checkForInvalidChars(userinfo, "username")
307+
}
308+
309+
username := userinfo[:colonIdx]
310+
if result := checkForInvalidChars(username, "username"); result.HasInvalid {
311+
return result
312+
}
313+
314+
password := userinfo[colonIdx+1:]
315+
return checkForInvalidChars(password, "password")
316+
}
317+
318+
func checkForInvalidChars(value, field string) *URLCredentialValidation {
319+
invalidChars := "/:#?[]@!$&'()*+,;="
320+
321+
for _, char := range value {
322+
if strings.ContainsRune(invalidChars, char) {
323+
return &URLCredentialValidation{
324+
Field: field,
325+
InvalidChar: char,
326+
HasInvalid: true,
327+
}
328+
}
329+
}
330+
331+
return &URLCredentialValidation{HasInvalid: false}
332+
}

pkg/interactive/validation_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ var _ = Describe("SubnetsValidator", func() {
131131

132132
err := validator(answers)
133133
Expect(err).To(HaveOccurred())
134-
Expect(err.Error()).To(ContainSubstring("The number of subnets for a public hosted " +
134+
Expect(err.Error()).To(ContainSubstring("the number of subnets for a public hosted " +
135135
"cluster should be at least two"))
136136
})
137137

@@ -153,7 +153,7 @@ var _ = Describe("SubnetsValidator", func() {
153153

154154
err := validator(answers)
155155
Expect(err).To(HaveOccurred())
156-
Expect(err.Error()).To(ContainSubstring("Must have at least one public subnet when not using both " +
156+
Expect(err.Error()).To(ContainSubstring("must have at least one public subnet when not using both " +
157157
"'private API' and 'private ingress'"))
158158
})
159159

@@ -169,7 +169,7 @@ var _ = Describe("SubnetsValidator", func() {
169169

170170
err := validator(answers)
171171
Expect(err).To(HaveOccurred())
172-
Expect(err.Error()).To(ContainSubstring("The number of subnets for a 'multi-AZ'"+
172+
Expect(err.Error()).To(ContainSubstring("the number of subnets for a 'multi-AZ'"+
173173
" 'private link cluster' should be '3', instead received: '%d'", len(answers)))
174174
})
175175

pkg/ocm/helpers.go

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ func ClusterNameValidator(name interface{}) error {
108108
if str, ok := name.(string); ok {
109109
str := strings.Trim(str, " \t")
110110
if !IsValidClusterName(str) {
111-
return fmt.Errorf("Cluster name must consist of no more than %d lowercase "+
111+
return fmt.Errorf("cluster name must consist of no more than %d lowercase "+
112112
"alphanumeric characters or '-', start with a letter, and end with an "+
113-
"alphanumeric character.", MaxClusterNameLength)
113+
"alphanumeric character", MaxClusterNameLength)
114114
}
115115
return nil
116116
}
@@ -125,30 +125,88 @@ func ClusterDomainPrefixValidator(domainPrefix interface{}) error {
125125
if str, ok := domainPrefix.(string); ok {
126126
str := strings.Trim(str, " \t")
127127
if str != "" && !IsValidClusterDomainPrefix(str) {
128-
return fmt.Errorf("Cluster domain prefix must consist of no more than %d lowercase "+
128+
return fmt.Errorf("cluster domain prefix must consist of no more than %d lowercase "+
129129
"alphanumeric characters or '-', start with a letter, and end with an "+
130-
"alphanumeric character.", MaxClusterDomainPrefixLength)
130+
"alphanumeric character", MaxClusterDomainPrefixLength)
131131
}
132132
return nil
133133
}
134134
return fmt.Errorf("can only validate strings, got '%v'", domainPrefix)
135135
}
136136

137-
func ValidateHTTPProxy(val interface{}) error {
138-
if httpProxy, ok := val.(string); ok {
139-
if httpProxy == "" {
140-
return nil
141-
}
142-
url, err := url.ParseRequestURI(httpProxy)
143-
if err != nil {
144-
return fmt.Errorf("Invalid http-proxy value '%s'", httpProxy)
137+
// validateProxyURL validates common proxy URL requirements
138+
func validateProxyURL(proxyURL string, proxyType string, expectedScheme string) error {
139+
if strings.Contains(proxyURL, " ") {
140+
return fmt.Errorf("invalid %s value: URL cannot contain spaces", proxyType)
141+
}
142+
143+
if credValidation := helper.ValidateURLCredentials(proxyURL); credValidation.HasInvalid {
144+
return fmt.Errorf("invalid %s value: %s contains invalid character '%c'",
145+
proxyType, credValidation.Field, credValidation.InvalidChar)
146+
}
147+
148+
parsedURL, err := url.Parse(proxyURL)
149+
if err != nil {
150+
return fmt.Errorf("invalid %s value: %v", proxyType, err)
151+
}
152+
153+
if parsedURL.Scheme == "" {
154+
return fmt.Errorf("invalid %s value: URL scheme is missing (e.g., %s://)", proxyType, expectedScheme)
155+
}
156+
if parsedURL.Scheme != expectedScheme {
157+
return errors.Errorf("%s", fmt.Sprintf("expected %s to have an %s:// scheme", proxyType, expectedScheme))
158+
}
159+
160+
if parsedURL.Host == "" {
161+
return fmt.Errorf("invalid %s value: host is missing", proxyType)
162+
}
163+
164+
// Proxy URLs shouldn't have a path
165+
if parsedURL.Path != "" && parsedURL.Path != "/" {
166+
return fmt.Errorf("invalid %s value: proxy URL should not contain a path", proxyType)
167+
}
168+
169+
// Validate hostname/IP and port
170+
host, port, err := net.SplitHostPort(parsedURL.Host)
171+
if err != nil {
172+
// No port specified, just validate the host
173+
host = parsedURL.Host
174+
} else {
175+
// Validate port range (url.Parse already validated it's numeric)
176+
portNum, _ := strconv.Atoi(port)
177+
if portNum < 1 || portNum > 65535 {
178+
return fmt.Errorf("invalid %s value: port must be between 1 and 65535", proxyType)
145179
}
146-
if url.Scheme != "http" {
147-
return errors.Errorf("%s", "Expected http-proxy to have an http:// scheme")
180+
}
181+
182+
// Validate host is a valid hostname or IP address
183+
if host != "" {
184+
// Check if it's an IP address
185+
if ip := net.ParseIP(host); ip == nil {
186+
// Not an IP, check if it's a valid hostname
187+
if strings.Contains(host, "/") {
188+
return fmt.Errorf("invalid %s value: invalid host", proxyType)
189+
}
148190
}
191+
}
192+
193+
return nil
194+
}
195+
196+
func ValidateHTTPProxy(val interface{}) error {
197+
httpProxy := val.(string)
198+
if httpProxy == "" {
149199
return nil
150200
}
151-
return fmt.Errorf("can only validate strings, got '%v'", val)
201+
return validateProxyURL(httpProxy, "http-proxy", "http")
202+
}
203+
204+
func ValidateHTTPSProxy(val interface{}) error {
205+
httpsProxy := val.(string)
206+
if httpsProxy == "" {
207+
return nil
208+
}
209+
return validateProxyURL(httpsProxy, "https-proxy", "https")
152210
}
153211

154212
func ValidateRegistryAdditionalCa(input map[string]string) error {
@@ -174,9 +232,9 @@ func ValidateAllowedRegistriesForImport(input interface{}) error {
174232
registries = strings.Split(input.(string), ",")
175233
for _, registry := range registries {
176234
if !idRE.MatchString(registry) {
177-
return fmt.Errorf("invalid identifier '%s' for 'allowed registries for import.' "+
235+
return fmt.Errorf("invalid identifier '%s' for 'allowed registries for import'. "+
178236
"Should be in a <registry>:<boolean> format. "+
179-
"The boolean indicates whether the registry is secure or not.", registry)
237+
"The boolean indicates whether the registry is secure or not", registry)
180238
}
181239
}
182240
case reflect.Slice:
@@ -745,7 +803,7 @@ func ValidateSubnetsCount(multiAZ bool, privateLink bool, subnetsInputCount int)
745803
if multiAZ {
746804
azPrefix = "multi-AZ"
747805
}
748-
return fmt.Errorf("The number of subnets for a '%s' '%s' should be '%d', "+
806+
return fmt.Errorf("the number of subnets for a '%s' '%s' should be '%d', "+
749807
"instead received: '%d'", azPrefix, clusterPrefix, expected, subnetsInputCount)
750808
}
751809
return nil
@@ -755,10 +813,10 @@ func ValidateHostedClusterSubnets(awsClient aws.Client, isPrivate bool, subnetID
755813
privateIngress bool) (int, error) {
756814

757815
if isPrivate && len(subnetIDs) < 1 {
758-
return 0, fmt.Errorf("The number of subnets for a private hosted cluster should be at least one")
816+
return 0, fmt.Errorf("the number of subnets for a private hosted cluster should be at least one")
759817
}
760818
if !isPrivate && len(subnetIDs) < 2 {
761-
return 0, fmt.Errorf("The number of subnets for a public hosted cluster should be at least two")
819+
return 0, fmt.Errorf("the number of subnets for a public hosted cluster should be at least two")
762820
}
763821
vpcSubnets, vpcSubnetsErr := awsClient.GetVPCSubnets(subnetIDs[0])
764822
if vpcSubnetsErr != nil {
@@ -785,11 +843,11 @@ func ValidateHostedClusterSubnets(awsClient aws.Client, isPrivate bool, subnetID
785843

786844
if isPrivate && privateIngress {
787845
if publicSubnetsCount > 0 {
788-
return 0, fmt.Errorf("The number of public subnets for a private hosted cluster should be zero")
846+
return 0, fmt.Errorf("the number of public subnets for a private hosted cluster should be zero")
789847
}
790848
} else {
791849
if publicSubnetsCount == 0 {
792-
return 0, fmt.Errorf("Must have at least one public subnet when not using both " +
850+
return 0, fmt.Errorf("must have at least one public subnet when not using both " +
793851
"'private API' and 'private ingress'")
794852
}
795853
}
@@ -1106,8 +1164,8 @@ func ValidateClaimValidationRules(input interface{}) error {
11061164
inputRules = strings.Split(input.(string), ",")
11071165
for _, inputRule := range inputRules {
11081166
if !idRE.MatchString(inputRule) {
1109-
return fmt.Errorf("invalid identifier '%s' for 'claim validation rule. '"+
1110-
"Should be in a <claim>:<required_value> format.", inputRule)
1167+
return fmt.Errorf("invalid identifier '%s' for 'claim validation rule'. "+
1168+
"Should be in a <claim>:<required_value> format", inputRule)
11111169
}
11121170
}
11131171
case reflect.Slice:

0 commit comments

Comments
 (0)